# 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"]