# Multi-stage Dockerfile. # # Why two stages # ────────────── # `canvas` is a native-binding npm package. It needs Cairo/Pango/etc. # at runtime, and python3+make+g++ at install-time to compile its # native bindings (no prebuilt binary covers the alpine musl+napi+linux # combination at this canvas version; prebuild-install falls back to # node-gyp, which needs the compile toolchain). # # The naive single-stage approach would either: # (a) keep python3+make+g++ in the final image — inflates the image # by ~200MB of build tools nothing uses at runtime, or # (b) try to install --omit=dev in a runtime-only image — fails # because canvas tries to compile and has no python (this was # the bug fixed in this revision). # # Two-stage solves both: install + compile + prune dev deps in the # builder (which has the toolchain), then COPY the resulting # node_modules into a slim runtime that only carries the C runtime # libs canvas needs at execution time. # ───────────────────────────────────────────────────────────────── # Stage 1: builder # Installs all deps (including dev), compiles native bindings, # builds the Vite bundle, then prunes dev deps so node_modules is # production-ready before we copy it. # ───────────────────────────────────────────────────────────────── FROM node:20-alpine AS builder # Native-binding build prerequisites: # • python3 / make / g++ — node-gyp toolchain for compiling canvas # • cairo-dev / pango-dev / etc. — headers canvas links against # These stay in the builder stage only; the runtime stage carries # the matching runtime libraries (no -dev suffix) without the # toolchain. RUN apk add --no-cache \ cairo-dev pango-dev libjpeg-turbo-dev giflib-dev librsvg-dev pixman-dev \ python3 make g++ WORKDIR /app # Copy package manifests first so docker can cache the npm install # layer when only application source changes. COPY package*.json ./ RUN npm install COPY . . # Pull Google Fonts into ./fonts/ so they end up in the runtime image. # If the build host has no network this step will produce missing-variant # warnings; the build still succeeds and the server falls back to system # fonts. RUN npm run fetch-fonts || echo "Font fetch had warnings; continuing with whatever was downloaded." RUN npm run build # Drop dev dependencies (vitest, playwright, eslint, etc.) so the # node_modules we copy to runtime is production-only. This is the # step that was missing before — without it, the runtime stage had # to re-run `npm install` from scratch, which would re-trigger # canvas's native compile in an image that doesn't have the # toolchain. By pruning here, the runtime stage just copies the # already-compiled directory. RUN npm prune --production # ───────────────────────────────────────────────────────────────── # Stage 2: runtime # Carries only the runtime C libraries canvas links against plus # the fonts the export pipeline needs. node_modules is copied # whole from the builder; no `npm install` happens here. # ───────────────────────────────────────────────────────────────── FROM node:20-alpine # Cairo + Pango are required by node-canvas at runtime. # The font packages cover the proprietary template families (Impact, # Times, Courier, Arial) via free equivalents that fontconfig will # alias, plus emoji rendering for sticker exports. RUN apk add --no-cache \ cairo pango libjpeg-turbo giflib librsvg pixman \ ttf-liberation ttf-dejavu font-noto font-noto-emoji \ fontconfig WORKDIR /app # Bring the production node_modules over from the builder rather # than installing fresh. This carries the already-compiled canvas # .node binary without needing python/make/g++ in this stage. COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/package*.json ./ # Application files. server.js is the entry point; dist/ is the # Vite build output served as static; fonts/ is the result of # fetch-fonts above. COPY server.js ./ COPY --from=builder /app/dist ./dist COPY --from=builder /app/fonts ./fonts RUN mkdir -p /app/uploads /app/exports # Refresh fontconfig's cache so the newly-copied custom fonts are # discoverable. RUN fc-cache -f /app/fonts || true HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:3001/api/health || exit 1 EXPOSE 3001 ENV NODE_ENV=production CMD ["node", "server.js"]