203 lines
9.1 KiB
Docker
203 lines
9.1 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). We pass an SSH private key in as a BuildKit secret named
|
|
# `ssh_key`. The secret is mounted into the build for just that one
|
|
# RUN step, used to authenticate the git fetch, then unmounted —
|
|
# nothing about the key ends up in any image layer or in
|
|
# `docker history`.
|
|
#
|
|
# Get an SSH key with read access to the goods-editor-module repo
|
|
# on Gitea registered first (the same key you use for git push works,
|
|
# OR a dedicated deploy key with read-only access — recommended for
|
|
# CI / servers). The public half goes in Gitea Settings → SSH Keys,
|
|
# or as a deploy key on the repo itself.
|
|
#
|
|
# Then build, picking whichever invocation matches where the key is:
|
|
#
|
|
# 1. From a file on disk (typical local dev / deployment server):
|
|
# docker build \
|
|
# --secret id=ssh_key,src=$HOME/.ssh/id_ed25519 \
|
|
# -t apparel-designer .
|
|
#
|
|
# 2. From an environment variable (typical CI — secret store
|
|
# injects the key as an env var, no file ever touches disk):
|
|
# docker build \
|
|
# --secret id=ssh_key,env=SSH_PRIVATE_KEY \
|
|
# -t apparel-designer .
|
|
# (BuildKit 1.5+ supports the `env=` source.)
|
|
#
|
|
# 3. Via docker-compose: see docker-compose.yml — the `secrets:`
|
|
# block there wires the same secret. Then:
|
|
# SSH_KEY_FILE=$HOME/.ssh/id_ed25519 docker compose build
|
|
#
|
|
# Requires Docker 23+ (BuildKit is the default; older Docker needs
|
|
# `DOCKER_BUILDKIT=1` exported in the environment).
|
|
#
|
|
# 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 using an SSH key passed in as a build secret.
|
|
#
|
|
# `--mount=type=secret,id=ssh_key` is a BuildKit feature that exposes
|
|
# a host-provided secret at /run/secrets/ssh_key inside this single
|
|
# RUN step. The secret is NEVER copied into any image layer; once
|
|
# the RUN command finishes, the mount disappears and nothing about
|
|
# the key remains in the built image. `docker history` won't show
|
|
# it either.
|
|
#
|
|
# Steps:
|
|
# 1. Create ~/.ssh with 0700 perms (ssh refuses to use a config
|
|
# directory that's world-readable).
|
|
# 2. ssh-keyscan git.kadil.dev → ~/.ssh/known_hosts. Without this,
|
|
# ssh would prompt "Are you sure you want to continue connecting?"
|
|
# with no stdin to answer it, and the install would hang forever.
|
|
# 3. Run npm install with GIT_SSH_COMMAND pointing at the mounted
|
|
# secret. npm shells out to git for the git+ssh: dependency, git
|
|
# respects GIT_SSH_COMMAND, ssh uses the key at /run/secrets/ssh_key.
|
|
# `IdentitiesOnly=yes` prevents ssh from trying any other key
|
|
# it might find (none in this container, but defensive).
|
|
#
|
|
# Why use GIT_SSH_COMMAND rather than copying the key to ~/.ssh/id_ed25519:
|
|
# the secret mount is a clean way to use a file without it ever
|
|
# touching the filesystem proper. If we copied, we'd have to be
|
|
# careful to `rm` it in the same RUN step before layer commit; with
|
|
# GIT_SSH_COMMAND, the file is only ever at /run/secrets/, which
|
|
# BuildKit clears automatically.
|
|
#
|
|
# If the build aborts here:
|
|
# • "Permission denied (publickey)" → the secret was provided but
|
|
# the key doesn't have access to the repo. Verify the public
|
|
# half is registered in Gitea (account settings or deploy key).
|
|
# • "required secret 'ssh_key' not provided" → forgot the `--secret`
|
|
# flag on the build command (see docblock at top of file).
|
|
# • Hangs at "Cloning into …" → ssh-keyscan didn't populate
|
|
# known_hosts (network issue inside the build). Try again with
|
|
# `--no-cache`.
|
|
RUN --mount=type=secret,id=ssh_key \
|
|
mkdir -p -m 0700 ~/.ssh && \
|
|
ssh-keyscan git.kadil.dev >> ~/.ssh/known_hosts 2>/dev/null && \
|
|
GIT_SSH_COMMAND="ssh -i /run/secrets/ssh_key -o IdentitiesOnly=yes -o UserKnownHostsFile=/root/.ssh/known_hosts" \
|
|
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"]
|