diff --git a/.env.example b/.env.example index 15c2468..ec3683e 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,70 @@ +# Example environment file for apparel-designer. +# +# Copy this to `.env` and adjust values as needed. Compose reads +# `.env` automatically for variable substitution in docker-compose.yml +# AND passes the values as runtime environment to the running +# container, so a single file covers both build-time and runtime. +# +# ────────────────────────────────────────────────────────────────── +# Runtime — values the server reads at startup +# ────────────────────────────────────────────────────────────────── + +# HTTP port the Express server binds to. Matches the host-side port +# in docker-compose.yml's `ports:` mapping. PORT=3001 + +# `production` enables prod-only behavior in server.js (production +# stickers path under dist/, production-mode logger). Use +# `development` locally to read stickers from public/ instead. NODE_ENV=development + +# Pino log level. Comment out for `info` (default). Useful values: +# trace, debug, info, warn, error, fatal +# Set to `debug` when diagnosing a problem; revert before deploy. +# LOG_LEVEL=info + +# CORS — uncomment and set when the frontend is served from a +# different origin than this API. Single origin or comma-separated. # CORS_ORIGIN=https://your-domain.com + +# Upload / export TTL and sweep interval, in milliseconds. +# Defaults (24h TTL, 1h sweep) work for most cases. Lower the TTL +# if disk space is tight; lower the interval if uploads churn fast +# and you want stale files cleaned sooner. +# FILE_TTL_MS=86400000 # 24 hours +# CLEANUP_INTERVAL_MS=3600000 # 1 hour + +# ────────────────────────────────────────────────────────────────── +# Build-time — values the `docker compose build` step uses +# ────────────────────────────────────────────────────────────────── +# These are only needed when BUILDING the image, not when running +# it. The image talks to git.kadil.dev to fetch the `goods-editor` +# git+ssh: npm dependency during `npm install`. See the docblock +# in Dockerfile for the full mechanism. +# +# ── Option A: file-based key (typical local dev / single deploy host) +# Set this to the absolute path of a private SSH key that has read +# access to the goods-editor-module repo. The key file stays on +# disk; only its contents are mounted into the build for the one +# RUN step that needs it, and nothing about it ends up in the +# image layers. +# +# Recommended: generate a dedicated read-only deploy key rather +# than reusing a personal key (see Dockerfile docblock for the +# ssh-keygen + Gitea deploy-key registration steps). +# +# Examples (uncomment one): +# SSH_KEY_FILE=/Users/khalid/.ssh/goods-editor-deploy +# SSH_KEY_FILE=/home/deploy/.ssh/goods-editor-deploy + +# ── Option B: env-var-based key (typical CI) +# Pass the key contents directly via SSH_PRIVATE_KEY. Don't put +# the actual key in this .env file (multi-line values are fragile +# in env files, and committing a key — even .env.example with +# real material — is a leak waiting to happen). Set it from the +# shell or from your CI's secret store: +# +# SSH_PRIVATE_KEY="$(cat ~/.ssh/goods-editor-deploy)" docker compose build +# +# To use this mode, also flip docker-compose.yml's `secrets.ssh_key` +# block from `file:` to `environment:` (see comments there). diff --git a/Dockerfile b/Dockerfile index 384cd36..4d0787e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,25 +4,38 @@ # ──────────── # 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. +# 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`. # -# 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 +# 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. # -# Build command: -# docker build --ssh default -t apparel-designer . +# Then build, picking whichever invocation matches where the key is: # -# 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 +# 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 # ────────────── @@ -78,32 +91,47 @@ WORKDIR /app # layer when only application source changes. COPY package*.json ./ -# Install dependencies with SSH agent forwarded from the host. +# Install dependencies using an SSH key passed in as a build secret. # -# `--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. +# `--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 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. +# 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). # -# 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 \ +# 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 . . diff --git a/docker-compose.yml b/docker-compose.yml index 3f2d2d7..a130586 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,25 +3,14 @@ services: 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 + # Pass an SSH private key in as a build secret so npm can + # authenticate against git.kadil.dev for the `goods-editor` + # git+ssh: dependency. The secret is only available during + # the single RUN step that mounts it; nothing about the key + # ends up in the built image. See Dockerfile docblock for + # the full mechanism. + secrets: + - ssh_key container_name: apparel-designer ports: ["3001:3001"] volumes: @@ -32,6 +21,36 @@ services: - PORT=3001 restart: unless-stopped +# Build-time secret declaration. +# +# Two ways to provide the key, pick whichever fits where you're +# building from: +# +# ── A. File-based (typical local dev / deployment server) ─────── +# Set SSH_KEY_FILE in the environment before running compose: +# +# SSH_KEY_FILE=$HOME/.ssh/id_ed25519 docker compose build +# +# The `file:` source below reads from that path. The fallback +# `/dev/null` exists so `docker compose config` (validation / +# linting) doesn't error out when the env var isn't set — actual +# builds against /dev/null will fail with a clear "permission +# denied" rather than a cryptic compose error. +# +# ── B. Env-var-based (typical CI) ────────────────────────────── +# Comment out the `file:` line and uncomment `environment:` below. +# Provide the key contents in the SSH_PRIVATE_KEY env var: +# +# SSH_PRIVATE_KEY="$(cat ~/.ssh/id_ed25519)" docker compose build +# +# In GitHub Actions / GitLab CI / etc., set SSH_PRIVATE_KEY from +# the platform's secret store. The `environment:` source for build +# secrets requires Docker Compose v2.23+. +secrets: + ssh_key: + file: ${SSH_KEY_FILE:-/dev/null} + # environment: SSH_PRIVATE_KEY + volumes: uploads_data: exports_data: