Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,14 @@ JOB_CONCURRENCY=1
# (ver .github/workflows/ci.yml); também serve para rodar a stack contra
# uma imagem publicada, ex.: ghcr.io/hirley/task_keeper_api:2.0.1.
APP_IMAGE=

# Opcional — "false" desliga o `bin/rails db:prepare` que o entrypoint roda
# antes de iniciar o Puma (ver bin/docker-entrypoint). Ligado por padrão,
# porque é o que faz `docker compose up` funcionar sem passo manual.
#
# Num deploy de verdade, defina "false" e rode as migrations numa etapa de
# release, antes de subir as réplicas — ver README, seção "Docker",
# subseção "Migrations: boot ou release". Com mais de uma réplica isso
# deixa de ser preferência: quem perde o advisory lock da migration morre
# no boot com ConcurrentMigrationError.
DB_PREPARE_ON_BOOT=
12 changes: 12 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,18 @@ jobs:
- name: O entrypoint gerou a chave efêmera (caminho sem SECRET_KEY_BASE)
run: docker compose logs web | grep -q 'gerando uma chave'

# A etapa de release que o README manda usar em deploy de verdade
# (DB_PREPARE_ON_BOOT=false + migrations antes de subir as réplicas).
# Sem isto ela seria um caminho documentado que nada exercita — a
# mesma lacuna que este smoke test veio fechar para a imagem inteira.
#
# `compose run` passa pelo ENTRYPOINT, e o entrypoint só prepara o
# banco quando o comando é `./bin/rails server`: é justamente isso
# que se verifica aqui, além de o db:prepare ser idempotente contra
# um banco que a stack já preparou.
- name: O caminho de release prepara o banco sem subir servidor
run: docker compose run --rm web ./bin/rails db:prepare

