Skip to content

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.

ConceitoDescrição
ImagemTemplate read-only com tudo que a aplicação precisa
ContainerInstância em execução de uma imagem
DockerfileArquivo com instruções para construir uma imagem
Docker ComposeOrquestra múltiplos containers via docker-compose.yml
VolumePersistência de dados fora do ciclo de vida do container
NetworkComunicaçã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 = 160MB

Sem 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)

bash
docker volume create meu_volume
docker run -v meu_volume:/var/lib/postgresql/data postgres

Docker gerencia a localização física (/var/lib/docker/volumes/).

Bind mounts

bash
docker run -v /caminho/no/host:/var/lib/postgresql/data postgres

Você 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

bash
docker volume ls
docker volume inspect nome
docker volume rm nome
docker volume prune          # remove volumes não usados por nenhum container

Bancos 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)

bash
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_backend
  • db e backend na mesma network → backend acessa via hostname db.
  • backend publica porta (-p) → só ele é acessível via localhost.
  • db sem -p → só alcançável de dentro da network, nunca via localhost.

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çãoFunção
FROMImagem base (sempre primeira instrução)
WORKDIRDiretório de trabalho dentro do container
COPYCopia arquivos do host para a imagem
ADDComo COPY, mas extrai .tar e aceita URLs — evitar, preferir COPY
RUNExecuta comando durante o build (cria layer)
ENVVariável de ambiente disponível em runtime
EXPOSEDocumentação da porta usada (não publica de fato)
CMDComando padrão ao iniciar o container (só um por Dockerfile)
ENTRYPOINTProcesso fixo do container; CMD complementa como argumento padrão
ARGVariável disponível só durante o build
USERDefine usuário de execução (evitar root)
VOLUMEDeclara 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)

dockerfile
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/alpinenode:20 (~1GB) vs node:20-alpine (~150-200MB).

4. Não rodar como root

dockerfile
RUN adduser -D appuser
USER appuser

5. Consolidar RUNs relacionados

