Executar aplicações multi-container em produção requer muito mais do que um arquivo docker-compose.yml básico. Embora o Docker Compose seja comumente associado ao desenvolvimento local, ele é uma ferramenta poderosa para fazer deploy de cargas de trabalho em produção em ambientes de host único e clusters pequenos quando configurado corretamente. Este guia leva você passo a passo pelo processo de fortalecer sua configuração Docker Compose para produção com health checks, políticas de reinício, limites de recursos, gestão de segredos, logging centralizado e proxy reverso Nginx com terminação SSL.

Pré-requisitos

  • Servidor Linux com Docker Engine 24+ e Docker Compose v2 instalados
  • Nome de domínio apontando para o IP público do seu servidor
  • Familiaridade básica com conceitos Docker (imagens, containers, volumes, redes)
  • Certificado SSL ou disposição para usar Let’s Encrypt com Certbot
  • Acesso SSH ao seu servidor de produção
  • Pelo menos 2 GB de RAM e 2 núcleos de CPU disponíveis para seu stack

Configuração do Docker Compose para Produção

Um docker-compose.yml pronto para produção difere significativamente de uma configuração de desenvolvimento. Você precisa de políticas de reinício explícitas, health checks, restrições de recursos e sistemas de arquivos somente leitura onde possível.

Políticas de Reinício

As políticas de reinício garantem que seus containers se recuperem automaticamente de crashes, OOM kills ou reinícios do host:

services:
  web:
    image: myapp:latest
    restart: unless-stopped
    deploy:
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
        window: 120s

Use unless-stopped para a maioria dos serviços. Isso reinicia containers após crashes e reinícios do host, mas respeita paradas manuais. Para serviços críticos que devem estar sempre em execução, use always. Evite no em produção — isso deixa containers com falha inativos até intervenção manual.

Health Checks

Os health checks permitem ao Docker monitorar se sua aplicação está realmente funcionando, não apenas se o processo está em execução:

services:
  api:
    image: myapp-api:latest
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

O start_period dá à sua aplicação tempo para inicializar antes que o Docker comece a contar as verificações falhadas. Configure-o mais alto que o tempo médio de inicialização da sua aplicação. Use endpoints de saúde específicos em vez de verificar se uma porta está aberta — um processo pode escutar em uma porta enquanto está em estado quebrado.

Limites de Recursos

Sem limites de recursos, um único container descontrolado pode consumir toda a memória do sistema e derrubar tudo:

services:
  api:
    image: myapp-api:latest
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 512M
        reservations:
          cpus: "0.25"
          memory: 128M

Configure limits para o máximo que um serviço deveria usar e reservations para garantir recursos mínimos. Monitore seus containers com docker stats por uma semana antes de definir os limites finais. Limites muito apertados causam OOM kills e desempenho degradado.

Sistemas de Arquivos Somente Leitura

Minimize a superfície de ataque executando containers com sistemas de arquivos raiz somente leitura:

services:
  api:
    image: myapp-api:latest
    read_only: true
    tmpfs:
      - /tmp
      - /var/run
    volumes:
      - app-data:/app/data

Isso impede que processos maliciosos escrevam no sistema de arquivos do container. Use tmpfs para diretórios que precisam de acesso temporário de escrita e volumes nomeados para dados persistentes.

Gestão de Segredos e Variáveis de Ambiente

Credenciais codificadas diretamente no seu arquivo compose são um risco de segurança. O Docker Compose suporta múltiplas abordagens para gestão de segredos.

Usando Arquivos .env

Crie um arquivo .env junto ao seu arquivo compose:

# .env - NUNCA faça commit deste arquivo
POSTGRES_PASSWORD=sua_senha_segura_aqui
API_SECRET_KEY=outro_valor_seguro
REDIS_PASSWORD=senha_redis_aqui

Referencie-os no seu arquivo compose:

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}

Adicione .env ao seu .gitignore imediatamente. Para pipelines CI/CD, injete variáveis de ambiente do seu gerenciador de segredos (GitHub Secrets, AWS Secrets Manager ou HashiCorp Vault).

Docker Secrets

Para um manuseio mais seguro, use Docker secrets que são montados como arquivos em vez de variáveis de ambiente:

