Pular para o conteúdo

O banco em contêiner

Subir Postgres, MySQL, Redis ou Mongo com Docker e conectar — incluindo a espera que todo compose precisa e ninguém escreve.

O caminho normal de desenvolvimento hoje é o banco num contêiner. Esta página cobre as três coisas que quebram nesse caminho — e as três têm resposta na biblioteca.

1. Subir o banco#

bash
# O jeito curto, para experimentar:
docker run -d --name loja-db \
  -e POSTGRES_USER=forge -e POSTGRES_PASSWORD=segredo \
  -e POSTGRES_DB=loja \
  -p 5432:5432 postgres:16-alpine

E o docker-compose.yml sai da mesma URL que a aplicação usa. Escrever os dois à mão é como eles divergem: o compose sobe POSTGRES_DB=loja e a aplicação procura loja_dev, e o erro só aparece na primeira consulta.

dataforge
adopt Arcane.Forge as Forge
adopt Arcane.Serialization as S

steady URL := "postgres://forge:segredo@localhost:5432/loja"

c := Forge.compose(URL, servico := "banco")

out $"servico:     {c['servico']}"
out $"url interna: {c['url_interna']}"
out ""
out S.to_json(c["definicao"], yes)

2. Esperar — “Up” não quer dizer “pronto”#

O docker compose up volta, o contêiner aparece como Up, e a aplicação morre no primeiro conectar com connection refused. O contêiner do Postgres sobe, cria o cluster, reinicia o servidor uma vez durante a inicialização, e só então passa a escutar — são segundos. O do MySQL demora mais.

A saída comumPor que falha
sleep 5 no script de partidafalha na máquina lenta, e desperdiça quatro segundos na rápida
laço de retentativa escrito à mãoquase sempre insiste também em credencial errada, e esconde a causa atrás do prazo
depends_on sem conditionespera o contêiner começar, não ficar pronto
dataforge
adopt Arcane.Forge as Forge

// A espera e por RESPOSTA, e nao por relogio.
db := Forge.esperar("postgres://forge:segredo@localhost:5432/loja",
    prazo := 30.0)

out Forge.versao(db)
Forge.fechar(db)

3. A URL vem do ambiente#

Uma URL de banco no código é um segredo no repositório: ela carrega usuário e senha. Num contêiner ela nunca está no código — está no ambiente, e é isso que permite a mesma imagem rodar em desenvolvimento, em teste e em produção.

dataforge
adopt Arcane.Forge as Forge

// Procura DATABASE_URL, DB_URL e FORGE_DATABASE_URL, nesta ordem.
// Sem nenhuma delas e SEM padrao, isto e ERRO — e nao um SQLite
// calado, que e o defeito que faz alguem rodar uma semana contra o
// banco errado.
db := Forge.de_ambiente(padrao := ":memory:")

Forge.executar(db, "create table t (id integer primary key, v text)")
Forge.executar(db, "insert into t (v) values (?)", ["ok"])
assert Forge.consultar(db, "select v from t")[0]["v"] is "ok"

out "conectado pelo ambiente"
Forge.fechar(db)

O compose completo#

yaml
services:
  banco:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: forge
      POSTGRES_PASSWORD: segredo
      POSTGRES_DB: loja
    ports: ["5432:5432"]
    volumes: ["banco-dados:/var/lib/postgresql/data"]
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U forge"]
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 20s

  app:
    build: .
    environment:
      # O host e o NOME DO SERVICO, e a porta e a interna.
      DATABASE_URL: postgres://forge:segredo@banco:5432/loja
    depends_on:
      banco:
        condition: service_healthy

volumes:
  banco-dados:
No arquivoSem ele
healthcheck com start_periodas falhas normais da inicialização contam como não saudável e derrubam o serviço
depends_on: service_healthya app sobe antes do banco e falha na primeira consulta, de forma intermitente
volumeso banco começa vazio a cada down
DATABASE_URL no ambientea senha vai para a imagem, e docker history a mostra

Os quatro motores em contêiner#

MotorImagemURL
PostgreSQLpostgres:16-alpinepostgres://forge:segredo@localhost:5432/loja
MySQLmysql:8mysql://forge:segredo@localhost:3306/loja
MariaDBmariadb:11mariadb://forge:segredo@localhost:3306/loja
Redisredis:7-alpineredis://localhost:6379
MongoDBmongo:7mongo://forge:segredo@localhost:27017/loja

Continue em Cada motor e Produção.