Docker — Guia de Estudos
Guia consolidado sobre Docker, cobrindo conceitos fundamentais, boas práticas de Dockerfile, Compose, CLI avançado, registries, otimização de imagens, segurança e orquestração (Swarm/Kubernetes).
Conceitos fundamentais
O que é Docker
Docker empacota uma aplicação com tudo que ela precisa pra rodar (dependências, configs) em um container — ambiente isolado e portátil.
| Conceito | Descrição |
|---|---|
| Imagem | Template read-only com tudo que a aplicação precisa |
| Container | Instância em execução de uma imagem |
| Dockerfile | Arquivo com instruções para construir uma imagem |
| Docker Compose | Orquestra múltiplos containers via docker-compose.yml |
| Volume | Persistência de dados fora do ciclo de vida do container |
| Network | Comunicação entre containers |
Containers vs VMs
- VM: roda um SO completo sobre um hypervisor. Boot lento, consumo alto de RAM/disco.
- Container: compartilha o kernel do host. Isolamento via namespaces (isolamento) e cgroups (limitação de recursos). Sobe em milissegundos/segundos.
Camadas (layers) e compartilhamento
Imagens são compostas por camadas read-only empilhadas. Múltiplos containers da mesma imagem compartilham essas camadas — não há duplicação em disco.
Cada container ganha apenas uma writable layer própria e fina por cima, para as mudanças em runtime. Isso é feito via union filesystem (overlay2) com copy-on-write: ao alterar um arquivo de uma camada read-only, ele é copiado para a writable layer do container antes da alteração.
Exemplo: imagem de 100MB + 2 containers com 30MB de writable layer cada:
Total real = 100MB (compartilhado) + 30MB + 30MB = 160MBSem compartilhamento, seria 260MB (2× imagem completa + writable layers).
Isso vale para disco. Em RAM, cada container carrega seu próprio processo em execução — não há esse mesmo compartilhamento.
Volumes
Mecanismo para persistir dados fora do ciclo de vida do container. Necessário porque a writable layer morre junto com o container.
Tipos
Named volumes (recomendado)
docker volume create meu_volume
docker run -v meu_volume:/var/lib/postgresql/data postgresDocker gerencia a localização física (/var/lib/docker/volumes/).
Bind mounts
docker run -v /caminho/no/host:/var/lib/postgresql/data postgresVocê escolhe a pasta do host. Bom para desenvolvimento (hot-reload de código).
tmpfs mounts Fica somente em RAM, some quando o container para. Usado para dados sensíveis temporários.
Comandos úteis
docker volume ls
docker volume inspect nome
docker volume rm nome
docker volume prune # remove volumes não usados por nenhum containerBancos de dados devem sempre usar volume para os dados — volumes têm I/O quase nativo, sem passar pela camada union filesystem com copy-on-write.
Networks
Containers na mesma network se enxergam por nome (DNS interno). Containers em networks diferentes não se enxergam.
Tipos
- bridge (padrão): comunicação por IP, sem DNS automático por nome.
- user-defined bridge: DNS automático por nome do container/serviço. Use sempre este tipo.
- host: container usa a rede do host diretamente, sem isolamento.
- none: sem rede.
Isolando um banco de dados (só acessível pelo backend)
docker network create minha_rede
docker run -d --name db --network minha_rede -v pg_data:/var/lib/postgresql/data postgres
docker run -d --name backend --network minha_rede -p 3000:3000 minha_app_backenddbebackendna mesma network → backend acessa via hostnamedb.- Só
backendpublica porta (-p) → só ele é acessível vialocalhost. dbsem-p→ só alcançável de dentro da network, nunca vialocalhost.
Com Compose, isso já é o padrão: serviços sem ports não são expostos ao host, mas se enxergam entre si pelo nome do serviço.
Dockerfile
Principais instruções
| Instrução | Função |
|---|---|
FROM | Imagem base (sempre primeira instrução) |
WORKDIR | Diretório de trabalho dentro do container |
COPY | Copia arquivos do host para a imagem |
ADD | Como COPY, mas extrai .tar e aceita URLs — evitar, preferir COPY |
RUN | Executa comando durante o build (cria layer) |
ENV | Variável de ambiente disponível em runtime |
EXPOSE | Documentação da porta usada (não publica de fato) |
CMD | Comando padrão ao iniciar o container (só um por Dockerfile) |
ENTRYPOINT | Processo fixo do container; CMD complementa como argumento padrão |
ARG | Variável disponível só durante o build |
USER | Define usuário de execução (evitar root) |
VOLUME | Declara ponto de montagem |
.dockerignore — lista o que não copiar para o contexto de build (equivalente ao .gitignore).
Boas práticas
1. Ordem por frequência de mudança (cache de layers)
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install # só reroda se package.json mudar
COPY . . # código muda sempre, vem depois
RUN npm run build
CMD ["node", "dist/main.js"]2. Multi-stage build — separa ambiente de build do ambiente final, reduzindo drasticamente o tamanho da imagem.
3. Imagens base slim/alpine — node:20 (~1GB) vs node:20-alpine (~150-200MB).
4. Não rodar como root
RUN adduser -D appuser
USER appuser5. Consolidar RUNs relacionados
RUN apt-get update && apt-get install -y \
curl git \
&& rm -rf /var/lib/apt/lists/*Limpar em
RUNseparado não reduz o tamanho — a layer anterior já gravou os arquivos. A limpeza só é efetiva no mesmoRUN.
6. .dockerignore sempre presente
node_modules
.git
.env
dist
*.log7. Tags específicas, nunca latest
FROM node:20.11-alpine # reprodutívelExemplo completo — NestJS + Prisma (multi-stage)
# ---------- Stage 1: dependencies + build ----------
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
COPY prisma ./prisma/
RUN npm ci
RUN npx prisma generate
COPY . .
RUN npm run build
RUN npm prune --omit=dev
# ---------- Stage 2: imagem final de produção ----------
FROM node:20-alpine AS production
WORKDIR /app
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
ENV NODE_ENV=production
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/prisma ./prisma
# Se usar output customizado do Prisma (ex: src/generated/prisma):
# COPY --from=builder /app/src/generated ./src/generated
USER appuser
EXPOSE 3000
CMD ["node", "dist/main.js"]Resultado típico: de ~1GB para ~150-250MB.
Build e run
docker build -t minha_app:1.0 .
docker run -p 3000:3000 minha_app:1.0Docker Compose
Define e executa aplicações multi-container através de um único arquivo YAML.
Estrutura básica
services:
backend:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgresql://user:pass@db:5432/mydb
depends_on:
- db
networks:
- minha_rede
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: pass
POSTGRES_DB: mydb
volumes:
- pg_data:/var/lib/postgresql/data
networks:
- minha_rede
volumes:
pg_data:
networks:
minha_rede:Principais chaves
| Chave | Função |
|---|---|
image | Usa imagem pronta |
build | Constrói a partir de Dockerfile |
ports | "host:container" — único jeito de expor ao localhost |
environment | Variáveis de ambiente |
env_file | Carrega variáveis de um arquivo .env |
volumes | Named volumes ou bind mounts |
depends_on | Ordem de start (não garante "pronto") |
restart | no, always, on-failure, unless-stopped |
command | Sobrescreve o CMD da imagem |
healthcheck + depends_on
depends_on sozinho só garante ordem de start, não que o serviço esteja pronto (ex: Postgres aceitando conexões):
services:
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user"]
interval: 5s
timeout: 5s
retries: 5
backend:
build: .
depends_on:
db:
condition: service_healthyComandos principais
docker compose up -d
docker compose down # remove containers + networks (mantém volumes)
docker compose down -v # remove também volumes
docker compose up --build
docker compose logs -f backend
docker compose ps
docker compose exec backend shBoas práticas
- Use
.envpara variáveis sensíveis, referenciadas como${VAR}. - Volumes nomeados para dados persistentes; bind mounts só para dev.
- Múltiplos arquivos compose por ambiente, combinados com
-f:
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -dCLI avançado
Inspeção e monitoramento
docker ps # containers rodando
docker ps -a # todos, incluindo parados
docker inspect <container> # detalhes completos em JSON
docker stats # uso de CPU/RAM/rede/disco em tempo real
docker stats --no-stream # snapshot únicodocker inspect com filtro (Go templates):
docker inspect --format '{{.State.Status}}' <container>
docker inspect --format '{{.NetworkSettings.IPAddress}}' <container>
docker inspect --format '{{.Mounts}}' <container>
docker inspect --format '{{.Config.Env}}' <container>Colunas do docker stats:
| Coluna | Observação |
|---|---|
| CPU % | Pode passar de 100% (múltiplos cores) |
| MEM USAGE/LIMIT | Sem limite definido, "limit" = RAM total do host |
| NET I/O | Tráfego acumulado desde o start |
| BLOCK I/O | Leitura/escrita em disco acumulada |
| PIDS | Pico repentino pode indicar processo descontrolado |
Limitando recursos:
docker run --memory=512m --cpus=1 minha_appservices:
backend:
deploy:
resources:
limits:
cpus: "1"
memory: 512MLogs
docker logs -f <container>
docker logs --tail 100 <container>
docker logs --since 10m <container>Docker captura stdout/stderr do PID 1 do container via logging driver (default json-file). Se a app loga em arquivo, docker logs não mostra nada — sempre logar em stdout/stderr.
Limitar crescimento dos logs:
services:
backend:
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"Executando comandos em containers rodando
docker exec -it <container> sh
docker exec -it db psql -U user -d mydbexec roda um processo adicional num container já rodando — diferente de run, que cria um container novo. Falha se o container estiver parado.
Copiando arquivos
docker cp <container>:/app/log.txt ./log.txt
docker cp ./config.json <container>:/app/Parar e remover
docker stop <container> # SIGTERM, aguarda graceful shutdown (~10s), depois SIGKILL
docker kill <container> # SIGKILL imediato
docker rm <container> # remove (precisa estar parado, ou -f)Limpeza (prune)
docker container prune
docker image prune # só imagens "dangling" (sem tag)
docker image prune -a # todas as imagens não usadas por nenhum container
docker volume prune # cuidado: remove volumes não referenciados por nenhum container
docker network prune
docker system prune -a --volumes # remove tudo — cuidado
docker system df # resumo de espaço usado
docker system df -v # detalhadoSempre rodar
docker system df -vantes de um prune agressivo em produção.
Node.js: RAM constante em VPS pequena
Se o uso de RAM está constante (não crescente), geralmente não é memory leak — é o baseline normal (SO + cache de disco + heap do V8, que reserva memória generosa por padrão).
Diagnóstico:
free -h # used vs available/buff-cache
docker stats --no-stream # uso real por containerMitigação:
services:
api:
mem_limit: 400m
environment:
NODE_OPTIONS: "--max-old-space-size=350"
restart: unless-stoppedmem_limit: teto — se ultrapassar, OOM killer mata o processo (comrestart, reinicia limpo). Rede de segurança, não reduz consumo real.NODE_OPTIONS --max-old-space-size: limita o heap do V8 diretamente, reduz consumo real.- Configurar swap na VPS é recomendado como proteção adicional.
Registries
Docker Hub (padrão)
docker pull postgres:16 # sem registry explícito = Docker Hub
docker login
docker tag minha_app:1.0 seu_usuario/minha_app:1.0
docker push seu_usuario/minha_app:1.0GitHub Container Registry (GHCR)
docker login ghcr.io -u seu_usuario -p seu_token
docker tag minha_app:1.0 ghcr.io/seu_usuario/minha_app:1.0
docker push ghcr.io/seu_usuario/minha_app:1.0
docker pull ghcr.io/seu_usuario/minha_app:1.0Token precisa de escopo write:packages/read:packages. Em CI/CD (GitHub Actions), normalmente se usa o GITHUB_TOKEN automático.
Tags e versionamento
- Nunca depender só de
latestem produção (mutável). - Usar semver:
1.0.0,1.1.0. - Taggear com hash do commit para rastreabilidade:
ghcr.io/user/app:a1b2c3d.
docker build -t minha_app:1.2.0 -t minha_app:latest .
docker push minha_app:1.2.0
docker push minha_app:latestVerificando imagens
docker images
docker manifest inspect ghcr.io/seu_usuario/minha_app:1.0Multi-arquitetura
docker buildx build --platform linux/amd64,linux/arm64 -t seu_usuario/minha_app:1.0 --push .Evita incompatibilidade entre build local (ex: Mac ARM) e VPS (x86).
Fluxo típico de CI/CD
- Push no GitHub → Action builda a imagem.
- Tag com hash do commit +
latest. - Push para o GHCR.
- VPS faz
docker pullda nova tag e recria o container.
Otimização de imagens
docker history
docker history minha_app:1.0
docker history --no-trunc minha_app:1.0Mostra cada layer, comando e tamanho adicionado.
RUN apt-get installeRUN rm -rf /var/lib/apt/lists/*em comandos separados não reduz o tamanho — a limpeza só é efetiva no mesmoRUN.
dive
Ferramenta externa de inspeção visual, camada por camada.
dive minha_app:1.0Mostra:
- Wasted space: arquivos sobrescritos/removidos em layers posteriores (espaço não recuperado).
- Score de eficiência (0-100%).
- Navegação por layer.
Táticas de redução (por impacto)
- Imagem base menor:
node:20(~1GB) →node:20-slim(~200MB) →node:20-alpine(~150MB). Atenção: alpine usamuslem vez deglibc, pode ter incompatibilidade com libs nativas. - Multi-stage build — maior ganho isolado.
.dockerignorerigoroso.- Consolidar
RUNs relacionados. - Ordem de cache (
COPY package*.jsonantes deCOPY . .). - Evitar devDependencies em produção:
npm ci --omit=dev. - Limpar cache do package manager no mesmo
RUN:npm ci --omit=dev && npm cache clean --force.
Fluxo prático
- Build normal.
docker images→ tamanho total.dive minha_app:1.0→ localizar desperdício.- Ajustar Dockerfile.
- Rebuild e comparar.
Segurança
Scan de vulnerabilidades
docker scout quickview minha_app:1.0
docker scout cves minha_app:1.0Trivy (alternativa, bom para CI):
trivy image --severity HIGH,CRITICAL minha_app:1.0Rodar scan no CI antes do push, falhando o pipeline em CRITICAL sem fix disponível.
Secrets — nunca em ENV/Dockerfile
# ERRADO — fica gravado na layer, visível via `docker history`
ENV DATABASE_PASSWORD=minhasenha123Formas corretas:
Compose secrets:
services:
api:
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txtApp lê de /run/secrets/db_password.
Variáveis via .env + environment (não vira layer, mas aparece em docker inspect):
environment:
DATABASE_PASSWORD: ${DB_PASSWORD}BuildKit --secret (secrets durante o build, sem gravar em layer):
RUN --mount=type=secret,id=npm_token \
NPM_TOKEN=$(cat /run/secrets/npm_token) npm cidocker build --secret id=npm_token,src=./npm_token.txt .Vault/gerenciadores externos (HashiCorp Vault, AWS Secrets Manager) — para produção séria.
Rootless / não rodar como root
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuserdocker exec <container> whoami # deve mostrar appuserRootless Docker: o próprio daemon roda sem privilégios root no host (diferente de container rodando como non-root user). Configuração mais avançada.
Filesystem read-only
services:
api:
read_only: true
tmpfs:
- /tmpOutras práticas
services:
api:
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
security_opt:
- no-new-privileges:true.dockerignoreincluindo.env,.git, chaves SSH.- Manter imagens base atualizadas (CVEs corrigidas em patches).
- Assinar imagens (Docker Content Trust / cosign) contra adulteração no registry.
Prioridade prática para stack com dados sensíveis (ex: CPF/CNS em sistema de saúde com LGPD): secrets nunca em Dockerfile/ENV commitado,
USERnão-root, e scan de vulnerabilidades no CI.
Orquestração
Por que ir além do Compose
Compose roda bem numa única máquina. Orquestração é necessária para:
- Múltiplos hosts
- Auto-healing (container morto sobe em outro nó automaticamente)
- Escala horizontal com balanceamento
- Rolling updates sem downtime
- Service discovery entre máquinas
Docker Swarm
Orquestrador nativo do Docker, reusa a sintaxe do Compose.
Inicializando cluster:
docker swarm init --advertise-addr <IP_DO_MANAGER>
docker swarm join --token ... <IP_MANAGER>:2377 # rodar nos workersConceitos:
| Termo | Descrição |
|---|---|
| Node | Máquina no cluster (manager ou worker) |
| Manager | Decide onde rodar containers, mantém estado |
| Worker | Executa containers |
| Service | Unidade de deploy (imagem, réplicas, portas) |
| Stack | Conjunto de services definido em compose, deployado como unidade |
Deploy:
services:
api:
image: ghcr.io/seu_usuario/minha_api:1.0
deploy:
replicas: 3
restart_policy:
condition: on-failure
resources:
limits:
memory: 400M
update_config:
parallelism: 1
delay: 10s
order: start-first # zero downtime
networks:
- minha_rede
networks:
minha_rede:
driver: overlay # rede cross-hostdocker stack deploy -c docker-compose.yml minha_stack
docker stack services minha_stack
docker service scale minha_stack_api=5
docker service logs minha_stack_apiKubernetes
Mais poderoso e flexível, complexidade maior.
Principais objetos:
| Objeto | Função |
|---|---|
| Pod | Menor unidade — um ou mais containers com rede/storage compartilhados |
| Deployment | Gerencia réplicas de um Pod, rolling update, rollback |
| Service | IP/DNS estável para um conjunto de Pods |
| ConfigMap / Secret | Configuração e segredos como objetos do cluster |
| Ingress | Roteamento HTTP para dentro do cluster (papel similar ao Traefik) |
| Namespace | Isolamento lógico dentro do cluster |
Exemplo de Deployment + Service:
apiVersion: apps/v1
kind: Deployment
metadata:
name: minha-api
spec:
replicas: 3
selector:
matchLabels:
app: minha-api
template:
metadata:
labels:
app: minha-api
spec:
containers:
- name: api
image: ghcr.io/seu_usuario/minha_api:1.0
ports:
- containerPort: 3000
resources:
limits:
memory: "400Mi"
cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
name: minha-api-service
spec:
selector:
app: minha-api
ports:
- port: 80
targetPort: 3000kubectl apply -f deployment.yaml
kubectl get pods
kubectl scale deployment minha-api --replicas=5
kubectl rollout status deployment minha-apiSwarm vs Kubernetes
| Swarm | Kubernetes | |
|---|---|---|
| Curva de aprendizado | Baixa (reusa Compose) | Alta |
| Overhead de infra | Leve | Pesado |
| Ideal para | Poucas VPS, projetos pequenos/médios | Escala grande, cloud-native |
| Ecossistema | Menor | Enorme (Helm, operators, service mesh) |
Para VPS única/poucas VPS com Traefik já configurado, Swarm resolve a maior parte das necessidades sem o overhead operacional do Kubernetes. Kubernetes vale pela relevância de mercado/carreira.
Testando localmente
Swarm:
# Single-node, mesma máquina
docker swarm init
docker stack deploy -c docker-compose.yml minha_stackPara multi-node real: Docker-in-Docker (DinD) simulando "máquinas", ou VMs com Vagrant + VirtualBox/Multipass (cenário mais realista).
Kubernetes:
| Ferramenta | Multi-node | Leveza | Indicado para |
|---|---|---|---|
| Minikube | Limitado | Média | Primeiro contato, tem dashboard visual |
| kind | Sim, fácil | Leve | Testar cenários reais, roda nodes como containers Docker |
| k3d | Sim, fácil | Muito leve | Máquina com poucos recursos |
| Docker Desktop K8s | Não | Leve | Testes rápidos, single-node |
# kind - cluster multi-node
kind create cluster --config kind-config.yaml
kubectl get nodes# kind-config.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
- role: workerRecomendado começar com
kindpor já ter domínio de Docker — curva de entrada baixa, simula multi-node em segundos.
CI/CD com Docker
Objetivo
Automatizar build, teste e publicação de imagens a cada push/merge, eliminando build manual e garantindo que a imagem em produção corresponda exatamente ao código versionado.
Pipeline típico (GitHub Actions + GHCR)
# .github/workflows/deploy.yml
name: Build and Deploy
on:
push:
branches: [main]
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Login no GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extrair metadados (tags)
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=sha,prefix=
type=raw,value=latest
- name: Build e push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Deploy na VPS via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
script: |
cd /caminho/do/projeto
docker compose pull
docker compose up -dPontos-chave:
GITHUB_TOKENautomático dispensa criar um PAT manual — já tem permissão depackages: writequando declarado no job.docker/metadata-actiongera tags automaticamente (hash do commit +latest), evitando hardcode.cache-from/cache-to type=ghareaproveita cache de build entre execuções do workflow, acelerando builds subsequentes (equivalente ao cache de layers, mas persistido no GitHub Actions).- O deploy via SSH faz
docker compose pull(baixa a imagem nova do GHCR) +up -d(recria containers com a imagem atualizada) — desde que odocker-compose.ymlna VPS referencie a taglatestou a tag correta.
Testes antes do build (etapa recomendada)
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run test
- run: npm run lint
build-and-push:
needs: test # só builda se os testes passarem
runs-on: ubuntu-latest
# ...Scan de segurança no pipeline
Integrando o Trivy (visto no ponto de Segurança) direto no CI, falhando o pipeline em vulnerabilidades críticas:
- name: Scan de vulnerabilidades
uses: aquasecurity/trivy-action@master
with:
image-ref: ghcr.io/${{ github.repository }}:latest
severity: CRITICAL,HIGH
exit-code: 1 # falha o pipeline se encontrarEstratégias de deploy
| Estratégia | Como funciona | Downtime |
|---|---|---|
| Recreate | Para o container antigo, sobe o novo | Sim, breve |
| Rolling update | Sobe réplicas novas gradualmente, derruba as antigas | Não (se houver múltiplas réplicas) |
| Blue-green | Ambiente novo ("green") sobe em paralelo ao atual ("blue"); tráfego é trocado quando validado | Não |
Em Compose puro (single-node, sem Swarm), rolling update real exige múltiplas réplicas + um proxy (Traefik) fazendo health check antes de rotear tráfego. Com Swarm, isso já é nativo via update_config (visto no ponto de Orquestração).
Blue-green simplificado com Traefik + Compose, usando labels dinâmicas para trocar qual serviço recebe tráfego:
services:
api-blue:
image: ghcr.io/seu_usuario/api:1.0
labels:
- "traefik.enable=true"
- "traefik.http.routers.api.rule=Host(`api.exemplo.com`)"
api-green:
image: ghcr.io/seu_usuario/api:1.1
labels:
- "traefik.enable=false" # ainda não recebe tráfegoApós validar api-green manualmente (ou via smoke test automatizado no pipeline), inverte-se os labels e recarrega o Traefik — o tráfego passa a ir para a nova versão sem downtime.
Rollback
Como as imagens são versionadas por hash de commit, rollback é simples:
docker compose pull # ou especificar tag antiga manualmente
docker tag ghcr.io/user/api:a1b2c3d ghcr.io/user/api:latest
docker compose up -dRedes avançadas
Overlay networks — comunicação multi-host
Vista brevemente no ponto de Orquestração: uma rede overlay permite que containers em máquinas físicas diferentes se comuniquem como se estivessem na mesma rede local — essencial em Swarm/Kubernetes, onde os containers de um mesmo serviço podem estar espalhados por vários nós.
docker network create --driver overlay --attachable minha_rede_overlay--attachablepermite que containers standalone (fora de umservicedo Swarm) também se conectem manualmente à rede — útil para debug.- Sem Swarm ativo, o driver
overlaynão funciona — é exclusivo de cluster.
Como funciona por baixo: o tráfego entre nós é encapsulado (VXLAN) e trafega pela rede física entre os hosts do cluster. Isso exige que as portas de gerenciamento do Swarm (2377/tcp, 7946/tcp+udp, 4789/udp) estejam liberadas entre os nós.
Reverse proxy com Traefik na frente de múltiplos serviços
Padrão comum (e já usado no seu setup): um único ponto de entrada (Traefik) roteia requisições HTTP/HTTPS para múltiplos containers/serviços internos, baseado em hostname ou path — sem expor porta de cada serviço individualmente.
services:
traefik:
image: traefik:v3.0
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.le.acme.httpchallenge=true"
- "--certificatesresolvers.le.acme.httpchallenge.entrypoint=web"
- "--certificatesresolvers.le.acme.email=seu@email.com"
- "--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json"
ports:
- "80:80"
- "443:443"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "letsencrypt:/letsencrypt"
networks:
- proxy
api:
image: ghcr.io/seu_usuario/api:1.0
labels:
- "traefik.enable=true"
- "traefik.http.routers.api.rule=Host(`api.exemplo.com`)"
- "traefik.http.routers.api.entrypoints=websecure"
- "traefik.http.routers.api.tls.certresolver=le"
- "traefik.http.services.api.loadbalancer.server.port=3000"
networks:
- proxy
# sem "ports" — só acessível via Traefik
bot:
image: ghcr.io/seu_usuario/bot:1.0
networks:
- proxy
labels:
- "traefik.enable=false" # bot Discord não expõe HTTP
networks:
proxy:
external: true
volumes:
letsencrypt:Pontos-chave:
exposedbydefault=false— só containers comtraefik.enable=trueexplícito são roteados; evita expor algo sem querer.- Certificado TLS automático via Let's Encrypt (
certresolver), renovado automaticamente pelo Traefik. - Nenhum serviço além do Traefik publica
ports— todo tráfego externo entra por um único ponto, reduzindo superfície de ataque. docker.sockmontado como read-only (:ro) — Traefik só precisa ler eventos do Docker (containers subindo/descendo) para atualizar rotas dinamicamente, nunca escrever.
Debug de conectividade entre containers
docker network inspect minha_rede # ver quais containers estão conectados
docker exec -it backend ping db # testar resolução DNS + conectividade
docker exec -it backend nslookup db # confirmar resolução DNS interna
docker exec -it backend curl -v http://db:5432 # testar porta específicaProblema comum: dois containers na mesma docker-compose.yml mas em networks diferentes (declaradas explicitamente) não se enxergam, mesmo estando no mesmo arquivo — a network precisa ser compartilhada explicitamente entre os serviços.
Múltiplas networks por serviço (segmentação)
Útil para isolar, por exemplo, o banco de dados de tudo que não seja o backend, mesmo dentro do mesmo compose:
services:
backend:
networks:
- frontend_net
- backend_net
db:
networks:
- backend_net # não participa da frontend_net
frontend:
networks:
- frontend_net # não enxerga o db
networks:
frontend_net:
backend_net:backend transita entre as duas redes; frontend e db nunca se enxergam diretamente.
Observabilidade
Por que importa em produção
Sem observabilidade, problemas em produção (memory leak, latência, container reiniciando em loop) só são percebidos quando o usuário reclama. O objetivo é ter visibilidade antes disso.
Três pilares: logs, métricas e health checks.
Logging centralizado
Por padrão, docker logs só funciona enquanto o container existe — se ele for removido, o histórico de log some junto (a menos que o driver json-file ainda tenha o arquivo, o que também é apagado na remoção).
Em produção, o ideal é enviar logs para fora do container, para um sistema central:
Opção simples: driver de log nativo
services:
api:
logging:
driver: "syslog"
options:
syslog-address: "udp://logs.exemplo.com:514"Opção mais robusta: stack de logging (ex: Loki + Grafana, leve e comum em setups pequenos/médios)
services:
api:
logging:
driver: loki
options:
loki-url: "http://loki:3100/loki/api/v1/push"
loki:
image: grafana/loki:latest
ports:
- "3100:3100"
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
depends_on:
- lokiLoki indexa logs por labels (não faz full-text index pesado como Elasticsearch), sendo mais leve — boa opção para VPS com recursos limitados.
Alternativa consolidada (ELK/EFK): Elasticsearch + Logstash/Fluentd + Kibana — mais poderoso, porém mais pesado em recursos; geralmente reservado para setups com mais capacidade de infra.
Métricas — Prometheus + cAdvisor
cAdvisor coleta métricas de containers (CPU, memória, rede, disco) automaticamente, sem precisar instrumentar a aplicação:
services:
cadvisor:
image: gcr.io/cadvisor/cadvisor:latest
ports:
- "8080:8080"
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
- /dev/disk/:/dev/disk:ro
privileged: true
prometheus:
image: prom/prometheus:latest
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
depends_on:
- prometheusprometheus.yml (configura de onde o Prometheus coleta métricas):
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'cadvisor'
static_configs:
- targets: ['cadvisor:8080']Fluxo: cAdvisor expõe métricas → Prometheus coleta e armazena (time-series) → Grafana consome o Prometheus como fonte de dados e exibe em dashboards.
Métricas de aplicação (além de infraestrutura): para métricas de negócio/aplicação (ex: quantidade de requisições, latência por endpoint na API NestJS), a aplicação expõe um endpoint /metrics no formato Prometheus, usando uma lib como prom-client (Node.js):
// exemplo NestJS
import { Controller, Get } from '@nestjs/common';
import { register } from 'prom-client';
@Controller('metrics')
export class MetricsController {
@Get()
async getMetrics() {
return register.metrics();
}
}Adiciona-se esse endpoint como mais um target no prometheus.yml.
Health checks
Além de métricas passivas, health checks ativos permitem que o próprio Docker/orquestrador saiba se o container está realmente saudável (não só "rodando"):
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1Ou via Compose:
services:
api:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 5s
retries: 3docker ps # coluna STATUS mostra "healthy"/"unhealthy"Isso é o que permite depends_on: condition: service_healthy (visto no ponto de Compose) e o auto-healing do Swarm/Kubernetes funcionarem de verdade — sem healthcheck, o orquestrador só sabe se o processo está rodando, não se está respondendo corretamente.
Alertas
Com Prometheus + Alertmanager, é possível disparar notificações (Discord, Slack, e-mail) quando uma métrica ultrapassa um limite:
# regra de exemplo no Prometheus
groups:
- name: alertas
rules:
- alert: MemoriaAlta
expr: container_memory_usage_bytes{name="api"} > 400000000
for: 5m
annotations:
summary: "Container api com uso de memória acima de 400MB por 5 minutos"Dado que um dos seus projetos é um bot Discord, um webhook do Alertmanager para um canal do Discord é uma integração natural para notificações de infraestrutura.
Resumo do fluxo de observabilidade completo
Containers (logs + métricas)
↓ ↓
Loki/ELK cAdvisor + Prometheus
↓ ↓
Grafana (dashboards unificados)
↓
Alertmanager (notificações)Para o seu contexto (VPS pequena/média, Traefik já configurado): a combinação cAdvisor + Prometheus + Grafana + Loki é leve o suficiente para rodar junto dos próprios projetos, sem exigir infraestrutura dedicada — é o setup mais comum em ambientes de porte pequeno/médio que ainda assim querem observabilidade real.
Dúvidas Esclarecidas
Registro de perguntas específicas feitas durante o estudo de Docker, com as respectivas respostas.
Containers são mais rápidos que VMs por quê? E as camadas read-only das imagens são compartilhadas entre containers, ou cada um cria uma réplica?
Containers são mais rápidos porque compartilham o kernel do host — não bootam um SO completo como uma VM, só iniciam o processo isolado via namespaces e cgroups.
Sobre o compartilhamento: sim, existe. Imagens são feitas de camadas read-only empilhadas. Quando múltiplos containers usam a mesma imagem, essas camadas ficam armazenadas uma única vez no disco do host e são compartilhadas entre eles. Cada container ganha apenas uma writable layer própria e fina por cima, para as mudanças em runtime — via union filesystem (overlay2) com copy-on-write.
No caso de 4 containers Postgres: os binários/libs do Postgres (camada read-only) ficam em uma cópia só, compartilhada pelos 4. Os dados de cada banco, que crescem com o uso, normalmente ficam em volumes separados — não na writable layer.
Numa conta básica onde a imagem consome 100MB e cada writable layer consumiria 30MB: com 2 containers, o consumo real é 160MB e não 260MB?
Sim, exatamente:
Imagem (read-only, compartilhada): 100MB — uma única vez
Container 1: +30MB (writable layer própria)
Container 2: +30MB (writable layer própria)
Total: 100 + 30 + 30 = 160MBSem compartilhamento, seriam 2 cópias completas (2×100MB) + 2 writable layers (2×30MB) = 260MB.
Isso vale para disco. Em RAM, não há esse mesmo compartilhamento — cada container carrega seu próprio processo em execução, mesmo que os binários venham da mesma imagem no disco.
É possível limitar a network para acesso local — por exemplo, o banco de dados só ser acessível pelo backend, e eu em localhost só conseguir acessar o backend?
Sim. Basta não publicar a porta (-p) do banco e deixar os dois containers na mesma network.
docker network create minha_rede
docker run -d --name db --network minha_rede -v pg_data:/var/lib/postgresql/data postgres
docker run -d --name backend --network minha_rede -p 3000:3000 minha_app_backenddbebackendna mesma network → backend acessa o banco pelo hostnamedb.- Só
backendtem-p 3000:3000→ só ele fica acessível vialocalhost. dbsem-p→ só alcançável de dentro da network, nunca vialocalhost.
O -p é o único mecanismo que expõe algo ao host/localhost; sem ele, o container só existe "dentro" da rede Docker.
Tenho 2 docker compose rodando numa VPS de 2GB e o uso de RAM sempre fica em 1900MB. Tem algo a ver? O que posso fazer? Um limitador de RAM resolve? Um é bot do Discord, outro é API.
Primeiro, é preciso diferenciar RAM realmente usada de cache de disco: rode free -h e compare used com available/buff-cache — o Linux usa RAM livre para cache, que é liberado automaticamente se necessário.
Se for consumo real, o próximo passo é docker stats --no-stream ao longo do tempo: se o valor cresce continuamente, é sinal de memory leak. Se fica constante, é apenas o baseline normal das aplicações (Node.js por padrão já reserva um heap generoso no V8, mesmo sem estar "precisando" de fato).
No caso descrito, o uso era constante — ou seja, não é leak, é baseline normal de bot Discord + API rodando numa VPS de 2GB.
Sobre o limitador de RAM: ele não resolve a causa, mas evita o pior cenário. mem_limit/--memory define um teto — se ultrapassado, o kernel mata o processo (OOM killer) e, com restart: unless-stopped, ele reinicia limpo. Sem limite, uma aplicação com leak pode consumir RAM até travar o host inteiro.
Para reduzir o consumo real (não só limitar), usar NODE_OPTIONS junto com mem_limit:
services:
bot:
mem_limit: 300m
environment:
NODE_OPTIONS: "--max-old-space-size=256"
restart: unless-stopped
api:
mem_limit: 400m
environment:
NODE_OPTIONS: "--max-old-space-size=350"
restart: unless-stopped--max-old-space-size limita o heap do V8 diretamente, reduzindo consumo real — diferente do mem_limit, que só define um teto de kill.
Também vale configurar swap na VPS como proteção adicional, já que 2GB sem swap deixa a margem apertada para picos.
Dê um exemplo de multi-stage build em NestJS
# ---------- Stage 1: dependencies + build ----------
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
COPY prisma ./prisma/
RUN npm ci
RUN npx prisma generate
COPY . .
RUN npm run build
RUN npm prune --omit=dev
# ---------- Stage 2: imagem final de produção ----------
FROM node:20-alpine AS production
WORKDIR /app
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
ENV NODE_ENV=production
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/prisma ./prisma
# Se usar output customizado do Prisma (ex: src/generated/prisma):
# COPY --from=builder /app/src/generated ./src/generated
USER appuser
EXPOSE 3000
CMD ["node", "dist/main.js"]Pontos-chave:
- Stage
builder: tem compilador, devDependencies e Prisma CLI — só existe durante o build. npm prune --omit=dev: remove devDependencies donode_modulesainda dentro do builder, antes de copiar para o stage final.prisma generate: precisa rodar no builder porque gera código usado no build/runtime.COPY --from=builder: copia só os artefatos necessários, ignorando compilador, cache npm e source TS bruto.USER appuser: roda como usuário não-root no container final.
Resultado típico: de ~1GB para ~150-250MB.
Quero aprender mais sobre Swarm e Kubernetes — como testar localmente? É possível?
Sim, ambos dão para testar localmente sem gastar com VPS/cloud.
Swarm:
- Single-node (mesma máquina, mais simples):
docker swarm init
docker stack deploy -c docker-compose.yml minha_stack- Multi-node real: simular com Docker-in-Docker (DinD) ou, de forma mais prática, VMs com Vagrant + VirtualBox/Multipass — cenário mais realista, "como se fossem várias VPS".
Kubernetes — existem ferramentas próprias para isso:
| Ferramenta | Multi-node | Leveza | Indicado para |
|---|---|---|---|
| Minikube | Limitado | Média | Primeiro contato, tem dashboard visual |
| kind | Sim, fácil | Leve | Roda nodes como containers Docker, simula cluster real em segundos |
| k3d | Sim, fácil | Muito leve | Máquina com poucos recursos |
| Docker Desktop K8s | Não | Leve | Testes rápidos, single-node |
kind create cluster --config kind-config.yaml
kubectl get nodes# kind-config.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
- role: workerRecomendação: começar com kind, já que há domínio prévio de Docker — curva de entrada baixa, simula cluster multi-node de verdade em segundos, incluindo teste de Ingress (papel parecido com o Traefik) e deployments com múltiplas réplicas.