23 out 2026 · docker, compose, infraestrutura · 5 min
Como subir app, banco e proxy reverso com docker compose do zero
Uma pilha com app, banco e proxy reverso sobe com um único arquivo compose.yaml e um .env. O app só inicia depois que o banco passa no healthcheck, o proxy só inicia depois do app, e o banco guarda seus dados num volume nomeado. Subi do zero numa pilha de teste: o banco ficou saudável em cerca de 27 segundos, o app respondeu db=ok e o proxy devolveu HTTP 200 na porta configurada.
O que cada serviço faz
O db é o Postgres. Ele carrega as credenciais do .env, cria o banco na primeira subida e expõe um healthcheck com pg_isready. Sem esse healthcheck, o app tentaria conectar antes do banco aceitar conexão e falharia na primeira subida.
O app é um servidor HTTP mínimo em Python. Ele tenta abrir uma conexão TCP com o banco na porta 5432 e responde db=ok ou db=fail. A escolha de testar a conexão em vez de rodar uma consulta deixa o exemplo curto e ainda prova o caminho: proxy, app, rede interna, banco.
O proxy é o Nginx. Ele é o único serviço com porta publicada (${APP_PORT}:80), e repassa tudo para app:8000. O app e o banco não têm porta publicada, então não são alcançáveis de fora da rede do compose.
O compose.yaml inteiro
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- db-dados:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 5s
timeout: 3s
retries: 5
restart: unless-stopped
app:
image: python:3.12-slim
working_dir: /app
volumes:
- ./app.py:/app/app.py:ro
environment:
DB_HOST: db
DB_PORT: "5432"
command: ["python", "app.py"]
depends_on:
db:
condition: service_healthy
restart: unless-stopped
proxy:
image: nginx:alpine
ports:
- "${APP_PORT}:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- app
restart: unless-stopped
volumes:
db-dados:
O depends_on com condition: service_healthy é a parte que a maioria dos exemplos omite. Sem ele, o compose inicia os três ao mesmo tempo e o app morre na primeira requisição porque o banco ainda não aceita conexão.
O .env
POSTGRES_USER=app
POSTGRES_PASSWORD=troque-esta-senha
POSTGRES_DB=appdb
APP_PORT=8080
Troque a senha antes de usar. O .env não entra no git: adicione ao .gitignore e mantenha um .env.example com valores falsos no repositório. O compose lê o .env automaticamente quando está no mesmo diretório.
O app.py
import http.server, socket, os
def db_ok():
try:
with socket.create_connection(
(os.getenv("DB_HOST", "db"), int(os.getenv("DB_PORT", "5432"))),
timeout=2,
):
return True
except OSError:
return False
class H(http.server.BaseHTTPRequestHandler):
def do_GET(self):
ok = db_ok()
body = ("db=%s\n" % ("ok" if ok else "fail")).encode()
self.send_response(200 if ok else 503)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, *a):
pass
if __name__ == "__main__":
http.server.HTTPServer(("0.0.0.0", 8000), H).serve_forever()
A imagem python:3.12-slim não tem o código: ele entra por volume somente leitura (:ro). Assim a imagem fica genérica e o código fica versionado com o compose.
O nginx.conf
server {
listen 80;
location / {
proxy_pass http://app:8000;
proxy_set_header Host $host;
}
}
O nome app resolve porque o compose cria uma rede interna e registra cada serviço pelo nome. Fora dessa rede, app não existe.
Como verificar
Suba a pilha e confira o caminho da requisição:
docker compose up -d
docker compose ps
curl -sS http://127.0.0.1:8080/
Saída esperada: o banco com status healthy, o app e o proxy como Up, e o curl imprimindo db=ok com HTTP 200. Se o banco ainda não estiver saudável, o app não terá subido, porque o depends_on segura a ordem.
Para conferir se um compose tem os campos que importam sem subir nada:
#!/usr/bin/env bash
# verifica-compose.sh: confere os campos de um compose sem subir container.
# uso: bash verifica-compose.sh compose.yaml (sem argumento, usa o exemplo embutido)
arquivo="${1:-}"
if [ -n "$arquivo" ] && [ -f "$arquivo" ]; then
origem="$arquivo"
else
origem=$(mktemp)
cat > "$origem" <<'YAML'
services:
db:
image: postgres:16-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready"]
app:
depends_on:
db:
condition: service_healthy
proxy:
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
YAML
fi
conteudo=$(tr '\n' ' ' < "$origem")
faltando=0
conferir() {
if printf '%s' "$conteudo" | grep -q "$1"; then
echo "ok $2"
else
echo "FALTA $2"
faltando=$((faltando + 1))
fi
}
conferir 'postgres:16-alpine' "imagem do banco"
conferir 'healthcheck:' "healthcheck do banco"
conferir 'condition: service_healthy' "app espera o banco saudavel"
conferir 'proxy_pass\|nginx.conf' "proxy apontando para o app"
if [ "$faltando" -eq 0 ]; then
echo "compose completo"
exit 0
else
echo "compose incompleto: $faltando campo(s)"
exit 1
fi
Se der errado
port is already allocated ao subir o proxy. Outro serviço já usa a porta 8080. Mude APP_PORT no .env para outra livre, por exemplo 8081, e suba de novo. O compose recria só o proxy.
app não sobe ou fica reiniciando. O banco ainda não ficou saudável, ou a imagem do app não encontra o app.py. Confira docker compose logs app e verifique se o volume ./app.py:/app/app.py:ro aponta para o arquivo certo.
db=ok não aparece, o curl retorna 503. O app não alcança o banco. Confira se DB_HOST é db (o nome do serviço, não localhost) e se os dois estão na mesma rede padrão do compose.
Tabela comparativa
| Peça | O que resolve | Alternativa comum |
|---|---|---|
| healthcheck no db | Ordem de subida | sleep no script de entrada |
| volume nomeado | Dados sobrevivem a down |
bind mount para pasta do host |
| proxy no compose | Um único ponto de entrada | publicar a porta do app direto |
.env |
Segredo fora do compose | variáveis no shell do host |
A alternativa de publicar a porta do app direto funciona, mas expõe o serviço sem cabeçalhos, compressão ou rota. O proxy centraliza isso.
FAQ
Preciso de Docker Compose ou basta o docker compose plugin? O comando docker compose (com espaço) é o plugin atual, instalado junto com o Docker Engine. O antigo docker-compose (com hífen) está descontinuado. Use sempre o plugin.
O volume db-dados some quando eu rodo docker compose down? Não. down remove contêineres e rede, mas mantém volumes nomeados. Para apagar os dados, use docker compose down -v.
Posso usar outra imagem de banco? Sim. Troque a imagem e ajuste o healthcheck: para MySQL use mysqladmin ping, para Redis use redis-cli ping. O resto do compose não muda.
Por que o app testa TCP em vez de rodar uma consulta? Para manter o exemplo curto e sem dependência de driver. Em produção, o healthcheck do app deve verificar o que realmente importa, por exemplo uma rota /saude que consulta o banco.
O proxy precisa de build? Não. A imagem nginx:alpine já vem pronta, e a configuração entra por volume somente leitura. Sem Dockerfile, sem build, sem cache para invalidar.
Fontes
- Docker Compose specification (acessado em 23 out 2026)
- Docker Compose healthcheck (acessado em 23 out 2026)
- Postgres image on Docker Hub (acessado em 23 out 2026)
- Nginx image on Docker Hub (acessado em 23 out 2026)
Veja também como limitar memória, log e reinício de um contêiner e contêiner sem root: user, cap drop e read-only.