Files
sharper.ly/apps/cms/Dockerfile
2026-09-11 02:46:56 +02:00

87 lines
3.4 KiB
Docker

# syntax=docker/dockerfile:1
###############################################################################
# SHARP-ER CMS - Strapi 5 on Node 22
#
# The build context is the REPOSITORY ROOT, not apps/cms: this is a pnpm
# workspace, so pnpm-lock.yaml, pnpm-workspace.yaml and .npmrc all live one
# level up and pnpm refuses to do a frozen install without them.
#
# docker build -f apps/cms/Dockerfile -t sharper-cms .
# docker compose build cms # does exactly that
#
# Debian slim rather than Alpine: Strapi's image pipeline (sharp) ships glibc
# prebuilt binaries, so this avoids a source build of libvips.
###############################################################################
# --- base: the toolchain both stages share ----------------------------------
FROM node:22-bookworm-slim AS base
ENV PNPM_VERSION=9.12.3
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates \
&& rm -rf /var/lib/apt/lists/* \
&& npm install --global "pnpm@${PNPM_VERSION}"
WORKDIR /repo
# --- build: install dependencies and compile the admin panel ----------------
FROM base AS build
# Manifests first, so the (slow) install layer is cached until a dependency
# actually changes. Both workspace manifests are copied even though only the
# CMS is installed: pnpm compares the lockfile against every project in the
# workspace and calls the lockfile stale if one is missing.
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
COPY apps/cms/package.json ./apps/cms/
COPY apps/web/package.json ./apps/web/
# `--filter cms...` = the CMS and anything in the workspace it depends on.
# devDependencies are needed here: `strapi build` compiles TypeScript.
RUN pnpm install --frozen-lockfile --filter cms...
COPY apps/cms ./apps/cms
# `strapi build` loads config/*.ts, which reads these. They are throwaway
# build-time values that never reach the runtime stage below - the real ones
# are injected by docker compose at start-up.
ENV NODE_ENV=production \
APP_KEYS=build-only-key-1,build-only-key-2 \
API_TOKEN_SALT=build-only \
ADMIN_JWT_SECRET=build-only \
JWT_SECRET=build-only \
TRANSFER_TOKEN_SALT=build-only \
ENCRYPTION_KEY=build-only \
DATABASE_CLIENT=sqlite
RUN pnpm --filter cms build
# --- runtime ----------------------------------------------------------------
FROM base AS runtime
ENV NODE_ENV=production \
HOST=0.0.0.0 \
PORT=1337
# node_modules is carried over rather than reinstalled with --prod: the Strapi
# CLI still resolves its TypeScript toolchain at start-up, so pruning
# devDependencies breaks `strapi start` on a TypeScript project.
COPY --from=build /repo/node_modules ./node_modules
COPY --from=build /repo/package.json /repo/pnpm-workspace.yaml /repo/.npmrc ./
COPY --from=build /repo/apps/cms ./apps/cms
# The Media Library writes here. Ownership is set before the named volume is
# mounted, so the volume inherits it on first start.
RUN mkdir -p /repo/apps/cms/public/uploads \
&& chown -R node:node /repo/apps/cms
USER node
WORKDIR /repo/apps/cms
EXPOSE 1337
# Strapi answers /_health with 204 once the HTTP server is listening. Node 22
# has fetch built in, so the image needs no curl or wget.
HEALTHCHECK --interval=15s --timeout=5s --start-period=90s --retries=10 \
CMD node -e "fetch('http://127.0.0.1:'+(process.env.PORT||1337)+'/_health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
# `strapi start` serves the API and the pre-built admin panel from dist/.
CMD ["pnpm", "start"]