Files
apparel-designer/Dockerfile
2026-05-24 08:49:27 -05:00

175 lines
7.5 KiB
Docker

# Multi-stage Dockerfile.
#
# How to build
# ────────────
# This Dockerfile needs SSH access to git.kadil.dev during the
# `npm install` step (the `goods-editor` dependency is a git+ssh:
# URL). The build forwards your host's SSH agent into the build
# context via BuildKit's --ssh flag, so no keys are ever baked into
# the image.
#
# Prerequisites on the build host:
# • Docker 23+ (BuildKit is the default; older Docker needs
# `DOCKER_BUILDKIT=1` exported in the environment).
# • An ssh-agent running with the key that has read access to
# git.kadil.dev loaded into it:
# ssh-add ~/.ssh/id_ed25519 # or whichever key
# ssh-add -l # verify
#
# Build command:
# docker build --ssh default -t apparel-designer .
#
# With docker-compose, the corresponding wiring is in
# docker-compose.yml (build.ssh: ["default"]). Compose forwards
# the agent the same way; just run:
# docker compose build
#
# 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.
#
# 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
# Build-time packages, grouped by why they're here:
#
# Native-binding compile toolchain (for `canvas` + node-gyp):
# python3, make, g++
#
# Native-binding headers (canvas links against these at compile
# time; the runtime stage carries the matching shared libs):
# cairo-dev, pango-dev, libjpeg-turbo-dev, giflib-dev,
# librsvg-dev, pixman-dev
#
# Git over SSH (for the `goods-editor` git+ssh: dependency):
# git, openssh-client
#
# git is npm's transport for git+ssh URLs; openssh-client provides
# the ssh binary that git invokes plus the ssh-keyscan utility we
# use below to populate known_hosts.
RUN apk add --no-cache \
cairo-dev pango-dev libjpeg-turbo-dev giflib-dev librsvg-dev pixman-dev \
python3 make g++ \
git openssh-client
WORKDIR /app
# Copy package manifests first so docker can cache the npm install
# layer when only application source changes.
COPY package*.json ./
# Install dependencies with SSH agent forwarded from the host.
#
# `--mount=type=ssh` is a BuildKit feature that exposes the host's
# SSH agent socket inside this single RUN step. The agent is NOT
# baked into the image — it's only available for the duration of
# this command. After the layer is committed, no SSH material
# remains.
#
# Steps:
# 1. Create ~/.ssh with 0700 perms (ssh refuses to read keys from
# world-readable directories).
# 2. ssh-keyscan git.kadil.dev — adds the server's host key to
# known_hosts so ssh doesn't prompt "Are you sure you want to
# continue connecting?" during the npm fetch. Without this,
# git's ssh invocation hangs forever (no stdin to answer the
# prompt from inside a Docker build).
# 3. npm install — for the git+ssh dep, npm shells out to git,
# which shells out to ssh, which uses the forwarded agent.
#
# If the build aborts here with "Could not read from remote
# repository" or "Permission denied (publickey)", the agent isn't
# forwarding correctly. Verify with `ssh-add -l` on the host (the
# key should be listed) and that you're invoking with `--ssh default`.
RUN --mount=type=ssh \
mkdir -p -m 0700 ~/.ssh && \
ssh-keyscan git.kadil.dev >> ~/.ssh/known_hosts 2>/dev/null && \
npm install
COPY . .
RUN npm run build
# Drop dev dependencies (eslint, etc.) so the node_modules we copy
# to runtime is production-only. Without this step, the runtime
# stage would either ship dev deps it doesn't use, or re-run
# `npm install --omit=dev` in an image without the compile
# toolchain — which would fail when canvas tries to rebuild.
RUN npm prune --production
# ─────────────────────────────────────────────────────────────────
# Stage 2: runtime
# Carries only the runtime C libraries canvas links against plus
# system fonts. node_modules is copied whole from the builder;
# no `npm install` happens here, so no SSH is needed at runtime.
# ─────────────────────────────────────────────────────────────────
FROM node:20-alpine
# Runtime packages:
#
# Native-binding runtime libs (matching the -dev headers in the
# builder stage):
# cairo, pango, libjpeg-turbo, giflib, librsvg, pixman
#
# System fonts for the export pipeline. node-canvas's text rendering
# uses fontconfig to resolve family names; these provide free
# equivalents of the proprietary template families (Impact, Times,
# Courier, Arial) plus emoji support for sticker exports. The
# editor module's own bundled fonts (under node_modules/goods-editor/
# fonts/) are registered explicitly via registerFont() — they don't
# go through fontconfig and don't need to live in /usr/share/fonts.
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. This
# carries the already-compiled canvas .node binary AND the editor
# module's own fonts (under node_modules/goods-editor/fonts/), so
# no separate font-copy step is needed.
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.
COPY server.js ./
COPY --from=builder /app/dist ./dist
RUN mkdir -p /app/uploads /app/exports
# Refresh fontconfig's cache so the apk-installed system fonts are
# discoverable. The module's own fonts under node_modules are
# registered explicitly by the module's server code (via
# registerFont() — see the host's server.js comment about font
# registration moving to the module), so they don't need to be
# in fontconfig's index.
RUN fc-cache -f || 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"]