secrets:
  db_password:
    file: ./secrets/db_password.txt

services:
  db:
    image: postgres:16
    secrets:
      - db_password
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password

Muitas imagens oficiais do Docker suportam a convenção do sufixo _FILE, lendo o segredo de um arquivo montado em vez de uma variável de ambiente. Isso mantém as credenciais fora da saída do docker inspect e das listagens de variáveis de ambiente do processo.

Proxy Reverso e SSL com Nginx

Expor portas de aplicação diretamente é inseguro e inflexível. Use Nginx como proxy reverso com terminação SSL:

services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - certbot-webroot:/var/www/certbot:ro
      - certbot-certs:/etc/letsencrypt:ro
    depends_on:
      api:
        condition: service_healthy
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/health"]
      interval: 30s
      timeout: 5s
      retries: 3

  certbot:
    image: certbot/certbot
    volumes:
      - certbot-webroot:/var/www/certbot
      - certbot-certs:/etc/letsencrypt
    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done'"

volumes:
  certbot-webroot:
  certbot-certs:

A configuração do Nginx deve fazer proxy para seus serviços internos:

upstream api_backend {
    server api:8080;
}

server {
    listen 80;
    server_name example.com;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    location / {
        proxy_pass http://api_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Observe que apenas o serviço Nginx expõe portas ao host. Todos os outros serviços se comunicam através da rede interna do Docker, reduzindo significativamente a superfície de ataque.

Logging e Monitoramento

Deploys em produção precisam de logging estruturado com rotação para evitar esgotamento de disco.

Logging JSON File com Rotação

services:
  api:
    image: myapp-api:latest
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "5"
        tag: "{{.Name}}"

Sem max-size e max-file, os logs do Docker crescem indefinidamente e podem encher seu disco. O driver json-file é o padrão e funciona bem para a maioria das configurações. Para ambientes multi-host, considere encaminhar logs para um sistema centralizado.

Encaminhamento para Sistemas Externos

Para observabilidade em produção, encaminhe logs para um serviço de agregação:

services:
  api:
    image: myapp-api:latest
    logging:
      driver: syslog
      options:
        syslog-address: "tcp://logserver:514"
        tag: "myapp-api"

As alternativas incluem os drivers fluentd e gelf para stacks ELK ou Graylog. Qualquer que seja sua escolha, sempre configure a rotação de logs no nível do daemon do Docker como rede de segurança em /etc/docker/daemon.json:

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}

Monitoramento Básico

Adicione monitoramento de containers com um stack leve:

services:
  prometheus:
    image: prom/prometheus:latest
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus-data:/prometheus
    deploy:
      resources:
        limits:
          memory: 256M

  cadvisor:
    image: gcr.io/cadvisor/cadvisor:latest
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:ro
      - /sys:/sys:ro
      - /var/lib/docker/:/var/lib/docker:ro
    deploy:
      resources:
        limits:
          memory: 128M

Comparação: Docker Compose vs Kubernetes vs Docker Swarm

RecursoDocker ComposeKubernetesDocker Swarm
Complexidade de configuraçãoBaixaAltaMédia
Suporte multi-hostNão (host único)Sim (cluster)Sim (cluster)
Auto-scalingNãoSim (HPA)Limitado
Descoberta de serviçosBaseado em DNSDNS + IngressBaseado em DNS
Gestão de segredosBaseada em arquivosIntegrada (etcd)Integrada
Atualizações progressivasManualIntegradaIntegrada
Health checksSimSim (liveness/readiness)Sim
Limites de recursosSimSim (requests/limits)Sim
Curva de aprendizadoSuaveÍngremeModerada
Ideal paraHost único, equipes pequenasLarga escala, multi-equipeClusters pequenos

Docker Compose é a escolha certa quando você faz deploy em um único servidor ou um número pequeno de servidores, sua equipe é pequena e você não precisa de auto-scaling. Muitos produtos SaaS bem-sucedidos rodam inteiramente em Docker Compose em produção.

Cenário Real

Você está fazendo deploy de uma aplicação web composta por uma API Node.js, um banco de dados PostgreSQL, um cache Redis e um proxy reverso Nginx. O servidor tem 4 GB de RAM e 2 núcleos de CPU.

Aqui está o arquivo compose completo de produção:

services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - certbot-certs:/etc/letsencrypt:ro
    depends_on:
      api:
        condition: service_healthy
    restart: unless-stopped
    deploy:
      resources:
        limits:
          cpus: "0.25"
          memory: 64M
    logging:
      driver: json-file
      options:
        max-size: "5m"
        max-file: "3"

  api:
    image: myapp-api:1.2.3
    env_file: .env
    secrets:
      - db_password
      - api_key
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped
    read_only: true
    tmpfs:
      - /tmp
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 30s
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 512M
        reservations:
          cpus: "0.25"
          memory: 128M
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "5"

  db:
    image: postgres:16-alpine
    volumes:
      - pgdata:/var/lib/postgresql/data
    secrets:
      - db_password
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
      POSTGRES_DB: myapp
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
    deploy:
      resources:
        limits:
          cpus: "0.5"
          memory: 1G
        reservations:
          memory: 256M
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

  redis:
    image: redis:7-alpine
    command: redis-server --requirepass ${REDIS_PASSWORD}
    volumes:
      - redisdata:/data
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
      interval: 10s
      timeout: 5s
      retries: 3
    deploy:
      resources:
        limits:
          cpus: "0.25"
          memory: 256M
    logging:
      driver: json-file
      options:
        max-size: "5m"
        max-file: "3"

secrets:
  db_password:
    file: ./secrets/db_password.txt
  api_key:
    file: ./secrets/api_key.txt

volumes:
  pgdata:
  redisdata:
  certbot-certs:

Faça o deploy com:

docker compose -f docker-compose.prod.yml up -d
docker compose -f docker-compose.prod.yml ps
docker compose -f docker-compose.prod.yml logs --tail=50

Esta configuração aloca aproximadamente 1.8 GB dos 4 GB disponíveis, deixando margem para o sistema operacional e picos de uso.

Armadilhas e Casos Especiais

A ordem de inicialização dos containers não garante disponibilidade. Usar depends_on sozinho apenas espera o container iniciar, não que o serviço interno esteja pronto. Sempre combine depends_on com condition: service_healthy e health checks adequados.

Containers órfãos se acumulam. Quando você remove um serviço do arquivo compose, o container antigo continua em execução. Sempre execute docker compose up -d --remove-orphans para limpar containers obsoletos.

Permissões de volumes causam falhas silenciosas. Se o seu container roda como usuário não-root, o volume montado pode não ser gravável. Pré-crie volumes com a propriedade correta ou use um padrão de container init.

Docker Compose não baixa novas imagens ao reconstruir por padrão. Executar docker compose up -d reutiliza imagens em cache. Use docker compose pull && docker compose up -d para garantir que você faz deploy da versão mais recente.

A expansão de variáveis do arquivo .env pode gerar conflitos. Docker Compose interpola ${VAR} no arquivo compose antes de passá-lo ao container. Se a configuração da sua aplicação usa caracteres $, escape-os com $$ no arquivo compose.

A rotação de logs deve ser configurada por serviço e globalmente. As opções de log no nível do serviço não sobrescrevem os padrões do daemon para outros containers. Configure ambos para cobertura completa.

Redes bridge têm problemas de resolução DNS com underscores. Nomes de serviço com underscores podem não resolver corretamente em versões antigas do Docker. Use hifens nos nomes de serviço para máxima compatibilidade.

Resumo

  • Use restart: unless-stopped e health checks em cada serviço de produção
  • Configure limites de memória e CPU com deploy.resources para evitar containers descontrolados
  • Gerencie segredos com Docker secrets ou arquivos .env — nunca codifique credenciais diretamente
  • Coloque Nginx como proxy reverso na frente dos serviços de aplicação com terminação SSL
  • Configure rotação de logs com max-size e max-file para evitar esgotamento de disco
  • Use depends_on com condition: service_healthy para ordenamento confiável de inicialização
  • Execute docker compose up -d --remove-orphans para limpar containers obsoletos em cada deploy
  • Docker Compose está pronto para produção em deploys de host único e equipes pequenas

Artigos Relacionados