Imagem Docker e compose reproduzíveis para homologação
Não existia nenhum artefato de deploy: sem Dockerfile, sem compose, sem pin de Node. O runbook pedia `npm run start` e `npm run worker` à mão, o que não é um ambiente reproduzível, e o worker dependia de `tsx`, uma devDependency, que `npm ci --omit=dev` removeria. - Dockerfile multistage. Web e worker saem da mesma imagem: mesmo código, mesmas dependências, só o subcomando difere. - `tsx` foi para `dependencies`. Conferido em diretório limpo: os 13 pacotes externos que src e scripts usam existem no install de produção. - A sobreposição das duas árvores de node_modules é segura e foi medida: 318 pacotes em produção contra 153 traçados, zero divergência de versão entre os compartilhados, e 3 que existem só na traçada, que o COPY mescla sem remover. O package.json do standalone não é sobrescrito. - Entrypoint com subcomandos, que garante `node --import tsx` em todos os scripts. Usar `node` direto convidaria de volta o defeito do SIGTERM. - Healthcheck de liveness na web e de heartbeat no worker. - Compose local reproduz a topologia com Postgres e Redis de verdade. Compose de homologação não declara Postgres (é o banco gerenciado), usa Redis isolado com senha e volume próprios, publica porta só em 127.0.0.1 e tem nome de projeto, rede e volume exclusivos. Nenhum serviço de outro aplicativo do ecossistema é declarado, tocado ou reiniciado. - `.env.staging.example` sem segredo, `.nvmrc`, `engines` e `packageManager`. - `config/staging-seed.json` e `.env.staging` no gitignore. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ca8a1112f0
commit
dea510c3b4
11 files changed
+598
-28
No files matched your search
@@ -0,0 +1,118 @@
|
||||
# Homologação em hml-ada.elopro.app, no servidor compartilhado do ecossistema.
|
||||
#
|
||||
# Regras que este arquivo respeita, por construção:
|
||||
# - não declara, não reinicia e não depende de nenhum serviço de outro
|
||||
# aplicativo: rede, volume e nome de projeto são exclusivos deste módulo;
|
||||
# - não expõe porta em 0.0.0.0: só 127.0.0.1, para o Nginx fazer o proxy;
|
||||
# - Redis é um contêiner isolado deste aplicativo, com senha e volume próprios,
|
||||
# nunca o Redis compartilhado dos outros aplicativos;
|
||||
# - Postgres não é declarado aqui: é o banco gerenciado do Elo Pro, informado
|
||||
# em DATABASE_URL. Nenhuma migration roda no start — é passo manual aprovado.
|
||||
#
|
||||
# docker compose -f docker-compose.staging.yml --env-file .env.staging up -d --build
|
||||
# docker compose -f docker-compose.staging.yml --env-file .env.staging \
|
||||
# run --rm web migrate --dry-run
|
||||
|
||||
name: adaptador-planilhas-hml
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- "redis-server"
|
||||
- "--requirepass"
|
||||
- "${ADAPTER_REDIS_PASSWORD:?defina ADAPTER_REDIS_PASSWORD}"
|
||||
- "--save"
|
||||
- "900"
|
||||
- "1"
|
||||
- "--appendonly"
|
||||
- "no"
|
||||
- "--maxmemory"
|
||||
- "256mb"
|
||||
- "--maxmemory-policy"
|
||||
- "noeviction"
|
||||
ports:
|
||||
- "127.0.0.1:${ADAPTER_REDIS_PORT:-56380}:6379"
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "redis-cli -a \"$$REDIS_PASSWORD\" ping | grep -q PONG"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
environment:
|
||||
REDIS_PASSWORD: ${ADAPTER_REDIS_PASSWORD}
|
||||
networks: [adaptador-hml]
|
||||
|
||||
web:
|
||||
image: ${ADAPTER_IMAGE:-adaptador-planilhas:local}
|
||||
build:
|
||||
context: .
|
||||
target: runner
|
||||
restart: unless-stopped
|
||||
command: ["web"]
|
||||
env_file:
|
||||
- .env.staging
|
||||
environment:
|
||||
ADAPTER_ENV: staging
|
||||
NODE_ENV: production
|
||||
PORT: "3000"
|
||||
HOSTNAME: 0.0.0.0
|
||||
ports:
|
||||
- "127.0.0.1:${ADAPTER_WEB_PORT:-3110}:3000"
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
- node
|
||||
- -e
|
||||
- "fetch('http://127.0.0.1:3000/api/health/live').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
start_period: 20s
|
||||
retries: 3
|
||||
logging: &logging
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "5"
|
||||
networks: [adaptador-hml]
|
||||
|
||||
worker:
|
||||
image: ${ADAPTER_IMAGE:-adaptador-planilhas:local}
|
||||
build:
|
||||
context: .
|
||||
target: runner
|
||||
restart: unless-stopped
|
||||
command: ["worker"]
|
||||
env_file:
|
||||
- .env.staging
|
||||
environment:
|
||||
ADAPTER_ENV: staging
|
||||
NODE_ENV: production
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
# SIGTERM chega ao processo do worker porque o entrypoint usa
|
||||
# `node --import tsx` (sem processo filho). O handler espera o tick corrente
|
||||
# para o `finally` liberar o lock. 30s cobrem um job médio.
|
||||
stop_grace_period: 30s
|
||||
healthcheck:
|
||||
test: ["CMD", "/app/docker/entrypoint.sh", "worker-health"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
start_period: 20s
|
||||
retries: 3
|
||||
logging: *logging
|
||||
networks: [adaptador-hml]
|
||||
|
||||
networks:
|
||||
adaptador-hml:
|
||||
name: adaptador-planilhas-hml
|
||||
|
||||
volumes:
|
||||
redis-data:
|
||||
name: adaptador-planilhas-hml-redis
|
||||
Reference in new issue
Block a user