>_ josetilabs.com
← blog

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.

Terminal mostrando o compose subindo db saudável, app e proxy, e a resposta curl db=ok passando pelo proxy na porta 8080
O caminho da requisição: proxy, app e banco, com o banco saudável antes do app subir.

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

yaml
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

text
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

python
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

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

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

bash
#!/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

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.

Leia também

// Por e-mail

Os textos publicados aqui também vão por e-mail. O envio ainda não começou: quem deixar o endereço agora entra na primeira lista.

Guardo só o seu e-mail, a data e o registro desta autorização. Sem cookies, sem rastreador e nada compartilhado com terceiros. Detalhes na política de privacidade.