From 53d9af548e085603bfeaba91032cee8f3fa77625 Mon Sep 17 00:00:00 2001 From: Austin Burdine Date: Thu, 27 Aug 2026 17:20:20 -0400 Subject: [PATCH] Switched next variant to be a single-stage build no ref - per doi maintainer feedback, converting this image to a single-stage build --- 6-next/alpine3.23/Dockerfile | 101 +++++++++---------- 6-next/bookworm/Dockerfile | 142 ++++++++++++++------------- Dockerfile-next.template | 177 ++++++++++++++++------------------ generate-stackbrew-library.jq | 1 - 4 files changed, 200 insertions(+), 221 deletions(-) diff --git a/6-next/alpine3.23/Dockerfile b/6-next/alpine3.23/Dockerfile index c9537e70..6df162a2 100644 --- a/6-next/alpine3.23/Dockerfile +++ b/6-next/alpine3.23/Dockerfile @@ -4,17 +4,14 @@ # PLEASE DO NOT EDIT IT DIRECTLY. # -# ---- build: fetch Ghost, resolve its dependency tree, compile native modules ---- -FROM node:22-alpine3.23 AS build - -# nothing here reaches the runtime stage, so there is no cleanup to do -RUN set -eux; \ - apk add --no-cache dpkg gnupg +FROM node:22-alpine3.23 # grab gosu for easy step-down from root (ahead of the Ghost install so it stays cached) # https://github.com/tianon/gosu/releases ENV GOSU_VERSION=1.19 RUN set -eux; \ + apk add --no-cache --virtual .gosu-deps ca-certificates dpkg gnupg; \ + \ dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \ wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \ wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch.asc"; \ @@ -26,11 +23,26 @@ RUN set -eux; \ gpgconf --kill all; \ rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; \ \ + apk del --no-network .gosu-deps; \ + \ chmod +x /usr/local/bin/gosu; \ gosu --version; \ gosu nobody true +# the node image claims uid/gid 1000; hand it to Ghost instead, keeping the uid the CLI-based image +# ran as so existing bind mounts still work. "deluser" exists on both bases, so this needs neither +# usermod/groupmod (absent on Alpine) nor a home dir -- the install below creates it. +RUN set -eux; \ + deluser node; \ + rm -rf /home/node; \ + addgroup -g 1000 ghost; \ + adduser -u 1000 -G ghost -h /home/ghost -s /bin/sh -H -D ghost + +ENV NODE_ENV=production + ENV GHOST_INSTALL=/home/ghost +ENV GHOST_CONTENT=/home/ghost/content + ENV GHOST_VERSION=6.61.0 # resolved by "versions.sh" so a build cannot pick up a different tarball than the one reviewed @@ -38,70 +50,54 @@ ENV GHOST_TARBALL=https://github.com/TryGhost/Ghost/releases/download/v6.61.0/gh ENV GHOST_SHA256=d7a4de32641979fd57fa98bd29c128e406aa5421f3dadc04ffe28cb96507e69c RUN set -eux; \ +# Ghost pins its pnpm by hash in "packageManager", so corepack fetches exactly that one + corepack enable; \ + \ +# everything under the install dir is created by "ghost" rather than chowned afterwards: a recursive +# chown would copy the whole tree into a new layer mkdir -p "$GHOST_INSTALL"; \ + chown ghost:ghost "$GHOST_INSTALL"; \ cd "$GHOST_INSTALL"; \ \ - wget -O ghost.tgz "$GHOST_TARBALL"; \ +# corepack downloads and pnpm's cache/store derive their location from these; /tmp keeps them out of +# the install dir, and they are removed below + export XDG_CACHE_HOME=/tmp/xdg-cache XDG_DATA_HOME=/tmp/xdg-data; \ + \ + gosu ghost wget -O ghost.tgz "$GHOST_TARBALL"; \ echo "$GHOST_SHA256 ghost.tgz" | sha256sum -c -; \ # the release tarball has no leading "package/" component, unlike an npm pack - tar --extract --file ghost.tgz; \ + gosu ghost tar --extract --file ghost.tgz; \ rm ghost.tgz; \ \ -# Ghost pins its pnpm by hash in "packageManager", so corepack fetches exactly that one - corepack enable; \ - \ -# corepack downloads and pnpm's cache/store derive their location from these; /tmp keeps them -# out of the copied tree - export XDG_CACHE_HOME=/tmp/xdg-cache XDG_DATA_HOME=/tmp/xdg-data; \ - \ # the tarball ships a pruned lockfile, so the tree is installed exactly, not re-solved per build - pnpm install --prod --frozen-lockfile; \ + gosu ghost pnpm install --prod --frozen-lockfile; \ \ # install-time inputs only: package.json points 18 deps at "file:components/*.tgz", but pnpm # extracts them into its virtual store and nothing resolves back here afterwards - rm -rf "$GHOST_INSTALL/components"; \ + gosu ghost rm -rf "$GHOST_INSTALL/components"; \ \ # Ghost's own pruner, as used by its production image: drops dependency TypeScript, sourcemaps, # READMEs and vendored C/C++ (keeping licences and prebuilt .node). The tarball already has the # "archive" profile applied, so this is mostly node_modules. Node 22 strips the .mts types itself. - node scripts/prune.mts "$GHOST_INSTALL" --profile=image; \ + gosu ghost node scripts/prune.mts "$GHOST_INSTALL" --profile=image; \ \ # make a config.json symlink for NODE_ENV=development (config.production.json is copied in below) - ln -s config.production.json "$GHOST_INSTALL/config.development.json"; \ + gosu ghost ln -s config.production.json "$GHOST_INSTALL/config.development.json"; \ \ # need to save initial content for pre-seeding empty volumes - mv "$GHOST_INSTALL/content" "$GHOST_INSTALL/content.orig"; \ - mkdir -p "$GHOST_INSTALL/content"; \ + gosu ghost mv "$GHOST_INSTALL/content" "$GHOST_INSTALL/content.orig"; \ + gosu ghost mkdir -p "$GHOST_INSTALL/content"; \ \ -# test that the optional dependencies are installed and loadable - node -e 'require("better-sqlite3"); if (!require("@tryghost/image-transform").canTransformFiles()) throw new Error("sharp not installed");' - -# ---- runtime ---- -FROM alpine:3.23 - -RUN set -eux; \ -# libstdc++ is what node itself links against, plus better-sqlite3 and sharp - apk add --no-cache ca-certificates libstdc++; \ +# "corepack enable" above repointed the yarn symlinks at corepack, orphaning the standalone copy. +# This does not shrink the image -- those bytes live in a base layer and only get whited out -- but +# it keeps a dead tree out of the runtime filesystem + rm -rf /opt/yarn-*; \ + rm -rf /tmp/xdg-cache /tmp/xdg-data; \ + npm cache clean --force; \ \ -# uid 1000 is unclaimed on this base and matches what the CLI-based image ran as, so existing bind -# mounts keep working. No home dir: the COPY below creates it, without /etc/skel's dotfiles - addgroup -g 1000 ghost; \ - adduser -u 1000 -G ghost -h /home/ghost -s /bin/sh -H -D ghost - -# node, npm, npx, corepack and gosu all live here. "corepack enable" in the build stage repointed -# the yarn symlinks at corepack instead of the standalone yarn under /opt, so this tree resolves on -# its own. Keeping npm/corepack matches the tooling the Ghost-CLI image exposed. -# (node's own docker-entrypoint.sh tags along; ours is copied over it below.) -COPY --from=build /usr/local /usr/local - -ENV NODE_ENV=production - -ENV GHOST_INSTALL=/home/ghost -ENV GHOST_CONTENT=/home/ghost/content - -ENV GHOST_VERSION=6.61.0 - -COPY --from=build --chown=ghost:ghost /home/ghost /home/ghost +# test that the optional dependencies are installed and loadable + node --version; \ + gosu ghost node -e 'require("better-sqlite3"); if (!require("@tryghost/image-transform").canTransformFiles()) throw new Error("sharp not installed");' # Ghost-CLI used to generate this. Everything in it is overridable via Ghost's "__" env vars, and # "process" is deliberately absent -- it only ever told Ghost-CLI which process manager to use @@ -110,14 +106,9 @@ COPY --chown=ghost:ghost config.production.json $GHOST_INSTALL/ RUN set -eux; \ # a mount point, so it stays writable whatever uid ends up owning what gets mounted over it chmod 1777 "$GHOST_CONTENT"; \ - \ -# node --version catches a missing libstdc++/libatomic; the module loads prove the copied tree -# still resolves outside the build stage - node --version; \ cd "$GHOST_INSTALL"; \ node -e 'JSON.parse(require("fs").readFileSync("config.production.json"))'; \ - [ "$(readlink -f config.development.json)" = "$GHOST_INSTALL/config.production.json" ]; \ - gosu ghost node -e 'require("better-sqlite3"); if (!require("@tryghost/image-transform").canTransformFiles()) throw new Error("sharp not installed");' + [ "$(readlink -f config.development.json)" = "$GHOST_INSTALL/config.production.json" ] WORKDIR $GHOST_INSTALL VOLUME $GHOST_CONTENT diff --git a/6-next/bookworm/Dockerfile b/6-next/bookworm/Dockerfile index 63311903..bd65286f 100644 --- a/6-next/bookworm/Dockerfile +++ b/6-next/bookworm/Dockerfile @@ -4,28 +4,17 @@ # PLEASE DO NOT EDIT IT DIRECTLY. # -# ---- build: fetch Ghost, resolve its dependency tree, compile native modules ---- -FROM node:22-bookworm-slim AS build - -# nothing here reaches the runtime stage, so there is no cleanup to do -RUN set -eux; \ - apt-get update; \ -# unlike the Alpine image, "node:*-slim" ships neither wget nor ca-certificates - apt-get install -y --no-install-recommends \ - ca-certificates \ - gnupg \ - wget \ -# only arm32v7 needs these: it has no prebuilt better-sqlite3 or re2, so they fall back to node-gyp - g++ \ - make \ - python3 \ - ; \ - rm -rf /var/lib/apt/lists/* +FROM node:22-bookworm-slim # grab gosu for easy step-down from root (ahead of the Ghost install so it stays cached) # https://github.com/tianon/gosu/releases ENV GOSU_VERSION=1.19 RUN set -eux; \ + savedAptMark="$(apt-mark showmanual)"; \ + apt-get update; \ +# unlike the Alpine image, "node:*-slim" ships neither wget nor ca-certificates + apt-get install -y --no-install-recommends ca-certificates gnupg wget; \ + \ dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \ wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \ wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch.asc"; \ @@ -37,11 +26,29 @@ RUN set -eux; \ gpgconf --kill all; \ rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; \ \ + apt-mark auto '.*' > /dev/null; \ + [ -z "$savedAptMark" ] || apt-mark manual $savedAptMark > /dev/null; \ + apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \ + rm -rf /var/lib/apt/lists/*; \ + \ chmod +x /usr/local/bin/gosu; \ gosu --version; \ gosu nobody true +# the node image claims uid/gid 1000; hand it to Ghost instead, keeping the uid the CLI-based image +# ran as so existing bind mounts still work. "deluser" exists on both bases, so this needs neither +# usermod/groupmod (absent on Alpine) nor a home dir -- the install below creates it. +RUN set -eux; \ + deluser node; \ + rm -rf /home/node; \ + groupadd --gid 1000 ghost; \ + useradd --uid 1000 --gid 1000 --home-dir /home/ghost --no-create-home --shell /bin/bash ghost + +ENV NODE_ENV=production + ENV GHOST_INSTALL=/home/ghost +ENV GHOST_CONTENT=/home/ghost/content + ENV GHOST_VERSION=6.61.0 # resolved by "versions.sh" so a build cannot pick up a different tarball than the one reviewed @@ -49,77 +56,73 @@ ENV GHOST_TARBALL=https://github.com/TryGhost/Ghost/releases/download/v6.61.0/gh ENV GHOST_SHA256=d7a4de32641979fd57fa98bd29c128e406aa5421f3dadc04ffe28cb96507e69c RUN set -eux; \ + savedAptMark="$(apt-mark showmanual)"; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + ca-certificates \ + wget \ +# only arm32v7 needs these: it has no prebuilt better-sqlite3 or re2, so they fall back to node-gyp + g++ \ + make \ + python3 \ + ; \ + \ +# Ghost pins its pnpm by hash in "packageManager", so corepack fetches exactly that one + corepack enable; \ + \ +# everything under the install dir is created by "ghost" rather than chowned afterwards: a recursive +# chown would copy the whole tree into a new layer mkdir -p "$GHOST_INSTALL"; \ + chown ghost:ghost "$GHOST_INSTALL"; \ cd "$GHOST_INSTALL"; \ \ - wget -O ghost.tgz "$GHOST_TARBALL"; \ +# corepack downloads and pnpm's cache/store derive their location from these; /tmp keeps them out of +# the install dir, and they are removed below + export XDG_CACHE_HOME=/tmp/xdg-cache XDG_DATA_HOME=/tmp/xdg-data; \ + \ + gosu ghost wget -O ghost.tgz "$GHOST_TARBALL"; \ echo "$GHOST_SHA256 ghost.tgz" | sha256sum -c -; \ # the release tarball has no leading "package/" component, unlike an npm pack - tar --extract --file ghost.tgz; \ + gosu ghost tar --extract --file ghost.tgz; \ rm ghost.tgz; \ \ -# Ghost pins its pnpm by hash in "packageManager", so corepack fetches exactly that one - corepack enable; \ - \ -# corepack downloads and pnpm's cache/store derive their location from these; /tmp keeps them -# out of the copied tree - export XDG_CACHE_HOME=/tmp/xdg-cache XDG_DATA_HOME=/tmp/xdg-data; \ - \ # the tarball ships a pruned lockfile, so the tree is installed exactly, not re-solved per build - pnpm install --prod --frozen-lockfile; \ + gosu ghost pnpm install --prod --frozen-lockfile; \ \ # install-time inputs only: package.json points 18 deps at "file:components/*.tgz", but pnpm # extracts them into its virtual store and nothing resolves back here afterwards - rm -rf "$GHOST_INSTALL/components"; \ + gosu ghost rm -rf "$GHOST_INSTALL/components"; \ \ # Ghost's own pruner, as used by its production image: drops dependency TypeScript, sourcemaps, # READMEs and vendored C/C++ (keeping licences and prebuilt .node). The tarball already has the # "archive" profile applied, so this is mostly node_modules. Node 22 strips the .mts types itself. - node scripts/prune.mts "$GHOST_INSTALL" --profile=image; \ + gosu ghost node scripts/prune.mts "$GHOST_INSTALL" --profile=image; \ \ # make a config.json symlink for NODE_ENV=development (config.production.json is copied in below) - ln -s config.production.json "$GHOST_INSTALL/config.development.json"; \ + gosu ghost ln -s config.production.json "$GHOST_INSTALL/config.development.json"; \ \ # need to save initial content for pre-seeding empty volumes - mv "$GHOST_INSTALL/content" "$GHOST_INSTALL/content.orig"; \ - mkdir -p "$GHOST_INSTALL/content"; \ + gosu ghost mv "$GHOST_INSTALL/content" "$GHOST_INSTALL/content.orig"; \ + gosu ghost mkdir -p "$GHOST_INSTALL/content"; \ \ -# test that the optional dependencies are installed and loadable - node -e 'require("better-sqlite3"); if (!require("@tryghost/image-transform").canTransformFiles()) throw new Error("sharp not installed");' - -# ---- runtime ---- -FROM debian:bookworm-slim - -RUN set -eux; \ - apt-get update; \ - apt-get install -y --no-install-recommends \ -# libstdc++6 is what node itself links against, plus better-sqlite3 and sharp; libatomic1 is not -# needed on every arch, but node's own image installs it unconditionally - libstdc++6 \ - libatomic1 \ - ca-certificates \ - ; \ +# "corepack enable" above repointed the yarn symlinks at corepack, orphaning the standalone copy. +# This does not shrink the image -- those bytes live in a base layer and only get whited out -- but +# it keeps a dead tree out of the runtime filesystem + rm -rf /opt/yarn-*; \ + rm -rf /tmp/xdg-cache /tmp/xdg-data; \ + npm cache clean --force; \ + \ + apt-mark auto '.*' > /dev/null; \ + [ -z "$savedAptMark" ] || apt-mark manual $savedAptMark > /dev/null; \ +# node's image ships no CA bundle of its own, and the purge below would take the one installed +# above with it; Node has a bundled store, but anything shelling out needs the system one + apt-mark manual ca-certificates > /dev/null; \ + apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \ rm -rf /var/lib/apt/lists/*; \ \ -# uid 1000 is unclaimed on this base and matches what the CLI-based image ran as, so existing bind -# mounts keep working. No home dir: the COPY below creates it, without /etc/skel's dotfiles - groupadd --gid 1000 ghost; \ - useradd --uid 1000 --gid 1000 --home-dir /home/ghost --no-create-home --shell /bin/bash ghost - -# node, npm, npx, corepack and gosu all live here. "corepack enable" in the build stage repointed -# the yarn symlinks at corepack instead of the standalone yarn under /opt, so this tree resolves on -# its own. Keeping npm/corepack matches the tooling the Ghost-CLI image exposed. -# (node's own docker-entrypoint.sh tags along; ours is copied over it below.) -COPY --from=build /usr/local /usr/local - -ENV NODE_ENV=production - -ENV GHOST_INSTALL=/home/ghost -ENV GHOST_CONTENT=/home/ghost/content - -ENV GHOST_VERSION=6.61.0 - -COPY --from=build --chown=ghost:ghost /home/ghost /home/ghost +# test that the optional dependencies are installed and loadable + node --version; \ + gosu ghost node -e 'require("better-sqlite3"); if (!require("@tryghost/image-transform").canTransformFiles()) throw new Error("sharp not installed");' # Ghost-CLI used to generate this. Everything in it is overridable via Ghost's "__" env vars, and # "process" is deliberately absent -- it only ever told Ghost-CLI which process manager to use @@ -128,14 +131,9 @@ COPY --chown=ghost:ghost config.production.json $GHOST_INSTALL/ RUN set -eux; \ # a mount point, so it stays writable whatever uid ends up owning what gets mounted over it chmod 1777 "$GHOST_CONTENT"; \ - \ -# node --version catches a missing libstdc++/libatomic; the module loads prove the copied tree -# still resolves outside the build stage - node --version; \ cd "$GHOST_INSTALL"; \ node -e 'JSON.parse(require("fs").readFileSync("config.production.json"))'; \ - [ "$(readlink -f config.development.json)" = "$GHOST_INSTALL/config.production.json" ]; \ - gosu ghost node -e 'require("better-sqlite3"); if (!require("@tryghost/image-transform").canTransformFiles()) throw new Error("sharp not installed");' + [ "$(readlink -f config.development.json)" = "$GHOST_INSTALL/config.production.json" ] WORKDIR $GHOST_INSTALL VOLUME $GHOST_CONTENT diff --git a/Dockerfile-next.template b/Dockerfile-next.template index 6e8cb69d..3bca6203 100644 --- a/Dockerfile-next.template +++ b/Dockerfile-next.template @@ -7,43 +7,22 @@ if env.variant | contains("bookworm") then "rm -rf /var/lib/apt/lists/*" else "apt-get dist-clean" end - ; - # the runtime stage starts from the distro under "node:*", not node itself: node ships a "node" - # user at 1000:1000 and Ghost wants that uid, so this way there is no user to renumber (which - # Alpine could not do anyway -- no usermod/groupmod) - def runtime_base: - if is_alpine then - env.variant | sub("^alpine"; "alpine:") - else - "debian:\(env.variant)-slim" - end -}} -# ---- build: fetch Ghost, resolve its dependency tree, compile native modules ---- -FROM {{ .variants[env.variant].from }} AS build +FROM {{ .variants[env.variant].from }} -# nothing here reaches the runtime stage, so there is no cleanup to do +# grab gosu for easy step-down from root (ahead of the Ghost install so it stays cached) +# https://github.com/tianon/gosu/releases +ENV GOSU_VERSION=1.19 RUN set -eux; \ {{ if is_alpine then ( -}} - apk add --no-cache dpkg gnupg + apk add --no-cache --virtual .gosu-deps ca-certificates dpkg gnupg; \ {{ ) else ( -}} + savedAptMark="$(apt-mark showmanual)"; \ apt-get update; \ # unlike the Alpine image, "node:*-slim" ships neither wget nor ca-certificates - apt-get install -y --no-install-recommends \ - ca-certificates \ - gnupg \ - wget \ -# only arm32v7 needs these: it has no prebuilt better-sqlite3 or re2, so they fall back to node-gyp - g++ \ - make \ - python3 \ - ; \ - {{ clean_apt }} + apt-get install -y --no-install-recommends ca-certificates gnupg wget; \ {{ ) end -}} - -# grab gosu for easy step-down from root (ahead of the Ghost install so it stays cached) -# https://github.com/tianon/gosu/releases -ENV GOSU_VERSION=1.19 -RUN set -eux; \ + \ dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \ wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \ wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch.asc"; \ @@ -55,11 +34,38 @@ RUN set -eux; \ gpgconf --kill all; \ rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; \ \ +{{ if is_alpine then ( -}} + apk del --no-network .gosu-deps; \ +{{ ) else ( -}} + apt-mark auto '.*' > /dev/null; \ + [ -z "$savedAptMark" ] || apt-mark manual $savedAptMark > /dev/null; \ + apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \ + {{ clean_apt }}; \ +{{ ) end -}} + \ chmod +x /usr/local/bin/gosu; \ gosu --version; \ gosu nobody true +# the node image claims uid/gid 1000; hand it to Ghost instead, keeping the uid the CLI-based image +# ran as so existing bind mounts still work. "deluser" exists on both bases, so this needs neither +# usermod/groupmod (absent on Alpine) nor a home dir -- the install below creates it. +RUN set -eux; \ + deluser node; \ + rm -rf /home/node; \ +{{ if is_alpine then ( -}} + addgroup -g 1000 ghost; \ + adduser -u 1000 -G ghost -h /home/ghost -s /bin/sh -H -D ghost +{{ ) else ( -}} + groupadd --gid 1000 ghost; \ + useradd --uid 1000 --gid 1000 --home-dir /home/ghost --no-create-home --shell /bin/bash ghost +{{ ) end -}} + +ENV NODE_ENV=production + ENV GHOST_INSTALL=/home/ghost +ENV GHOST_CONTENT=/home/ghost/content + ENV GHOST_VERSION={{ .version }} # resolved by "versions.sh" so a build cannot pick up a different tarball than the one reviewed @@ -67,87 +73,77 @@ ENV GHOST_TARBALL={{ .tarball.url }} ENV GHOST_SHA256={{ .tarball.sha256 }} RUN set -eux; \ +{{ if is_alpine then "" else ( -}} + savedAptMark="$(apt-mark showmanual)"; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + ca-certificates \ + wget \ +# only arm32v7 needs these: it has no prebuilt better-sqlite3 or re2, so they fall back to node-gyp + g++ \ + make \ + python3 \ + ; \ + \ +{{ ) end -}} +# Ghost pins its pnpm by hash in "packageManager", so corepack fetches exactly that one + corepack enable; \ + \ +# everything under the install dir is created by "ghost" rather than chowned afterwards: a recursive +# chown would copy the whole tree into a new layer mkdir -p "$GHOST_INSTALL"; \ + chown ghost:ghost "$GHOST_INSTALL"; \ cd "$GHOST_INSTALL"; \ \ - wget -O ghost.tgz "$GHOST_TARBALL"; \ +# corepack downloads and pnpm's cache/store derive their location from these; /tmp keeps them out of +# the install dir, and they are removed below + export XDG_CACHE_HOME=/tmp/xdg-cache XDG_DATA_HOME=/tmp/xdg-data; \ + \ + gosu ghost wget -O ghost.tgz "$GHOST_TARBALL"; \ echo "$GHOST_SHA256 ghost.tgz" | sha256sum -c -; \ # the release tarball has no leading "package/" component, unlike an npm pack - tar --extract --file ghost.tgz; \ + gosu ghost tar --extract --file ghost.tgz; \ rm ghost.tgz; \ \ -# Ghost pins its pnpm by hash in "packageManager", so corepack fetches exactly that one - corepack enable; \ - \ -# corepack downloads and pnpm's cache/store derive their location from these; /tmp keeps them -# out of the copied tree - export XDG_CACHE_HOME=/tmp/xdg-cache XDG_DATA_HOME=/tmp/xdg-data; \ - \ # the tarball ships a pruned lockfile, so the tree is installed exactly, not re-solved per build - pnpm install --prod --frozen-lockfile; \ + gosu ghost pnpm install --prod --frozen-lockfile; \ \ # install-time inputs only: package.json points 18 deps at "file:components/*.tgz", but pnpm # extracts them into its virtual store and nothing resolves back here afterwards - rm -rf "$GHOST_INSTALL/components"; \ + gosu ghost rm -rf "$GHOST_INSTALL/components"; \ \ # Ghost's own pruner, as used by its production image: drops dependency TypeScript, sourcemaps, # READMEs and vendored C/C++ (keeping licences and prebuilt .node). The tarball already has the # "archive" profile applied, so this is mostly node_modules. Node 22 strips the .mts types itself. - node scripts/prune.mts "$GHOST_INSTALL" --profile=image; \ + gosu ghost node scripts/prune.mts "$GHOST_INSTALL" --profile=image; \ \ # make a config.json symlink for NODE_ENV=development (config.production.json is copied in below) - ln -s config.production.json "$GHOST_INSTALL/config.development.json"; \ + gosu ghost ln -s config.production.json "$GHOST_INSTALL/config.development.json"; \ \ # need to save initial content for pre-seeding empty volumes - mv "$GHOST_INSTALL/content" "$GHOST_INSTALL/content.orig"; \ - mkdir -p "$GHOST_INSTALL/content"; \ + gosu ghost mv "$GHOST_INSTALL/content" "$GHOST_INSTALL/content.orig"; \ + gosu ghost mkdir -p "$GHOST_INSTALL/content"; \ \ -# test that the optional dependencies are installed and loadable - node -e 'require("better-sqlite3"); if (!require("@tryghost/image-transform").canTransformFiles()) throw new Error("sharp not installed");' - -# ---- runtime ---- -FROM {{ runtime_base }} - -RUN set -eux; \ -{{ if is_alpine then ( -}} -# libstdc++ is what node itself links against, plus better-sqlite3 and sharp - apk add --no-cache ca-certificates libstdc++; \ -{{ ) else ( -}} - apt-get update; \ - apt-get install -y --no-install-recommends \ -# libstdc++6 is what node itself links against, plus better-sqlite3 and sharp; libatomic1 is not -# needed on every arch, but node's own image installs it unconditionally - libstdc++6 \ - libatomic1 \ - ca-certificates \ - ; \ +# "corepack enable" above repointed the yarn symlinks at corepack, orphaning the standalone copy. +# This does not shrink the image -- those bytes live in a base layer and only get whited out -- but +# it keeps a dead tree out of the runtime filesystem + rm -rf /opt/yarn-*; \ + rm -rf /tmp/xdg-cache /tmp/xdg-data; \ + npm cache clean --force; \ + \ +{{ if is_alpine then "" else ( -}} + apt-mark auto '.*' > /dev/null; \ + [ -z "$savedAptMark" ] || apt-mark manual $savedAptMark > /dev/null; \ +# node's image ships no CA bundle of its own, and the purge below would take the one installed +# above with it; Node has a bundled store, but anything shelling out needs the system one + apt-mark manual ca-certificates > /dev/null; \ + apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \ {{ clean_apt }}; \ -{{ ) end -}} \ -# uid 1000 is unclaimed on this base and matches what the CLI-based image ran as, so existing bind -# mounts keep working. No home dir: the COPY below creates it, without /etc/skel's dotfiles -{{ if is_alpine then ( -}} - addgroup -g 1000 ghost; \ - adduser -u 1000 -G ghost -h /home/ghost -s /bin/sh -H -D ghost -{{ ) else ( -}} - groupadd --gid 1000 ghost; \ - useradd --uid 1000 --gid 1000 --home-dir /home/ghost --no-create-home --shell /bin/bash ghost {{ ) end -}} - -# node, npm, npx, corepack and gosu all live here. "corepack enable" in the build stage repointed -# the yarn symlinks at corepack instead of the standalone yarn under /opt, so this tree resolves on -# its own. Keeping npm/corepack matches the tooling the Ghost-CLI image exposed. -# (node's own docker-entrypoint.sh tags along; ours is copied over it below.) -COPY --from=build /usr/local /usr/local - -ENV NODE_ENV=production - -ENV GHOST_INSTALL=/home/ghost -ENV GHOST_CONTENT=/home/ghost/content - -ENV GHOST_VERSION={{ .version }} - -COPY --from=build --chown=ghost:ghost /home/ghost /home/ghost +# test that the optional dependencies are installed and loadable + node --version; \ + gosu ghost node -e 'require("better-sqlite3"); if (!require("@tryghost/image-transform").canTransformFiles()) throw new Error("sharp not installed");' # Ghost-CLI used to generate this. Everything in it is overridable via Ghost's "__" env vars, and # "process" is deliberately absent -- it only ever told Ghost-CLI which process manager to use @@ -156,14 +152,9 @@ COPY --chown=ghost:ghost config.production.json $GHOST_INSTALL/ RUN set -eux; \ # a mount point, so it stays writable whatever uid ends up owning what gets mounted over it chmod 1777 "$GHOST_CONTENT"; \ - \ -# node --version catches a missing libstdc++/libatomic; the module loads prove the copied tree -# still resolves outside the build stage - node --version; \ cd "$GHOST_INSTALL"; \ node -e 'JSON.parse(require("fs").readFileSync("config.production.json"))'; \ - [ "$(readlink -f config.development.json)" = "$GHOST_INSTALL/config.production.json" ]; \ - gosu ghost node -e 'require("better-sqlite3"); if (!require("@tryghost/image-transform").canTransformFiles()) throw new Error("sharp not installed");' + [ "$(readlink -f config.development.json)" = "$GHOST_INSTALL/config.production.json" ] WORKDIR $GHOST_INSTALL VOLUME $GHOST_CONTENT diff --git a/generate-stackbrew-library.jq b/generate-stackbrew-library.jq index aa8f3cbc..8345fc42 100644 --- a/generate-stackbrew-library.jq +++ b/generate-stackbrew-library.jq @@ -83,6 +83,5 @@ else . end "Tags: \($tags | join(", "))", "Directory: \($majorVersion)/\($variant)", "Architectures: \(.arches - (.arches - $parentArches[.from]) | join(", "))", - (if $majorVersion | isNext then "Builder: buildkit" else empty end), empty )