# # NOTE: THIS DOCKERFILE IS GENERATED VIA "apply-templates.sh" # # PLEASE DO NOT EDIT IT DIRECTLY. # FROM node:22-alpine3.23 RUN apk add --no-cache \ # add "bash" for "[[" bash # grab gosu for easy step-down from root # 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"; \ \ # verify the signature export GNUPGHOME="$(mktemp -d)"; \ gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \ gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \ 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 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.33.4 ENV GHOST_CLI_SHA=630700fa3443b0f522dbbe7f1fe1fdc80d38fc48 ENV GHOST_CLI_INSTALL=/usr/local/lib/ghost-cli RUN set -eux; \ \ apk add --no-cache --virtual .ghost-cli-deps git; \ \ # pnpm 12 is a Rust binary with no 32-bit ARM target and no JavaScript fallback, so corepack cannot # produce the version "packageManager" pins on arm32v7. pnpm 11 is the last JavaScript # implementation and reads the same lockfile format, so that arch installs with it instead if [ "$(node -p 'process.arch')" = 'arm' ]; then \ npm install -g 'pnpm@11.28.2'; \ else \ corepack enable; \ fi; \ \ # pnpm 11 would otherwise honour "packageManager" and switch back to the binary it cannot run; # exported because Ghost-CLI spawns pnpm itself. Inert under corepack, which owns version switching export pnpm_config_pm_on_fail=ignore; \ \ # 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 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" ]; \ \ # "--frozen-lockfile" is the point of all this: it installs pnpm-lock.yaml exactly, or fails pnpm install --prod --frozen-lockfile; \ \ ln -s "$GHOST_CLI_INSTALL/bin/ghost" /usr/local/bin/ghost; \ \ # drop what "npm publish" would not have shipped (see "files" in Ghost-CLI's package.json) rm -rf .git test .github; \ \ apk del --no-network .ghost-cli-deps; \ \ 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.70.0 RUN set -eux; \ mkdir -p "$GHOST_INSTALL"; \ chown node:node "$GHOST_INSTALL"; \ \ # "ghost install" and "pnpm store prune" below both run pnpm; see the note in the Ghost-CLI stage export pnpm_config_pm_on_fail=ignore; \ \ 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"; \ # in-development tables are for working on Ghost itself, not for running it (and NODE_ENV=development turns them on by default) gosu node node -e 'const fs = require("fs"); const f = "config.production.json"; const c = JSON.parse(fs.readFileSync(f)); c.createInDevelopmentTables = false; fs.writeFileSync(f, JSON.stringify(c, null, 2) + "\n");'; \ \ # 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"; \ \ 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");' WORKDIR $GHOST_INSTALL VOLUME $GHOST_CONTENT COPY docker-entrypoint.sh /usr/local/bin/ ENTRYPOINT ["docker-entrypoint.sh"] EXPOSE 2368 CMD ["node", "current/index.js"]