Tutoriais de VPS
Como configurar o Docker Compose para produção em uma VPS (2026): healthcheck, limites de recursos, logs, segredos e checklist de rollback
O Docker Compose conseguir subir os serviços não significa que eles já estejam prontos para produção. Os problemas mais comuns em uma VPS são contêineres sem healthcheck, uso errado das políticas de restart, CPU/memória sem limite, logs enchendo o disco, segredos vazando pelo .env, falta de backup do Volume antes de atualizar e ausência de plano de rollback. Este artigo explica como reforçar o Docker Compose para produção seguindo a ordem real de uma entrada em produção.

O Docker Compose combina muito bem com uma VPS: uma máquina, alguns serviços, um compose.yml, e é bem mais limpo do que instalar Nginx, banco de dados e Redis na mão.
Mas o arquivo Compose de muita gente só alcança o "consegue iniciar":
services:
app:
image: myapp:latest
restart: always
ports:
- "3000:3000"
Isso basta em ambiente de teste, mas em produção é perigoso. Um contêiner cai e ninguém fica sabendo, os logs enchem o disco do sistema, o banco de dados não tem healthcheck, na atualização faz pull de latest direto, o .env está cheio de segredos e, quando dá problema, ninguém sabe como fazer rollback.
Este artigo não ensina como instalar o Docker. Para a instalação básica, veja:
Este artigo resolve um único problema: seu projeto Docker Compose já funciona, como transformá-lo em uma configuração mais adequada para um ambiente de produção em VPS?
Uma configuração de produção de Docker Compose em VPS razoavelmente confiável deve considerar pelo menos:
- Fixar as versões das imagens, sem usar
latestàs cegas; - Cada serviço crítico ter um
healthcheck; - Uma política de
restartrazoável; - Limites de CPU, memória, número de processos ou ulimit;
- Rotação de logs para evitar encher o disco;
- Não escrever os segredos diretamente no arquivo compose;
- Backup de volumes de dados, bancos de dados e diretórios de upload;
- Um plano de rollback antes de atualizar.
Se você só quer lembrar de uma frase: o Compose não é o problema; o problema é levar o Compose do ambiente de desenvolvimento para produção sem alterar nada.
Muitos tutoriais gostam de escrever:
image: nginx:latest
image: postgres:latest
image: redis:latest
Isso é prático para testes locais, mas em ambiente de produção em VPS causa incidentes com facilidade. Hoje você faz docker compose pull, amanhã a imagem upstream é atualizada, a versão maior muda, o comportamento da configuração muda, a compatibilidade do banco de dados muda e o site pode simplesmente não subir.
Uma forma mais estável é fixar a versão:
services:
web:
image: nginx:1.26-alpine
db:
image: postgres:16-alpine
redis:
image: redis:7.2-alpine
Para as imagens de aplicação também é recomendável usar tags explícitas:
image: ghcr.io/example/myapp:2026-06-14-1
Não transforme produção em "uma caixa surpresa a cada atualização".
Se você precisar usar latest, pelo menos faça o seguinte:
- Faça backup dos volumes de dados antes de atualizar;
- Suba primeiro em uma máquina de teste;
- Confirme os scripts de migração;
- Mantenha a imagem da versão anterior;
- Tenha comandos de rollback claros.
O depends_on no Compose só resolve a "ordem de inicialização"; não prova que o serviço está realmente disponível.
Por exemplo, o processo do contêiner do banco de dados inicia mas ainda não aceita conexões; o contêiner da aplicação inicia mas a migração do banco de dados falha; o Nginx inicia mas o backend do proxy reverso não responde. Sem healthcheck, você só vê o status do contêiner como running, mas os usuários já estão vendo o site fora do ar.
Uma aplicação web pode ser escrita assim:
services:
app:
image: ghcr.io/example/myapp:2026-06-14-1
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30s
PostgreSQL:
services:
db:
image: postgres:16-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 20s
MySQL / MariaDB:
services:
db:
image: mariadb:11
healthcheck:
test: ["CMD-SHELL", "mariadb-admin ping -h 127.0.0.1 -u root -p$${MARIADB_ROOT_PASSWORD} --silent"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
Redis:
services:
redis:
image: redis:7.2-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
Depois faça a aplicação esperar o banco de dados ficar healthy:
services:
app:
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
Atenção: condition: service_healthy depende do Compose V2. Tutoriais antigos podem não se aplicar.
Muitos templates escrevem uniformemente:
restart: always
Não está errado, mas não serve para todos os serviços.
Opções comuns:
| Política | Cenários adequados | Riscos |
|---|---|---|
no | Tarefas pontuais, scripts de migração | Não se recupera automaticamente se falhar |
on-failure | Serviços do tipo tarefa, workers | Não reinicia se a saída não for anormal |
unless-stopped | A maioria dos serviços web | Não inicia automaticamente com o restart do Docker após ser parado manualmente |
always | Serviços base que precisam ficar sempre ativos | Pode reiniciar mesmo após ser parado manualmente, fácil de causar mal-entendido |
Para projetos pequenos em uma VPS, normalmente se recomenda:
restart: unless-stopped
Para tarefas de migração e tarefas de inicialização, não coloque reinício automático. Caso contrário, uma migração que falhou pode ser executada repetidamente, piorando quanto mais você tenta corrigir.
Se o seu problema é que os serviços não se recuperam após reiniciar a VPS, veja este artigo:
Em uma VPS, o maior medo é um contêiner sair do controle, consumir toda a memória da máquina e, no fim, o OOM Killer matar processos aleatoriamente.
O Compose pode limitar recursos. O suporte a deploy.resources já variou historicamente conforme o cenário de Docker/Compose; hoje o Compose V2 é mais comum, mas você ainda precisa validar no seu próprio ambiente.
Exemplo:
services:
app:
image: ghcr.io/example/myapp:2026-06-14-1
deploy:
resources:
limits:
cpus: "1.0"
memory: 768M
reservations:
memory: 256M
Se não funcionar no seu ambiente, você pode usar a forma compatível:
services:
app:
mem_limit: 768m
mem_reservation: 256m
cpus: 1.0
Também é possível limitar o número de processos:
services:
app:
pids_limit: 256
E o número de arquivos abertos:
services:
nginx:
ulimits:
nofile:
soft: 65535
hard: 65535
Como configurar de forma razoável? Primeiro veja o tipo de serviço:
| Serviço | Abordagem de limitação sugerida |
|---|---|
| Nginx/Caddy | A CPU não precisa ser alta; nofile pode ser ampliado |
| Aplicações Node/Python | Priorize limitar a memória para evitar que um vazamento derrube a máquina |
| MySQL/PostgreSQL | Não é recomendável deixar pequeno demais; o banco de dados precisa de memória estável |
| Redis | É obrigatório configurar a política maxmemory do próprio Redis |
| Worker/filas | Limite CPU/memória para evitar que tarefas em lote saturem a VPS |
Se a sua VPS já está sofrendo OOM, veja:
Os logs json-file padrão do Docker, se não forem limitados, enchem o disco com o funcionamento prolongado.
Esse tipo de falha é muito comum:
- Loops de erro da aplicação;
- Crescimento descontrolado do access log do Nginx;
- Crawlers ou ataques CC martelando APIs;
- Contêineres imprimindo SQL ou logs de debug no stdout;
- Um mês depois, o disco do sistema de repente em 100%.
Adicione limites de log aos serviços:
services:
app:
logging:
driver: json-file
options:
max-size: "50m"
max-file: "3"
Recomenda-se para Nginx, aplicações e workers. Com os logs do banco de dados é preciso mais cuidado para evitar que informações de diagnóstico importantes sejam sobrescritas cedo demais, mas eles também não podem crescer sem limite.
Verifique o uso de logs do Docker:
docker system df
sudo du -h /var/lib/docker/containers | sort -h | tail -20
Se o Docker já encheu o disco, veja:
Não faça isso:
environment:
DATABASE_PASSWORD: my-secret-password
API_KEY: sk_live_xxx
O compose.yml facilmente vai parar no Git e até é copiado para tickets, chats em grupo e documentos.
Uma prática melhor é:
services:
app:
env_file:
- .env.production
Não faça commit do .env.production no Git:
.env
.env.*
!.env.example
Forneça um .env.example:
DATABASE_URL=postgres://user:password@db:5432/app
REDIS_URL=redis://redis:6379/0
APP_SECRET=change-me
Se você usa Docker Swarm ou um ambiente mais complexo, pode considerar Docker secrets. Mas para uma VPS única, gerenciar bem o .env, definir as permissões e criptografar os backups já evita a maioria dos incidentes.
Defina as permissões:
chmod 600 .env.production
Não escreva segredos na imagem durante a fase de build. Diferencie argumentos de build de variáveis de ambiente em tempo de execução.
Muitos arquivos Compose gostam de fazer assim:
ports:
- "5432:5432"
- "6379:6379"
- "3000:3000"
Isso expõe o banco de dados, o Redis e as portas da aplicação diretamente para a internet. Em produção normalmente não é necessário.
Uma forma mais segura é:
- Expor apenas Nginx/Caddy nas portas 80/443;
- Expor os serviços de aplicação apenas na rede interna do Docker;
- Não mapear bancos de dados e Redis para portas públicas do host;
- Colocar o painel administrativo atrás de VPN, túnel SSH ou lista de permissões.
Exemplo:
services:
proxy:
image: caddy:2
ports:
- "80:80"
- "443:443"
networks:
- frontend
app:
image: ghcr.io/example/myapp:2026-06-14-1
expose:
- "3000"
networks:
- frontend
- backend
db:
image: postgres:16-alpine
networks:
- backend
networks:
frontend:
backend:
internal: true
O internal: true pode impedir que a rede backend acesse diretamente a rede externa, o que serve para isolar a camada de banco de dados. Mas atenção: se alguns serviços precisam de acesso à internet para atualizar dados, não os coloque em uma rede totalmente internal.
Não abra o banco de dados para a internet; isso já foi tratado aqui:
Contêineres podem ser apagados, imagens podem ser baixadas de novo, mas perder um Volume é um incidente de verdade.
Primeiro liste onde estão os dados:
docker volume ls
docker volume inspect volume_name
No Compose, recomenda-se nomeá-los explicitamente:
volumes:
postgres_data:
redis_data:
uploads:
Monte-os no serviço:
services:
db:
volumes:
- postgres_data:/var/lib/postgresql/data
app:
volumes:
- uploads:/app/uploads
Antes de atualizar, faça pelo menos três coisas:
- Dump do banco de dados;
- Backup do Volume;
- Salvar o
compose.ymle o.envatuais.
Se quiser fazer um simulado de recuperação, veja:
Não pense que os snapshots da VPS bastam. Snapshots servem para rollback de curto prazo; o backup de longo prazo depende de um repositório de backup independente.
Em produção, não faça diretamente:
docker compose pull
docker compose up -d
Um processo mais estável:
# 1. 记录当前版本
docker compose ps
docker images | grep myapp
# 2. 备份 compose 和 env
cp compose.yml compose.yml.bak.$(date +%F-%H%M)
cp .env.production .env.production.bak.$(date +%F-%H%M)
# 3. 备份数据库和关键 volume
# 这里按你的数据库类型执行 dump
# 4. 拉取新镜像
docker compose pull
# 5. 启动
docker compose up -d
# 6. 看健康状态
docker compose ps
docker compose logs --tail=100 app
Se falhar, faça rollback:
# 改回旧镜像 tag
vim compose.yml
docker compose up -d
Por isso a tag da imagem precisa estar fixada. Se você usa latest, no rollback você nem sabe qual era a versão anterior.
Abaixo está um template simplificado; não é uma configuração universal, mas inclui várias categorias de coisas que produção precisa considerar:
services:
proxy:
image: caddy:2.8
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
logging:
driver: json-file
options:
max-size: "50m"
max-file: "3"
networks:
- frontend
app:
image: ghcr.io/example/myapp:2026-06-14-1
restart: unless-stopped
env_file:
- .env.production
expose:
- "3000"
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30s
mem_limit: 768m
cpus: 1.0
pids_limit: 256
logging:
driver: json-file
options:
max-size: "50m"
max-file: "3"
networks:
- frontend
- backend
db:
image: postgres:16-alpine
restart: unless-stopped
env_file:
- .env.production
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 20s
logging:
driver: json-file
options:
max-size: "100m"
max-file: "5"
networks:
- backend
redis:
image: redis:7.2-alpine
restart: unless-stopped
command: ["redis-server", "--appendonly", "yes", "--maxmemory", "256mb", "--maxmemory-policy", "allkeys-lru"]
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
networks:
- backend
networks:
frontend:
backend:
internal: true
volumes:
caddy_data:
caddy_config:
postgres_data:
redis_data:
Verificação antes de publicar:
docker compose config
docker compose up -d
docker compose ps
docker compose logs --tail=100
O docker compose config é muito útil: ele expande a configuração final mesclada e ajuda a detectar antecipadamente problemas de sintaxe e variáveis.
Antes de colocar um projeto Compose em um ambiente de produção em VPS, confirme pelo menos:
- As tags das imagens estão fixadas e não dependem de
latest; - Web, banco de dados e Redis têm healthcheck;
- Os serviços críticos usam
restart: unless-stopped; - Os contêineres de aplicação têm limites de CPU/memória/PID;
- Os logs do Docker têm max-size/max-file configurados;
- O
.env.productionnão está no Git e tem permissões 600; - O banco de dados e o Redis não expõem portas públicas;
- Os nomes dos Volumes são claros e a estratégia de backup está definida;
- Antes de atualizar, faz-se backup do banco de dados, dos Volumes, do arquivo compose e do env;
- É possível fazer rollback com a tag da imagem anterior;
- O proxy reverso Nginx/Caddy e o HTTPS funcionam corretamente;
- O
docker compose psnão mostra serviços unhealthy.
É adequado para projetos pequenos e médios, SaaS pessoais, sites de ferramentas, blogs, sistemas internos e aplicações multisserviço de baixa complexidade. Não é adequado para orquestração complexa multinó, mas para rodar alguns serviços em uma VPS, o Compose é mais simples e controlável do que o Kubernetes.
Para a maioria dos serviços de produção em VPS, unless-stopped é mais intuitivo. Depois de parar o serviço manualmente, ele não volta a subir automaticamente quando o Docker reinicia. Para serviços que precisam ficar sempre ativos e não devem ser afetados por uma parada manual, você pode considerar always.
Não necessariamente. O healthcheck marca o estado de saúde; se vai reiniciar ou não depende também do comportamento do serviço e da orquestração externa. No mínimo, ele permite usar docker compose ps, sistemas de monitoramento e scripts de implantação para avaliar se o serviço está realmente disponível.
Limites pequenos demais afetam o desempenho, mas não limitar nada é mais perigoso. Em uma VPS, recomenda-se definir primeiro limites superiores conservadores para evitar que um único contêiner derrube a máquina, e depois ajustar gradualmente com base no monitoramento.
Não. O .env só evita escrever segredos no arquivo compose. Você também precisa garantir que ele não entre no Git, que as permissões sejam rígidas o suficiente, que os backups sejam criptografados e que os segredos não sejam impressos nos logs.
Rodar Docker Compose em produção em uma VPS não tem problema, desde que você não trate uma configuração que "consegue iniciar" como uma configuração que "funciona de forma estável a longo prazo".
O que realmente precisa ser completado é: versões fixas, healthchecks, limites de recursos, rotação de logs, gestão de segredos, isolamento de portas, backup de Volumes e atualizações com rollback. Com tudo isso feito, um projeto Compose em uma VPS comum passa de "implantação de brinquedo" para "implantação de produção sustentável".