Skip to content
Closed
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
298 changes: 103 additions & 195 deletions v5/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,201 +1,109 @@
# syntax=docker/dockerfile:1

# ----------------------------------------------
# 1a) ENV variables (core for all our projects at FirePress)
# ----------------------------------------------
ARG APP_NAME="ghostfire"
ARG VERSION="5.121.0"

ARG GITHUB_USER="firepress-org"
ARG DEFAULT_BRANCH="master"
ARG GITHUB_ORG="firepress-org"
ARG DOCKERHUB_USER="devmtl"
ARG GITHUB_REGISTRY="registry"

# ----------------------------------------------
# 1b) ENV variables (for this project)
# Various docs about our Dockerfile - https://github.com/firepress-org/ghostfire/issues/529
# ----------------------------------------------
ARG GHOST_CLI_VERSION="1.27.0"
ARG NODE_VERSION="20.19.2-alpine3.22"
ARG BASE_OS="alpine"
ARG USER="node"

# ----------------------------------------------
# 2) LAYER to manage base image versioning
# ----------------------------------------------
FROM node:${NODE_VERSION} AS mynode

ARG VERSION
ARG GHOST_CLI_VERSION
ARG USER
ARG NODE_VERSION
ARG ALPINE_VERSION

ENV GHOST_INSTALL="/var/lib/ghost" \
GHOST_CONTENT="/var/lib/ghost/content" \
NODE_ENV="production" \
USER="${USER}" \
NODE_VERSION="${NODE_VERSION}" \
VERSION="${VERSION}" \
GHOST_CLI_VERSION="${GHOST_CLI_VERSION}"

LABEL org.opencontainers.image.authors="Pascal Andy https://firepress.org/en/contact/" \
org.opencontainers.image.vendor="https://firepress.org/" \
org.opencontainers.image.created="${BUILD_DATE}" \
org.opencontainers.image.revision="${VCS_REF}" \
org.opencontainers.image.title="Ghost" \
org.opencontainers.image.description="Docker image for Ghost ${VERSION}" \
org.opencontainers.image.url="https://hub.docker.com/r/devmtl/ghostfire/tags/" \
###############################################################################
# 1) CI-visible build-time arguments (unchanged) #
###############################################################################
# ----- FirePress common ------------------------------------------------------
ARG APP_NAME="ghostfire"
ARG VERSION="5.121.0"
ARG GITHUB_USER="firepress-org"
ARG DEFAULT_BRANCH="master"
ARG GITHUB_ORG="firepress-org"
ARG DOCKERHUB_USER="devmtl"
ARG GITHUB_REGISTRY="registry"

# ----- Image specific --------------------------------------------------------
ARG GHOST_CLI_VERSION="1.27.0"
ARG NODE_VERSION="20-alpine3.20"
ARG BASE_OS="alpine"
ARG USER="node"

# Optional but useful for reproducibility
ARG RUNTIME_BASE="gcr.io/distroless/nodejs20-debian12"
###############################################################################
# 2) Build (compile / install) stage #
###############################################################################
FROM node:${NODE_VERSION} AS builder

# re-declare args inside the stage
ARG VERSION
ARG GHOST_CLI_VERSION
ARG USER

# ---- system packages --------------------------------------------------------
RUN apk update && apk upgrade --no-cache \
&& apk add --no-cache --virtual .build-deps \
g++ make python3 pkgconfig libc6-compat vips-dev \
&& apk add --no-cache bash curl tzdata
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Consider pinning package versions for reproducible builds
Using apk add without explicit version pins can lead to non-reproducible images when upstream packages update. You may define and use versioned args or specify package=version.

🧰 Tools
🪛 Hadolint (2.12.0)

[warning] 34-34: Pin versions in apk add. Instead of apk add <package> use apk add <package>=<version>

(DL3018)

🤖 Prompt for AI Agents
In v5/Dockerfile around lines 34 to 37, the apk packages are installed without
specifying versions, which can cause non-reproducible builds. To fix this, pin
the versions of each package by either defining ARG variables for the package
versions or specifying the exact version with the package name (e.g.,
package=version) in the apk add commands. This ensures consistent and
reproducible Docker images.


# ---- runtime environment ----------------------------------------------------
ENV GHOST_INSTALL=/opt/ghost \
GHOST_CONTENT=/opt/ghost/content \
NODE_ENV=production

