Update 6 to 6.61.0 - #22147
Conversation
This comment has been minimized.
This comment has been minimized.
closes docker-library#22081 - added new 6-next tagged images
This comment has been minimized.
This comment has been minimized.
|
I usually reject PRs that attempt to use a multi-stage build since they are often just avoiding clean up steps. But this is a little more nuanced than that. The differences that I see compared to just building
While I would reject if it was just doing the first two since a multi-stage build just adds complication and those can be more simply accomplished without it, I am enticed by the removal of yarn. That is the default in the newest node images, but if it is reasonable to also remove |
|
@yosifkit The original approach just copied the node binary from the base image, the reason I copied npm as well is that Ghost's adapters are often shipped as npm packages, and it's easiest to install them by fetching the tarball from npm - |
|
also see #22081 (comment) - my understanding of this bit of @tianon's comment:
was to use multi-stage, though I might have misunderstood what was meant there |
|
I think my conceptual was maybe more surgical than just copying everything - if you're just copying everything, renaming the user instead is pretty lightweight (and the other tool cleanup code already exists 😅). |
|
Made TryGhost/docker-library-ghost#482 switching it back to a single-stage image. Adds about ~17mb of overhead but that's probably fine, esp since this next variant is still several hundred mb smaller than the current one overall. |
|
I think TryGhost/docker-library-ghost#482 looks good. |
Diff for a96de87:diff --git a/_bashbrew-cat b/_bashbrew-cat
index ff806f5..2165c8b 100644
--- a/_bashbrew-cat
+++ b/_bashbrew-cat
@@ -1,11 +1,21 @@
Maintainers: Austin Burdine <austin@ghost.org> (@acburdine)
GitRepo: https://github.com/TryGhost/docker-library-ghost.git
-GitCommit: 4a9626af1d9f6a8f903ab6246fda29e56dcf106f
+GitCommit: 653aed98404d4e3428aa09a77347a2d3d7d775f2
-Tags: 6.60.0-alpine3.23, 6.60.0-alpine, 6.60-alpine3.23, 6.60-alpine, 6-alpine3.23, 6-alpine, alpine3.23, alpine
+Tags: 6.61.0-alpine3.23, 6.61.0-alpine, 6.61-alpine3.23, 6.61-alpine, 6-alpine3.23, 6-alpine, alpine3.23, alpine
Architectures: amd64, arm64v8
Directory: 6/alpine3.23
-Tags: 6.60.0-bookworm, 6.60.0, 6.60-bookworm, 6.60, 6-bookworm, 6, bookworm, latest
+Tags: 6.61.0-bookworm, 6.61.0, 6.61-bookworm, 6.61, 6-bookworm, 6, bookworm, latest
Architectures: amd64, arm32v7, arm64v8
Directory: 6/bookworm
+
+Tags: 6.61.0-next-alpine3.23, 6.61.0-next-alpine, 6.61-next-alpine3.23, 6.61-next-alpine, 6-next-alpine3.23, 6-next-alpine, next-alpine3.23, next-alpine
+Architectures: amd64, arm64v8
+Directory: 6-next/alpine3.23
+Builder: buildkit
+
+Tags: 6.61.0-next-bookworm, 6.61.0-next, 6.61-next-bookworm, 6.61-next, 6-next-bookworm, 6-next, next-bookworm, next
+Architectures: amd64, arm32v7, arm64v8
+Directory: 6-next/bookworm
+Builder: buildkit
diff --git a/_bashbrew-list b/_bashbrew-list
index 55069e3..539886a 100644
--- a/_bashbrew-list
+++ b/_bashbrew-list
@@ -2,15 +2,31 @@ ghost:6
ghost:6-alpine
ghost:6-alpine3.23
ghost:6-bookworm
-ghost:6.60
-ghost:6.60-alpine
-ghost:6.60-alpine3.23
-ghost:6.60-bookworm
-ghost:6.60.0
-ghost:6.60.0-alpine
-ghost:6.60.0-alpine3.23
-ghost:6.60.0-bookworm
+ghost:6-next
+ghost:6-next-alpine
+ghost:6-next-alpine3.23
+ghost:6-next-bookworm
+ghost:6.61
+ghost:6.61-alpine
+ghost:6.61-alpine3.23
+ghost:6.61-bookworm
+ghost:6.61-next
+ghost:6.61-next-alpine
+ghost:6.61-next-alpine3.23
+ghost:6.61-next-bookworm
+ghost:6.61.0
+ghost:6.61.0-alpine
+ghost:6.61.0-alpine3.23
+ghost:6.61.0-bookworm
+ghost:6.61.0-next
+ghost:6.61.0-next-alpine
+ghost:6.61.0-next-alpine3.23
+ghost:6.61.0-next-bookworm
ghost:alpine
ghost:alpine3.23
ghost:bookworm
ghost:latest
+ghost:next
+ghost:next-alpine
+ghost:next-alpine3.23
+ghost:next-bookworm
diff --git a/_bashbrew-list-build-order b/_bashbrew-list-build-order
index 31c0ef3..6fdef7b 100644
--- a/_bashbrew-list-build-order
+++ b/_bashbrew-list-build-order
@@ -1,2 +1,4 @@
ghost:alpine
ghost:latest
+ghost:next
+ghost:next-alpine
diff --git a/ghost_alpine/Dockerfile b/ghost_alpine/Dockerfile
index 7ae8a11..533fef6 100644
--- a/ghost_alpine/Dockerfile
+++ b/ghost_alpine/Dockerfile
@@ -86,7 +86,7 @@ RUN set -eux; \
ENV GHOST_INSTALL=/var/lib/ghost
ENV GHOST_CONTENT=/var/lib/ghost/content
-ENV GHOST_VERSION=6.60.0
+ENV GHOST_VERSION=6.61.0
RUN set -eux; \
mkdir -p "$GHOST_INSTALL"; \
diff --git a/ghost_latest/Dockerfile b/ghost_latest/Dockerfile
index 4374f60..4b92495 100644
--- a/ghost_latest/Dockerfile
+++ b/ghost_latest/Dockerfile
@@ -88,7 +88,7 @@ RUN set -eux; \
ENV GHOST_INSTALL=/var/lib/ghost
ENV GHOST_CONTENT=/var/lib/ghost/content
-ENV GHOST_VERSION=6.60.0
+ENV GHOST_VERSION=6.61.0
RUN set -eux; \
mkdir -p "$GHOST_INSTALL"; \
diff --git a/ghost_alpine/Dockerfile b/ghost_next-alpine/Dockerfile
similarity index 28%
copy from ghost_alpine/Dockerfile
copy to ghost_next-alpine/Dockerfile
index 7ae8a11..6df162a 100644
--- a/ghost_alpine/Dockerfile
+++ b/ghost_next-alpine/Dockerfile
@@ -6,20 +6,11 @@
FROM node:22-alpine3.23
-RUN apk add --no-cache \
-# add "bash" for "[["
- bash
-
-# grab gosu for easy step-down from root
+# 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 \
- ; \
+ 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"; \
@@ -32,97 +23,92 @@ RUN set -eux; \
gpgconf --kill all; \
rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; \
\
-# clean up fetch dependencies
apk del --no-network .gosu-deps; \
\
chmod +x /usr/local/bin/gosu; \
-# verify that the binary works
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
-# Ghost-CLI is installed from source rather than "npm install -g ghost-cli" so that
-# pnpm can resolve the dependency tree from the lockfile committed alongside the tag
-# ("npm install -g" ignores lockfiles entirely, so the tree it produces varies by build date)
-ENV GHOST_CLI_VERSION=1.32.2
-ENV GHOST_CLI_SHA=5e0b07b7619207459956dd9728bafc5cd7d15f01
-ENV GHOST_CLI_INSTALL=/usr/local/lib/ghost-cli
+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
+ENV GHOST_TARBALL=https://github.com/TryGhost/Ghost/releases/download/v6.61.0/ghost-6.61.0.tgz
+ENV GHOST_SHA256=d7a4de32641979fd57fa98bd29c128e406aa5421f3dadc04ffe28cb96507e69c
+
RUN set -eux; \
- \
- apk add --no-cache --virtual .ghost-cli-deps git; \
- \
+# Ghost pins its pnpm by hash in "packageManager", so corepack fetches exactly that one
corepack enable; \
\
-# corepack's downloads, pnpm's metadata cache and pnpm's store all derive their location
-# from these, so pointing them at /tmp keeps every throwaway byte in one place
+# 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"; \
+ \
+# 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; \
\
- mkdir -p "$GHOST_CLI_INSTALL"; \
- cd "$GHOST_CLI_INSTALL"; \
- git init --quiet .; \
- git remote add origin https://github.com/TryGhost/Ghost-CLI.git; \
-# fetching the commit by hash means git's own object verification pins the source
-# (a moved tag or a re-rolled release tarball cannot change what we get)
- git fetch --quiet --depth 1 origin "$GHOST_CLI_SHA"; \
- git checkout --quiet FETCH_HEAD; \
- [ "$(node -p 'require("./package.json").version')" = "$GHOST_CLI_VERSION" ]; \
+ 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
+ gosu ghost tar --extract --file ghost.tgz; \
+ rm ghost.tgz; \
+ \
+# the tarball ships a pruned lockfile, so the tree is installed exactly, not re-solved per build
+ gosu ghost pnpm install --prod --frozen-lockfile; \
\
-# "--frozen-lockfile" is the point of all this: it installs pnpm-lock.yaml exactly, or fails
- 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
+ gosu ghost rm -rf "$GHOST_INSTALL/components"; \
\
- ln -s "$GHOST_CLI_INSTALL/bin/ghost" /usr/local/bin/ghost; \
+# 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.
+ gosu ghost node scripts/prune.mts "$GHOST_INSTALL" --profile=image; \
\
-# drop what "npm publish" would not have shipped (see "files" in Ghost-CLI's package.json)
- rm -rf .git test .github; \
+# make a config.json symlink for NODE_ENV=development (config.production.json is copied in below)
+ gosu ghost ln -s config.production.json "$GHOST_INSTALL/config.development.json"; \
\
- apk del --no-network .ghost-cli-deps; \
+# need to save initial content for pre-seeding empty volumes
+ gosu ghost mv "$GHOST_INSTALL/content" "$GHOST_INSTALL/content.orig"; \
+ gosu ghost mkdir -p "$GHOST_INSTALL/content"; \
\
+# "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; \
\
- ghost --version
+# 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");'
-ENV GHOST_INSTALL=/var/lib/ghost
-ENV GHOST_CONTENT=/var/lib/ghost/content
-
-ENV GHOST_VERSION=6.60.0
+# 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
+COPY --chown=ghost:ghost config.production.json $GHOST_INSTALL/
RUN set -eux; \
- mkdir -p "$GHOST_INSTALL"; \
- chown node:node "$GHOST_INSTALL"; \
- \
- apk add --no-cache --virtual .build-deps-ghost g++ linux-headers make python3 py3-setuptools; \
- \
- gosu node ghost install "$GHOST_VERSION" --db mysql --dbhost mysql --no-prompt --no-stack --no-setup --dir "$GHOST_INSTALL"; \
- \
-# Tell Ghost to listen on all ips and not prompt for additional configuration
- cd "$GHOST_INSTALL"; \
- gosu node ghost config --no-prompt --ip '::' --port 2368 --url 'http://localhost:2368'; \
- gosu node ghost config paths.contentPath "$GHOST_CONTENT"; \
- \
-# make a config.json symlink for NODE_ENV=development (and sanity check that it's correct)
- gosu node ln -s config.production.json "$GHOST_INSTALL/config.development.json"; \
- readlink -f "$GHOST_INSTALL/config.development.json"; \
- \
-# need to save initial content for pre-seeding empty volumes
- mv "$GHOST_CONTENT" "$GHOST_INSTALL/content.orig"; \
- mkdir -p "$GHOST_CONTENT"; \
- chown node:node "$GHOST_CONTENT"; \
+# a mount point, so it stays writable whatever uid ends up owning what gets mounted over it
chmod 1777 "$GHOST_CONTENT"; \
- \
- apk del --no-network .build-deps-ghost; \
- \
- gosu node pnpm store prune; \
- gosu node npm cache clean --force; \
- npm cache clean --force; \
-# none of these are needed to run Ghost: corepack re-downloads pnpm on demand and node-gyp
-# only matters while compiling native modules, which is finished by this point
- rm -rf /home/node/.cache/node/corepack /home/node/.cache/node-gyp /home/node/.cache/pnpm; \
- \
- # test that the optional dependencies are installed and loadable
- cd current; \
- gosu node node -e 'require("better-sqlite3"); if (!require("@tryghost/image-transform").canTransformFiles()) throw new Error("sharp not installed");'
+ cd "$GHOST_INSTALL"; \
+ node -e 'JSON.parse(require("fs").readFileSync("config.production.json"))'; \
+ [ "$(readlink -f config.development.json)" = "$GHOST_INSTALL/config.production.json" ]
WORKDIR $GHOST_INSTALL
VOLUME $GHOST_CONTENT
@@ -131,4 +117,4 @@ COPY docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT ["docker-entrypoint.sh"]
EXPOSE 2368
-CMD ["node", "current/index.js"]
+CMD ["node", "index.js"]
diff --git a/ghost_next-alpine/config.production.json b/ghost_next-alpine/config.production.json
new file mode 100644
index 0000000..31ec76a
--- /dev/null
+++ b/ghost_next-alpine/config.production.json
@@ -0,0 +1,22 @@
+{
+ "url": "http://localhost:2368",
+ "server": {
+ "port": 2368,
+ "host": "::"
+ },
+ "mail": {
+ "transport": "Direct"
+ },
+ "logging": {
+ "transports": [
+ "file",
+ "stdout"
+ ]
+ },
+ "security": {
+ "staffDeviceVerification": true
+ },
+ "paths": {
+ "contentPath": "/home/ghost/content"
+ }
+}
diff --git a/ghost_next-alpine/docker-entrypoint.sh b/ghost_next-alpine/docker-entrypoint.sh
new file mode 100755
index 0000000..bafa8d6
--- /dev/null
+++ b/ghost_next-alpine/docker-entrypoint.sh
@@ -0,0 +1,96 @@
+#!/bin/sh
+set -e
+
+# POSIX sh, not bash: avoids an Alpine bash package just for "[[ ... ]]"
+case "$*" in
+ node*index.js*) isGhost=1 ;;
+ *) isGhost= ;;
+esac
+
+# ghost:6 installed to /var/lib/ghost; this image uses /home/ghost. An old -v target would boot an
+# empty site while the real content sat unread, so: content there is fatal, an empty mount warns.
+OLD_INSTALL='/var/lib/ghost'
+OLD_CONTENT="$OLD_INSTALL/content"
+
+_is_mountpoint() {
+ # BusyBox "mountpoint" compares only dev/ino, so it misses bind mounts -- i.e. nearly every
+ # Docker volume, and exactly what is being looked for here. Fall back to the mount table.
+ # https://github.com/docker-library/postgres/blob/master/docker-entrypoint.sh
+ mountpoint -q "$1" 2>/dev/null \
+ || awk -v dir="$1" '$5 == dir { found = 1 } END { exit !found }' /proc/self/mountinfo 2>/dev/null
+}
+
+_has_ghost_content() {
+ [ -d "$1" ] || return 1
+ # any of these means a real site, not the empty dir Docker creates for a mount
+ for marker in "$1"/data/*.db "$1"/settings/routes.yaml "$1"/images/* "$1"/themes/*; do
+ if [ -e "$marker" ]; then
+ return 0
+ fi
+ done
+ return 1
+}
+
+# the root pass below re-execs through gosu, so a marker keeps these from printing twice
+if [ -n "$isGhost" ] && [ -z "${GHOST_OLD_PATH_CHECKED:-}" ]; then
+ export GHOST_OLD_PATH_CHECKED=1
+
+ if _has_ghost_content "$OLD_CONTENT" || [ -e "$OLD_INSTALL/.ghost-cli" ]; then
+ cat >&2 <<-EOE
+ Error: found existing Ghost content at $OLD_CONTENT, but this image stores
+ Ghost in /home/ghost.
+
+ ghost:6 and earlier installed to /var/lib/ghost. This tag moves that to
+ /home/ghost, so the content mounted at the old path is not being read.
+ Starting would create an empty site and leave that content untouched, so
+ this container will not start.
+
+ Point the mount at the new location instead:
+
+ -v ghost_content:/home/ghost/content
+
+ rather than:
+
+ -v ghost_content:$OLD_CONTENT
+
+ Nothing at $OLD_INSTALL has been modified.
+ EOE
+ exit 1
+ fi
+
+ oldMount=
+ if _is_mountpoint "$OLD_CONTENT"; then
+ oldMount="$OLD_CONTENT"
+ elif _is_mountpoint "$OLD_INSTALL"; then
+ oldMount="$OLD_INSTALL"
+ fi
+ if [ -n "$oldMount" ]; then
+ cat >&2 <<-EOW
+ Warning: something is mounted at $oldMount, but this image stores Ghost in
+ /home/ghost, so that mount is unused.
+
+ It looks empty, so this is most likely a stale -v left over from ghost:6.
+ Move it to /home/ghost/content.
+ EOW
+ fi
+fi
+
+# allow the container to be started with `--user`
+if [ -n "$isGhost" ] && [ "$(id -u)" = '0' ]; then
+ find "$GHOST_CONTENT" \! -user ghost -exec chown ghost '{}' +
+ exec gosu ghost "$0" "$@"
+fi
+
+if [ -n "$isGhost" ]; then
+ baseDir="$GHOST_INSTALL/content.orig"
+ for src in "$baseDir"/*/ "$baseDir"/themes/*; do
+ src="${src%/}"
+ target="$GHOST_CONTENT/${src#$baseDir/}"
+ mkdir -p "$(dirname "$target")"
+ if [ ! -e "$target" ]; then
+ tar -cC "$(dirname "$src")" "$(basename "$src")" | tar -xC "$(dirname "$target")"
+ fi
+ done
+fi
+
+exec "$@"
diff --git a/ghost_latest/Dockerfile b/ghost_next/Dockerfile
similarity index 32%
copy from ghost_latest/Dockerfile
copy to ghost_next/Dockerfile
index 4374f60..bd65286 100644
--- a/ghost_latest/Dockerfile
+++ b/ghost_next/Dockerfile
@@ -6,13 +6,13 @@
FROM node:22-bookworm-slim
-# grab gosu for easy step-down from root
+# 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; \
-# save list of currently installed packages for later so we can clean up
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 }')"; \
@@ -26,110 +26,114 @@ RUN set -eux; \
gpgconf --kill all; \
rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; \
\
-# clean up fetch dependencies
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; \
-# verify that the binary works
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
-# Ghost-CLI is installed from source rather than "npm install -g ghost-cli" so that
-# pnpm can resolve the dependency tree from the lockfile committed alongside the tag
-# ("npm install -g" ignores lockfiles entirely, so the tree it produces varies by build date)
-ENV GHOST_CLI_VERSION=1.32.2
-ENV GHOST_CLI_SHA=5e0b07b7619207459956dd9728bafc5cd7d15f01
-ENV GHOST_CLI_INSTALL=/usr/local/lib/ghost-cli
+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
+ENV GHOST_TARBALL=https://github.com/TryGhost/Ghost/releases/download/v6.61.0/ghost-6.61.0.tgz
+ENV GHOST_SHA256=d7a4de32641979fd57fa98bd29c128e406aa5421f3dadc04ffe28cb96507e69c
+
RUN set -eux; \
- \
savedAptMark="$(apt-mark showmanual)"; \
apt-get update; \
- apt-get install -y --no-install-recommends ca-certificates git; \
- \
+ 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; \
\
-# corepack's downloads, pnpm's metadata cache and pnpm's store all derive their location
-# from these, so pointing them at /tmp keeps every throwaway byte in one place
+# 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"; \
+ \
+# 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; \
\
- mkdir -p "$GHOST_CLI_INSTALL"; \
- cd "$GHOST_CLI_INSTALL"; \
- git init --quiet .; \
- git remote add origin https://github.com/TryGhost/Ghost-CLI.git; \
-# fetching the commit by hash means git's own object verification pins the source
-# (a moved tag or a re-rolled release tarball cannot change what we get)
- git fetch --quiet --depth 1 origin "$GHOST_CLI_SHA"; \
- git checkout --quiet FETCH_HEAD; \
- [ "$(node -p 'require("./package.json").version')" = "$GHOST_CLI_VERSION" ]; \
+ 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
+ gosu ghost tar --extract --file ghost.tgz; \
+ rm ghost.tgz; \
\
-# "--frozen-lockfile" is the point of all this: it installs pnpm-lock.yaml exactly, or fails
- pnpm install --prod --frozen-lockfile; \
+# the tarball ships a pruned lockfile, so the tree is installed exactly, not re-solved per build
+ gosu ghost pnpm install --prod --frozen-lockfile; \
\
- ln -s "$GHOST_CLI_INSTALL/bin/ghost" /usr/local/bin/ghost; \
+# 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
+ gosu ghost rm -rf "$GHOST_INSTALL/components"; \
\
-# drop what "npm publish" would not have shipped (see "files" in Ghost-CLI's package.json)
- rm -rf .git test .github; \
+# 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.
+ gosu ghost node scripts/prune.mts "$GHOST_INSTALL" --profile=image; \
\
- 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/*; \
+# make a config.json symlink for NODE_ENV=development (config.production.json is copied in below)
+ gosu ghost ln -s config.production.json "$GHOST_INSTALL/config.development.json"; \
+ \
+# need to save initial content for pre-seeding empty volumes
+ gosu ghost mv "$GHOST_INSTALL/content" "$GHOST_INSTALL/content.orig"; \
+ gosu ghost mkdir -p "$GHOST_INSTALL/content"; \
\
+# "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; \
\
- ghost --version
-
-ENV GHOST_INSTALL=/var/lib/ghost
-ENV GHOST_CONTENT=/var/lib/ghost/content
-
-ENV GHOST_VERSION=6.60.0
-
-RUN set -eux; \
- mkdir -p "$GHOST_INSTALL"; \
- chown node:node "$GHOST_INSTALL"; \
- \
- savedAptMark="$(apt-mark showmanual)"; \
- apt-get update; \
- apt-get install -y --no-install-recommends g++ make python3; \
- \
- gosu node ghost install "$GHOST_VERSION" --db mysql --dbhost mysql --no-prompt --no-stack --no-setup --dir "$GHOST_INSTALL"; \
- \
-# Tell Ghost to listen on all ips and not prompt for additional configuration
- cd "$GHOST_INSTALL"; \
- gosu node ghost config --no-prompt --ip '::' --port 2368 --url 'http://localhost:2368'; \
- gosu node ghost config paths.contentPath "$GHOST_CONTENT"; \
- \
-# make a config.json symlink for NODE_ENV=development (and sanity check that it's correct)
- gosu node ln -s config.production.json "$GHOST_INSTALL/config.development.json"; \
- readlink -f "$GHOST_INSTALL/config.development.json"; \
- \
-# need to save initial content for pre-seeding empty volumes
- mv "$GHOST_CONTENT" "$GHOST_INSTALL/content.orig"; \
- mkdir -p "$GHOST_CONTENT"; \
- chown node:node "$GHOST_CONTENT"; \
- chmod 1777 "$GHOST_CONTENT"; \
- \
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/*; \
\
- gosu node pnpm store prune; \
- gosu node npm cache clean --force; \
- npm cache clean --force; \
-# none of these are needed to run Ghost: corepack re-downloads pnpm on demand and node-gyp
-# only matters while compiling native modules, which is finished by this point
- rm -rf /home/node/.cache/node/corepack /home/node/.cache/node-gyp /home/node/.cache/pnpm; \
- \
- # test that the optional dependencies are installed and loadable
- cd current; \
- gosu node node -e 'require("better-sqlite3"); if (!require("@tryghost/image-transform").canTransformFiles()) throw new Error("sharp not installed");'
+# 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
+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"; \
+ cd "$GHOST_INSTALL"; \
+ node -e 'JSON.parse(require("fs").readFileSync("config.production.json"))'; \
+ [ "$(readlink -f config.development.json)" = "$GHOST_INSTALL/config.production.json" ]
WORKDIR $GHOST_INSTALL
VOLUME $GHOST_CONTENT
@@ -138,4 +142,4 @@ COPY docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT ["docker-entrypoint.sh"]
EXPOSE 2368
-CMD ["node", "current/index.js"]
+CMD ["node", "index.js"]
diff --git a/ghost_next/config.production.json b/ghost_next/config.production.json
new file mode 100644
index 0000000..31ec76a
--- /dev/null
+++ b/ghost_next/config.production.json
@@ -0,0 +1,22 @@
+{
+ "url": "http://localhost:2368",
+ "server": {
+ "port": 2368,
+ "host": "::"
+ },
+ "mail": {
+ "transport": "Direct"
+ },
+ "logging": {
+ "transports": [
+ "file",
+ "stdout"
+ ]
+ },
+ "security": {
+ "staffDeviceVerification": true
+ },
+ "paths": {
+ "contentPath": "/home/ghost/content"
+ }
+}
diff --git a/ghost_next/docker-entrypoint.sh b/ghost_next/docker-entrypoint.sh
new file mode 100755
index 0000000..bafa8d6
--- /dev/null
+++ b/ghost_next/docker-entrypoint.sh
@@ -0,0 +1,96 @@
+#!/bin/sh
+set -e
+
+# POSIX sh, not bash: avoids an Alpine bash package just for "[[ ... ]]"
+case "$*" in
+ node*index.js*) isGhost=1 ;;
+ *) isGhost= ;;
+esac
+
+# ghost:6 installed to /var/lib/ghost; this image uses /home/ghost. An old -v target would boot an
+# empty site while the real content sat unread, so: content there is fatal, an empty mount warns.
+OLD_INSTALL='/var/lib/ghost'
+OLD_CONTENT="$OLD_INSTALL/content"
+
+_is_mountpoint() {
+ # BusyBox "mountpoint" compares only dev/ino, so it misses bind mounts -- i.e. nearly every
+ # Docker volume, and exactly what is being looked for here. Fall back to the mount table.
+ # https://github.com/docker-library/postgres/blob/master/docker-entrypoint.sh
+ mountpoint -q "$1" 2>/dev/null \
+ || awk -v dir="$1" '$5 == dir { found = 1 } END { exit !found }' /proc/self/mountinfo 2>/dev/null
+}
+
+_has_ghost_content() {
+ [ -d "$1" ] || return 1
+ # any of these means a real site, not the empty dir Docker creates for a mount
+ for marker in "$1"/data/*.db "$1"/settings/routes.yaml "$1"/images/* "$1"/themes/*; do
+ if [ -e "$marker" ]; then
+ return 0
+ fi
+ done
+ return 1
+}
+
+# the root pass below re-execs through gosu, so a marker keeps these from printing twice
+if [ -n "$isGhost" ] && [ -z "${GHOST_OLD_PATH_CHECKED:-}" ]; then
+ export GHOST_OLD_PATH_CHECKED=1
+
+ if _has_ghost_content "$OLD_CONTENT" || [ -e "$OLD_INSTALL/.ghost-cli" ]; then
+ cat >&2 <<-EOE
+ Error: found existing Ghost content at $OLD_CONTENT, but this image stores
+ Ghost in /home/ghost.
+
+ ghost:6 and earlier installed to /var/lib/ghost. This tag moves that to
+ /home/ghost, so the content mounted at the old path is not being read.
+ Starting would create an empty site and leave that content untouched, so
+ this container will not start.
+
+ Point the mount at the new location instead:
+
+ -v ghost_content:/home/ghost/content
+
+ rather than:
+
+ -v ghost_content:$OLD_CONTENT
+
+ Nothing at $OLD_INSTALL has been modified.
+ EOE
+ exit 1
+ fi
+
+ oldMount=
+ if _is_mountpoint "$OLD_CONTENT"; then
+ oldMount="$OLD_CONTENT"
+ elif _is_mountpoint "$OLD_INSTALL"; then
+ oldMount="$OLD_INSTALL"
+ fi
+ if [ -n "$oldMount" ]; then
+ cat >&2 <<-EOW
+ Warning: something is mounted at $oldMount, but this image stores Ghost in
+ /home/ghost, so that mount is unused.
+
+ It looks empty, so this is most likely a stale -v left over from ghost:6.
+ Move it to /home/ghost/content.
+ EOW
+ fi
+fi
+
+# allow the container to be started with `--user`
+if [ -n "$isGhost" ] && [ "$(id -u)" = '0' ]; then
+ find "$GHOST_CONTENT" \! -user ghost -exec chown ghost '{}' +
+ exec gosu ghost "$0" "$@"
+fi
+
+if [ -n "$isGhost" ]; then
+ baseDir="$GHOST_INSTALL/content.orig"
+ for src in "$baseDir"/*/ "$baseDir"/themes/*; do
+ src="${src%/}"
+ target="$GHOST_CONTENT/${src#$baseDir/}"
+ mkdir -p "$(dirname "$target")"
+ if [ ! -e "$target" ]; then
+ tar -cC "$(dirname "$src")" "$(basename "$src")" | tar -xC "$(dirname "$target")"
+ fi
+ done
+fi
+
+exec "$@"Relevant Maintainers:
|
Update 6 to 6.61.0
Generated from TryGhost/docker-library-ghost@820b574.
closes #22081