Files
pn-new-crm/docker-compose.prod.yml
Matt 0ea8d94d26 fix(audit-wave-10): build-auditor fixes — CSP, server externals, healthcheck
Address the highest-leverage CRITICAL/HIGH/MEDIUM items from the
build-auditor that weren't already covered by Wave 1 (EMAIL_REDIRECT_TO
production guard) or the existing `.dockerignore`.

**C3 — socket.io in standalone trace**
- Add socket.io + @socket.io/redis-adapter to serverExternalPackages
  in next.config so the build system sees the dependency (the custom
  server is the only importer, no Next route touches it).
- Belt-and-braces: COPY both from the deps stage into the runner stage
  of Dockerfile, mirroring the audit's suggested fix.

**H1 — CSP `'unsafe-inline'` in prod**
- Audit recommends nonce-based scripts. Implementing nonces requires
  middleware that emits a per-request nonce + threading it through
  Next's RSC bootstrap + Server Actions. Out of scope for this wave;
  documented the rationale at the CSP definition so the next pass
  knows where to start, and noted that the in-the-wild XSS surfaces
  are already closed via escapeHtml/escapeUrl in the email + webhook
  pipelines.

**H2 — NEXT_PUBLIC_APP_URL validation**
- Add `NEXT_PUBLIC_APP_URL: z.string().url()` to the env schema so a
  missing build-time value fails validation instead of silently
  inlining the empty string into the client bundle and breaking
  multi-origin deploys.

**M3 — serverExternalPackages completeness**
- Add imapflow, mailparser, pdf-lib, sharp, tesseract.js,
  @react-pdf/renderer, unpdf — all heavy native/CJS-leaning
  server-only deps that should not be route-traced.

**H5 — healthcheck PORT templatization**
- docker-compose.{,prod.}yml: replace hardcoded
  `http://localhost:3000/api/health` with `${PORT:-3000}` so
  overriding PORT via .env doesn't put the container into a
  restart loop.

**M9 — NODE_ENV=production in builder**
- Dockerfile builder stage now sets NODE_ENV=production above
  `RUN pnpm build` so the prod-only branches in next.config
  (CSP, etc.) compile deterministically.

**M7 — HEALTHCHECK directive in image**
- Add image-level HEALTHCHECK to the app Dockerfile (mirrors the
  one in Dockerfile.worker for Redis) so the image is
  self-describing for non-compose orchestrators.

Items already addressed prior to this wave:
- C1 (.dockerignore exists, comprehensive)
- C2 (EMAIL_REDIRECT_TO production refusal — Wave 1)
- H4 (compose resource + log limits — already in prod compose)

Tests 1315/1315 throughout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:30:22 +02:00

123 lines
3.2 KiB
YAML

services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: port_nimara_crm
POSTGRES_USER: ${DB_USER:-crm}
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
- ./docker/postgres/init.sql:/docker-entrypoint-initdb.d/01-init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-crm} -d port_nimara_crm"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
# build-auditor HIGH: bound memory + log rotation so a stuck query or
# noisy log doesn't fill the host disk. Postgres respects shared
# buffers env via init.sql; the hard limit here is the container
# ceiling.
deploy:
resources:
limits:
memory: 2g
logging:
driver: json-file
options:
max-size: "20m"
max-file: "5"
networks:
- internal
redis:
image: redis:7-alpine
command: redis-server --requirepass ${REDIS_PASSWORD} --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
- redisdata:/data
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
deploy:
resources:
limits:
memory: 512m
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
networks:
- internal
crm-app:
image: code.letsbe.solutions/letsbe/pn-new-crm/crm-app:latest
env_file: .env
ports:
- "7100:3000"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
# build-auditor H5: env.PORT is configurable (default 3000), so
# template the port into the healthcheck URL. Otherwise overriding
# PORT=8080 via .env makes the container healthy-check itself on
# the wrong port and enter a restart loop.
test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:${PORT:-3000}/api/health"]
interval: 15s
timeout: 5s
retries: 3
# Give the SIGTERM handler in src/server.ts time to drain in-flight
# HTTP requests, close Socket.io, and disconnect Redis before Docker
# SIGKILLs the process. The internal hard timeout is 25s.
stop_grace_period: 30s
restart: unless-stopped
deploy:
resources:
limits:
memory: 1g
logging:
driver: json-file
options:
max-size: "20m"
max-file: "5"
networks:
- internal
crm-worker:
image: code.letsbe.solutions/letsbe/pn-new-crm/crm-worker:latest
env_file: .env
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
# Match the app: BullMQ jobs need time to finish or be released back
# to the queue when worker.ts handles SIGTERM.
stop_grace_period: 30s
restart: unless-stopped
deploy:
resources:
limits:
memory: 1g
logging:
driver: json-file
options:
max-size: "20m"
max-file: "5"
networks:
- internal
volumes:
pgdata:
redisdata:
networks:
internal:
driver: bridge