Dev → Produção — Checklist e Estratégia
Análise completa de tudo que precisa mudar antes do primeiro deploy em produção, com estratégia para manter o ambiente local funcionando durante o processo.Estratégia: dois compose, um codebase
A abordagem adotada é manter dois arquivos Docker Compose separados:| Arquivo | Usado quando |
|---|---|
docker-compose.yml | Desenvolvimento local (como está hoje) |
docker-compose.prod.yml | Produção (ou teste de prod localmente) |
docker-compose.yml não é alterado — o ambiente de dev continua funcionando igual. Todas as mudanças de produção vão para o docker-compose.prod.yml e para Dockerfiles separados com multi-stage build.
Para rodar localmente simulando produção:
Arquivos .env usados em produção
O projeto tem três arquivos .env separados — todos precisam existir e estar preenchidos no servidor antes do build:
| Arquivo | Para quê serve | Quem lê |
|---|---|---|
.env (raiz) | Infra Docker: credenciais do Postgres, Redis, Orthanc, ORTHANC_WEBHOOK_SECRET, ACME_EMAIL, ALLOWED_ORIGIN e os NEXT_PUBLIC_* que serão passados ao build do frontend | docker-compose.yml, docker-compose.prod.yml, Caddy |
backend/.env | Laravel: APP_KEY, JWT_SECRET, DB_*, REDIS_*, ORTHANC_URL, ORTHANC_WEBHOOK_SECRET, FRONTEND_URL, SESSION_SECURE_COOKIE=true, APP_DEBUG=false, APP_ENV=production, LOG_LEVEL=error | Laravel/Octane em runtime |
frontend/.env (ou .env.local) | Somente dev. Em produção as NEXT_PUBLIC_* são embutidas no bundle via ARG do Dockerfile (vindas do .env raiz), então nenhum frontend/.env é lido em runtime de prod. Criar frontend/.env.local só é necessário em dev | Next.js em dev |
NEXT_PUBLIC_API_BASE_URL e NEXT_PUBLIC_OHIF_VIEWER_URL são obrigatórias no .env raiz antes do docker compose build, porque ficam compiladas no bundle JavaScript. Mudar depois exige rebuild.
Valores de exemplo estão nos três .env.example versionados. Copiar cada .env.example → .env (ou .env.local no frontend) e preencher com secrets gerados no servidor (openssl rand -hex 32, php artisan jwt:secret).
Checklist por prioridade
🔴 Crítico — segurança (obrigatório antes do deploy)
✅ 1. Cookie JWT sem httpOnly
Problema resolvido:
O token JWT era escrito pelo frontend via document.cookie sem httpOnly, expondo-o a XSS. Qualquer script malicioso na página podia ler e exfiltrar o token.
Desafio técnico — cross-origin em dev:
Frontend (:3000) e backend (:8000) estão em origens diferentes. Um cookie httpOnly setado pelo backend fica scoped para :8000 — o JS em :3000 não lê e o middleware Next.js em :3000 também não. A solução foi usar dois cookies com funções separadas:
pacs_access_token→ httpOnly, scoped ao backend, enviado automaticamente viawithCredentials: truepacs_user_role→ não-httpOnly, scoped ao frontend, lido pelo middleware Next.js para controle de rotas
AuthController.php:
app/Http/Middleware/SetBearerFromCookie.php (novo):
bootstrap/app.php via $middleware->prependToGroup('api', ...).
Backend — config/cors.php (novo):
FRONTEND_URL no backend/.env.
Frontend — client.ts:
cookies.ts:
- Remove toda manipulação do
pacs_access_token - Mantém apenas leitura/escrita do
pacs_user_role
auth-store.ts:
bootstrapSession: chamaGET /mediretamente (sem ler cookie JS) — se o cookie httpOnly for válido, o backend autentica; se não, 401loginWithCredentials: não salva mais o token no estado — apenas o userlogout: limpa só o role cookie (backend limpa o JWT via Set-Cookie)
proxy.ts:
- Usa
pacs_user_rolecomo indicador de sessão (substituiupacs_access_token) - Lógica de redirecionamento por role mantida
SESSION_SECURE_COOKIE=false, cookie funciona em HTTP. Não precisa de HTTPS.
Como funciona em prod: SESSION_SECURE_COOKIE=true, cookie exige HTTPS.
✅ 2. CORS * no nginx proxy
Problema resolvido:
nginx.conf foi transformado em nginx.conf.template. O ${ALLOWED_ORIGIN} é substituído pelo envsubst antes do nginx iniciar, via command no compose. Os demais $var do nginx ($host, $remote_addr, etc.) ficam intactos porque envsubst recebe a lista explícita de variáveis a substituir.
docker/orthanc-cors-proxy/nginx.conf.template:
docker-compose.yml (dev):
docker-compose.prod.yml (prod):
.env.example:
ALLOWED_ORIGIN deve ser a origem do OHIF viewer (:3001), não do frontend (:3000). É o browser abrindo o OHIF que faz as requisições CORS para o proxy — o header Origin que o nginx recebe é http://localhost:3001.
Como funciona em dev: ALLOWED_ORIGIN usa o fallback http://localhost:3001 — sem precisar definir no .env.
Como funciona em prod: definir ALLOWED_ORIGIN=https://viewer.seudominio.com.br no .env do servidor.
✅ 3. PostgreSQL e Redis expostos ao host
Problema atual (docker-compose.yml):
docker-compose.prod.yml: simplesmente não declarar os ports: — os serviços continuam acessíveis internamente via pacs_network.
Em dev: manter os ports expostos é útil para usar ferramentas locais como TablePlus, RedisInsight, etc.
✅ 4. Redis sem senha
Problema: Redis acessível sem autenticação dentro da rede Docker. Implementado com expansão condicional — só ativa--requirepass se REDIS_PASSWORD não estiver vazia. Com isso, o mesmo docker-compose.prod.yml funciona em dev (sem senha) e em prod (com senha):
.env raiz (prod): definir REDIS_PASSWORD=senha-forte-gerada
backend/.env (prod): definir REDIS_PASSWORD=senha-forte-gerada (mesmo valor)
worker/main.py: já lê REDIS_PASSWORD do ambiente e passa na conexão Redis.
docker-compose.yml (dev): sem senha — continua igual.
✅ 5. Secrets fracos
Resolvido no servidor: todos os secrets foram gerados comopenssl rand -hex 32 e php artisan jwt:secret. Nenhum valor entra no repositório — ficam apenas no .env e backend/.env do servidor.
🟠 Alto — build e performance (obrigatório antes do deploy)
✅ 6 e 7. Dockerfiles de produção (frontend, backend, worker)
Problema: todos os serviços usavam volume mount do código fonte enext dev / sem build otimizado.
O que foi criado:
docker/php/Dockerfile.prod:
- Copia o código do backend para dentro da imagem (
COPY backend/ .) composer install --no-dev --optimize-autoloader— sem pacotes de dev, autoloader otimizado./vendor/bin/rr get-binary— baixa o binário do RoadRunner (está no.gitignore, precisa ser baixado no build)php artisan config:cache && route:cache && view:cache— caches de produção do Laravelchown www-data storage bootstrap/cache— permissões corretas
docker/nextjs/Dockerfile.prod:
- 3 stages:
deps(npm ci) →builder(npm run build) →runner(node server.js) NEXT_PUBLIC_*passadas comoARGno build — obrigatório pois são embutidas no bundle em compile time- Runtime com usuário não-root
nextjs:nodejs - Imagem final usa apenas o output
.next/standalone— muito menor que a de dev - Requer
output: "standalone"nonext.config.ts— adicionado
docker/python/Dockerfile.prod:
- Código baked na imagem, sem volume mount
docker-compose.prod.yml — overrides de build:
docker-compose.yml → Dockerfiles originais com hot reload e volume mount.
✅ 8. APP_DEBUG, LOG_LEVEL e DEBUG_WORKER
| Variável | Dev | Prod |
|---|---|---|
APP_DEBUG | true | false |
LOG_LEVEL | debug | error |
DEBUG_WORKER | 1 | 0 ou removido |
VERBOSE_ENABLED (Orthanc) | true | false ou removido |
APP_ENV | local | production |
✅ 9. Octane com workers fixos
Atual:--workers=4
Prod: --workers=auto (usa todos os CPUs do servidor)
🟡 Médio — infraestrutura (idealmente antes, pode ser pós-deploy inicial)
✅ 10. HTTPS / TLS
Implementado com Caddy como container nodocker-compose.prod.yml.
Arquitetura final — Caddy roteia 3 subdomínios e fecha as portas de todos os serviços internos:
/dicom-proxy/), eliminando a necessidade de um 4º subdomínio. O app-config.prod.js do OHIF aponta para esse path.
Arquivos criados:
docker/caddy/Caddyfile— configuração dos 3 virtual hostsdocker/ohif/app-config.prod.js— URLs de produção, patch de token atualizado para checarDICOM_PROXY_ROOTao invés de:8043
docker-compose.prod.yml — Caddy service:
.env raiz do servidor:
caddy_data.
✅ 11. Rate limiting no login
Implementado embackend/routes/api.php:
✅ 12. allowedDevOrigins no next.config.ts
frontend/next.config.ts:
allowedDevOrigins só é lida pelo servidor de dev (next dev), não tem efeito em produção mesmo que fique no arquivo. Mas é boa prática remover.
13. JWT TTL e renovação
Atual: cookie commax-age de 30 dias, sem refresh.
Recomendação para produção:
- TTL do JWT: 8 horas (configurável via
JWT_TTLnoconfig/jwt.php) - Implementar refresh automático: quando o interceptor do Axios receber 401, tentar
POST /api/refreshantes de redirecionar para o login
🔵 Não bloqueante (backlog pós-deploy)
14. Backup dos volumes
| Volume | Conteúdo | Estratégia |
|---|---|---|
pacs_pg_data | Banco de dados completo | pg_dump em cron diário → S3 |
pacs_orthanc_data | Imagens DICOM originais | Snapshot de volume → S3 |
pacs_storage | Thumbnails gerados | Podem ser regerados — baixa prioridade |
15. Resource limits nos containers
✅ 16. Health checks no backend e frontend
Implementado nodocker-compose.prod.yml:
- Backend:
curl -f http://localhost:8000/upa cada 30s - Frontend:
node http.geta cada 30s
✅ 17. Queue worker Laravel automático
O jobProcessStudyMetadata (disparo pelo webhook do Orthanc) rodava manualmente. Foi adicionado um container dedicado pacs_queue nos dois composes rodando php artisan queue:work --tries=3.
18. Reprocessamento de estudos com job falho (conhecido)
Problema: quando o Orthanc recebe uma imagem DICOM e o jobProcessStudyMetadata falha (ex: configuração ausente, timeout), a imagem fica salva no Orthanc mas o estudo não aparece no sistema. Como o Orthanc não envia webhook para imagens duplicadas, o job não é re-disparado automaticamente.
Workaround disponível — o comando pacs:requeue re-enfileira todos os estudos do Orthanc que não estão indexados:
queue:failed) e alertar via log ou webhook quando houver falhas acumuladas.
✅ 19. OHIF carregando config padrão (CloudFront) em vez do proxy interno
Problema identificado em homolog: odocker-compose.prod.yml tentava montar o app-config.prod.js via volumes: !reset + bind mount, mas a imagem ohif/app:latest não recebia o mount (Mounts: [] no docker inspect). Com isso o OHIF usava seu config padrão apontando para d14fa38qiwhyfd.cloudfront.net — o servidor demo público da OHIF. O 403 retornado era da CloudFront rejeitando o estudo (não CORS, não backend).
Como o entrypoint do OHIF funciona (/usr/src/entrypoint.sh):
- Se
APP_CONFIGenv var estiver definida, escreve seu conteúdo emapp-config.js - Se
app-config.jsexistir e não estiver vazio: gzipa → criaapp-config.js.gz→ esvaziaapp-config.jscomtouch - nginx com
gzip_static onserve o.gz— o.jssempre fica em 0 bytes após o boot
docker/ohif/Dockerfile.prod que bake o config na imagem antes do entrypoint rodar:
app-config.js.gz e esvazia o .js. nginx serve o .gz com o config correto.
O docker-compose.prod.yml usa build: dockerfile: ./docker/ohif/Dockerfile.prod em vez de volume mount.
Observação: após rebuild do OHIF, limpar cache do browser (Ctrl+Shift+R ou aba anônima) — o browser pode servir o config antigo da CloudFront por horas.
✅ 20. Volumes não montados no backend/worker em produção (!reset vs !override)
Problema: docker inspect pacs_backend/pacs_worker mostrava Mounts: [] mesmo com volumes declarados no docker-compose.prod.yml. Thumbnails escritos pelo worker não apareciam no backend.
Causa raiz: no Docker Compose, !reset aplicado a uma sequência zera a lista para vazia e ignora os itens abaixo. O comportamento correto é:
| Tag | Comportamento |
|---|---|
!reset [] | Substitui por lista vazia — correto para remover ports |
!reset + itens abaixo | Zera para vazio, itens são ignorados — bug silencioso |
!override + itens abaixo | Substitui a lista base pelos novos itens — correto para substituir volumes |
volumes: !reset por volumes: !override nos serviços que precisam de volumes nomeados:
ports: !reset [] continua correto (lista explicitamente vazia).
Resumo de prioridades
| # | Item | Prioridade | Quebra dev? |
|---|---|---|---|
| 1 | Cookie JWT httpOnly | 🔴 Crítico | Não — dev usa HTTP sem Secure |
| 2 | CORS * → domínio real | 🔴 Crítico | Não — variável de ambiente |
| 3 | PostgreSQL/Redis sem ports expostos | 🔴 Crítico | Não — só no compose.prod |
| 4 | Redis com senha | 🔴 Crítico | Não — só no compose.prod |
| 5 | Secrets fortes | ✅ Feito | Não — valores diferentes por env |
| 6 | Frontend multi-stage build | 🟠 Alto | Não — Dockerfile.prod separado |
| 7 | Backend/worker código baked | 🟠 Alto | Não — Dockerfile.prod separado |
| 8 | DEBUG desativado em prod | 🟠 Alto | Não — variáveis de ambiente |
| 9 | Octane —workers=auto | 🟠 Alto | Não — só no compose.prod |
| 10 | HTTPS/TLS | ✅ Feito | Não — Caddy em Docker |
| 11 | Rate limiting no login | 🟡 Médio | Não — throttle permissivo em dev |
| 12 | Remover allowedDevOrigins | 🟡 Médio | Não — afeta só next dev |
| 13 | JWT TTL menor + refresh | ✅ Feito | Não — configurável por env |
| 14 | Backup dos volumes | ✅ Feito | Não — scripts/backup.sh + cron |
| 15 | Resource limits | ✅ Feito | Não — via variáveis de ambiente |
| 16 | Health checks | ✅ Feito | Não — backend e frontend |
| 17 | Queue worker automático | ✅ Feito | Não — container pacs_queue |
| 18 | Reprocessamento de estudos falhos | 🔵 Workaround | Não — pacs:requeue disponível |
| 19 | OHIF carregando config padrão (CloudFront) | ✅ Feito | Não — Dockerfile.prod com bake |
| 20 | Volumes não montados (!reset vs !override) | ✅ Feito | Não — só no compose.prod |
docker-compose.yml (dev) / docker-compose.prod.yml (prod) é justamente para garantir isso.
Ordem de implementação sugerida
V1 Stable — o que entra no primeiro release
Esta seção marca o escopo do primeiro release estável (v1) — o que está incluso e o que fica para versões futuras. Após o deploy em produção, verV1_STABLE.md (criado pós-release) para descrição do estado congelado.
O que está pronto (código)
- ✅ Stack completa Docker Compose (9 containers em dev, +1 Caddy em prod)
- ✅ Ingestão DICOM (Orthanc → webhook → worker Python → PostgreSQL)
- ✅ Multi-tenant com 3 roles (super admin, tenant admin, doutor)
- ✅ Sistema de laudos: rascunho, finalização, correção, audit trail (
ReportRevision) - ✅ Templates de laudo com editor visual + preview A4 fiel ao PDF
- ✅ Presets de texto por tenant (sugestão automática por modalidade/descrição)
- ✅ OHIF Viewer com isolamento multi-tenant via token temporário + nginx auth_request
- ✅ Refactor UI/UX do sistema de laudos concluído (ver
REPORTS_SYSTEM.md) - ✅ Hardening de produção (cookie httpOnly, CORS, rate limiting, HTTPS via Caddy)
Pendente antes do PR develop → main
- Deploy da branch
developem homolog (servidor de staging) - Rodar migrations no homolog
- Validação manual completa via
HOMOLOG_CHECKLIST.md(checklist por persona + fluxos transversais) - Abrir PR
develop → mainapós checklist 100% verde
Após o deploy em produção
- Criar
V1_STABLE.mddescrevendo o estado congelado da v1 (features entregues, limites conhecidos, funcionalidades que virão em v1.x/v2) - Publicar doc Mintlify em mintlify.com (passo 6 do projeto docs — ver
project_mintlify.md)
Fora do escopo da v1 (roadmap)
Refactors grandes planejados depois do release v1 estável. VerMEMBERSHIPS_REFACTOR.md e STORAGE_ARCHITECTURE.md para detalhes:
| Item | Onde está planejado |
|---|---|
Memberships — usuário em múltiplas clínicas/unidades + role referring_physician (médico solicitante) | MEMBERSHIPS_REFACTOR.md |
| Orthanc SQLite → PostgreSQL — index do Orthanc em Postgres para multi-tenant concorrente | STORAGE_ARCHITECTURE.md §2 |
| Object storage — MinIO ou S3 para extrair blobs base64 do PostgreSQL (Fase 1 do tiered storage) | STORAGE_ARCHITECTURE.md §3 |
| Cold storage — arquivamento de estudos antigos (S3 Glacier) após período de retenção legal | STORAGE_ARCHITECTURE.md §3 |
| VPN/WireGuard — infraestrutura para aparelhos enviarem DICOM via túnel isolado (código pronto, infra pendente) | memory/project_vpn_ingestion_plan.md |
| Painel super admin redesign — onboarding guiado, visão consolidada por tenant (depende do refactor de memberships) | memory/project_rbac_refactor.md |