# Desde a adoção do Solid Queue o deploy tem DOIS processos, e o
# segundo nunca foi exercitado por nada automatizado. Um worker que
# sobe e não consome nada é invisível: a tela responde normalmente e
Expand Down
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,32 @@ docker run -p 3000:3000 \
task_keeper_api
```

### Migrations: boot ou release

`bin/docker-entrypoint` roda `bin/rails db:prepare` (idempotente) toda vez que o container sobe, antes de iniciar o Puma — então o banco é criado/migrado automaticamente, sem passo manual (mas o PostgreSQL em si precisa já estar de pé e acessível; o Dockerfile não sobe um banco dentro do próprio container da aplicação).

Isso é **conveniência de demonstração**, e é o que faz `docker compose up --build` funcionar de primeira. Num deploy de verdade, o modo certo é o outro:

| | Quando | Como |
|---|---|---|
| **No boot** (default) | `docker compose up`, `docker run`, demo de uma réplica só | nada a fazer — o entrypoint cuida |
| **Etapa de release** | qualquer deploy real, e obrigatório com mais de uma réplica | `DB_PREPARE_ON_BOOT=false` no serviço web, e as migrations rodam antes de subir as réplicas |

O comando da etapa de release é o mesmo binário, com outro argumento — o entrypoint só prepara o banco quando o comando é `./bin/rails server`, então qualquer outro comando passa direto:

```bash
docker compose run --rm web ./bin/rails db:prepare
```

Fora do compose, é a mesma ideia: `docker run --rm -e DATABASE_URL=... ghcr.io/hirley/task_keeper_api:latest ./bin/rails db:prepare`. No Railway, um *pre-deploy command* com `bin/rails db:prepare`.

**O que se ganha desligando não é evitar corrupção de schema.** As migrations do Rails pegam um advisory lock no Postgres, então o schema está protegido de qualquer jeito. São duas outras coisas:

- **com mais de uma réplica, quem perde o lock não espera**: o Rails usa `pg_try_advisory_lock`, que é não-bloqueante, e levanta `ConcurrentMigrationError`. Como o entrypoint roda com `bash -e`, o `db:prepare` que falha derruba o container — a réplica **morre no boot** em vez de simplesmente subir depois da que migrou;
- **a disponibilidade do web deixa de depender das migrations**: uma migration longa atrasa o boot, e uma que falha impede a aplicação de subir mesmo que o código já rodasse contra o schema antigo.

O default continua sendo ligado de propósito. Inverter deixaria o `docker run` acima e o compose local subindo contra um banco vazio — um jeito pior de falhar do que o problema que se quer evitar. O CI exercita os dois caminhos: o smoke test sobe a stack pelo boot automático e, em seguida, roda a etapa de release isolada (ver "Integração contínua").

Este projeto não tem `config/master.key`/`config/credentials.yml.enc`, então `SECRET_KEY_BASE` (variável de ambiente) é obrigatória em produção — sem ela, o container não sobe. As variáveis de conexão com o banco (`DATABASE_URL` ou `DB_HOST`/`DB_PORT`/`DB_USERNAME`/`DB_PASSWORD`/`DB_NAME`) e as demais (`TELEGRAM_BOT_TOKEN`, `APP_HOST`, `RAILS_MAX_THREADS`) são opcionais/têm default; ver `.env.example` para a lista completa e o que cada uma faz.

**Sobre a plataforma do `Gemfile.lock`**: o lockfile deste projeto foi gerado originalmente numa máquina Windows — a seção `PLATFORMS` só tem `x64-mingw-ucrt`, sem a plataforma Linux. Sem isso, `bundle install` falha dentro de um container Linux ao tentar resolver as gems com extensão nativa (`pg`, `nokogiri`). O `Dockerfile` já corrige isso sozinho (roda `bundle lock --add-platform x86_64-linux` antes do `bundle install`, dentro da própria imagem), então não é preciso fazer nada manualmente por causa disso — mas é bom saber que esse ajuste existe, caso apareça algum erro de plataforma ao rodar `bundle install` fora do Docker também (nesse caso, `bundle lock --add-platform x86_64-linux` resolve, e o mesmo vale se você desenvolver num Mac Apple Silicon: `bundle lock --add-platform arm64-darwin`).
Expand Down
29 changes: 26 additions & 3 deletions bin/docker-entrypoint
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,32 @@ if [ -z "${SECRET_KEY_BASE}" ]; then
fi

# Prepara o banco (cria se não existir, aplica migrations pendentes) toda
# vez que o servidor sobe — bin/rails db:prepare é idempotente, então é
# seguro rodar isso a cada restart do container.
if [ "${1}" == "./bin/rails" ] && [ "${2}" == "server" ]; then
# vez que o servidor sobe. É idempotente, então é seguro a cada restart —
# e é o que faz `docker compose up` funcionar sem nenhum passo manual.
#
# Isso é conveniência de DEMONSTRAÇÃO, e num deploy de verdade deve ser
# desligado com DB_PREPARE_ON_BOOT=false, rodando as migrations numa etapa
# de release própria, antes de subir as réplicas (ver README, seção
# "Docker", subseção "Migrations: boot ou release").
#
# O que se ganha desligando não é evitar corrupção de schema — as
# migrations do Rails pegam um advisory lock no Postgres, então o schema
# está protegido de qualquer jeito. São duas outras coisas:
#
# * com mais de uma réplica, quem PERDE o lock não espera: o Rails usa
# pg_try_advisory_lock (não-bloqueante) e levanta
# ConcurrentMigrationError. Como este script roda com `bash -e`, o
# db:prepare que falha derruba o container inteiro — a réplica morre
# no boot em vez de simplesmente subir depois;
# * a disponibilidade do web deixa de depender do tempo e do sucesso das
# migrations. Uma migration longa atrasa o boot; uma que falha impede
# a aplicação de subir, mesmo que o código já estivesse pronto para
# rodar contra o schema antigo.
#
# O default continua sendo ligado: desligar sem uma etapa de release no
# lugar deixaria o `docker run` documentado no README (e o compose local)
# subindo contra um banco vazio, que é um jeito pior de falhar.
if [ "${1}" == "./bin/rails" ] && [ "${2}" == "server" ] && [ "${DB_PREPARE_ON_BOOT:-true}" != "false" ]; then
./bin/rails db:prepare
fi

Expand Down
5 changes: 5 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ x-app-env: &app-env
# variável não é definida, e o default de production.rb (ligado)
# vale. Ver config/environments/production.rb.
FORCE_SSL: ${FORCE_SSL:-false}
# Vazio = ligado (default do entrypoint), que é o que mantém o
# `docker compose up` sem passo manual. Defina "false" para exercitar
# aqui o modo de produção: migrations numa etapa de release separada,
# antes de subir o web. Ver bin/docker-entrypoint e o README.
DB_PREPARE_ON_BOOT: ${DB_PREPARE_ON_BOOT:-}
DB_HOST: db
DB_PORT: 5432
DB_USERNAME: ${DB_USERNAME:-postgres}
Expand Down
Loading