PostgreSQL em produção com Docker

Rodar PostgreSQL em produção com Docker exige mais que um docker run com a imagem oficial. Você precisa garantir persistência de dados, healthcheck para dependências de startup, ajuste de memória para a VPS, logs estruturados e nunca expor o banco diretamente na internet. Este artigo cobre a configuração completa.

docker-compose.yml para produção

A configuração mínima recomendada para PostgreSQL em produção inclui volume nomeado, healthcheck, limites de recurso e reinício automático:

yaml
services:
  postgres:
    image: postgres:16-alpine
    restart: always
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      PGDATA: /var/lib/postgresql/data/pgdata
    volumes:
      - pg_data:/var/lib/postgresql/data
      - ./postgresql.conf:/etc/postgresql/postgresql.conf:ro
    command: postgres -c config_file=/etc/postgresql/postgresql.conf
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    deploy:
      resources:
        limits:
          memory: 1G
        reservations:
          memory: 256M
    networks:
      - db_network

volumes:
  pg_data:

networks:
  db_network:
    driver: bridge

Tuning de memória (postgresql.conf)

O PostgreSQL usa os padrões conservadores da imagem oficial, adequados para ambientes de desenvolvimento. Para produção, ajuste conforme a RAM da VPS:

ini
# postgresql.conf para VPS de 2 GB RAM dedicados ao banco
# Regras gerais:
#   shared_buffers = 25% da RAM
#   effective_cache_size = 75% da RAM
#   work_mem = RAM / (max_connections * 2)

# Para VPS 2 GB:
shared_buffers = 512MB
effective_cache_size = 1536MB
maintenance_work_mem = 128MB
work_mem = 10MB
max_connections = 100
wal_buffers = 16MB

# Checkpoints (reduz I/O de pico)
checkpoint_completion_target = 0.9
checkpoint_timeout = 10min

# Paralelismo (para VPS com 2+ vCPUs)
max_parallel_workers_per_gather = 2
max_parallel_workers = 4

# Logs
log_min_duration_statement = 500
log_checkpoints = on
log_connections = off
log_lock_waits = on

Segurança: nunca expor o banco na internet

O PostgreSQL deve ser acessível apenas dentro da rede Docker (pelo nome do serviço) ou via SSH tunnel. Nunca mapeie a porta 5432 em produção:

yaml
# CORRETO: sem mapeamento de porta (banco inacessível externamente)
services:
  postgres:
    image: postgres:16-alpine
    # SEM "ports:" — acesso apenas pela rede Docker interna
    networks:
      - db_network

  app:
    environment:
      DATABASE_URL: postgres://user:pass@postgres:5432/db
    networks:
      - db_network

# ERRADO para produção (expõe o banco na internet):
# ports:
#   - "5432:5432"

# Para acesso externo seguro, use SSH tunnel:
ssh -L 5432:localhost:5432 root@IP_DA_VPS
# Depois conecte em localhost:5432 no seu DBeaver/pgAdmin local

Verificar integridade e manutenção

Manutenção regular previne degradação de performance e corrupção silenciosa:

bash
# VACUUM ANALYZE — limpar tuplas mortas e atualizar estatísticas
docker compose exec postgres psql -U postgres -d meu_banco \
  -c "VACUUM ANALYZE;"

# VACUUM FULL — reescreve tabelas (requer lock, use em manutenção)
docker compose exec postgres psql -U postgres -d meu_banco \
  -c "VACUUM FULL ANALYZE;"

# Ver tabelas com mais tuplas mortas (candidatas a VACUUM)
docker compose exec postgres psql -U postgres -d meu_banco -c "
SELECT schemaname, tablename, n_dead_tup, n_live_tup,
       round(n_dead_tup::numeric/NULLIF(n_live_tup,0)*100, 2) AS dead_pct
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 10;"

# Verificar tamanho dos bancos
docker compose exec postgres psql -U postgres \
  -c "SELECT pg_database.datname, pg_size_pretty(pg_database_size(pg_database.datname)) FROM pg_database ORDER BY pg_database_size(pg_database.datname) DESC;"

Atualizar o PostgreSQL com zero downtime

Para atualizar a versão minor (16.1 → 16.5), basta puxar a nova imagem. Para major (15 → 16), é necessário pg_upgrade:

bash
# Atualização minor (patch) — sem perda de dados
docker compose pull postgres
docker compose up -d postgres

# Verificar nova versão
docker compose exec postgres psql -U postgres -c "SELECT version();"

# Atualização major (ex: PG 15 → 16):
# 1. Fazer backup completo com pg_dumpall
docker compose exec postgres pg_dumpall -U postgres > backup-before-upgrade.sql

# 2. Parar o banco e remover o volume antigo
docker compose down
docker volume rm nome_do_projeto_pg_data

# 3. Mudar a tag da imagem no docker-compose.yml para postgres:16-alpine
# 4. Subir e restaurar
docker compose up -d postgres
cat backup-before-upgrade.sql | docker compose exec -T postgres psql -U postgres

Não quer configurar manualmente?

Implante o PostgreSQL em menos de 5 minutos com a Runstack. Infraestrutura da OPEN Datacenter, com servidores em São Paulo.

Perguntas frequentes

Sempre use versão específica em produção (ex: postgres:16-alpine). A tag :latest muda com major releases e pode quebrar a inicialização do volume se o formato interno do banco mudar. Alpine é ~50% menor que a imagem padrão sem perda de funcionalidade.

Volume nomeado (pg_data:) é recomendado para produção: Docker gerencia localização e permissões, portátil entre sistemas. Bind mount (./data:/var/lib/postgresql/data) é útil quando você precisa acessar os arquivos diretamente pelo host, mais frequente em desenvolvimento.

docker stats mostra o uso de memória do container. Dentro do banco: SELECT * FROM pg_stat_bgwriter;, high buffers_alloc e maxwritten_clean indicam que o shared_buffers está pequeno. Aumente shared_buffers e reinicie o container.

pg_stat_statements (análise de queries lentas), pgcrypto (criptografia), uuid-ossp (geração de UUIDs), timescaledb (séries temporais), postgis (dados geográficos), pg_trgm (busca de texto com similaridade). Ative com: CREATE EXTENSION nome_da_extensao;

Depende do parâmetro. Mudanças em work_mem, log_min_duration_statement e outros são aplicadas com pg_reload_conf() (sem reinício). Mudanças em shared_buffers, max_connections e wal_level requerem reinício completo do PostgreSQL.