A maioria dos servidores em produção que "parou do nada" tinha uma coisa em comum: Docker sem healthchecks. Docker Compose é frequentemente descartado como ferramenta de desenvolvimento — e essa percepção leva equipes a adotar Kubernetes antes de precisar, ou a operar Compose em produção da mesma forma que testam localmente, sem as configurações que fazem a diferença entre um servidor que aguenta e um que cai silenciosamente.
Resumo rápido: Docker Compose é uma escolha legítima para produção em operações com até dezenas de serviços num único servidor ou em poucos servidores. A diferença entre hobby e operação séria está em configurações específicas: restart policies, healthchecks, volumes nomeados, secrets fora do repositório, reverse proxy com TLS, backup automatizado e logs centralizados. Nenhum desses itens é complexo — mas todos são ignorados com frequência.

Por que Docker Compose ainda é relevante em produção
Antes de entrar no checklist, vale estabelecer em que contexto o Compose faz sentido em produção.
Compose é adequado quando você tem um conjunto de serviços que rodam no mesmo servidor (ou em poucos servidores com deploy independente), um time pequeno sem necessidade de isolamento por namespace, e cargas que não exigem autoscaling horizontal automático entre múltiplos nodes.
Nesses cenários — que representam boa parte das operações reais — Compose entrega simplicidade operacional que Kubernetes nunca vai entregar. O arquivo docker-compose.yml é legível por qualquer pessoa que entenda Docker. O deploy é um único comando. O rollback também.
O problema não é o Compose. O problema é rodar Compose em produção sem as configurações que transformam um ambiente de desenvolvimento funcional numa operação que resiste a falhas, escala dentro dos limites do servidor e pode ser auditada quando algo der errado.
Item 1: Restart Policy — o mínimo que todo serviço precisa
A configuração mais simples e mais ignorada. Sem restart: unless-stopped, um container que para por qualquer motivo fica parado até alguém notar.
services:
api:
image: minha-api:latest
restart: unless-stopped
Use unless-stopped em vez de always para manter controle: containers parados manualmente não sobem sozinhos, mas containers que pararam por erro reiniciam automaticamente. Isso evita que uma parada planejada para manutenção vire reinicialização em loop.
Para serviços críticos onde você quer reinicialização agressiva mesmo após parada manual, always faz sentido — mas documente a escolha. Nos deploys que o departamento de DevOps da MaxVision entrega, essa decisão vai direto para o runbook de cada serviço.
Item 2: Healthchecks — o que separa "container rodando" de "serviço funcionando"
Um container pode estar "up" e o processo dentro dele pode estar travado, em loop de erro, ou respondendo com 500 para tudo. O Docker não sabe a diferença sem um healthcheck.
services:
api:
image: minha-api:latest
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
O start_period é crucial para serviços que demoram para inicializar (JVM, conexões com banco na inicialização). Sem ele, o Docker começa a contar falhas antes do serviço estar pronto e pode reiniciá-lo desnecessariamente.
Healthchecks também habilitam a diretiva depends_on com condição service_healthy, permitindo que serviços dependentes só subam quando suas dependências estiverem prontas de fato — não apenas "iniciadas".
Item 3: Volumes Nomeados — dados que sobrevivem ao container
Dados em containers sem volumes desaparecem quando o container é removido. Para banco de dados, uploads, logs persistentes e qualquer dado que precisa sobreviver a um deploy, volumes nomeados são obrigatórios.
services:
postgres:
image: postgres:16
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
volumes:
postgres_data:
driver: local
Volumes nomeados são gerenciados pelo Docker e sobrevivem a docker-compose down. Bind mounts (mapeamento direto de diretório do host) são aceitáveis para configurações e arquivos de código, mas para dados de aplicação use volumes nomeados: eles são portáveis, têm backup mais simples e não criam dependência de path absoluto no host.
Item 4: Secrets fora do repositório — o erro mais caro
Secrets no docker-compose.yml comprometido no repositório é um dos vetores mais comuns de comprometimento de credenciais. O padrão correto usa variáveis de ambiente via arquivo .env que não entra no repositório, ou o mecanismo nativo de secrets do Docker.
O .env precisa obrigatoriamente estar no .gitignore. O repositório deve conter um .env.example com as variáveis listadas mas sem valores reais:
# .env.example (entra no git)
POSTGRES_PASSWORD=
JWT_SECRET=
API_KEY=
# .env (nunca entra no git — .gitignore obrigatório)
POSTGRES_PASSWORD=s3nh@_real_aqui
JWT_SECRET=jwt_secret_real
API_KEY=chave_real
Para ambientes de produção com múltiplos operadores, o Docker Secrets é mais seguro: os valores ficam criptografados no swarm state e não aparecem em docker inspect. Em Compose puro (sem Swarm), a abordagem com .env é aceitável desde que o arquivo nunca vá ao repositório e o servidor tenha as permissões corretas (apenas root e o usuário da aplicação lêem o arquivo).
Item 5: Reverse Proxy com TLS — Traefik ou Caddy
Expor serviços diretamente na porta 80/443 sem reverse proxy é uma combinação de problemas: sem roteamento por hostname, sem SSL automático, sem rate limiting centralizado, sem logging de acesso unificado.
Traefik e Caddy são as duas escolhas mais comuns para Compose. O Caddy tem a curva de aprendizado menor e configura SSL automático via Let's Encrypt com o arquivo Caddyfile mais conciso que existe. O Traefik tem integração nativa com labels do Docker, o que facilita adicionar serviços sem editar configuração central.
Exemplo minimalista com Caddy:
services:
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
api:
image: minha-api:latest
restart: unless-stopped
# sem ports expostos — apenas Caddy acessa
volumes:
caddy_data:
caddy_config:
O Caddyfile:
api.exemplo.com {
reverse_proxy api:3000
}
Isso cobre renovação automática de SSL, redirect HTTP→HTTPS e headers de segurança básicos por padrão. O Let's Encrypt faz a emissão e renovação de certificados sem intervenção manual.

