1. Introdução
Em 2026, saber trabalhar com Docker deixou de ser diferencial e passou a ser requisito em praticamente qualquer vaga de desenvolvedor back-end ou full-stack. Container virou a linguagem universal do deploy.
Mas ainda existe muita confusão sobre como estruturar corretamente um Dockerfile, usar multi-stage builds, gerenciar variáveis de ambiente com segurança e compor serviços com Docker Compose.
Neste artigo você vai ver tudo isso na prática, partindo do zero com uma aplicação Node.js + TypeScript até um setup pronto para produção.
2. O Que É Docker (e Por Que Você Deveria Usar)
Docker é uma plataforma de containerização: você empacota sua aplicação junto com todas as suas dependências numa imagem isolada. Isso resolve o clássico problema "funciona na minha máquina".
2.1 Conceitos essenciais
- Imagem — snapshot imutável do ambiente da aplicação
- Container — instância em execução de uma imagem
- Dockerfile — receita para construir uma imagem
- Docker Compose — orquestrador para múltiplos containers locais
- Registry — repositório de imagens (Docker Hub, GHCR, ECR etc.)
2.2 Vantagens práticas
- Ambiente idêntico em dev, staging e produção
- Onboarding de novos devs em minutos
- Rollback rápido — basta trocar a tag da imagem
- Isolamento de dependências entre projetos
- Base para Kubernetes e outras orquestrações
3. O Projeto Base: API Node.js + TypeScript
Vamos containerizar uma API REST simples com Express e TypeScript. Estrutura inicial:
minha-api/
├── src/
│ └── index.ts
├── package.json
├── tsconfig.json
└── .env
// src/index.ts
import express from 'express';
const app = express();
const PORT = process.env.PORT ?? 3000;
app.use(express.json());
app.get('/health', (_req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
app.get('/api/hello', (_req, res) => {
const name = process.env.APP_NAME ?? 'World';
res.json({ message: `Hello, ${name}!` });
});
app.listen(PORT, () => {
console.log(`🚀 API rodando na porta ${PORT}`);
});
4. Seu Primeiro Dockerfile
Um Dockerfile ruim gera imagens enormes, lentas e com problemas de segurança. Veja um exemplo ingênuo e seus problemas:
# ❌ Dockerfile ingênuo — NÃO use em produção
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
CMD ["node", "dist/index.js"]
Problemas dessa abordagem:
- Imagem base pesada (~1 GB)
- Dependências de desenvolvimento incluídas
- Código-fonte exposto na imagem final
- Cache do Docker mal aproveitado
5. Multi-Stage Build: A Forma Certa
Com multi-stage builds, você usa um container para compilar e outro, menor, para rodar:
# ✅ Dockerfile otimizado com multi-stage build
# ── Stage 1: Build ──────────────────────────────────────────────
FROM node:20-alpine AS builder
WORKDIR /build
# Copiar manifests primeiro para aproveitar cache
COPY package*.json ./
RUN npm ci --include=dev
# Compilar TypeScript
COPY tsconfig.json ./
COPY src/ ./src/
RUN npm run build
# ── Stage 2: Produção ───────────────────────────────────────────
FROM node:20-alpine AS production
WORKDIR /app
# Criar usuário não-root por segurança
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
# Apenas dependências de produção
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
# Copiar apenas o build final, não o código-fonte
COPY --from=builder /build/dist ./dist
# Usar usuário não-root
USER appuser
# Expor porta e definir variáveis padrão
EXPOSE 3000
ENV NODE_ENV=production
# Healthcheck
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s \
CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "dist/index.js"]
Resultado: imagem de ~180 MB em vez de ~1 GB, sem código-fonte exposto e com usuário não-root.
.dockerignore: Tão Importante Quanto .gitignore
Sem um .dockerignore, o Docker envia tudo para o build context — incluindo node_modules locais e arquivos sensíveis:
# .dockerignore
node_modules/
dist/
.git/
.gitignore
*.log
.env
.env.*
coverage/
.nyc_output/
README.md
docker-compose*.yml
6. Docker Compose: Orquestrando Múltiplos Serviços
Sua API provavelmente precisa de banco de dados, cache e talvez um serviço de fila. O Docker Compose resolve isso localmente:
# docker-compose.yml
version: "3.9"
services:
api:
build:
context: .
target: production
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://user:password@postgres:5432/mydb
- REDIS_URL=redis://redis:6379
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
restart: unless-stopped
networks:
- app-net
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: password
POSTGRES_DB: mydb
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
interval: 10s
timeout: 5s
retries: 5
networks:
- app-net
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
networks:
- app-net
volumes:
postgres_data:
redis_data:
networks:
app-net:
driver: bridge
Comandos do dia a dia:
# Subir todos os serviços em background
docker compose up -d
# Ver logs em tempo real
docker compose logs -f api
# Recriar apenas a API após uma mudança
docker compose up -d --build api
# Derrubar tudo (preserva volumes)
docker compose down
# Derrubar tudo e apagar volumes (cuidado em produção!)
docker compose down -v
7. Variáveis de Ambiente com Segurança
Nunca comite segredos no Dockerfile ou no docker-compose.yml. Use arquivos .env:
# .env (adicione ao .gitignore!)
DATABASE_URL=postgresql://user:senha_secreta@postgres:5432/mydb
JWT_SECRET=minha_chave_super_secreta_gerada_com_o_amigo_do_dev
REDIS_URL=redis://redis:6379
# docker-compose.yml — referenciar .env
services:
api:
env_file:
- .env
💡 Dica: Use o Gerador de JWT Secret do Amigo do Dev para criar chaves criptograficamente seguras para o seu JWT_SECRET.
8. Boas Práticas Resumidas
8.1 Segurança
- ✅ Sempre use usuário não-root (
USER appuser) - ✅ Imagens alpine ou distroless reduzem superfície de ataque
- ✅ Não embuta segredos na imagem — use variáveis de ambiente ou secrets
- ✅ Escaneie vulnerabilidades com
docker scoutou Trivy - ✅ Defina
--read-onlyno filesystem quando possível
8.2 Performance
- ✅ Ordene camadas do Dockerfile do que muda menos para o que muda mais
- ✅ Use
npm ciem vez denpm installpara builds reproduzíveis - ✅ Multi-stage build sempre em produção
- ✅ Adicione
--no-cacheno CI para evitar builds stale
8.3 Observabilidade
- ✅ Implemente
HEALTHCHECKno Dockerfile - ✅ Exponha métricas em
/metricspara Prometheus - ✅ Use logs em formato JSON para facilitar ingestão no Loki/Datadog
9. Integração com CI/CD (GitHub Actions)
Automatize o build e push da imagem no merge para main:
# .github/workflows/docker.yml
name: Build & Push Docker Image
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
target: production
push: true
tags: |
ghcr.io/${{ github.repository }}:latest
ghcr.io/${{ github.repository }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
10. Conclusão
Você agora tem um setup de Docker profissional, com:
- Multi-stage build para imagens pequenas e seguras
- .dockerignore bem configurado
- Docker Compose com banco de dados e cache
- Variáveis de ambiente gerenciadas corretamente
- CI/CD automatizado com GitHub Actions
Docker é apenas o começo da jornada DevOps. O próximo passo natural é Kubernetes para orquestração em escala. Mas com o que vimos aqui, você já está pronto para subir aplicações em qualquer VPS, Railway, Render ou serviço de cloud.
Não se esqueça de explorar as ferramentas do Amigo do Dev que podem ajudar no seu dia a dia como dev!
