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#
# 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-alpineE 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.
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 comum | Por que falha |
|---|---|
sleep 5 no script de partida | falha na máquina lenta, e desperdiça quatro segundos na rápida |
| laço de retentativa escrito à mão | quase sempre insiste também em credencial errada, e esconde a causa atrás do prazo |
depends_on sem condition | espera o contêiner começar, não ficar pronto |
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.
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#
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 arquivo | Sem ele |
|---|---|
healthcheck com start_period | as falhas normais da inicialização contam como não saudável e derrubam o serviço |
depends_on: service_healthy | a app sobe antes do banco e falha na primeira consulta, de forma intermitente |
volumes | o banco começa vazio a cada down |
DATABASE_URL no ambiente | a senha vai para a imagem, e docker history a mostra |
Os quatro motores em contêiner#
| Motor | Imagem | URL |
|---|---|---|
| PostgreSQL | postgres:16-alpine | postgres://forge:segredo@localhost:5432/loja |
| MySQL | mysql:8 | mysql://forge:segredo@localhost:3306/loja |
| MariaDB | mariadb:11 | mariadb://forge:segredo@localhost:3306/loja |
| Redis | redis:7-alpine | redis://localhost:6379 |
| MongoDB | mongo:7 | mongo://forge:segredo@localhost:27017/loja |
Continue em Cada motor e Produção.