diff --git a/.env.example b/.env.example index 59245c7..1e38383 100644 --- a/.env.example +++ b/.env.example @@ -51,3 +51,11 @@ RAILS_MAX_THREADS=5 # config/queue.yml e o serviço "worker" no docker-compose.yml). Um só # basta para o volume atual; existe para escalar sem editar arquivo. JOB_CONCURRENCY=1 + +# Opcional — nome/tag da imagem que os serviços web e worker usam no +# docker-compose.yml. O default (task-keeper-api:local) é o que o +# `docker compose up --build` monta. Existe para o smoke test do CI poder +# apontar o compose para uma imagem já buildada, em vez de buildar de novo +# (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= diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4a985b0..13fa623 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,7 +86,7 @@ jobs: run: bundle exec rspec docker: - name: Build da imagem Docker + name: Build e smoke test da imagem Docker runs-on: ubuntu-latest needs: [rubocop, rspec] # Só precisa de permissão de escrita no registry (packages: write) para @@ -95,15 +95,160 @@ jobs: permissions: contents: read packages: write + # As três anulam, só dentro deste job, o `env:` do workflow — que + # descreve o ambiente de TESTE, e aqui o que sobe é a imagem de + # PRODUÇÃO, pelo docker-compose.yml: + # + # * APP_IMAGE aponta o compose pra imagem que o passo de build + # acabou de carregar no daemon, em vez de buildar tudo de novo + # (ver o comentário da âncora x-app-image no docker-compose.yml); + # * SECRET_KEY_BASE vazio é o ponto do smoke test, não descuido: é + # o que exercita o caminho de chave efêmera do entrypoint, que + # nasceu na v2.0.0 e nunca teve verificação automatizada; + # * DB_NAME volta pro banco de produção — o global aponta pro banco + # de teste, que não existe nesta stack. + env: + APP_IMAGE: task-keeper-api:ci + SECRET_KEY_BASE: '' + DB_NAME: task_keeper_api_production steps: - uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 + # Build em duas etapas de propósito. Esta carrega a imagem no daemon + # local (load) para o smoke test rodar contra o artefato de verdade, + # antes de qualquer publicação — uma imagem que não sobe não deve + # chegar ao registry. A etapa que publica vem depois de tudo passar, + # e acerta o cache do gha, então custa segundos. + - name: Build da imagem (carrega no daemon local, pro smoke test) + uses: docker/build-push-action@v6 + with: + context: . + load: true + tags: ${{ env.APP_IMAGE }} + cache-from: type=gha + cache-to: type=gha,mode=max + + # O CI buildava a imagem e nunca a executava, então tudo que só falha + # em runtime passava batido: entrypoint, db:prepare, permissões do + # usuário não-root, healthcheck, publicação de porta, variável de + # ambiente faltando. + # + # Os dois primeiros bugs de build deste projeto (CRLF em bin/*, + # plataforma ausente no Gemfile.lock) NÃO são exemplo disso — os + # dois quebravam o próprio build, porque o Dockerfile executa + # ./bin/rails assets:precompile, e o job de build já os pegava. + # + # O terceiro é: bin/jobs entrou no repositório sem bit de execução + # (criado no Windows, onde o sistema de arquivos não tem esse bit), + # a imagem buildava perfeitamente e o worker morria com exit 126 ao + # subir. Este passo achou isso na primeira vez que rodou, com a + # imagem publicada em main já quebrada havia dois merges. + # + # --wait é pedido só para db e web, de propósito: ele exige que todo + # serviço nomeado tenha healthcheck, e falha com "container ... has + # no healthcheck configured" para os que não têm. O worker não tem — + # não serve HTTP nenhum, e o healthcheck do Dockerfile (curl na + # 3000) está desligado nele por isso (ver docker-compose.yml). + # + # Não inventamos um healthcheck de processo para o worker só para + # satisfazer o --wait: quem prova que ele está vivo é o passo "O + # worker consome um job da fila", mais abaixo, que mede o trabalho + # em vez do pulso. + # + # (O compose do Docker Desktop aceita --wait com healthcheck + # desligado e reporta o serviço como "Healthy"; o do runner trata + # como erro. Foi o que quebrou a primeira versão deste job.) + - name: Sobe o banco e o web, e espera ficarem saudáveis + run: docker compose up --detach --wait --wait-timeout 240 db web + + - name: Sobe o worker + run: docker compose up --detach worker + + # Falha rápido e com o motivo na mão. Sem isto, um worker que morre + # no boot só apareceria 60s depois, como "não consumiu o job" — foi + # exatamente o que aconteceu com o bin/jobs sem bit de execução, que + # este smoke test pegou (exit 126) na primeira vez que rodou. + - name: O worker continua de pé depois de subir + run: | + cid=$(docker compose ps --quiet worker) + + for _ in $(seq 1 10); do + estado=$(docker inspect --format '{{.State.Status}}' "$cid") + + if [ "$estado" = "running" ]; then + echo "worker de pé" + exit 0 + fi + + if [ "$estado" = "exited" ] || [ "$estado" = "dead" ]; then + codigo=$(docker inspect --format '{{.State.ExitCode}}' "$cid") + echo "::error::o worker morreu no boot (estado: $estado, exit code: $codigo)" + exit 1 + fi + + sleep 2 + done + + echo "::error::o worker não chegou a rodar em 20s" + exit 1 + + # O healthcheck já bate em /acessibilidade de dentro do container. + # Este curl é de fora: verifica também a publicação da porta. + - name: A rota pública responde pela porta publicada + run: curl --fail --silent --show-error --max-time 10 -o /dev/null http://localhost:3000/acessibilidade + + - name: O entrypoint gerou a chave efêmera (caminho sem SECRET_KEY_BASE) + run: docker compose logs web | grep -q 'gerando uma chave' + + # 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 + # a entrega simplesmente não acontece. + # + # O job escolhido não tem efeito colateral nenhum — a assinatura 0 + # não existe, então WebhookDelivery recusa antes de tocar a rede + # (ver o contrato de retorno em WebhookDelivery#entregar). O que se + # verifica aqui é o percurso: o web enfileira, o Postgres guarda, o + # worker reclama e executa. + # + # O -e SECRET_KEY_BASE é necessário porque `docker compose exec` não + # passa pelo ENTRYPOINT, então não herda a chave efêmera que o + # entrypoint gerou pro processo do Puma. + - name: O worker consome um job da fila + run: | + docker compose exec -T -e SECRET_KEY_BASE=smoke-test web \ + bin/rails runner 'WebhookDeliveryJob.perform_later(0, "smoke_test", {})' + + for _ in $(seq 1 30); do + if docker compose logs worker | grep -q 'Performed WebhookDeliveryJob'; then + echo "O worker executou o job enfileirado pelo web." + exit 0 + fi + sleep 2 + done + + echo "::error::o worker não executou o job em 60s" + exit 1 + + # Sem isto, um smoke test vermelho não diz nada além de "falhou": o + # container que morreu já foi derrubado pelo passo seguinte. + - name: Estado e logs da stack (só quando algum passo acima falha) + if: failure() + run: | + docker compose ps --all + docker compose logs --no-color + + - name: Derruba a stack + if: always() + run: docker compose down --volumes --remove-orphans + # Só publica quando o CI roda por push (ex.: merge em main) — em - # pull_request o job continua só validando que o Dockerfile builda, - # sem publicar imagem de branch/fork ainda não revisado. + # pull_request o job continua só validando que o Dockerfile builda e + # que a imagem sobe, sem publicar imagem de branch/fork ainda não + # revisado. - name: Login no GitHub Container Registry if: github.event_name == 'push' uses: docker/login-action@v3 @@ -160,12 +305,22 @@ jobs: type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} - - name: Build da imagem (publica só em push; em pull_request só valida o build) + # Segunda etapa do build: mesmo contexto, então acerta inteiro o + # cache gravado pelo build local acima — o que ela acrescenta são as + # tags/labels definitivos e o push. Sem cache-to justamente por isso: + # gravar o mesmo cache de novo só custaria tempo. + # + # Não roda em pull_request: com push=false ela seria um build inteiro + # (ainda que de cache) cujo resultado é descartado — o Dockerfile já + # foi validado pelo build local acima, e a imagem, pelo smoke test. + # O passo de metadados continua rodando em PR, então as regras de tag + # seguem sendo exercitadas nos dois casos. + - name: Publica a imagem no GHCR + if: github.event_name == 'push' uses: docker/build-push-action@v6 with: context: . - push: ${{ github.event_name == 'push' }} + push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/CLAUDE.md b/CLAUDE.md index 9081389..17399b0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -42,7 +42,9 @@ Se você mexeu nessas áreas, suba a app e verifique de verdade — console sem **`db/schema.rb` regenerado no container vem com ruído.** O Postgres 16 do container acrescenta `enable_extension "pg_catalog.plpgsql"` e reordena as opções da coluna `events`. Para migration que só mexe em dados, edite **apenas** a linha `define(version:)` à mão e descarte o resto. -**Git Bash converte caminho em argumento de `docker`.** `-w /app` vira `C:/Program Files/Git/app`. Prefixe os comandos com `MSYS_NO_PATHCONV=1`. +**Git Bash converte caminho em argumento de `docker`.** `-w /app` vira `C:/Program Files/Git/app`. Prefixe os comandos com `MSYS_NO_PATHCONV=1`. Cuidado ao exportar essa variável para o shell inteiro: aí o `curl` do Git Bash também deixa de converter, e `-o /dev/null` falha com `curl: (23)` — parece erro da aplicação e não é. + +**Script novo em `bin/` nasce sem bit de execução.** O sistema de arquivos do Windows não tem esse bit, então o Git registra `100644`. Um `docker build` daqui não percebe (o contexto vem do disco, onde tudo parece executável) e um checkout Linux respeita o índice — foi assim que `bin/jobs` derrubou o container do worker com `exit 126`, com a imagem de `main` quebrada por dois merges. Depois de criar um script em `bin/`, rode `git update-index --chmod=+x bin/` e confira com `git ls-files -s bin/`. O `chmod +x bin/*` do Dockerfile é rede de segurança da imagem, não do repositório. **Cops que já morderam:** diff --git a/Dockerfile b/Dockerfile index 8570410..cacbfb6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -71,7 +71,17 @@ COPY . . # não corrige um checkout já existente sem um passo manual do usuário # (`git add --renormalize .` ou re-clonar) — este `sed` cobre esse caso # sem depender disso. -RUN sed -i 's/\r$//' bin/* +# +# O chmod +x vai junto pelo mesmo motivo, e a falta dele já custou uma +# imagem quebrada: quem cria um script em bin/ no Windows não tem bit de +# execução no sistema de arquivos, então o Git registra o arquivo como +# 100644 — foi o que aconteceu com bin/jobs. O `docker build` no Windows +# não percebe (o contexto vem do disco, onde tudo parece executável), mas +# o checkout num runner Linux respeita o modo do índice, e o container do +# worker morria com exit 126 ("command not executable"). Corrigido no +# índice também (`git update-index --chmod=+x`); esta linha é a rede de +# segurança para o próximo arquivo criado no Windows. +RUN sed -i 's/\r$//' bin/* && chmod +x bin/* # O Gemfile.lock deste projeto foi gerado originalmente numa máquina # Windows — a seção PLATFORMS só tinha "x64-mingw-ucrt", sem a diff --git a/README.md b/README.md index 9864959..16c28e2 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Aplicação Ruby on Rails full-stack (API REST + interface web) para times pequenos organizarem e acompanharem demandas do dia a dia: quem é responsável por quê, o que está atrasado, e um jeito rápido de saber "o que precisa da minha atenção agora?". -Três papéis com permissões diferentes (executor/líder/admin), autenticação e autorização (Devise + CanCanCan), notificação automática de atraso via Telegram, webhooks de saída (Slack/Teams/Discord/n8n), relatório semanal em PDF, e uma API JSON versionada ao lado da tela web — tudo com suíte de testes automatizados, CI (RuboCop + RSpec + build/publish da imagem Docker) e deploy em container. +Três papéis com permissões diferentes (executor/líder/admin), autenticação e autorização (Devise + CanCanCan), notificação automática de atraso via Telegram, webhooks de saída (Slack/Teams/Discord/n8n), relatório semanal em PDF, e uma API JSON versionada ao lado da tela web — tudo com suíte de testes automatizados, CI (RuboCop + RSpec + build, smoke test e publish da imagem Docker) e deploy em container. **Rodando em produção:** ver seção "Docker" — imagem publicada automaticamente em `ghcr.io/hirley/task_keeper_api` a cada merge em `main`. @@ -12,7 +12,7 @@ Três papéis com permissões diferentes (executor/líder/admin), autenticação - HAML + Bootstrap (interface web) + Ransack (filtro/ordenação — ver "Identidade visual e busca/paginação") - Prawn + prawn-table (PDF do relatório semanal — ver "Relatório semanal") - RSpec + FactoryBot + Shoulda Matchers (testes) · RuboCop (estilo) -- Docker + GitHub Actions (CI: lint, testes, build e publish da imagem) +- Docker + GitHub Actions (CI: lint, testes, build, smoke test e publish da imagem) ## Regras de negócio @@ -108,10 +108,13 @@ Este projeto não tem `config/master.key`/`config/credentials.yml.enc`, então ` **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`). -O build é validado automaticamente a cada push/PR pelo CI (ver seção "Integração contínua"). Durante o desenvolvimento, dois bugs reais de build já apareceram e foram corrigidos: +O build é validado automaticamente a cada push/PR pelo CI, que também **sobe a imagem e verifica que ela funciona** (ver seção "Integração contínua"). Durante o desenvolvimento, dois bugs reais de build já apareceram e foram corrigidos: 1. **`COPY . .` sobrescrevendo o `Gemfile.lock` corrigido**: rodava depois do `bundle lock --add-platform`, apagando silenciosamente o ajuste de plataforma antes do `bootsnap precompile app/ lib/` seguinte (`bundle exec` revalida a plataforma a cada chamada). Corrigido copiando o projeto inteiro antes de mexer no `Gemfile.lock`. 2. **CRLF em `bin/*`**: quem desenvolve no Windows normalmente tem `core.autocrlf=true` no Git, que converte os scripts de `bin/` (LF no repositório) para CRLF no checkout local; como `docker build` copia o contexto direto do disco (não do objeto Git), o CRLF ia parar no container e o shebang `#!/usr/bin/env ruby` de `bin/rails` virava `ruby\r` — `env: 'ruby\r': No such file or directory`. Corrigido normalizando `bin/*` para LF em tempo de build (`sed -i 's/\r$//' bin/*`, logo após o `COPY . .`), além de um `.gitattributes` (`* text=auto eol=lf`) pra evitar isso em checkouts novos. +3. **`bin/jobs` sem bit de execução**: quem cria um script em `bin/` no Windows não tem bit de execução no sistema de arquivos, então o Git registra o arquivo como `100644` — foi o que aconteceu com `bin/jobs` quando o Solid Queue entrou. Um `docker build` **no Windows** não percebe (o contexto vem do disco, onde tudo parece executável), mas o checkout num runner Linux respeita o modo do índice, e o container do `worker` morria com `exit 126` assim que subia. A imagem buildava, e a aplicação buildada não tinha worker. Corrigido no índice (`git update-index --chmod=+x bin/jobs`) e com um `chmod +x bin/*` em tempo de build, ao lado do `sed` acima. + + Este é o único dos três que **não** quebrava o `docker build` — os outros dois quebravam, porque o Dockerfile executa `./bin/rails assets:precompile`. Ele só aparecia ao *rodar* a imagem, e foi encontrado pelo smoke test descrito em "Integração contínua", na primeira vez que ele rodou. ## Integração contínua @@ -119,7 +122,29 @@ O build é validado automaticamente a cada push/PR pelo CI (ver seção "Integra - **rubocop** — `bundle exec rubocop` (usa o `.rubocop.yml` já existente no repositório); - **rspec** — sobe um serviço `postgres:16-alpine`, roda `bin/rails db:prepare`, depois `bin/rails zeitwerk:check` (eager load isolado num processo à parte, só pra pegar erro de autoload cedo — ver comentário em `config/environments/test.rb` sobre por que isso não é feito via `config.eager_load = true` no ambiente de teste) e por fim `bundle exec rspec` contra o banco `task_keeper_api_test`; -- **docker** — builda a imagem de produção (`docker/build-push-action`); só roda depois que `rubocop` e `rspec` passam. Em pull request, só valida que o `Dockerfile` builda (sem publicar). Em push pra `main`, publica a imagem no GitHub Container Registry (`ghcr.io/hirley/task_keeper_api`), usando o `GITHUB_TOKEN` automático do Actions — não exige nenhum secret configurado manualmente. +- **docker** — builda a imagem de produção, **sobe a stack inteira e verifica que ela funciona**, e só então publica; roda depois que `rubocop` e `rspec` passam. Em pull request, valida o build e o smoke test sem publicar. Em push pra `main`, publica a imagem no GitHub Container Registry (`ghcr.io/hirley/task_keeper_api`), usando o `GITHUB_TOKEN` automático do Actions — não exige nenhum secret configurado manualmente. + + **O smoke test** existe porque validar que a imagem *builda* não diz nada sobre ela *subir*: entrypoint, `db:prepare`, permissões do usuário não-root, healthcheck, publicação de porta e variável de ambiente faltando só falham em runtime. + + Dos três bugs listados na seção "Docker", os dois primeiros **não** são exemplo disso — quebravam durante o `docker build`, e o job de build já os pegava. O terceiro é: `bin/jobs` sem bit de execução buildava perfeitamente e derrubava o `worker` com `exit 126` ao subir. **Foi encontrado por este smoke test na primeira vez que ele rodou**, num repositório onde a imagem publicada em `main` já estava assim havia dois merges. + + O job sobe `db` + `web` + `worker` e checa cinco coisas: + + | Verificação | O que quebraria sem ela | + |---|---| + | `up --wait db web` volta sem erro | container que morre no boot, entrypoint quebrado, healthcheck quebrado, `db:prepare` falhando | + | `curl` em `/acessibilidade` de fora do container | porta não publicada, Puma escutando só em loopback | + | a linha de chave efêmera no log do entrypoint | o caminho "sem `SECRET_KEY_BASE`" da v2.0.0, que nada exercitava | + | o `worker` ainda está `running` depois de subir | worker que morre no boot — dá o exit code na hora, em vez de virar um timeout obscuro | + | o `worker` executa um job enfileirado pelo `web` | worker que sobe e não consome nada — invisível, porque a tela responde normal e a entrega só não acontece | + + O `--wait` é pedido só para `db` e `web`: ele exige healthcheck em todo serviço nomeado, e o `worker` não tem um (não serve HTTP — ver `docker-compose.yml`). Não inventamos um healthcheck de processo só para satisfazê-lo; quem prova que o worker está vivo é a última linha da tabela, que mede o trabalho em vez do pulso. + + O `SECRET_KEY_BASE` do job é vazio **de propósito**: é o que faz o entrypoint gerar a chave efêmera. O job enfileirado é `WebhookDeliveryJob` para uma assinatura inexistente, que `WebhookDelivery` recusa antes de tocar a rede — o que se verifica é o percurso (web → Postgres → worker), não a entrega. + + A imagem é buildada uma vez e carregada no daemon local (`load`), o smoke test roda contra ela, e só depois vem a etapa que publica — que acerta o cache inteiro e custa segundos, e só existe em push (em pull request seria um build descartado). **Uma imagem que não sobe nunca chega ao registry.** + + Custo medido no runner: o smoke test em si leva **~26s** (18s esperando `db`+`web` ficarem saudáveis, 3s subindo o worker, 3s para o job ir do `web` ao `worker`, 1s de teardown). O que pesa é o build com `load`, ~1m45s contra ~1m10s de um build sem exportar a imagem — o preço de testar exatamente o artefato que vai ser publicado. O job inteiro fica em ~2m50s, e o CI completo em ~4min. As tags da imagem dependem do que disparou o build: @@ -141,7 +166,7 @@ O build é validado automaticamente a cada push/PR pelo CI (ver seção "Integra ⚠️ **Passo manual único**: por padrão, um pacote novo no GHCR nasce privado, mesmo em repositório público — depois do primeiro push em `main` que publicar a imagem, é preciso ir em *Package settings* (na página do pacote em `github.com/Hirley?tab=packages`) e trocar a visibilidade pra pública, se quiser puxar a imagem (`docker pull`) sem autenticação. -`SECRET_KEY_BASE` no workflow é um valor fixo só para o boot da aplicação em CI (não é usado em nenhum ambiente real — produção continua exigindo a variável de ambiente própria, como descrito na seção "Docker"). As demais variáveis de banco seguem o mesmo padrão de `.env.example`/`config/database.yml`. +`SECRET_KEY_BASE` no workflow é um valor fixo só para o boot da aplicação em CI (não é usado em nenhum ambiente real — produção continua exigindo a variável de ambiente própria, como descrito na seção "Docker"). As demais variáveis de banco seguem o mesmo padrão de `.env.example`/`config/database.yml`. O job **docker** sobrescreve três delas, porque o `env:` do workflow descreve o ambiente de *teste* e ali o que sobe é a imagem de *produção* — ver o comentário no próprio `ci.yml`. ## Fluxo de desenvolvimento com agentes diff --git a/bin/jobs b/bin/jobs old mode 100644 new mode 100755 diff --git a/config/queue.yml b/config/queue.yml index 7db3348..ba12e55 100644 --- a/config/queue.yml +++ b/config/queue.yml @@ -1,8 +1,10 @@ # Configuração do Solid Queue (ver Gemfile e a seção "Webhooks de saída" # do README). Só vale onde o adapter é :solid_queue — hoje, produção. # -# Um worker só, com 3 threads, é folgado para o volume deste projeto: a -# única coisa que roda em background é a entrega de webhooks. JOB_CONCURRENCY +# Um worker só, com 3 threads, é folgado para o volume deste projeto: o +# que roda em background é a entrega de webhooks, o envio do relatório +# semanal por Telegram e o link de redefinição de senha por Telegram — +# tudo disparado por ação humana numa equipe pequena. JOB_CONCURRENCY # existe para escalar sem mexer no arquivo, se um dia isso mudar. default: &default dispatchers: diff --git a/docker-compose.yml b/docker-compose.yml index c86da8b..0b4d139 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,6 +25,20 @@ x-app-build: &app-build args: RUBY_VERSION: ${RUBY_VERSION:-4.0.6} +# Nome da imagem que web e worker usam. Com `--build` o compose builda e +# marca a imagem com este nome; sem `--build`, ele reaproveita uma imagem +# já existente com esse nome em vez de buildar. +# +# É esse segundo comportamento que o smoke test do CI usa: lá a imagem já +# foi buildada pelo passo anterior do job (com cache), e APP_IMAGE aponta +# pra ela — buildar de novo aqui dobraria o tempo do job pra produzir +# exatamente o mesmo artefato. Ver .github/workflows/ci.yml. +# +# Efeito colateral local: se você já tem uma task-keeper-api:local antiga +# e roda `docker compose up` sem `--build`, sobe a antiga. O README manda +# usar `--build`, que é o que resolve. +x-app-image: &app-image ${APP_IMAGE:-task-keeper-api:local} + x-app-env: &app-env # Sem valor default de propósito: quando esta variável chega vazia, # bin/docker-entrypoint gera uma chave efêmera só para a execução @@ -68,6 +82,7 @@ services: web: build: *app-build + image: *app-image ports: - "${PORT:-3000}:3000" environment: *app-env @@ -79,6 +94,7 @@ services: worker: build: *app-build + image: *app-image command: ["./bin/jobs"] environment: *app-env volumes: