#!/usr/bin/env bash # # Boot do container do Rails (deploy e todo restart do Docker). # # POR QUE ISSO VIROU UM SCRIPT: antes era um `command:` de uma linha só no # docker-compose.yml. Com `restart: unless-stopped` o boot passou a rodar # sozinho — depois de queda, de reboot do servidor e de cada deploy — e um # passo que falha em silêncio vira loop de reinício sem ninguém entender por # quê. Aqui cada etapa diz o que está fazendo no log do container # (`docker compose logs -f app`). # # ORDEM DOS PASSOS (não troque sem ler o motivo): # 1. relógio — só imprime, para diagnóstico (ver comentário abaixo). # 2. pid órfão — se o container morreu sem limpar, o Puma se recusa a subir. # 3. gems — o volume bundle_cache SOMBREIA os gems da imagem, então # gem nova no Gemfile não chega no container só com --build. # 4. migrations — db:prepare: cria o banco se não existir, roda as migrations # pendentes e só faz seed em banco recém-criado (idempotente). # 5. assets — em production o Propshaft NÃO compila sob demanda: o # arquivo digerido tem que estar em public/assets. # 6. cron — grava o crontab do config/schedule.rb e sobe o daemon. # 7. Puma — processo em primeiro plano (é ele que segura o container). set -uo pipefail log() { echo "[boot] $*"; } # ── 1. Relógio ─────────────────────────────────────────────────────────────── # O container NÃO tem relógio próprio: ele lê o clock do kernel do HOST. Não # adianta rodar NTP aqui dentro (exigiria CAP_SYS_TIME e mexeria na hora do # servidor inteiro) — quem sincroniza é o host, via chrony/NTS; ver # deploy/ntp-seguro.sh. O que é do container é só o FUSO (TZ), fixado no # Dockerfile. Se a linha abaixo aparecer em UTC, o rebuild da imagem não pegou. log "relógio do container: $(date '+%Y-%m-%d %H:%M:%S %Z (%z)') | UTC: $(date -u '+%H:%M:%S')" # ── 2. PID órfão do Puma ───────────────────────────────────────────────────── rm -f tmp/pids/server.pid # ── 3. Gems ────────────────────────────────────────────────────────────────── if ! bundle check >/dev/null 2>&1; then log "gem faltando no volume bundle_cache — rodando bundle install" bundle install || { log "ERRO: bundle install falhou"; exit 1; } fi # ── 4. Migrations ──────────────────────────────────────────────────────────── # ANTES de tentar, confere o DATABASE_URL. Este projeto NÃO tem # config/database.yml: a conexão inteira sai dessa única variável (as DB_HOST, # DB_USER etc. do .env não são lidas por ninguém — são só documentação). # # POR QUE ESTA CHECAGEM EXISTE: em 27/08/2026 o .env foi recriado a partir do # .env.example e o DATABASE_URL ficou com o valor de exemplo. O resultado foram # 10 tentativas cuspindo 40 linhas de backtrace até uma # `URI::InvalidURIError: ... SEU_IP` no fim — ilegível no log do Container # Manager. Placeholder e URL vazia não são "banco fora do ar": nenhuma espera # resolve, então falha na hora e diz o que fazer. if [ -z "${DATABASE_URL:-}" ]; then log "ERRO: DATABASE_URL vazia. Sem config/database.yml, é ela que define o banco." log " Formato: postgresql://USUARIO:SENHA@HOST:5432/NOME_DO_BANCO" exit 1 fi case "$DATABASE_URL" in *SEU_IP*|*senha_segura*|*SEU_HOST*) log "ERRO: DATABASE_URL ainda está com o valor de EXEMPLO do .env.example:" log " $(printf '%s' "$DATABASE_URL" | sed 's|://[^@]*@|://***:***@|')" log " Preencha com o IP, o usuário e a senha reais do PostgreSQL." exit 1 ;; esac # O PostgreSQL é EXTERNO ao compose: num reboot do servidor o Rails pode subir # antes do banco aceitar conexão. Sem a espera abaixo o container sairia com # erro e o `restart: unless-stopped` ficaria reiniciando em loop — parecendo # problema de migration quando é só ordem de boot. 10 tentativas × 6s = 1 min. tentativas=10 for i in $(seq 1 $tentativas); do log "migrations (db:prepare) — tentativa $i/$tentativas" if bundle exec rails db:prepare; then log "banco pronto e migrations aplicadas" break fi if [ "$i" -eq "$tentativas" ]; then # Aqui é falha DE VERDADE (migration quebrada ou credencial errada): sair # com erro é melhor do que subir a aplicação contra um schema desatualizado. log "ERRO: db:prepare falhou após $tentativas tentativas — veja o erro acima" exit 1 fi log "banco indisponível ou migration falhou; nova tentativa em 6s" sleep 6 done # ── 5. Assets ──────────────────────────────────────────────────────────────── # POR QUE ISTO EXISTE: com RAILS_ENV=production — que é como o teste e o servidor # rodam hoje — o Propshaft não serve asset dinamicamente. Ele resolve o caminho # pelo public/assets/.manifest.json e quem entrega o arquivo é o servidor de # estáticos; sem o arquivo em disco a requisição cai no roteador do Rails e volta # 404 (a página public/404.html, não um erro de asset — por isso é difícil de # reconhecer). Nada no deploy gerava esses arquivos: a linha do Dockerfile está # comentada de propósito (o bind-mount `.:/app` do compose cobriria o que ela # gerasse na imagem) e o boot não tocava no assunto. O que existia em # public/assets no servidor era o resto de algum precompile manual antigo. # # CASO REAL (28/08/2026): o HTML pedia # /assets/controllers/carrossel_controller-913b47f7.js e esse arquivo não existia # mais -> 404 -> "Failed to fetch dynamically imported module" no console, com o # carrossel (a paginação por tamanho de tela) e o editor de romaneio MORTOS no # ambiente de teste. Só esses dois quebraram porque são os dois mais recentes: # os outros dez controllers ainda vinham do cache do Cloudflare (max-age de 1 # ano), o que escondeu o problema por dias e fez parecer defeito de dois arquivos. # # A LIMPEZA ANTES do precompile é de propósito: sem ela um manifesto antigo # convive com arquivos novos e a divergência volta na primeira alteração de JS. # Assim manifesto e arquivos saem sempre da MESMA execução. É `rm -rf` e não # `rails assets:clobber` porque faz exatamente o mesmo (essas duas pastas), sem # pagar um boot inteiro do Rails só para apagar diretório. if [ "${RAILS_ENV:-development}" = "production" ]; then log "assets: limpando restos e pré-compilando (public/assets)" rm -rf public/assets tmp/cache/assets if bundle exec rails assets:precompile; then log "assets prontos" else # NÃO sai com erro: sem JS o sistema fica ruim, mas ainda dá para entrar e # ler os dados — e um `exit 1` aqui viraria loop de reinício com o # `restart: unless-stopped`, deixando TODO MUNDO de fora. O aviso é gritado # no log para não repetir a história de descobrir dias depois. log "ERRO: assets:precompile falhou — o sistema vai subir SEM JS/CSS novos." log " Rode 'docker compose exec app bundle exec rails assets:precompile'" log " e veja o erro completo." fi else # Em development o Propshaft calcula o digest do arquivo atual a cada # requisição. Sobra de precompile faz ele voltar a resolver pelo manifesto, e # aí toda alteração de JS/CSS só aparece depois de recompilar — parece que o # navegador está com cache quando não está. rm -rf public/assets fi # ── 6. Cron (best-effort) ──────────────────────────────────────────────────── # Best-effort de propósito: agendamento quebrado atrasa um relatório, mas NUNCA # pode impedir o sistema de subir. As variáveis do banco chegam ao job pelo # dotenv quando o rake carrega o Rails — o cron não precisa herdar o ENV. if bundle exec whenever --update-crontab && cron; then log "cron no ar (jobs do config/schedule.rb, no fuso ${TZ:-?})" else log "AVISO: cron/crontab indisponível (rode com --build) — seguindo sem agendamento" fi # ── 7. Puma ────────────────────────────────────────────────────────────────── log "subindo o Rails" exec bundle exec rails s -b 0.0.0.0