Item 6: Backup com Restic — o que salva quando tudo mais falha
Backup não é opcional em produção. Um servidor comprometido, um docker volume rm acidental, uma falha de disco — tudo isso é questão de quando, não de se.
Restic é a ferramenta de backup mais sólida para esse contexto: deduplicação, criptografia por padrão, suporte a múltiplos backends (S3, B2, SFTP, local) e verificação de integridade dos backups.
Para dados de banco de dados, o backup precisa ser do dump lógico, não apenas do volume — um volume com banco corrompido gera um backup de dado corrompido.
# backup postgres
docker exec postgres pg_dump -U postgres meudb > /backups/db_$(date +%Y%m%d_%H%M%S).sql
# backup com Restic
restic -r s3:s3.amazonaws.com/meu-bucket backup /backups \
--password-file /etc/restic-password
Automatize via cron e monitore os logs. Um backup que falha silenciosamente não é backup — é falsa segurança. Configure alertas para quando o job de backup não executa ou quando encontra erros.
Item 7: Logs centralizados — rastrear incidentes sem SSH em produção
docker logs no container individual não escala. Em produção, você precisa de logs acessíveis, indexados e com retenção definida.
A opção mais simples é o driver de log do Docker com rotação:
services:
api:
image: minha-api:latest
logging:
driver: "json-file"
options:
max-size: "50m"
max-file: "5"
Sem max-size, os logs de um serviço verboso podem consumir todo o disco do servidor — e esse é um modo de falha que acontece de forma gradual até que de repente para tudo.
Para centralização real, Loki + Promtail é a stack mais comum em Compose: o Promtail coleta logs de todos os containers e envia ao Loki; o Grafana visualiza e permite busca. É mais trabalho para configurar, mas entrega investigação de incidentes sem precisar acessar o servidor diretamente.
O checklist consolidado
| Item do Checklist | Risco se Ignorar | Como a MaxVision Resolve |
|---|---|---|
Restart policy unless-stopped | Container para e fica parado sem alerta | Padrao em todos os servicos desde o primeiro deploy |
| Healthcheck por servico | Container "up" com aplicacao travada, nenhum alerta | Endpoint /health padronizado + healthcheck no Compose |
| Volumes nomeados para dados | Dados perdidos em docker-compose down | Separacao entre config (bind) e dados (volume nomeado) |
Secrets via .env fora do git | Credenciais comprometidas no repositorio | .env.example no git, .env real no servidor, rotation documentada |
| Reverse proxy TLS (Caddy/Traefik) | Servico exposto sem SSL, sem roteamento, sem rate limit | Caddy ou Traefik padrao com SSL Let's Encrypt automatico |
| Backup Restic automatizado | Perda total de dados em falha de disco ou erro operacional | Job cron diario + verificacao semanal de integridade |
| Log rotation configurada | Disco cheio por logs, derruba servidor inteiro | max-size e max-file em todos os servicos |
| Usuario nao-root no container | Processo comprometido tem acesso root ao host | USER no Dockerfile, nunca rodar como root |
| Network isolation | Todos os containers se veem por padrao | Networks nomeadas separando camadas (web, app, db) |
| Monitoramento basico | Sem visibilidade de CPU/mem/disco antes do problema | Prometheus + Grafana ou UptimeRobot para alertas externos |
Os erros que derrubam sistemas em produção
chmod 777 em diretórios de dados. É o atalho para "resolver permissão agora" que cria superfície de ataque para qualquer processo que ganhe execução de código. Use o usuário correto no Dockerfile e defina permissões precisas.
Banco de dados sem autenticação na rede interna. Em Compose, serviços na mesma network se veem. Se o banco não tem senha, qualquer container comprometido lê todos os dados.
Secrets no repositório git. Uma credencial commitada no histórico do git permanece lá mesmo após remoção — qualquer clone feito antes inclui a credencial. A única solução após um commit de secret é rotacionar todas as credenciais expostas.
Deploy sem testar o rollback. O deploy em Compose é simples; o rollback também — mas precisa ter sido testado antes do incidente. Documente o procedimento de rollback e valide que ele funciona antes de precisar usá-lo sob pressão.
Sem monitoramento de disco. Containers geram logs, banco cresce, uploads acumulam. Disco cheio é um dos modos de falha mais comuns e mais evitáveis. Configure alertas para quando o disco ultrapassa determinado percentual.
Perguntas Frequentes
Docker Compose aguenta tráfego alto em produção?
Depende do que você considera "alto". Uma aplicação web com alguns milhares de usuários simultâneos pode rodar confortavelmente em Compose num servidor bem dimensionado. O Compose não é o gargalo — o servidor é. A limitação do Compose é que ele não distribui containers entre múltiplos servidores automaticamente. Se você precisa de múltiplos servidores, avalie K3s ou Kubernetes. Para um único servidor poderoso, Compose é suficiente para volumes consideráveis.
Traefik ou Caddy para iniciantes?
Caddy. O Caddyfile é mais conciso e o SSL automático funciona sem configuração adicional. Traefik tem mais poder de configuração e integração nativa com Docker labels, mas a curva de aprendizado é maior. Comece com Caddy, migre para Traefik quando você tiver necessidades específicas que o Caddy não resolve.
É necessário backup de volumes se eu uso banco gerenciado (RDS, Supabase)?
Se o banco é gerenciado, os backups do banco são responsabilidade do provedor — mas você ainda precisa de backup de outros volumes: uploads de usuários, arquivos gerados, configurações customizadas. Banco gerenciado não elimina a necessidade de backup, apenas muda o que você precisa fazer backup.
Como fazer deploy com zero downtime em Compose?
Compose não tem zero-downtime deploy nativo como Kubernetes. O padrão mais simples é usar Traefik com múltiplas réplicas do serviço: você sobe a nova versão como um segundo container enquanto o antigo ainda atende, e o Traefik distribui a carga automaticamente. O processo requer mais configuração do que um deploy K8s, mas é viável. Para serviços stateless, é a abordagem recomendada.
Preciso de root para rodar Docker em produção?
Não, e evitar root é recomendado. Docker tem suporte a modo rootless desde a versão 20.10. Rodar o daemon Docker como usuário não-privilegiado reduz a superfície de ataque em caso de escape de container. Em servidores dedicados, o modo rootless é a configuração que recomendamos.
Conclusão
Docker Compose em produção não é demérito — é pragmatismo. A ferramenta resolve a maioria dos problemas reais que startups e operações em crescimento enfrentam, desde que configurada com os itens que fazem a diferença entre um ambiente que aguenta e um que surpreende negativamente às 3h da manhã.
O checklist acima não é exaustivo, mas cobre os itens que aparecem com mais frequência em pós-mortems de incidentes: falta de healthcheck que mascara falhas, secrets expostos, disco cheio por log sem rotação, backup que nunca foi testado.
Montar essa stack corretamente — Caddy ou Traefik no lugar certo, backup Restic com verificação periódica, log rotation e hardening documentados — é o trabalho que o departamento de DevOps da MaxVision entrega com runbook de incidentes e SLA por escrito. Para dar o próximo passo, fale com a gente.