All checks were successful
CD / Build and push images (push) Successful in 4m3s
CI / Lint, typecheck, test (push) Successful in 3m5s
CI / Auth e2e pack (push) Successful in 4m7s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m17s
CD / Promote to Int (push) Successful in 12s
Server-side PDF export for reading/sharing (ADR 0009), rendered by a new internal Gotenberg (headless Chromium) sidecar. - Sidecar: `gotenberg/gotenberg:8` in the compose stack (internal, pinned, healthcheck); api `GOTENBERG_URL` env; a `renderer` readyz check at warning-level (mirrors the converter) so PDF export degrades gracefully when Gotenberg is down without failing readyz. - Export HTML: `buildPdfHtml` renders a self-contained document (no app chrome) — the page's content with images inlined as data URIs, the pond's fonts inlined as base64 `@font-face` + applied via CSS variables (ADR 0016), print CSS (A4, page-break rules, a title header), and page numbers from Gotenberg's footer. Plugin-block fallbacks are a marked TODO(#79) for M7. - Fonts in the api image: the api Dockerfile now bakes the font catalog in (`build-fonts.mjs` with FONTS_OUT) so the exporter can read a pond's chosen WOFF2 and inline them; a missing file falls back to the system stack. - Job flow: `POST /pages/:id/export {format: pdf}` builds the HTML (read permission checked by the guard) and enqueues an `export_pdf` job on the #62 queue with the HTML as input; the worker branches `to === 'pdf'` to the `GotenbergRenderer` (html → pdf) instead of pandoc, retrying an unreachable sidecar and failing a refused render (`renderer_unavailable`/`render_failed`, de+en). The client polls and downloads `GET /jobs/:id/result`. - Frontend: the page-menu PDF button is now a real export (PDF added to EXPORT_FORMATS; the disabled placeholder removed). - Tests: export.service.db PDF cases (HTML has title/font-variable/inlined image; renderer-down fails with `render_failed`); e2e PDF export self-skips without a Gotenberg sidecar (like the .docx case). Verified locally against real Gotenberg — a valid PDF with the pond font embedded. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
164 lines
5.1 KiB
YAML
164 lines
5.1 KiB
YAML
# Production Compose stack — one file for every stage and for self-hosters.
|
|
# Configuration comes from .env (see .env.example); the host reverse proxy
|
|
# routes to the two published localhost ports (deployment.md §Compose).
|
|
#
|
|
# Networks: `frontend` is what the reverse proxy reaches (via published
|
|
# ports); `internal` connects api/collab to db and (later) the converter
|
|
# sidecars, which are never exposed.
|
|
|
|
name: ${COMPOSE_PROJECT_NAME:-dorfteich}
|
|
|
|
x-logging: &logging
|
|
logging:
|
|
driver: json-file
|
|
options:
|
|
max-size: '10m'
|
|
max-file: '5'
|
|
|
|
services:
|
|
web:
|
|
image: ${IMAGE_PREFIX:-dorfteich}-web:${TAG:-latest}
|
|
build:
|
|
context: ../..
|
|
dockerfile: apps/web/Dockerfile
|
|
args:
|
|
APP_VERSION: ${TAG:-latest}
|
|
restart: unless-stopped
|
|
ports:
|
|
- '127.0.0.1:${WEB_PORT:-8100}:8080'
|
|
networks: [frontend]
|
|
depends_on:
|
|
api:
|
|
condition: service_started
|
|
<<: *logging
|
|
|
|
api:
|
|
image: ${IMAGE_PREFIX:-dorfteich}-api:${TAG:-latest}
|
|
build:
|
|
context: ../..
|
|
dockerfile: apps/api/Dockerfile
|
|
args:
|
|
APP_VERSION: ${TAG:-latest}
|
|
restart: unless-stopped
|
|
environment:
|
|
NODE_ENV: production
|
|
PORT: '3000'
|
|
LOG_LEVEL: ${LOG_LEVEL:-info}
|
|
DATABASE_URL: postgresql://dorfteich:${POSTGRES_PASSWORD:?set in .env}@db:5432/dorfteich
|
|
# Signs the short-lived collaboration tokens; the collab service below
|
|
# verifies them, so both MUST carry the same value (issue #34).
|
|
COLLAB_TOKEN_SECRET: ${COLLAB_TOKEN_SECRET:?set in .env}
|
|
# Public URL of this stage — e-mail links and the CSRF origin check
|
|
# depend on it matching what browsers actually use.
|
|
APP_BASE_URL: ${APP_BASE_URL:-http://localhost:5173}
|
|
# SMTP relay; defaults are only useful with the dev Mailpit overlay.
|
|
SMTP_HOST: ${SMTP_HOST:-localhost}
|
|
SMTP_PORT: ${SMTP_PORT:-1025}
|
|
SMTP_SECURE: ${SMTP_SECURE:-false}
|
|
SMTP_USER: ${SMTP_USER:-}
|
|
SMTP_PASS: ${SMTP_PASS:-}
|
|
SMTP_FROM: ${SMTP_FROM:-Dorfteich <no-reply@localhost>}
|
|
# Matches the `uploads` volume mount below (ADR 0011).
|
|
UPLOADS_DIR: /data/uploads
|
|
# Internal pandoc-server sidecar for import/export (ADR 0009, issue #62).
|
|
PANDOC_URL: http://pandoc:3030
|
|
# Internal Gotenberg sidecar for PDF export (ADR 0009, issue #67).
|
|
GOTENBERG_URL: http://gotenberg:3000
|
|
ports:
|
|
- '127.0.0.1:${API_PORT:-8101}:3000'
|
|
networks: [frontend, internal]
|
|
volumes:
|
|
- uploads:/data/uploads
|
|
depends_on:
|
|
db:
|
|
condition: service_healthy
|
|
pandoc:
|
|
condition: service_healthy
|
|
gotenberg:
|
|
condition: service_healthy
|
|
<<: *logging
|
|
|
|
collab:
|
|
image: ${IMAGE_PREFIX:-dorfteich}-collab:${TAG:-latest}
|
|
build:
|
|
context: ../..
|
|
dockerfile: apps/collab/Dockerfile
|
|
args:
|
|
APP_VERSION: ${TAG:-latest}
|
|
restart: unless-stopped
|
|
environment:
|
|
NODE_ENV: production
|
|
PORT: '3000'
|
|
LOG_LEVEL: ${LOG_LEVEL:-info}
|
|
DATABASE_URL: postgresql://dorfteich:${POSTGRES_PASSWORD:?set in .env}@db:5432/dorfteich
|
|
# Must match the api's value — this service verifies the tokens it signs.
|
|
COLLAB_TOKEN_SECRET: ${COLLAB_TOKEN_SECRET:?set in .env}
|
|
ports:
|
|
# The host reverse proxy routes /collab here with WebSocket upgrade
|
|
# (deployment.md, deploy/stages.md).
|
|
- '127.0.0.1:${COLLAB_PORT:-8102}:3000'
|
|
networks: [frontend, internal]
|
|
healthcheck:
|
|
test: ['CMD-SHELL', 'wget -q -O /dev/null http://127.0.0.1:3000/healthz || exit 1']
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|
|
depends_on:
|
|
db:
|
|
condition: service_healthy
|
|
<<: *logging
|
|
|
|
db:
|
|
image: postgres:17.5-alpine
|
|
restart: unless-stopped
|
|
environment:
|
|
POSTGRES_USER: dorfteich
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set in .env}
|
|
POSTGRES_DB: dorfteich
|
|
networks: [internal]
|
|
volumes:
|
|
- db-data:/var/lib/postgresql/data
|
|
healthcheck:
|
|
test: ['CMD-SHELL', 'pg_isready -U dorfteich -d dorfteich']
|
|
interval: 10s
|
|
timeout: 3s
|
|
retries: 12
|
|
<<: *logging
|
|
|
|
# Import/export converter (ADR 0009, issue #62): pandoc in HTTP server mode
|
|
# on the internal network only — never exposed. Pinned image; the api reaches
|
|
# it at http://pandoc:3030. `wget` ships in the (busybox-based) image.
|
|
pandoc:
|
|
image: pandoc/core:3.6
|
|
command: ['server']
|
|
restart: unless-stopped
|
|
networks: [internal]
|
|
healthcheck:
|
|
test: ['CMD-SHELL', 'wget -q -O /dev/null http://127.0.0.1:3030/version || exit 1']
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|
|
<<: *logging
|
|
|
|
# PDF export renderer (ADR 0009, issue #67): Gotenberg wraps headless Chromium
|
|
# on the internal network only — never exposed. Pinned image; the api reaches
|
|
# it at http://gotenberg:3000 and posts export HTML to its Chromium route.
|
|
gotenberg:
|
|
image: gotenberg/gotenberg:8
|
|
restart: unless-stopped
|
|
networks: [internal]
|
|
healthcheck:
|
|
test: ['CMD-SHELL', 'curl -sf http://127.0.0.1:3000/health || exit 1']
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|
|
<<: *logging
|
|
|
|
networks:
|
|
frontend:
|
|
internal:
|
|
|
|
volumes:
|
|
db-data:
|
|
uploads:
|