# user “node” already exists in the official Node image
USER ${USER}
WORKDIR ${GHOST_INSTALL}

# ---- Ghost CLI + Ghost core -------------------------------------------------
RUN npm i -g "ghost-cli@${GHOST_CLI_VERSION}" \
&& ghost install "${VERSION}" \
--db=mysql --dbhost=mysql \
--no-setup --no-prompt --no-stack \
--dir "${GHOST_INSTALL}" \
&& ghost config paths.contentPath "${GHOST_CONTENT}"

# ---- minimise JS CVEs -------------------------------------------------------
RUN npm audit fix --omit=dev \
&& yarn cache clean \
&& npm cache clean --force

# ---- clean build dependencies ----------------------------------------------
USER root
RUN apk del --no-network .build-deps \
&& rm -rf /root/.npm /root/.cache \
&& rm -rf /var/cache/apk/*

###############################################################################
# 3) Runtime stage (distroless) #
###############################################################################
FROM ${RUNTIME_BASE} AS runtime

# carry CI args for downstream logic / labels
ARG VERSION
ARG USER
ARG APP_NAME
ARG GITHUB_USER
ARG DEFAULT_BRANCH
ARG GITHUB_ORG
ARG DOCKERHUB_USER
ARG GITHUB_REGISTRY
ARG NODE_VERSION
ARG BASE_OS

# ---- labels -----------------------------------------------------------------
LABEL org.opencontainers.image.title="Ghost" \
org.opencontainers.image.description="Ghost ${VERSION} (FirePress build)" \
org.opencontainers.image.version="${VERSION}" \
org.opencontainers.image.source="https://github.com/firepress-org/ghostfire" \
org.opencontainers.image.licenses="GNUv3 https://github.com/pascalandy/GNU-GENERAL-PUBLIC-LICENSE/blob/master/LICENSE.md" \
com.firepress.image.ghost_cli_version="${GHOST_CLI_VERSION}" \
com.firepress.image.user="${USER}" \
com.firepress.image.node_env="${NODE_ENV}" \
com.firepress.image.node_version="${NODE_VERSION}" \
com.firepress.image.base_os="${BASE_OS}" \
com.firepress.image.schema_version="1.0"

# Install gosu for easy step-down from root
ENV GOSU_VERSION 1.17
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"; \
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; \
apk del --no-network .gosu-deps; \
chmod +x /usr/local/bin/gosu; \
gosu --version; \
gosu nobody true

# Add the backwards compatibility with the official dockerfile from dockerhub
RUN set -eux; ln -svf gosu /usr/local/bin/su-exec; su-exec nobody true

# Add bash and set timezone
RUN apk add --no-cache bash curl tzdata && \
cp /usr/share/zoneinfo/America/New_York /etc/localtime && \
echo "America/New_York" > /etc/timezone && \
apk del tzdata && \
rm -rvf /var/cache/apk/* /tmp/*

# ----------------------------------------------
# 3) LAYER debug
# If a package crash on layers 4 or 5, we don't know which one crashed.
# This layer reveal package(s) versions and keep a trace in the CI's logs.
# ----------------------------------------------
FROM mynode AS debug
RUN apk upgrade

# ----------------------------------------------
# 4) LAYER builder
# ----------------------------------------------
FROM mynode AS builder

# Use bash for shell commands, and fail builds if any command in a pipeline fails.
SHELL ["/bin/bash", "-o", "pipefail", "-c"]

RUN set -eux; \
npm install -g "ghost-cli@$GHOST_CLI_VERSION"; \
npm cache clean --force; \
mkdir -p "$GHOST_INSTALL"; \
chown node:node "$GHOST_INSTALL"; \
apkDel=; \
echo "Installing Ghost version: $VERSION"; \
installCmd='gosu node ghost install "$VERSION" --db mysql --dbhost mysql --no-prompt --no-stack --no-setup --dir "$GHOST_INSTALL"'; \
if ! eval "$installCmd"; then \
echo "Initial Ghost installation failed, installing build dependencies..."; \
virtual='.build-deps-ghost'; \
apkDel="$apkDel $virtual"; \
apk add --no-cache --virtual "$virtual" g++ linux-headers make python3 pkgconfig libc6-compat; \
echo "Retrying Ghost installation with build dependencies..."; \
eval "$installCmd"; \
fi; \
cd "$GHOST_INSTALL"; \
gosu node ghost config --no-prompt --ip '::' --port 2368 --url 'http://localhost:2368'; \
gosu node ghost config paths.contentPath "$GHOST_CONTENT"; \
gosu node ln -s config.production.json "$GHOST_INSTALL/config.development.json"; \
readlink -f "$GHOST_INSTALL/config.development.json"; \
mv "$GHOST_CONTENT" "$GHOST_INSTALL/content.orig"; \
mkdir -p "$GHOST_CONTENT"; \
chown node:node "$GHOST_CONTENT"; \
chmod 1777 "$GHOST_CONTENT"; \
cd "$GHOST_INSTALL/current"; \
packages="$(node -p ' \
var ghost = require("./package.json"); \
var sharpVersion = ""; \
try { \
var transform = require("./node_modules/@tryghost/image-transform/package.json"); \
sharpVersion = transform.optionalDependencies["sharp"] || transform.dependencies["sharp"]; \
} catch(e) { \
try { \
sharpVersion = ghost.optionalDependencies["sharp"] || ghost.dependencies["sharp"]; \
} catch(e2) { \
sharpVersion = "latest"; \
} \
} \
var sqlite3Version = ghost.optionalDependencies["sqlite3"] || ghost.dependencies["sqlite3"] || "latest"; \
[ \
"sharp@" + sharpVersion, \
"sqlite3@" + sqlite3Version, \
].join(" ") \
')"; \
echo "Detected packages to install: $packages"; \
if echo "$packages" | grep 'undefined'; then \
echo "Error: undefined package version detected"; \
exit 1; \
fi; \
for package in $packages; do \
echo "Installing package: $package"; \
installCmd='gosu node yarn add "$package" --force'; \
if ! eval "$installCmd"; then \
echo "Yarn installation failed, trying with npm: $package"; \
npmInstallCmd='gosu node npm install "$package" --save --force'; \
if ! eval "$npmInstallCmd"; then \
echo "Package installation failed, installing build dependencies for: $package"; \
virtualPackages='g++ make python3 pkgconfig vips-dev libc6-compat'; \
virtual=".build-deps-${package%%@*}"; \
apkDel="$apkDel $virtual"; \
apk add --no-cache --virtual "$virtual" $virtualPackages; \
echo "Retrying yarn installation with build-from-source: $package"; \
if ! eval "$installCmd --build-from-source"; then \
echo "Retrying npm installation with build-from-source: $package"; \
eval "$npmInstallCmd --build-from-source"; \
fi; \
fi; \
fi; \
echo "Successfully installed: $package"; \
done; \
if [ -n "$apkDel" ]; then \
apk del --no-network $apkDel; \
fi; \
gosu node yarn cache clean; \
gosu node npm cache clean --force; \
npm cache clean --force; \
rm -rv /tmp/yarn* /tmp/v8*

# ----------------------------------------------
# 5) LAYER final
# ----------------------------------------------
FROM mynode AS final

COPY --chown="${USER}":"${USER}" /v5/docker-entrypoint.sh /usr/local/bin
COPY --from=builder --chown="${USER}":"${USER}" "${GHOST_INSTALL}" "${GHOST_INSTALL}"

WORKDIR "${GHOST_INSTALL}"
VOLUME "${GHOST_CONTENT}"
USER "${USER}"
org.opencontainers.image.base_os="${BASE_OS}" \
org.opencontainers.image.node_version="${NODE_VERSION}"

# ---- environment ------------------------------------------------------------
ENV GHOST_INSTALL=/opt/ghost \
GHOST_CONTENT=/opt/ghost/content \
NODE_ENV=production

WORKDIR ${GHOST_INSTALL}

# ---- copy application from builder -----------------------------------------
COPY --from=builder --chown=nonroot:nonroot ${GHOST_INSTALL} ${GHOST_INSTALL}

USER nonroot
EXPOSE 2368

# HEALTHCHECK must be done during the runtime
# ---- health-check in a shell-less image ------------------------------------
HEALTHCHECK --interval=30s --timeout=5s --start-period=60s \
CMD node -e "require('http').get('http://127.0.0.1:2368',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))"

ENTRYPOINT [ "docker-entrypoint.sh" ]
CMD [ "node", "current/index.js" ]
ENTRYPOINT ["node", "current/index.js"]
Loading