dorfteich/apps/api/Dockerfile
Claude Opus 4.8 8a68ef68e7
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
Add PDF export via Gotenberg (#67)
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
2026-07-10 12:11:12 +02:00

40 lines
1.8 KiB
Docker

# Build context is the repository root (workspace build):
# docker build -f apps/api/Dockerfile .
FROM node:22.15-alpine AS build
WORKDIR /repo
RUN npm install -g pnpm@11
COPY pnpm-workspace.yaml pnpm-lock.yaml package.json tsconfig.base.json ./
COPY packages/shared ./packages/shared
COPY apps/api ./apps/api
COPY deploy/fonts ./deploy/fonts
RUN pnpm install --frozen-lockfile --filter @dorfteich/api... \
&& pnpm --filter @dorfteich/shared build \
&& pnpm --filter @dorfteich/api build \
# Bake the self-hosted font catalog in so the PDF exporter can inline a
# pond's fonts as base64 (ADR 0016). Same download as the web image; fails
# the build if a family lacks license info.
&& FONTS_OUT=/repo/fonts node deploy/fonts/build-fonts.mjs \
# Self-contained production bundle (prod deps only, incl. the prisma CLI
# needed for migrate-on-start) at /out.
&& pnpm --filter @dorfteich/api deploy --prod --legacy /out \
&& cp -r apps/api/dist /out/dist \
&& cp -r /repo/fonts /out/fonts
FROM node:22.15-alpine
ARG APP_VERSION=0.0.0-dev
ENV NODE_ENV=production APP_VERSION=${APP_VERSION}
WORKDIR /app
COPY --from=build --chown=node:node /out /app
# Generate the Prisma client for this image's platform.
RUN node node_modules/prisma/build/index.js generate
# A fresh named volume mounted at /data/uploads is created root-owned;
# pre-creating it here (Docker copies an image directory's ownership into
# a new volume on first mount) lets the non-root `node` user write to it.
RUN mkdir -p /data/uploads && chown -R node:node /data/uploads
USER node
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD node -e "fetch('http://127.0.0.1:'+(process.env.PORT||3000)+'/api/v1/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
CMD ["node", "dist/main.js"]