Update to use a base64 encoded key

This commit is contained in:
khalid@traclabs.com
2026-05-24 09:18:06 -05:00
parent 9fe0828986
commit f3327d29d4
3 changed files with 136 additions and 101 deletions

View File

@@ -37,34 +37,45 @@ NODE_ENV=development
# ──────────────────────────────────────────────────────────────────
# 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.
# 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 Dockerfile docblock
# 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.
# The Dockerfile expects a BASE64-ENCODED private key (not raw PEM)
# because that's the format Dokploy's Build-time Secrets textarea
# requires — it's dotenv-parsed and can't handle multi-line values.
# We use base64 for local builds too so the Dockerfile has one input
# format. Encode like this:
#
# 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).
# base64 < ~/.ssh/goods-editor-deploy | tr -d '\n' > /tmp/key.b64
#
# Examples (uncomment one):
# SSH_KEY_FILE=/Users/khalid/.ssh/goods-editor-deploy
# SSH_KEY_FILE=/home/deploy/.ssh/goods-editor-deploy
# 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).
#
# Path to a base64-encoded key file. Uncomment one for local builds:
# SSH_KEY_FILE=/Users/khalid/.ssh/goods-editor-deploy.b64
# SSH_KEY_FILE=/home/deploy/.ssh/goods-editor-deploy.b64
# ── 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:
# Alternative: env-var-based (CI). To use this mode, also flip
# docker-compose.yml's `secrets.ssh_key` block from `file:` to
# `environment:` (see comments there). Set the base64 contents via
# the shell or your CI secret store:
#
# SSH_PRIVATE_KEY="$(cat ~/.ssh/goods-editor-deploy)" docker compose build
# SSH_KEY_B64="$(base64 < ~/.ssh/goods-editor-deploy | tr -d '\n')" \
# docker compose build
#
# To use this mode, also flip docker-compose.yml's `secrets.ssh_key`
# block from `file:` to `environment:` (see comments there).
# Don't put the actual base64 in this .env file. The string is long
# enough that dotenv parsers may struggle, and committing real key
# material — even base64 — to a repo is a leak waiting to happen.
#
# ── Dokploy ───────────────────────────────────────────────────────
# The entries above DON'T apply to Dokploy. In Dokploy, paste the
# base64 directly into the application's Environment → Build-time
# Secrets textarea as a single line:
#
# ssh_key=<that base64 string>
#
# Dokploy parses that textarea as dotenv and passes the value into
# the build as a BuildKit secret named `ssh_key`.

View File