dockerfile
RUN apt-get update && apt-get install -y \
    curl git \
    && rm -rf /var/lib/apt/lists/*

Limpar em RUN separado não reduz o tamanho — a layer anterior já gravou os arquivos. A limpeza só é efetiva no mesmo RUN.

6. .dockerignore sempre presente

node_modules
.git
.env
dist
*.log

7. Tags específicas, nunca latest

dockerfile
FROM node:20.11-alpine   # reprodutível

Exemplo completo — NestJS + Prisma (multi-stage)

dockerfile
# ---------- 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

bash
docker build -t minha_app:1.0 .
docker run -p 3000:3000 minha_app:1.0

Docker Compose

Define e executa aplicações multi-container através de um único arquivo YAML.

Estrutura básica

yaml
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

ChaveFunção
imageUsa imagem pronta
buildConstrói a partir de Dockerfile
ports"host:container" — único jeito de expor ao localhost
environmentVariáveis de ambiente
env_fileCarrega variáveis de um arquivo .env
volumesNamed volumes ou bind mounts
depends_onOrdem de start (não garante "pronto")
restartno, always, on-failure, unless-stopped
commandSobrescreve 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):

yaml
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_healthy

Comandos principais

bash
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 sh

Boas práticas

  • Use .env para 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:
bash
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

CLI avançado

Inspeção e monitoramento

bash
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 único

docker inspect com filtro (Go templates):

bash
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:

ColunaObservação
CPU %Pode passar de 100% (múltiplos cores)
MEM USAGE/LIMITSem limite definido, "limit" = RAM total do host
NET I/OTráfego acumulado desde o start
BLOCK I/OLeitura/escrita em disco acumulada
PIDSPico repentino pode indicar processo descontrolado

Limitando recursos:

bash
docker run --memory=512m --cpus=1 minha_app
yaml
services:
  backend:
    deploy:
      resources:
        limits:
          cpus: "1"
          memory: 512M

Logs

bash
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:

yaml
services:
  backend:
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

Executando comandos em containers rodando

bash
docker exec -it <container> sh
docker exec -it db psql -U user -d mydb

exec roda um processo adicional num container já rodando — diferente de run, que cria um container novo. Falha se o container estiver parado.

Copiando arquivos

bash
docker cp <container>:/app/log.txt ./log.txt
docker cp ./config.json <container>:/app/

Parar e remover

bash
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)

bash
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         # detalhado

Sempre rodar docker system df -v antes 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:

bash
free -h                       # used vs available/buff-cache
docker stats --no-stream      # uso real por container

Mitigação:

yaml
services:
  api:
    mem_limit: 400m
    environment:
      NODE_OPTIONS: "--max-old-space-size=350"
    restart: unless-stopped
  • mem_limit: teto — se ultrapassar, OOM killer mata o processo (com restart, 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)

bash
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.0

GitHub Container Registry (GHCR)

bash
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.0

Token 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 latest em produção (mutável).
  • Usar semver: 1.0.0, 1.1.0.
  • Taggear com hash do commit para rastreabilidade: ghcr.io/user/app:a1b2c3d.
bash
docker build -t minha_app:1.2.0 -t minha_app:latest .
docker push minha_app:1.2.0
docker push minha_app:latest

Verificando imagens

bash
docker images
docker manifest inspect ghcr.io/seu_usuario/minha_app:1.0

Multi-arquitetura

bash
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

  1. Push no GitHub → Action builda a imagem.
  2. Tag com hash do commit + latest.
  3. Push para o GHCR.
  4. VPS faz docker pull da nova tag e recria o container.

Otimização de imagens

docker history

bash
docker history minha_app:1.0
docker history --no-trunc minha_app:1.0

Mostra cada layer, comando e tamanho adicionado.

RUN apt-get install e RUN rm -rf /var/lib/apt/lists/* em comandos separados não reduz o tamanho — a limpeza só é efetiva no mesmo RUN.

dive

Ferramenta externa de inspeção visual, camada por camada.

bash
dive minha_app:1.0

Mostra:

  • 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)

  1. Imagem base menor: node:20 (~1GB) → node:20-slim (~200MB) → node:20-alpine (~150MB). Atenção: alpine usa musl em vez de glibc, pode ter incompatibilidade com libs nativas.
  2. Multi-stage build — maior ganho isolado.
  3. .dockerignore rigoroso.
  4. Consolidar RUNs relacionados.
  5. Ordem de cache (COPY package*.json antes de COPY . .).
  6. Evitar devDependencies em produção: npm ci --omit=dev.
  7. Limpar cache do package manager no mesmo RUN: npm ci --omit=dev && npm cache clean --force.

Fluxo prático

  1. Build normal.
  2. docker images → tamanho total.
  3. dive minha_app:1.0 → localizar desperdício.
  4. Ajustar Dockerfile.
  5. Rebuild e comparar.

Segurança

Scan de vulnerabilidades

bash
docker scout quickview minha_app:1.0
docker scout cves minha_app:1.0

Trivy (alternativa, bom para CI):

bash
trivy image --severity HIGH,CRITICAL minha_app:1.0

Rodar scan no CI antes do push, falhando o pipeline em CRITICAL sem fix disponível.

Secrets — nunca em ENV/Dockerfile

dockerfile
# ERRADO — fica gravado na layer, visível via `docker history`
ENV DATABASE_PASSWORD=minhasenha123

Formas corretas:

Compose secrets:

yaml
services:
  api:
    secrets:
      - db_password
secrets:
  db_password:
    file: ./secrets/db_password.txt

App lê de /run/secrets/db_password.

Variáveis via .env + environment (não vira layer, mas aparece em docker inspect):

yaml
environment:
  DATABASE_PASSWORD: ${DB_PASSWORD}

BuildKit --secret (secrets durante o build, sem gravar em layer):

dockerfile
RUN --mount=type=secret,id=npm_token \
    NPM_TOKEN=$(cat /run/secrets/npm_token) npm ci
bash
docker 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

dockerfile
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
bash
docker exec <container> whoami   # deve mostrar appuser

Rootless 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

yaml
services:
  api:
    read_only: true
    tmpfs:
      - /tmp

Outras práticas

yaml
services:
  api:
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE
    security_opt:
      - no-new-privileges:true
  • .dockerignore incluindo .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, USER nã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:

bash
docker swarm init --advertise-addr <IP_DO_MANAGER>
docker swarm join --token ... <IP_MANAGER>:2377   # rodar nos workers

Conceitos:

TermoDescrição
NodeMáquina no cluster (manager ou worker)
ManagerDecide onde rodar containers, mantém estado
WorkerExecuta containers
ServiceUnidade de deploy (imagem, réplicas, portas)
StackConjunto de services definido em compose, deployado como unidade

Deploy:

yaml
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-host
bash
docker 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_api

Kubernetes

Mais poderoso e flexível, complexidade maior.

Principais objetos:

ObjetoFunção
PodMenor unidade — um ou mais containers com rede/storage compartilhados
DeploymentGerencia réplicas de um Pod, rolling update, rollback
ServiceIP/DNS estável para um conjunto de Pods
ConfigMap / SecretConfiguração e segredos como objetos do cluster
IngressRoteamento HTTP para dentro do cluster (papel similar ao Traefik)
NamespaceIsolamento lógico dentro do cluster

Exemplo de Deployment + Service:

yaml
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: 3000
bash
kubectl apply -f deployment.yaml
kubectl get pods
kubectl scale deployment minha-api --replicas=5
kubectl rollout status deployment minha-api

Swarm vs Kubernetes

SwarmKubernetes
Curva de aprendizadoBaixa (reusa Compose)Alta
Overhead de infraLevePesado
Ideal paraPoucas VPS, projetos pequenos/médiosEscala grande, cloud-native
EcossistemaMenorEnorme (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:

bash
# Single-node, mesma máquina
docker swarm init
docker stack deploy -c docker-compose.yml minha_stack

Para multi-node real: Docker-in-Docker (DinD) simulando "máquinas", ou VMs com Vagrant + VirtualBox/Multipass (cenário mais realista).

Kubernetes:

FerramentaMulti-nodeLevezaIndicado para
MinikubeLimitadoMédiaPrimeiro contato, tem dashboard visual
kindSim, fácilLeveTestar cenários reais, roda nodes como containers Docker
k3dSim, fácilMuito leveMáquina com poucos recursos
Docker Desktop K8sNãoLeveTestes rápidos, single-node
bash
# kind - cluster multi-node
kind create cluster --config kind-config.yaml
kubectl get nodes
yaml
# kind-config.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
  - role: control-plane
  - role: worker
  - role: worker

Recomendado começar com kind por 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)

yaml
# .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 -d

Pontos-chave:

  • GITHUB_TOKEN automático dispensa criar um PAT manual — já tem permissão de packages: write quando declarado no job.
  • docker/metadata-action gera tags automaticamente (hash do commit + latest), evitando hardcode.
  • cache-from/cache-to type=gha reaproveita 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 o docker-compose.yml na VPS referencie a tag latest ou a tag correta.

Testes antes do build (etapa recomendada)

yaml
  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:

yaml
      - 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 encontrar

Estratégias de deploy

EstratégiaComo funcionaDowntime
RecreatePara o container antigo, sobe o novoSim, breve
Rolling updateSobe réplicas novas gradualmente, derruba as antigasNão (se houver múltiplas réplicas)
Blue-greenAmbiente novo ("green") sobe em paralelo ao atual ("blue"); tráfego é trocado quando validadoNã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:

yaml
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áfego

Apó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:

bash
docker compose pull   # ou especificar tag antiga manualmente
docker tag ghcr.io/user/api:a1b2c3d ghcr.io/user/api:latest
docker compose up -d

Redes 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.

bash
docker network create --driver overlay --attachable minha_rede_overlay
  • --attachable permite que containers standalone (fora de um service do Swarm) também se conectem manualmente à rede — útil para debug.
  • Sem Swarm ativo, o driver overlay nã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.

yaml
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 com traefik.enable=true explí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.sock montado 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

bash
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ífica

Problema 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:

yaml
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

yaml
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)

yaml
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:
      - loki

Loki 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:

yaml
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:
      - prometheus

prometheus.yml (configura de onde o Prometheus coleta métricas):

yaml
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):

typescript
// 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"):

dockerfile
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1

Ou via Compose:

yaml
services:
  api:
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 5s
      retries: 3
bash
docker 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:

yaml
# 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 = 160MB

Sem 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.

bash
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_backend
  • db e backend na mesma network → backend acessa o banco pelo hostname db.
  • backend tem -p 3000:3000 → só ele fica acessível via localhost.
  • db sem -p → só alcançável de dentro da network, nunca via localhost.

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:

yaml
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

dockerfile
# ---------- 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 do node_modules ainda 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):
bash
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:

FerramentaMulti-nodeLevezaIndicado para
MinikubeLimitadoMédiaPrimeiro contato, tem dashboard visual
kindSim, fácilLeveRoda nodes como containers Docker, simula cluster real em segundos
k3dSim, fácilMuito leveMáquina com poucos recursos
Docker Desktop K8sNãoLeveTestes rápidos, single-node
bash
kind create cluster --config kind-config.yaml
kubectl get nodes
yaml
# kind-config.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
  - role: control-plane
  - role: worker
  - role: worker

Recomendaçã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.

Released under the License MIT. Versão 1.0.0