# Craftvia — Next.js App (Multi-Stage-Build) # # Base-Image: node:22-slim (Debian/glibc) statt node:22-alpine (musl). # Begründung (F-11): Mit einem glibc-Base greifen die im Lockfile hinterlegten # plattformspezifischen Optional-Deps (lightningcss/@tailwindcss/oxide *-linux-*-gnu, # ebenso die glibc-Prebuilds von @node-rs/argon2 und sharp) zuverlässig. Dadurch # funktioniert `npm ci` (Lockfile bindend, reproduzierbar) OHNE Lockfile-Änderung — # das package-lock.json enthält bereits alle vier Linux-Varianten (x64/arm64 × gnu/musl). # Der frühere Kommentar ("npm install nötig wegen fehlender musl-Binaries") entfällt damit. FROM node:22.14.0-slim AS deps WORKDIR /app # openssl/ca-certificates werden von Prisma (Engine) und für TLS beim npm-Install benötigt. RUN apt-get update && apt-get install -y --no-install-recommends openssl ca-certificates \ && rm -rf /var/lib/apt/lists/* # npm auf 11.x pinnen: Das committete package-lock.json wurde mit npm 11 erzeugt. # node:22.14.0 bringt npm 10.9.2 mit, das next@16.2.12 -> @swc/helpers ANDERS auflöst # (erwartet 0.5.23, Lock hält 0.5.15) und `npm ci` daher mit "out of sync" abbricht. # Der Resolver-Gleichstand (npm 11) macht `npm ci` grün OHNE Lockfile-Änderung. RUN npm i -g npm@11.19.0 COPY package.json package-lock.json ./ COPY prisma ./prisma # npm ci: installiert exakt die im Lockfile gepinnten Versionen (Supply-Chain-Integrität, # reproduzierbare Builds). --include=optional stellt sicher, dass die plattformspezifischen # Native-Binaries für das Zielimage (linux/glibc) mitkommen. RUN npm ci --include=optional --no-audit --no-fund # --- Build-Stage: kompletter Next.js-Build (standalone) --- FROM node:22.14.0-slim AS builder WORKDIR /app RUN apt-get update && apt-get install -y --no-install-recommends openssl ca-certificates \ && rm -rf /var/lib/apt/lists/* COPY --from=deps /app/node_modules ./node_modules COPY . . # Platzhalter nur für die Build-Zeit: prisma.config.ts löst env("DATABASE_URL") # beim Laden auf, und Coolify reicht die Variable nur als Build-ARG rein (nicht in # process.env). 'prisma generate' und 'next build' (nur dynamische Routen) bauen KEINE # echte DB-Verbindung auf. Zur Laufzeit überschreibt der echte DATABASE_URL aus der # Coolify-Env diesen Wert (im migrate-/app-Container). ENV DATABASE_URL="postgresql://build:build@localhost:5432/build?schema=public" RUN npx prisma generate && npm run build # --- Migrate-Stage: schlanker Job-Container für `prisma migrate deploy` (+ optional Seed/Bootstrap) --- # Bewusst OHNE `next build`: enthält damit weder den kompilierten Next-Server (.next/standalone) # noch den Build-Cache — deutlich kleinere Angriffsfläche als die builder-Stage (bisher genutzt). # Warum nicht "nur Prisma-CLI"? prisma/seed.ts und scripts/bootstrap-admin.ts importieren aus # ../src bzw. @/server (provisionTenant, rbac, modules) und laufen über tsx. Ein reiner # Prisma-CLI-Container würde diese Jobs brechen. Da src/** in dieser Lane nicht angefasst werden # darf, bleiben devDeps (tsx/prisma) + src hier nötig — eine spätere Entkopplung von Seed/Bootstrap # vom App-Code ist als Folgeänderung empfohlen. FROM node:22.14.0-slim AS migrate WORKDIR /app ENV NODE_ENV=production RUN apt-get update && apt-get install -y --no-install-recommends openssl ca-certificates \ && rm -rf /var/lib/apt/lists/* COPY --from=deps /app/node_modules ./node_modules COPY package.json package-lock.json prisma.config.ts tsconfig.json ./ COPY prisma ./prisma COPY scripts ./scripts COPY src ./src # Prisma-Client für Seed/Bootstrap generieren (migrate deploy selbst braucht nur schema+migrations). ENV DATABASE_URL="postgresql://build:build@localhost:5432/build?schema=public" RUN npx prisma generate RUN groupadd --system --gid 1001 app \ && useradd --system --uid 1001 --gid app --home-dir /app app USER app # Default-Kommando; im Compose (migrate-Service) überschrieben/erweitert. CMD ["npx", "prisma", "migrate", "deploy"] # --- Runner-Stage: schlanker Produktions-Container (nur Next-Standalone) --- FROM node:22.14.0-slim AS runner WORKDIR /app ENV NODE_ENV=production # Next.js standalone server.js bindet sonst an den von Docker gesetzten HOSTNAME # (= Container-ID) statt an alle Interfaces -> Healthcheck (127.0.0.1) und der # Coolify-/Traefik-Proxy erreichen den Container nicht (404). 0.0.0.0 behebt beides. ENV HOSTNAME=0.0.0.0 ENV PORT=3000 # Non-root-Nutzer (Debian: groupadd/useradd statt Alpine adduser -S). RUN groupadd --system --gid 1001 app \ && useradd --system --uid 1001 --gid app --home-dir /app app COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/static ./.next/static COPY --from=builder /app/public ./public # i18n-Kataloge (messages//.json) liegen über outputFileTracingIncludes # bereits im standalone-Output; der explizite COPY hält sie auch bei Tracing-Änderungen vor. COPY --from=builder /app/messages ./messages # Backup-/Restore-/DSGVO-Topologie (src/server/backup/topology.ts) liest zur Laufzeit # prisma/schema.prisma: Prisma 7 entfernt relationFromFields aus dem Laufzeit-DMMF, # die FK-Relationen kommen daher aus der Schema-Datei. Der Inline-Export (Direkt- # Download /api/platform/backup/download) läuft in der APP → ohne diese Datei ENOENT. COPY prisma/schema.prisma ./prisma/schema.prisma RUN chmod a+rX ./prisma/schema.prisma USER app EXPOSE 3000 CMD ["node", "server.js"] # --- Garage-Stage: Objektspeicher-Image mit EINGEBACKENER Config --- # Warum nicht das Upstream-Image + Bind-Mount der deploy/garage.toml? Coolify behandelt # relative Bind-Mount-Quellen als Storage und legt sie als VERZEICHNIS an, wenn die Datei # dort (noch) nicht existiert. Garage bekommt dann /etc/garage.toml als Ordner und bricht # mit „IO error: Is a directory (os error 21)" ab. Das Backen der Config ins Image umgeht # jeden Host-Mount. Secrets bleiben draußen (rpc_secret/admin_token kommen zur Laufzeit aus # GARAGE_RPC_SECRET/GARAGE_ADMIN_TOKEN); nur die secret-freie Basiskonfig wird kopiert. FROM dxflrs/garage:v1.2.0 AS garage COPY deploy/garage.toml /etc/garage.toml # --- Worker-Stage (Vorschlag Lane L5 Berichte): Craftvia-Job-Worker inkl. Chromium für PDF --- # ARCHITEKTUR §1: HTML → PDF läuft über playwright-core + Chromium NUR im Worker, nie im App-Container. # Debian-Chromium aus dem Paketspiegel statt Playwright-Download (reproduzierbar, Updates über das Base-Image); # render.ts nutzt PDF_CHROMIUM_PATH. fonts-dejavu/-liberation als Fallback, Inter wird eingebettet (src/app/fonts). # tsx + src/messages/prisma werden wie in der migrate-Stage zur Laufzeit gebraucht (Worker läuft über tsx). FROM node:22.14.0-slim AS worker WORKDIR /app ENV NODE_ENV=production ENV PDF_CHROMIUM_PATH=/usr/bin/chromium RUN apt-get update && apt-get install -y --no-install-recommends openssl ca-certificates chromium fonts-dejavu-core fonts-liberation \ && rm -rf /var/lib/apt/lists/* COPY --from=deps /app/node_modules ./node_modules COPY package.json package-lock.json prisma.config.ts tsconfig.json ./ COPY prisma ./prisma COPY scripts ./scripts COPY src ./src COPY messages ./messages ENV DATABASE_URL="postgresql://build:build@localhost:5432/build?schema=public" RUN npx prisma generate RUN groupadd --system --gid 1001 app \ && useradd --system --uid 1001 --gid app --home-dir /app app USER app CMD ["npx", "tsx", "scripts/craftvia-worker.ts"]