@@ -4,35 +4,47 @@
# ────────────
# 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`.
# URL). We pass a base64-encoded SSH private key as a BuildKit
# secret named `ssh_key`, then decode it inside the build.
#
# 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.
# Why base64-encoded rather than raw PEM
# ───────────────────────────────────────
# The primary deploy target is Dokploy, whose Build-time Secrets UI
# is a dotenv-parsed textarea (one KEY=VALUE per line). PEM keys are
# multi-line and break dotenv parsing. Base64 collapses the key to a
# single line of safe ASCII that any dotenv parser accepts. We use
# base64 everywhere (Dokploy and local builds) so the Dockerfile has
# exactly one input format — no branching logic for raw vs encoded.
#
# Then build, picking whichever invocation matches where the key is:
# Register a deploy key first
# ─────────────────────────────
# Generate a read-only deploy key dedicated to this build and add
# its public half to Gitea (repo Settings → Deploy Keys):
# ssh-keygen -t ed25519 -f ~/.ssh/goods-editor-deploy -N "" -C "deploy"
# cat ~/.ssh/goods-editor-deploy.pub # → paste into Gitea
#
# 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 .
# Then base64-encode the PRIVATE half for build-time use:
# base64 < ~/.ssh/goods-editor-deploy | tr -d '\n' | pbcopy
# The `tr -d '\n'` strips line wrapping so the output is a single
# physical line, which the Dokploy textarea (and dotenv parsers in
# general) require.
#
# 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.)
# Dokploy
# ───────
# Paste into the application's Environment → Build-time Secrets
# textarea as a single line:
# ssh_key=<that base64 string>
# Then redeploy.
#
# 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
# Local build with the same approach
# ───────────────────────────────────
# base64 < ~/.ssh/goods-editor-deploy | tr -d '\n' > /tmp/key.b64
# docker build --secret id=ssh_key,src=/tmp/key.b64 -t apparel-designer .
# rm /tmp/key.b64
#
# Or via env var:
# SSH_KEY_B64="$(base64 < ~/.ssh/goods-editor-deploy | tr -d '\n')" \
# docker build --secret id=ssh_key,env=SSH_KEY_B64 -t apparel-designer .
#
# Requires Docker 23+ (BuildKit is the default; older Docker needs
# `DOCKER_BUILDKIT=1` exported in the environment).
@@ -97,42 +109,53 @@ COPY package*.json ./
# 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.
# the key remains in the built image.
#
# 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).
# Dokploy-specific note: Dokploy's Build-time Secrets UI is a single
# textarea that gets parsed as dotenv (one KEY=VALUE per line),
# which doesn't tolerate multi-line values. PEM-formatted SSH keys
# ARE multi-line. The workaround is to base64-encode the key before
# pasting into Dokploy and decode here at use-time. Base64 produces
# a single line of ASCII with no characters that confuse dotenv
# parsers.
#
# 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.
# Encode locally:
# base64 < ~/.ssh/goods-editor-deploy | tr -d '\n' | pbcopy
# Then paste in Dokploy as:
# ssh_key=<that base64 string>
#
# Steps in this RUN:
# 1. mkdir ~/.ssh with 0700 (ssh refuses world-readable config dirs)
# 2. ssh-keyscan git.kadil.dev so we don't hang on the
# "continue connecting?" prompt
# 3. base64-decode /run/secrets/ssh_key into /tmp/ssh_key. The
# decoded file holds the original PEM private key.
# 4. chmod 0600 the decoded key so ssh accepts it.
# 5. Run npm install with GIT_SSH_COMMAND pointing at the decoded
# key. IdentitiesOnly=yes prevents ssh from trying any other
# key it might find.
# 6. rm the decoded key. Since we created it in the same RUN, it
# never gets committed to a layer.
#
# 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`.
# • "Permission denied (publickey)" → the key in Dokploy doesn't
# match the deploy key registered in Gitea. Re-base64-encode and
# re-paste, OR re-check the Gitea deploy key.
# • "Identity file /tmp/ssh_key not accessible: No such file or
# directory" → the secret wasn't mounted. Verify the Dokploy
# Build-time Secrets textarea contains exactly `ssh_key=...` as
# a single line.
# • "base64: invalid input" → the Dokploy textarea has trailing
# whitespace, line breaks inside the base64, or extra content.
# Re-paste from a fresh `base64 < key | tr -d '\n' | pbcopy`.
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
base64 -d /run/secrets/ssh_key > /tmp/ssh_key && \
chmod 0600 /tmp/ssh_key && \
GIT_SSH_COMMAND="ssh -i /tmp/ssh_key -o IdentitiesOnly=yes -o UserKnownHostsFile=/root/.ssh/known_hosts" \
npm install && \
rm -f /tmp/ssh_key
COPY . .

View File

@@ -3,12 +3,14 @@ services:
build:
context: .
dockerfile: Dockerfile
# 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.
# Pass a base64-encoded SSH private key in as a build secret so
# npm can authenticate against git.kadil.dev for the `goods-editor`
# git+ssh: dependency. The Dockerfile decodes the base64 inside
# the build. See Dockerfile docblock for the full mechanism and
# the rationale for base64 over raw PEM.
#
# The secret is only available during the single RUN step that
# mounts it; nothing about the key ends up in the built image.
secrets:
- ssh_key
container_name: apparel-designer
@@ -23,33 +25,32 @@ services:
# Build-time secret declaration.
#
# Two ways to provide the key, pick whichever fits where you're
# building from:
# The Dockerfile expects a BASE64-ENCODED private key (not the raw
# PEM). See the Dockerfile docblock for the encoding command. Two
# ways to provide it, 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:
# ── A. File-based (typical local dev) ──────────────────────────────
# Encode the key to a temp file once, then point SSH_KEY_FILE at it:
#
# SSH_KEY_FILE=$HOME/.ssh/id_ed25519 docker compose build
# base64 < ~/.ssh/goods-editor-deploy | tr -d '\n' > ~/.ssh/goods-editor-deploy.b64
# SSH_KEY_FILE=$HOME/.ssh/goods-editor-deploy.b64 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.
# The fallback `/dev/null` keeps `docker compose config` from erroring
# when SSH_KEY_FILE isn't set; actual builds against /dev/null fail
# with a clear base64-decode error rather than a cryptic compose error.
#
# ── B. Env-var-based (typical CI) ──────────────────────────────
# ── B. Env-var-based (CI) ──────────────────────────────────────────
# Comment out the `file:` line and uncomment `environment:` below.
# Provide the key contents in the SSH_PRIVATE_KEY env var:
# Provide the base64-encoded key in SSH_KEY_B64:
#
# SSH_PRIVATE_KEY="$(cat ~/.ssh/id_ed25519)" docker compose build
# SSH_KEY_B64="$(base64 < ~/.ssh/goods-editor-deploy | tr -d '\n')" \
# 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+.
# Requires Docker Compose v2.23+ for the `environment:` secret source.
secrets:
ssh_key:
file: ${SSH_KEY_FILE:-/dev/null}
# environment: SSH_PRIVATE_KEY
# environment: SSH_KEY_B64
volumes:
uploads_data: