From 6d6377580027e715b85809173c653dce58ef57cb Mon Sep 17 00:00:00 2001 From: "khalid@traclabs.com" Date: Sun, 24 May 2026 08:49:27 -0500 Subject: [PATCH] Fix ssh from host not working in docker --- Dockerfile | 141 +++++++++++++++++++++++++++++++++------------ docker-compose.yml | 23 +++++++- 2 files changed, 125 insertions(+), 39 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6593adb..384cd36 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,29 @@ # 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. @@ -12,8 +36,7 @@ # (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). +# 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 @@ -28,54 +51,93 @@ # ───────────────────────────────────────────────────────────────── 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. +# 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++ + 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 ./ -RUN npm install + +# 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 . . -# 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. +# 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 -# the fonts the export pipeline needs. node_modules is copied -# whole from the builder; no `npm install` happens here. +# 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 -# 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. +# 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 \ @@ -83,23 +145,26 @@ RUN apk add --no-cache \ 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. +# 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; fonts/ is the result of -# fetch-fonts above. +# Vite build output served as static. 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 +# 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 diff --git a/docker-compose.yml b/docker-compose.yml index da4db68..3f2d2d7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,27 @@ services: apparel-designer: - build: { context: ., dockerfile: Dockerfile } + build: + context: . + dockerfile: Dockerfile + # Forward the host's SSH agent into the build. Needed because + # the `goods-editor` npm dependency uses a git+ssh: URL, so + # `npm install` has to authenticate against git.kadil.dev + # during the build. "default" means "use the SSH_AUTH_SOCK + # environment variable on the host" — matches the build + # command `docker build --ssh default`. + # + # The host running `docker compose build` must have: + # • An ssh-agent running with the right key loaded + # (verify with `ssh-add -l`). + # • Docker 23+ (BuildKit is the default; older versions + # need `DOCKER_BUILDKIT=1` exported). + # + # No SSH material is baked into the image — the agent is only + # available during the single RUN step in the Dockerfile that + # has `--mount=type=ssh`. See the Dockerfile docblock for the + # full mechanism. + ssh: + - default container_name: apparel-designer ports: ["3001:3001"] volumes: