diff --git a/docker/imgproxy-runtime/Dockerfile b/docker/imgproxy-runtime/Dockerfile new file mode 100644 index 0000000..44a228a --- /dev/null +++ b/docker/imgproxy-runtime/Dockerfile @@ -0,0 +1,5 @@ +# This digest selects the linux/arm64 manifest of darthsim/imgproxy:latest as +# verified on 2026-07-15. Do not replace it with a mutable tag in production. +FROM darthsim/imgproxy@sha256:3819ccd5ee26b83b27f8184b17a0a0c6f0eb21052efa9f554b23a7c39a3e6a1f + +EXPOSE 8080 diff --git a/package.json b/package.json index 9e5b039..c5d970e 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "enrich:plaza": "node scripts/enrich-plaza-posts.mjs", "build": "vite build", "build:portal-runtime": "node scripts/build-portal-runtime.mjs", + "build:imgproxy-runtime": "bash scripts/build-imgproxy-runtime-image.sh", "build:mindspace-service-runtime": "node scripts/build-mindspace-service-runtime.mjs", "check:mindspace-public-links": "node scripts/check-mindspace-public-links.mjs --downloads-only", "check:mindspace-public-links:all": "node scripts/check-mindspace-public-links.mjs --all-links", diff --git a/scripts/build-imgproxy-runtime-image.sh b/scripts/build-imgproxy-runtime-image.sh new file mode 100644 index 0000000..dba1780 --- /dev/null +++ b/scripts/build-imgproxy-runtime-image.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +IMAGE_REF="${IMGPROXY_IMAGE_REF:-darthsim/imgproxy@sha256:3819ccd5ee26b83b27f8184b17a0a0c6f0eb21052efa9f554b23a7c39a3e6a1f}" +IMAGE_TAG="${IMGPROXY_IMAGE_TAG:-tkmind/imgproxy-runtime:20260715-3819ccd5}" +OUT_DIR="${IMGPROXY_RUNTIME_OUT_DIR:-${ROOT}/.runtime/imgproxy}" +DRY_RUN=0 + +[[ "${1:-}" != "--dry-run" ]] || DRY_RUN=1 + +mkdir -p "$OUT_DIR" +if [[ "$DRY_RUN" == 1 ]]; then + printf 'image_ref=%s\nimage_tag=%s\nout_dir=%s\n' "$IMAGE_REF" "$IMAGE_TAG" "$OUT_DIR" + exit 0 +fi + +command -v docker >/dev/null || { echo 'docker is required to build the imgproxy runtime image' >&2; exit 1; } +docker pull --platform linux/arm64 "$IMAGE_REF" +docker tag "$IMAGE_REF" "$IMAGE_TAG" +docker save "$IMAGE_TAG" | gzip -c > "$OUT_DIR/imgproxy-runtime-image.tar.gz" +shasum -a 256 "$OUT_DIR/imgproxy-runtime-image.tar.gz" > "$OUT_DIR/imgproxy-runtime-image.tar.gz.sha256" +printf '%s\n' "$IMAGE_TAG" > "$OUT_DIR/image-tag.txt" diff --git a/scripts/imgproxy-runtime-package.test.mjs b/scripts/imgproxy-runtime-package.test.mjs new file mode 100644 index 0000000..2da36f9 --- /dev/null +++ b/scripts/imgproxy-runtime-package.test.mjs @@ -0,0 +1,28 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import test from 'node:test'; + +const root = path.resolve(import.meta.dirname, '..'); +const read = (name) => fs.readFileSync(path.join(root, name), 'utf8'); + +test('imgproxy runtime pins an arm64 image digest', () => { + const dockerfile = read('docker/imgproxy-runtime/Dockerfile'); + assert.match(dockerfile, /FROM darthsim\/imgproxy@sha256:[a-f0-9]{64}/); +}); + +test('installer uses a read-only storage mount and both required ports', () => { + const installer = read('scripts/install-imgproxy-runtime-prod.sh'); + assert.match(installer, /-p 127\.0\.0\.1:20082:8080/); + assert.match(installer, /\$MINDSPACE_STORAGE_ROOT:\/mnt\/images:ro/); + assert.match(installer, /10\.10\.0\.2/); + assert.match(installer, /IMGPROXY_UPSTREAM/); +}); + +test('release verifies checksums before loading the image and backs up launch state', () => { + const release = read('scripts/release-imgproxy-runtime-prod.sh'); + assert.match(release, /shasum -a 256 -c/); + assert.match(release, /docker load/); + assert.match(release, /imgproxy-compat-\$RELEASE_ID\.plist/); + assert.match(release, /img\.tkmind\.cn\/health/); +}); diff --git a/scripts/install-imgproxy-runtime-prod.sh b/scripts/install-imgproxy-runtime-prod.sh new file mode 100644 index 0000000..9507817 --- /dev/null +++ b/scripts/install-imgproxy-runtime-prod.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# Runs only on 103 from a verified release directory. It never downloads images. +set -euo pipefail + +ROOT="${ROOT:-/Users/john/Project/Memind}" +RUNTIME_DIR="${IMGPROXY_RUNTIME_DIR:?IMGPROXY_RUNTIME_DIR is required}" +IMAGE_TAG="${IMGPROXY_IMAGE_TAG:?IMGPROXY_IMAGE_TAG is required}" +CONTAINER="${IMGPROXY_CONTAINER:-memind-imgproxy}" +NODE_BIN="${NODE_BIN:-/opt/homebrew/opt/node@24/bin/node}" +PLIST="$HOME/Library/LaunchAgents/cn.tkmind.imgproxy-compat.plist" +GUI="gui/$(id -u)" + +set -a +source "$ROOT/.env" +set +a +: "${IMGPROXY_SIGNING_KEY:?missing IMGPROXY_SIGNING_KEY}" +: "${IMGPROXY_SIGNING_SALT:?missing IMGPROXY_SIGNING_SALT}" +: "${MINDSPACE_STORAGE_ROOT:?missing MINDSPACE_STORAGE_ROOT}" + +docker rm -f "$CONTAINER" >/dev/null 2>&1 || true +docker run -d --name "$CONTAINER" --restart unless-stopped \ + -p 127.0.0.1:20082:8080 \ + -v "$MINDSPACE_STORAGE_ROOT:/mnt/images:ro" \ + -e IMGPROXY_LOCAL_FILESYSTEM_ROOT=/mnt/images \ + -e IMGPROXY_KEY="$IMGPROXY_SIGNING_KEY" \ + -e IMGPROXY_SALT="$IMGPROXY_SIGNING_SALT" \ + -e IMGPROXY_USE_ETAG=true \ + -e IMGPROXY_ENABLE_WEBP_DETECTION=true \ + "$IMAGE_TAG" >/dev/null + +for _ in $(seq 1 20); do + curl -fsS --max-time 2 http://127.0.0.1:20082/health >/dev/null && break + sleep 1 +done +curl -fsS --max-time 3 http://127.0.0.1:20082/health >/dev/null + +cat > "$PLIST" < + + +Labelcn.tkmind.imgproxy-compat +ProgramArguments$NODE_BIN$RUNTIME_DIR/imgproxy-compat-proxy.mjs +WorkingDirectory$RUNTIME_DIR +EnvironmentVariablesIMGPROXY_COMPAT_HOST10.10.0.2IMGPROXY_COMPAT_PORT20081IMGPROXY_UPSTREAMhttp://127.0.0.1:20082 +RunAtLoadKeepAliveThrottleInterval10 +StandardOutPath$HOME/Library/Logs/imgproxy-compat.logStandardErrorPath$HOME/Library/Logs/imgproxy-compat.log + +EOF +plutil -lint "$PLIST" >/dev/null +launchctl bootout "$GUI/cn.tkmind.imgproxy" 2>/dev/null || true +launchctl disable "$GUI/cn.tkmind.imgproxy" 2>/dev/null || true +launchctl bootout "$GUI/cn.tkmind.imgproxy-compat" 2>/dev/null || true +launchctl bootstrap "$GUI" "$PLIST" +launchctl enable "$GUI/cn.tkmind.imgproxy-compat" +launchctl kickstart -k "$GUI/cn.tkmind.imgproxy-compat" +curl -fsS --max-time 3 http://10.10.0.2:20081/health >/dev/null diff --git a/scripts/release-imgproxy-runtime-prod.sh b/scripts/release-imgproxy-runtime-prod.sh new file mode 100644 index 0000000..356b5cb --- /dev/null +++ b/scripts/release-imgproxy-runtime-prod.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# Release the independently versioned imgproxy runtime. Never run this from a +# feature branch; it is intentionally separate from the Portal runtime bundle. +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +HOST="${STUDIO_HOST:-john@58.38.22.103}" +REMOTE_ROOT="${STUDIO_REMOTE_ROOT:-/Users/john/Project}" +RUNTIME_BASE="$REMOTE_ROOT/imgproxy-runtime" +INCOMING="$REMOTE_ROOT/incoming/imgproxy-runtime" +RELEASE_ID="$(date +%Y%m%d-%H%M%S)-$(git -C "$ROOT" rev-parse --short HEAD)" +TMP="$(mktemp -d "${TMPDIR:-/tmp}/imgproxy-runtime-release.XXXXXX")" +cleanup() { rm -rf "$TMP"; } +trap cleanup EXIT + +[[ "$(git -C "$ROOT" branch --show-current)" == "main" ]] || { echo 'imgproxy production release must run from main' >&2; exit 1; } +[[ -z "$(git -C "$ROOT" status --porcelain)" ]] || { echo 'worktree must be clean' >&2; exit 1; } +git -C "$ROOT" fetch origin main --quiet +[[ "$(git -C "$ROOT" rev-parse HEAD)" == "$(git -C "$ROOT" rev-parse origin/main)" ]] || { echo 'main must match origin/main' >&2; exit 1; } +bash "$ROOT/scripts/check-release-ready.sh" + +bash "$ROOT/scripts/build-imgproxy-runtime-image.sh" +IMAGE_DIR="$ROOT/.runtime/imgproxy" +for file in imgproxy-runtime-image.tar.gz imgproxy-runtime-image.tar.gz.sha256 image-tag.txt; do + [[ -f "$IMAGE_DIR/$file" ]] || { echo "missing image artifact: $file" >&2; exit 1; } +done + +mkdir -p "$TMP/runtime" +cp "$IMAGE_DIR"/* "$TMP/runtime/" +cp "$ROOT/scripts/install-imgproxy-runtime-prod.sh" "$ROOT/scripts/imgproxy-compat-proxy.mjs" "$TMP/runtime/" +chmod 755 "$TMP/runtime/install-imgproxy-runtime-prod.sh" +tar -C "$TMP" -czf "$TMP/imgproxy-runtime-$RELEASE_ID.tar.gz" runtime +shasum -a 256 "$TMP/imgproxy-runtime-$RELEASE_ID.tar.gz" > "$TMP/imgproxy-runtime-$RELEASE_ID.tar.gz.sha256" + +ssh -o BatchMode=yes "$HOST" "mkdir -p '$INCOMING'" +scp -q "$TMP/imgproxy-runtime-$RELEASE_ID.tar.gz" "$TMP/imgproxy-runtime-$RELEASE_ID.tar.gz.sha256" "$HOST:$INCOMING/" +ssh -o BatchMode=yes "$HOST" "RELEASE_ID='$RELEASE_ID' RUNTIME_BASE='$RUNTIME_BASE' INCOMING='$INCOMING' bash -s" <<'REMOTE' +set -euo pipefail +archive="$INCOMING/imgproxy-runtime-$RELEASE_ID.tar.gz" +sha="$archive.sha256" +cd "$INCOMING" +shasum -a 256 -c "$sha" +release_dir="$RUNTIME_BASE/releases/$RELEASE_ID" +mkdir -p "$release_dir" "$RUNTIME_BASE/backups" +cp "$HOME/Library/LaunchAgents/cn.tkmind.imgproxy-compat.plist" "$RUNTIME_BASE/backups/imgproxy-compat-$RELEASE_ID.plist" 2>/dev/null || true +tar -xzf "$archive" -C "$release_dir" --strip-components=1 +cd "$release_dir" +shasum -a 256 -c imgproxy-runtime-image.tar.gz.sha256 +gunzip -c imgproxy-runtime-image.tar.gz | docker load +image_tag="$(cat image-tag.txt)" +IMGPROXY_RUNTIME_DIR="$release_dir" IMGPROXY_IMAGE_TAG="$image_tag" bash ./install-imgproxy-runtime-prod.sh +ln -sfn "$release_dir" "$RUNTIME_BASE/current" +curl -fsS --max-time 5 http://127.0.0.1:20082/health >/dev/null +curl -fsS --max-time 5 http://10.10.0.2:20081/health >/dev/null +REMOTE + +curl -kfsS --max-time 15 https://img.tkmind.cn/health >/dev/null +printf 'imgproxy release verified: %s\n' "$RELEASE_ID"