diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..9396a6be --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +/*/**/Dockerfile linguist-generated +/*/**/docker-entrypoint.sh linguist-generated +/Dockerfile.template linguist-language=Dockerfile diff --git a/.github/workflows/verify-templating.yml b/.github/workflows/verify-templating.yml new file mode 100644 index 00000000..e822ba6b --- /dev/null +++ b/.github/workflows/verify-templating.yml @@ -0,0 +1,19 @@ +name: Verify Templating + +on: + pull_request: + push: + workflow_dispatch: + +defaults: + run: + shell: 'bash -Eeuo pipefail -x {0}' + +jobs: + apply-templates: + name: Check For Uncomitted Changes + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: ./apply-templates.sh + - run: git diff --exit-code diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..d548f66d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.jq-template.awk diff --git a/5/alpine/Dockerfile b/5/alpine3.22/Dockerfile similarity index 52% rename from 5/alpine/Dockerfile rename to 5/alpine3.22/Dockerfile index 355f2e9e..80c9585c 100644 --- a/5/alpine/Dockerfile +++ b/5/alpine3.22/Dockerfile @@ -1,5 +1,9 @@ -# https://docs.ghost.org/faq/node-versions/ -# https://github.com/nodejs/Release (looking for "LTS") +# +# NOTE: THIS DOCKERFILE IS GENERATED VIA "apply-templates.sh" +# +# PLEASE DO NOT EDIT IT DIRECTLY. +# + FROM node:20-alpine3.22 RUN apk add --no-cache \ @@ -35,7 +39,7 @@ RUN set -eux; \ # verify that the binary works gosu --version; \ gosu nobody true -RUN set -eux; ln -svf gosu /usr/local/bin/su-exec; su-exec nobody true # backwards compatibility (TODO remove in Ghost 6+) +RUN set -eux; ln -svf gosu /usr/local/bin/su-exec; su-exec nobody true # backwards compatibility ENV NODE_ENV production @@ -53,15 +57,9 @@ RUN set -eux; \ mkdir -p "$GHOST_INSTALL"; \ chown node:node "$GHOST_INSTALL"; \ \ - apkDel=; \ + apk add --no-cache --virtual .build-deps-ghost g++ linux-headers make python3 py3-setuptools; \ \ - installCmd='gosu node ghost install "$GHOST_VERSION" --db mysql --dbhost mysql --no-prompt --no-stack --no-setup --dir "$GHOST_INSTALL"'; \ - if ! eval "$installCmd"; then \ - virtual='.build-deps-ghost'; \ - apkDel="$apkDel $virtual"; \ - apk add --no-cache --virtual "$virtual" g++ linux-headers make python3; \ - eval "$installCmd"; \ - fi; \ + 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"; \ @@ -78,50 +76,21 @@ RUN set -eux; \ chown node:node "$GHOST_CONTENT"; \ chmod 1777 "$GHOST_CONTENT"; \ \ -# force install a few extra packages manually since they're "optional" dependencies -# (which means that if it fails to install, like on ARM/ppc64le/s390x, the failure will be silently ignored and thus turn into a runtime error instead) -# see https://github.com/TryGhost/Ghost/pull/7677 for more details - cd "$GHOST_INSTALL/current"; \ -# scrape the expected versions directly from Ghost/dependencies - packages="$(node -p ' \ - var ghost = require("./package.json"); \ - var transform = require("./node_modules/@tryghost/image-transform/package.json"); \ - [ \ - "sharp@" + transform.optionalDependencies["sharp"], \ - "sqlite3@" + ghost.optionalDependencies["sqlite3"], \ - ].join(" ") \ - ')"; \ - if echo "$packages" | grep 'undefined'; then exit 1; fi; \ - for package in $packages; do \ - installCmd='gosu node yarn add "$package" --force'; \ - if ! eval "$installCmd"; then \ -# must be some non-amd64 architecture pre-built binaries aren't published for, so let's install some build deps and do-it-all-over-again - virtualPackages='g++ make python3 py3-setuptools'; \ - case "$package" in \ - # TODO sharp@*) virtualPackages="$virtualPackages pkgconf vips-dev"; \ - sharp@*) echo >&2 "sorry: libvips 8.12.1 in Alpine 3.15 is not new enough (8.12.2+) for sharp 0.30 😞"; continue ;; \ - esac; \ - virtual=".build-deps-${package%%@*}"; \ - apkDel="$apkDel $virtual"; \ - apk add --no-cache --virtual "$virtual" $virtualPackages; \ - \ - eval "$installCmd --build-from-source"; \ - fi; \ - done; \ - \ - if [ -n "$apkDel" ]; then \ - apk del --no-network $apkDel; \ - fi; \ + apk del --no-network .build-deps-ghost; \ \ gosu node yarn cache clean; \ gosu node npm cache clean --force; \ npm cache clean --force; \ - rm -rv /tmp/yarn* /tmp/v8* + rm -rv /tmp/yarn* /tmp/v8*; \ + \ + # test that the optional dependencies are installed and loadable + cd current; \ + gosu node node -e 'require("sqlite3"); require("sharp");' WORKDIR $GHOST_INSTALL VOLUME $GHOST_CONTENT -COPY docker-entrypoint.sh /usr/local/bin +COPY docker-entrypoint.sh /usr/local/bin/ ENTRYPOINT ["docker-entrypoint.sh"] EXPOSE 2368 diff --git a/5/alpine/docker-entrypoint.sh b/5/alpine3.22/docker-entrypoint.sh similarity index 100% rename from 5/alpine/docker-entrypoint.sh rename to 5/alpine3.22/docker-entrypoint.sh diff --git a/5/debian/Dockerfile b/5/bookworm/Dockerfile similarity index 55% rename from 5/debian/Dockerfile rename to 5/bookworm/Dockerfile index 47ba9dfd..3b3b7f85 100644 --- a/5/debian/Dockerfile +++ b/5/bookworm/Dockerfile @@ -1,5 +1,9 @@ -# https://docs.ghost.org/faq/node-versions/ -# https://github.com/nodejs/Release (looking for "LTS") +# +# NOTE: THIS DOCKERFILE IS GENERATED VIA "apply-templates.sh" +# +# PLEASE DO NOT EDIT IT DIRECTLY. +# + FROM node:20-bookworm-slim # grab gosu for easy step-down from root @@ -10,7 +14,6 @@ RUN set -eux; \ savedAptMark="$(apt-mark showmanual)"; \ apt-get update; \ apt-get install -y --no-install-recommends ca-certificates gnupg wget; \ - rm -rf /var/lib/apt/lists/*; \ \ 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"; \ @@ -27,6 +30,7 @@ RUN set -eux; \ 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 @@ -50,15 +54,10 @@ RUN set -eux; \ chown node:node "$GHOST_INSTALL"; \ \ savedAptMark="$(apt-mark showmanual)"; \ - aptPurge=; \ + apt-get update; \ + apt-get install -y --no-install-recommends g++ make python3; \ \ - installCmd='gosu node ghost install "$GHOST_VERSION" --db mysql --dbhost mysql --no-prompt --no-stack --no-setup --dir "$GHOST_INSTALL"'; \ - if ! eval "$installCmd"; then \ - aptPurge=1; \ - apt-get update; \ - apt-get install -y --no-install-recommends g++ make python3; \ - eval "$installCmd"; \ - fi; \ + 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"; \ @@ -75,52 +74,24 @@ RUN set -eux; \ chown node:node "$GHOST_CONTENT"; \ chmod 1777 "$GHOST_CONTENT"; \ \ -# force install a few extra packages manually since they're "optional" dependencies -# (which means that if it fails to install, like on ARM/ppc64le/s390x, the failure will be silently ignored and thus turn into a runtime error instead) -# see https://github.com/TryGhost/Ghost/pull/7677 for more details - cd "$GHOST_INSTALL/current"; \ -# scrape the expected versions directly from Ghost/dependencies - packages="$(node -p ' \ - var ghost = require("./package.json"); \ - var transform = require("./node_modules/@tryghost/image-transform/package.json"); \ - [ \ - "sharp@" + transform.optionalDependencies["sharp"], \ - "sqlite3@" + ghost.optionalDependencies["sqlite3"], \ - ].join(" ") \ - ')"; \ - if echo "$packages" | grep 'undefined'; then exit 1; fi; \ - for package in $packages; do \ - installCmd='gosu node yarn add "$package" --force'; \ - if ! eval "$installCmd"; then \ -# must be some non-amd64 architecture pre-built binaries aren't published for, so let's install some build deps and do-it-all-over-again - aptPurge=1; \ - apt-get update; \ - apt-get install -y --no-install-recommends g++ make python3; \ - case "$package" in \ - # TODO sharp@*) apt-get install -y --no-install-recommends libvips-dev ;; \ - sharp@*) echo >&2 "sorry: libvips 8.10 in Debian bullseye is not new enough (8.12.2+) for sharp 0.30 😞"; continue ;; \ - esac; \ - \ - eval "$installCmd --build-from-source"; \ - fi; \ - done; \ - \ - if [ -n "$aptPurge" ]; then \ - apt-mark showmanual | xargs apt-mark auto > /dev/null; \ - [ -z "$savedAptMark" ] || apt-mark manual $savedAptMark; \ - apt-get purge -y --auto-remove; \ - rm -rf /var/lib/apt/lists/*; \ - fi; \ + 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/*; \ \ gosu node yarn cache clean; \ gosu node npm cache clean --force; \ npm cache clean --force; \ - rm -rv /tmp/yarn* /tmp/v8* + rm -rv /tmp/yarn* /tmp/v8*; \ + \ + # test that the optional dependencies are installed and loadable + cd current; \ + gosu node node -e 'require("sqlite3"); require("sharp");' WORKDIR $GHOST_INSTALL VOLUME $GHOST_CONTENT -COPY docker-entrypoint.sh /usr/local/bin +COPY docker-entrypoint.sh /usr/local/bin/ ENTRYPOINT ["docker-entrypoint.sh"] EXPOSE 2368 diff --git a/5/debian/docker-entrypoint.sh b/5/bookworm/docker-entrypoint.sh similarity index 100% rename from 5/debian/docker-entrypoint.sh rename to 5/bookworm/docker-entrypoint.sh diff --git a/6/alpine/Dockerfile b/6/alpine3.22/Dockerfile similarity index 50% rename from 6/alpine/Dockerfile rename to 6/alpine3.22/Dockerfile index caa1d6c7..366a1c0d 100644 --- a/6/alpine/Dockerfile +++ b/6/alpine3.22/Dockerfile @@ -1,5 +1,9 @@ -# https://docs.ghost.org/faq/node-versions/ -# https://github.com/nodejs/Release (looking for "LTS") +# +# NOTE: THIS DOCKERFILE IS GENERATED VIA "apply-templates.sh" +# +# PLEASE DO NOT EDIT IT DIRECTLY. +# + FROM node:22-alpine3.22 RUN apk add --no-cache \ @@ -35,7 +39,6 @@ RUN set -eux; \ # verify that the binary works gosu --version; \ gosu nobody true -RUN set -eux; ln -svf gosu /usr/local/bin/su-exec; su-exec nobody true # backwards compatibility (TODO remove in Ghost 6+) ENV NODE_ENV production @@ -53,16 +56,9 @@ RUN set -eux; \ mkdir -p "$GHOST_INSTALL"; \ chown node:node "$GHOST_INSTALL"; \ \ - apkDel=; \ + apk add --no-cache --virtual .build-deps-ghost g++ linux-headers make python3 py3-setuptools; \ \ - installCmd='gosu node ghost install "$GHOST_VERSION" --db mysql --dbhost mysql --no-prompt --no-stack --no-setup --dir "$GHOST_INSTALL"'; \ - case "$GHOST_VERSION" in *-alpha* | *-beta* | *-rc*) installCmd="$installCmd --channel next" ;; esac; \ - if ! eval "$installCmd"; then \ - virtual='.build-deps-ghost'; \ - apkDel="$apkDel $virtual"; \ - apk add --no-cache --virtual "$virtual" g++ linux-headers make python3; \ - eval "$installCmd"; \ - fi; \ + 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"; \ @@ -79,50 +75,21 @@ RUN set -eux; \ chown node:node "$GHOST_CONTENT"; \ chmod 1777 "$GHOST_CONTENT"; \ \ -# force install a few extra packages manually since they're "optional" dependencies -# (which means that if it fails to install, like on ARM/ppc64le/s390x, the failure will be silently ignored and thus turn into a runtime error instead) -# see https://github.com/TryGhost/Ghost/pull/7677 for more details - cd "$GHOST_INSTALL/current"; \ -# scrape the expected versions directly from Ghost/dependencies - packages="$(node -p ' \ - var ghost = require("./package.json"); \ - var transform = require("./node_modules/@tryghost/image-transform/package.json"); \ - [ \ - "sharp@" + transform.optionalDependencies["sharp"], \ - "sqlite3@" + ghost.optionalDependencies["sqlite3"], \ - ].join(" ") \ - ')"; \ - if echo "$packages" | grep 'undefined'; then exit 1; fi; \ - for package in $packages; do \ - installCmd='gosu node yarn add "$package" --force'; \ - if ! eval "$installCmd"; then \ -# must be some non-amd64 architecture pre-built binaries aren't published for, so let's install some build deps and do-it-all-over-again - virtualPackages='g++ make python3 py3-setuptools'; \ - case "$package" in \ - # TODO sharp@*) virtualPackages="$virtualPackages pkgconf vips-dev"; \ - sharp@*) echo >&2 "sorry: libvips 8.12.1 in Alpine 3.15 is not new enough (8.12.2+) for sharp 0.30 😞"; continue ;; \ - esac; \ - virtual=".build-deps-${package%%@*}"; \ - apkDel="$apkDel $virtual"; \ - apk add --no-cache --virtual "$virtual" $virtualPackages; \ - \ - eval "$installCmd --build-from-source"; \ - fi; \ - done; \ - \ - if [ -n "$apkDel" ]; then \ - apk del --no-network $apkDel; \ - fi; \ + apk del --no-network .build-deps-ghost; \ \ gosu node yarn cache clean; \ gosu node npm cache clean --force; \ npm cache clean --force; \ - rm -rv /tmp/yarn* /tmp/v8* + rm -rv /tmp/yarn* /tmp/v8*; \ + \ + # test that the optional dependencies are installed and loadable + cd current; \ + gosu node node -e 'require("sqlite3"); require("sharp");' WORKDIR $GHOST_INSTALL VOLUME $GHOST_CONTENT -COPY docker-entrypoint.sh /usr/local/bin +COPY docker-entrypoint.sh /usr/local/bin/ ENTRYPOINT ["docker-entrypoint.sh"] EXPOSE 2368 diff --git a/6/alpine/docker-entrypoint.sh b/6/alpine3.22/docker-entrypoint.sh similarity index 100% rename from 6/alpine/docker-entrypoint.sh rename to 6/alpine3.22/docker-entrypoint.sh diff --git a/6/debian/Dockerfile b/6/bookworm/Dockerfile similarity index 54% rename from 6/debian/Dockerfile rename to 6/bookworm/Dockerfile index ace0a4ca..4a1d68c2 100644 --- a/6/debian/Dockerfile +++ b/6/bookworm/Dockerfile @@ -1,5 +1,9 @@ -# https://docs.ghost.org/faq/node-versions/ -# https://github.com/nodejs/Release (looking for "LTS") +# +# NOTE: THIS DOCKERFILE IS GENERATED VIA "apply-templates.sh" +# +# PLEASE DO NOT EDIT IT DIRECTLY. +# + FROM node:22-bookworm-slim # grab gosu for easy step-down from root @@ -10,7 +14,6 @@ RUN set -eux; \ savedAptMark="$(apt-mark showmanual)"; \ apt-get update; \ apt-get install -y --no-install-recommends ca-certificates gnupg wget; \ - rm -rf /var/lib/apt/lists/*; \ \ 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"; \ @@ -27,6 +30,7 @@ RUN set -eux; \ 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 @@ -50,16 +54,10 @@ RUN set -eux; \ chown node:node "$GHOST_INSTALL"; \ \ savedAptMark="$(apt-mark showmanual)"; \ - aptPurge=; \ + apt-get update; \ + apt-get install -y --no-install-recommends g++ make python3; \ \ - installCmd='gosu node ghost install "$GHOST_VERSION" --db mysql --dbhost mysql --no-prompt --no-stack --no-setup --dir "$GHOST_INSTALL"'; \ - case "$GHOST_VERSION" in *-alpha* | *-beta* | *-rc*) installCmd="$installCmd --channel next" ;; esac; \ - if ! eval "$installCmd"; then \ - aptPurge=1; \ - apt-get update; \ - apt-get install -y --no-install-recommends g++ make python3; \ - eval "$installCmd"; \ - fi; \ + 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"; \ @@ -76,52 +74,24 @@ RUN set -eux; \ chown node:node "$GHOST_CONTENT"; \ chmod 1777 "$GHOST_CONTENT"; \ \ -# force install a few extra packages manually since they're "optional" dependencies -# (which means that if it fails to install, like on ARM/ppc64le/s390x, the failure will be silently ignored and thus turn into a runtime error instead) -# see https://github.com/TryGhost/Ghost/pull/7677 for more details - cd "$GHOST_INSTALL/current"; \ -# scrape the expected versions directly from Ghost/dependencies - packages="$(node -p ' \ - var ghost = require("./package.json"); \ - var transform = require("./node_modules/@tryghost/image-transform/package.json"); \ - [ \ - "sharp@" + transform.optionalDependencies["sharp"], \ - "sqlite3@" + ghost.optionalDependencies["sqlite3"], \ - ].join(" ") \ - ')"; \ - if echo "$packages" | grep 'undefined'; then exit 1; fi; \ - for package in $packages; do \ - installCmd='gosu node yarn add "$package" --force'; \ - if ! eval "$installCmd"; then \ -# must be some non-amd64 architecture pre-built binaries aren't published for, so let's install some build deps and do-it-all-over-again - aptPurge=1; \ - apt-get update; \ - apt-get install -y --no-install-recommends g++ make python3; \ - case "$package" in \ - # TODO sharp@*) apt-get install -y --no-install-recommends libvips-dev ;; \ - sharp@*) echo >&2 "sorry: libvips 8.10 in Debian bullseye is not new enough (8.12.2+) for sharp 0.30 😞"; continue ;; \ - esac; \ - \ - eval "$installCmd --build-from-source"; \ - fi; \ - done; \ - \ - if [ -n "$aptPurge" ]; then \ - apt-mark showmanual | xargs apt-mark auto > /dev/null; \ - [ -z "$savedAptMark" ] || apt-mark manual $savedAptMark; \ - apt-get purge -y --auto-remove; \ - rm -rf /var/lib/apt/lists/*; \ - fi; \ + 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/*; \ \ gosu node yarn cache clean; \ gosu node npm cache clean --force; \ npm cache clean --force; \ - rm -rv /tmp/yarn* /tmp/v8* + rm -rv /tmp/yarn* /tmp/v8*; \ + \ + # test that the optional dependencies are installed and loadable + cd current; \ + gosu node node -e 'require("sqlite3"); require("sharp");' WORKDIR $GHOST_INSTALL VOLUME $GHOST_CONTENT -COPY docker-entrypoint.sh /usr/local/bin +COPY docker-entrypoint.sh /usr/local/bin/ ENTRYPOINT ["docker-entrypoint.sh"] EXPOSE 2368 diff --git a/6/debian/docker-entrypoint.sh b/6/bookworm/docker-entrypoint.sh similarity index 100% rename from 6/debian/docker-entrypoint.sh rename to 6/bookworm/docker-entrypoint.sh diff --git a/Dockerfile.template b/Dockerfile.template new file mode 100644 index 00000000..9ce74670 --- /dev/null +++ b/Dockerfile.template @@ -0,0 +1,132 @@ +{{ + def is_alpine: + env.variant | startswith("alpine") + ; + def clean_apt: + # TODO once bookworm is EOL, remove this and just hard-code "apt-get dist-clean" instead + if env.variant | contains("bookworm") then + "rm -rf /var/lib/apt/lists/*" + else "apt-get dist-clean" end +-}} +FROM {{ .variants[env.variant].from }} + +{{ if is_alpine then ( -}} +RUN apk add --no-cache \ +# add "bash" for "[[" + bash + +{{ ) else "" end -}} +# grab gosu for easy step-down from root +# https://github.com/tianon/gosu/releases +ENV GOSU_VERSION 1.19 +RUN set -eux; \ +{{ if is_alpine then ( -}} + \ + apk add --no-cache --virtual .gosu-deps \ + ca-certificates \ + dpkg \ + gnupg \ + ; \ +{{ ) else ( -}} +# save list of currently installed packages for later so we can clean up + savedAptMark="$(apt-mark showmanual)"; \ + apt-get update; \ + apt-get install -y --no-install-recommends ca-certificates gnupg wget; \ +{{ ) end -}} + \ + 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 +{{ 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; \ +# verify that the binary works + gosu --version; \ + gosu nobody true +{{ if is_alpine and (.version | IN("6.5.3", "5.130.5")) then ( -}} +RUN set -eux; ln -svf gosu /usr/local/bin/su-exec; su-exec nobody true # backwards compatibility +{{ ) else "" end -}} + +ENV NODE_ENV production + +ENV GHOST_CLI_VERSION {{ .cli.version }} +RUN set -eux; \ + npm install -g "ghost-cli@$GHOST_CLI_VERSION"; \ + npm cache clean --force + +ENV GHOST_INSTALL /var/lib/ghost +ENV GHOST_CONTENT /var/lib/ghost/content + +ENV GHOST_VERSION {{ .version }} + +RUN set -eux; \ + mkdir -p "$GHOST_INSTALL"; \ + chown node:node "$GHOST_INSTALL"; \ + \ +{{ if is_alpine then ( -}} + apk add --no-cache --virtual .build-deps-ghost g++ linux-headers make python3 py3-setuptools; \ +{{ ) else ( -}} + savedAptMark="$(apt-mark showmanual)"; \ + apt-get update; \ + apt-get install -y --no-install-recommends g++ make python3; \ +{{ ) end -}} + \ + gosu node ghost install "$GHOST_VERSION" --db mysql --dbhost mysql --no-prompt --no-stack --no-setup --dir "$GHOST_INSTALL"{{ if env.version | endswith("-rc") then ( }} --channel next{{ ) else "" end }}; \ + \ +# 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"; \ + \ +{{ if is_alpine then ( -}} + apk del --no-network .build-deps-ghost; \ +{{ ) 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 -}} + \ + gosu node yarn cache clean; \ + gosu node npm cache clean --force; \ + npm cache clean --force; \ + rm -rv /tmp/yarn* /tmp/v8*; \ + \ + # test that the optional dependencies are installed and loadable + cd current; \ + gosu node node -e 'require("sqlite3"); require("sharp");' + +WORKDIR $GHOST_INSTALL +VOLUME $GHOST_CONTENT + +COPY docker-entrypoint.sh /usr/local/bin/ +ENTRYPOINT ["docker-entrypoint.sh"] + +EXPOSE 2368 +CMD ["node", "current/index.js"] diff --git a/apply-templates.sh b/apply-templates.sh new file mode 100755 index 00000000..b9861298 --- /dev/null +++ b/apply-templates.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +cd "$(dirname "$(readlink -f "$BASH_SOURCE")")" + +[ -f versions.json ] # run "versions.sh" first + +jqt='.jq-template.awk' +if [ -n "${BASHBREW_SCRIPTS:-}" ]; then + jqt="$BASHBREW_SCRIPTS/jq-template.awk" +elif [ "$BASH_SOURCE" -nt "$jqt" ]; then + # https://github.com/docker-library/bashbrew/blob/master/scripts/jq-template.awk + wget -qO "$jqt" 'https://github.com/docker-library/bashbrew/raw/9f6a35772ac863a0241f147c820354e4008edf38/scripts/jq-template.awk' +fi + +if [ "$#" -eq 0 ]; then + versions="$(jq -r 'keys | map(@sh) | join(" ")' versions.json)" + eval "set -- $versions" +fi + +generated_warning() { + cat <<-EOH + # + # NOTE: THIS DOCKERFILE IS GENERATED VIA "apply-templates.sh" + # + # PLEASE DO NOT EDIT IT DIRECTLY. + # + + EOH +} + +for version; do + export version + + rm -rf "$version" + + if jq -e '.[env.version] | not' versions.json > /dev/null; then + echo "skipping $version ..." + continue + fi + + variants="$(jq -r '.[env.version].variants | keys_unsorted | map(@sh) | join(" ")' versions.json)" + eval "variants=( $variants )" + + for variant in "${variants[@]}"; do + export variant + + dir="$version/$variant" + mkdir -p "$dir" + + echo "processing $dir ..." + + { + generated_warning + gawk -f "$jqt" Dockerfile.template + } > "$dir/Dockerfile" + + cp -a docker-entrypoint.sh "$dir/" + done +done diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100755 index 00000000..f4614d4e --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -e + +# allow the container to be started with `--user` +if [[ "$*" == node*current/index.js* ]] && [ "$(id -u)" = '0' ]; then + find "$GHOST_CONTENT" \! -user node -exec chown node '{}' + + exec gosu node "$BASH_SOURCE" "$@" +fi + +if [[ "$*" == node*current/index.js* ]]; 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/generate-stackbrew-library.jq b/generate-stackbrew-library.jq new file mode 100644 index 00000000..903af88c --- /dev/null +++ b/generate-stackbrew-library.jq @@ -0,0 +1,77 @@ +to_entries +# latest version is the first one since it is already in version order +| first(.[] | select(.value and (.key | endswith("-rc") | not))) as $latestMajor +# latest suite of each variant in the latest version +| first($latestMajor.value.variants | keys_unsorted[] | select(contains("alpine"))) as $latestAlpine +| first($latestMajor.value.variants | keys_unsorted[] | select(contains("alpine") | not)) as $latestDebian +| $latestMajor.key as $latestMajor + +# loop over major versions +| .[] +| .key as $majorVersion +| .value + +# only the major versions asked for "./generate-stackbrew-library.sh X Y ..." +| if $ARGS.positional | length > 0 then + select(IN($majorVersion; $ARGS.positional[])) +else . end + +# explode a version to a list of tags +# e.g. "1.2.3" -> [ "1.2.3", "1.2", "1", "" ] +# e.g. "1.2.3-beta.1" -> [ "1.2.3-beta.1", "1-rc" ] +| ( + .version + # this is an RC, so don't explode the version + | if $majorVersion | endswith("-rc") then + [ ., $majorVersion ] + else + split(".") + # TODO if more than one minor of a major version is tracked (6.1 and 6.0), then this needs to be filtered when not the latest (so only one would get `6`) + | [ foreach .[] as $c ([]; .+=[$c]) | join(".") ] + | reverse + # add plain variant/latest if this is the newest version + | if $majorVersion == $latestMajor then + . + [ "" ] # empty for plain variant tag and latest + else . end + end +) as $versionTags + +# loop over variants (in versions.json order) +| .variants | to_entries[] +| .key as $variant +| .value + +# generate a list of tags for this variant +| ( + [ + $versionTags[] as $version + # create a list of variant suffixes + | [ + $variant, # "alpine3.22" or "bookworm" + + # if it is the newest alpine, add suffix without alpine version + if $variant == $latestAlpine then + "alpine" + else empty end, + + # if it is the newest debian, add an empty suffix to get plain tags + if $variant == $latestDebian then + "" + else empty end, + empty + ][] as $suffix + + # join each version with each suffix + | [ $version, $suffix | select(. != "") ] # remove empty strings so that join doesn't create "-alpine" or "-" + | join("-") + | if . == "" then "latest" else . end + ] +) as $tags + +| ( + "", # newline between library entries + "Tags: \($tags | join(", "))", + "Directory: \($majorVersion)/\($variant)", + "Architectures: \(.arches - (.arches - $parentArches[.from]) | join(", "))", + empty +) diff --git a/generate-stackbrew-library.sh b/generate-stackbrew-library.sh index b930bd1a..34f5ccbb 100755 --- a/generate-stackbrew-library.sh +++ b/generate-stackbrew-library.sh @@ -1,118 +1,43 @@ #!/usr/bin/env bash -set -eu - -declare -A aliases=( - [6]='latest' -) -defaultVariant='debian' +set -Eeuo pipefail self="$(basename "$BASH_SOURCE")" cd "$(dirname "$(readlink -f "$BASH_SOURCE")")" -versions=( */ ) -versions=( "${versions[@]%/}" ) - -# sort version numbers with highest first -IFS=$'\n'; versions=( $(echo "${versions[*]}" | sort -rV) ); unset IFS - -# get the most recent commit which modified any of "$@" -fileCommit() { - git log -1 --format='format:%H' HEAD -- "$@" -} - -# get the most recent commit which modified "$1/Dockerfile" or any file COPY'd from "$1/Dockerfile" -dirCommit() { - local dir="$1"; shift - ( - cd "$dir" - fileCommit \ - Dockerfile \ - $(git show HEAD:./Dockerfile | awk ' - toupper($1) == "COPY" { - for (i = 2; i < NF; i++) { - print $i - } - } - ') - ) -} - -getArches() { +getArchesJson() { local repo="$1"; shift - local officialImagesBase="${BASHBREW_LIBRARY:-https://github.com/docker-library/official-images/raw/HEAD/library}/" - - local parentRepoToArchesStr - parentRepoToArchesStr="$( - find -name 'Dockerfile' -exec awk -v officialImagesBase="$officialImagesBase" ' - toupper($1) == "FROM" && $2 !~ /^('"$repo"'|scratch|.*\/.*)(:|$)/ { - printf "%s%s\n", officialImagesBase, $2 - } - ' '{}' + \ - | sort -u \ - | xargs -r bashbrew cat --format '["{{ .RepoName }}:{{ .TagName }}"]="{{ join " " .TagEntry.Architectures }}"' - )" - eval "declare -g -A parentRepoToArches=( $parentRepoToArchesStr )" + local oiBase="${BASHBREW_LIBRARY:-https://github.com/docker-library/official-images/raw/HEAD/library}/" + + # grab supported architectures for each parent image, except self-referential + jq --raw-output \ + --arg oiBase "$oiBase" \ + --arg repo "$repo" ' + [ $oiBase + .[].variants[].from | select(index($repo + ":") | not) ] | unique[] + ' versions.json \ + | xargs -r bashbrew cat --format '{ {{ join ":" .RepoName .TagName | json }}: {{ json .TagEntry.Architectures }} }' \ + | jq --compact-output --slurp 'add' } -getArches 'ghost' +parentArchesJson="$(getArchesJson 'ghost')" + +# last commit that changed files related to a build context +commit="$(git log -1 --format='format:%H' HEAD -- '[^.]*/**')" +# generate the header +selfCommit="$(git log -1 --format='format:%H' HEAD -- "$self")" cat <<-EOH -# this file is generated via https://github.com/docker-library/ghost/blob/$(fileCommit "$self")/$self +# this file is generated via https://github.com/docker-library/ghost/blob/$selfCommit/$self Maintainers: Tianon Gravi (@tianon), Joseph Ferguson (@yosifkit), Austin Burdine (@acburdine) GitRepo: https://github.com/docker-library/ghost.git +GitCommit: $commit EOH -# prints "$2$1$3$1...$N" -join() { - local sep="$1"; shift - local out; printf -v out "${sep//%/%%}%s" "$@" - echo "${out#$sep}" -} - -for version in "${versions[@]}"; do - rcVersion="${version%-rc}" - - for variant in debian alpine; do - commit="$(dirCommit "$version/$variant")" - - fullVersion="$(git show "$commit":"$version/$variant/Dockerfile" | awk '$1 == "ENV" && $2 == "GHOST_VERSION" { print $3; exit }')" - - versionAliases=() - if [ "$version" = "$rcVersion" ]; then - while [ "$fullVersion" != "$version" -a "${fullVersion%[.-]*}" != "$fullVersion" ]; do - versionAliases+=( $fullVersion ) - fullVersion="${fullVersion%[.-]*}" - done - fi - versionAliases+=( - $fullVersion - ${aliases[$version]:-} - ) - - if [ "$variant" = "$defaultVariant" ]; then - variantAliases=( "${versionAliases[@]}" ) - else - variantAliases=( "${versionAliases[@]/%/-$variant}" ) - variantAliases=( "${variantAliases[@]//latest-/}" ) - fi - - variantParent="$(awk 'toupper($1) == "FROM" { print $2 }' "$version/$variant/Dockerfile")" - variantArches="${parentRepoToArches[$variantParent]}" - - if [ "$variant" = 'alpine' ]; then - # ERROR: unsatisfiable constraints: - # vips-dev (missing): - variantArches="$(sed -e 's/ ppc64le / /g' -e 's/ s390x / /g' <<<" $variantArches ")" - fi - - echo - cat <<-EOE - Tags: $(join ', ' "${variantAliases[@]}") - Architectures: $(join ', ' $variantArches) - GitCommit: $commit - Directory: $version/$variant - EOE - done -done +# generate the entries +exec jq \ + --raw-output \ + --argjson parentArches "$parentArchesJson" \ + --from-file generate-stackbrew-library.jq \ + versions.json \ + --args -- "$@" diff --git a/update.sh b/update.sh index 9133b97f..bac2d758 100755 --- a/update.sh +++ b/update.sh @@ -1,53 +1,7 @@ #!/usr/bin/env bash -set -e +set -Eeuo pipefail cd "$(dirname "$(readlink -f "$BASH_SOURCE")")" -versions=( "$@" ) -if [ ${#versions[@]} -eq 0 ]; then - versions=( */ ) -fi -versions=( "${versions[@]%/}" ) - -allVersions="$( - git ls-remote --tags https://github.com/TryGhost/Ghost.git \ - | sed -rne 's!^.*\trefs/tags/v?|\^\{\}$!!g; /^[0-9][.][0-9]+/p' \ - | sort -ruV -)" - -cliVersion="$( - git ls-remote --tags https://github.com/TryGhost/Ghost-CLI.git \ - | sed -rne 's!^.*\trefs/tags/v?|\^\{\}$!!g; /^[0-9][.][0-9]+/p' \ - | grep -vE -- '-(alpha|beta|rc)' \ - | sort -ruV \ - | head -n1 -)" - -for version in "${versions[@]}"; do - rcVersion="${version%-rc}" - rcGrepV='-v' - if [ "$rcVersion" != "$version" ]; then - rcGrepV= - fi - rcGrepV+=' -E' - rcGrepExpr='alpha|beta|rc' - - fullVersion="$( - echo "$allVersions" \ - | grep -E "^${rcVersion}([.-]|$)" \ - | grep $rcGrepV -- "$rcGrepExpr" \ - | head -1 - )" - if [ -z "$fullVersion" ]; then - echo >&2 "error: cannot determine full version for '$version'" - exit 1 - fi - - ( - set -x - sed -ri \ - -e 's/^(ENV GHOST_VERSION) .*/\1 '"$fullVersion"'/' \ - -e 's/^(ENV GHOST_CLI_VERSION) .*/\1 '"$cliVersion"'/' \ - "$version"/*/Dockerfile - ) -done +./versions.sh "$@" +./apply-templates.sh "$@" diff --git a/versions.json b/versions.json new file mode 100644 index 00000000..36a23b37 --- /dev/null +++ b/versions.json @@ -0,0 +1,56 @@ +{ + "6": { + "version": "6.6.0", + "cli": { + "version": "1.28.3" + }, + "node": { + "version": "22" + }, + "variants": { + "bookworm": { + "arches": [ + "amd64", + "arm32v7", + "arm64v8", + "s390x" + ], + "from": "node:22-bookworm-slim" + }, + "alpine3.22": { + "arches": [ + "amd64", + "arm64v8" + ], + "from": "node:22-alpine3.22" + } + } + }, + "5": { + "version": "5.130.5", + "cli": { + "version": "1.28.3" + }, + "node": { + "version": "20" + }, + "variants": { + "bookworm": { + "arches": [ + "amd64", + "arm32v7", + "arm64v8", + "s390x" + ], + "from": "node:20-bookworm-slim" + }, + "alpine3.22": { + "arches": [ + "amd64", + "arm64v8" + ], + "from": "node:20-alpine3.22" + } + } + } +} diff --git a/versions.sh b/versions.sh new file mode 100755 index 00000000..799fab55 --- /dev/null +++ b/versions.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +cd "$(dirname "$(readlink -f "$BASH_SOURCE")")" + +versions=( "$@" ) +if [ ${#versions[@]} -eq 0 ]; then + versions=( */ ) + json='{}' +else + json="$(< versions.json)" +fi +versions=( "${versions[@]%/}" ) + +allVersions="$( + git ls-remote --tags https://github.com/TryGhost/Ghost.git \ + | sed -rne 's!^.*\trefs/tags/v?|\^\{\}$!!g; /^[0-9][.][0-9]+/p' \ + | sort -ruV +)" + +cliVersion="$( + git ls-remote --tags https://github.com/TryGhost/Ghost-CLI.git \ + | sed -rne 's!^.*\trefs/tags/v?|\^\{\}$!!g; /^[0-9][.][0-9]+/p' \ + | grep -vE -- '-(alpha|beta|rc)' \ + | sort -ruV \ + | head -n1 +)" + +for version in "${versions[@]}"; do + rcVersion="${version%-rc}" + rcGrepV='-v' + if [ "$rcVersion" != "$version" ]; then + rcGrepV= + fi + rcGrepV+=' -E' + rcGrepExpr='alpha|beta|rc' + export version + + fullVersion="$( + echo "$allVersions" \ + | grep -E "^${rcVersion}([.-]|$)" \ + | grep $rcGrepV -- "$rcGrepExpr" \ + | head -1 + )" + if [ -z "$fullVersion" ]; then + echo >&2 "error: cannot determine full version for '$version'" + exit 1 + fi + + # get a list of architectures supported by the sharp module's prebuilt libraries + # we cannot build it on other arches since the dep, libvips, is usually too old in Debian and Alpine + doc="$(curl -fsSL "https://raw.githubusercontent.com/TryGhost/Ghost/refs/tags/v$fullVersion/yarn.lock" \ + | jq --compact-output --raw-input --null-input ' + reduce ( + inputs + | capture("^ *\"@img/sharp-(?linux[a-z]*)-(?[a-z0-9]+)\" \"[0-9.]+\"$") + ) as $item ({ + # this controls the variant ordering + linux: [], # non-Alpine first + linuxmusl: [], # Alpine second + }; .[$item.dist] += [ $item.arch ]) + | with_entries( + select(.value | length > 0) + | .key = { + # each of these should be a single distro version unless something *really* exceptional happens + # if there is more than one, they should be in descending order + linux: [ "bookworm" ], + linuxmusl: [ "alpine3.22" ], + }[.key][] + | .value = { + arches: ( + .value | map({ + x64: "amd64", + arm64: "arm64v8", + arm: "arm32v7", + s390x: "s390x", + }[.] // empty) # TODO maybe warn/error on unexpected values? + | sort + ) + } + ) + ' + )" + + export fullVersion cliVersion + json="$(jq <<<"$json" --compact-output --argjson doc "$doc" ' + { + # https://docs.ghost.org/faq/node-versions + # https://github.com/nodejs/Release (looking for "LTS") + "5": "20", + "6": "22", + }[env.version] as $nodeVersion + | .[env.version] = { + version: env.fullVersion, + cli: { version: env.cliVersion }, + node: { version: $nodeVersion }, + variants: ( + $doc + | with_entries( + # add image FROM for Dockerfile template and parent arch lookup in generate-stackbrew-library.sh + # e.g. "node:22-alpine3.22" or "node:22-trixie-slim" + .value.from = "node:\($nodeVersion)-\(.key)\( + if .key | startswith("alpine") then "" else "-slim" end + )" + ) + ), + } + ')" +done + +jq <<<"$json" ' + to_entries + + # sort by version number, descending + | sort_by(.value.version | split("[.-]"; "") | map(tonumber? // .)) + | reverse + + | from_entries +' > versions.json