# syntax=docker/dockerfile:1 # @personal-blog/server — EPPP public server application image. # # [E00-S02-T01/T02/T03] baseline: builds the workspace server package with the # pinned toolchain (Node 24.19.0 + pnpm 11.23.0, frozen lockfile) and runs the # compiled entrypoint. Since T03 the entrypoint is a minimal Node `node:http` # server answering `GET /health` with `{"status":"ok"}` (HTTP 200) on port # 3000, so the app container stays up and the health endpoint succeeds. The # Fastify 5 application shell (and the real HTTP API) lands in a later story; # DB volume persistence (T04), read-only root filesystem (T06) and multi-arch # build targets (T07) are Compose-level concerns (see compose.yaml — the # `db-data` volume mount, the app service's `read_only: true` + `/tmp` tmpfs, # and its `build.platforms` list; this image is unchanged: both stages use the # official multi-arch node:24.19.0-bookworm-slim base and the build has no # native dependencies, so the amd64/arm64 targets need no image change). Since # T05 the runtime stage drops root privileges (runs as the image's non-root # `node` user). # # T08: the image embeds no secrets. The Dockerfile declares no secret-bearing # ARG/ENV instruction (the only ENV is `NODE_ENV=production`) and every COPY # copies a fixed, non-secret path (manifests, source, compiled dist) — never # `.env` or credential files; `.dockerignore` additionally excludes env and # credential files from the build context at the context root AND at any # nested depth (its patterns are `**/`-prefixed because Docker's matcher # anchors slash-less patterns to the context root), so a local secret file # cannot be embedded even by mistake. Runtime credentials (e.g. DATABASE_URL) # are injected by Compose at run time (compose.yaml `app.environment`), never # baked into the image. Tests: tests/secrets-not-embedded.test.mjs. # # Image base: node:24.19.0-bookworm-slim (glibc Debian) per Technology-Stack # §5.4 — argon2 is a native dependency and musl/Alpine causes native-module # build surprises, so the image must stay on a glibc base. # --- build stage: install the frozen workspace and compile the server -------- FROM node:24.19.0-bookworm-slim AS build WORKDIR /app # Enable the pinned pnpm (11.23.0, via packageManager in the root package.json) # with Corepack, which ships with the Node image. RUN corepack enable # Copy only the manifests needed for resolution first, so source edits do not # invalidate the dependency layer, then install against the committed lockfile # (the same `--frozen-lockfile` path CI and developers use). Every workspace # package manifest is copied so the in-image workspace matches the lockfile # importers exactly (apps/server, packages/core, extensions/example). COPY package.json pnpm-lock.yaml pnpm-workspace.yaml tsconfig.base.json ./ COPY apps/server/package.json apps/server/package.json COPY packages/core/package.json packages/core/package.json COPY extensions/example/package.json extensions/example/package.json RUN pnpm install --frozen-lockfile # Compile the server package (tsc -p apps/server/tsconfig.json -> dist/). COPY apps/server apps/server RUN pnpm --filter @personal-blog/server build # --- runtime stage: Node 24.19.0 (bookworm-slim) + compiled output only ------ FROM node:24.19.0-bookworm-slim AS runtime WORKDIR /app ENV NODE_ENV=production # The workspace install (devDependencies included — image-size pruning is a # later E00-S02 concern) plus the compiled server output and manifest. COPY --from=build /app/node_modules ./node_modules COPY --from=build /app/apps/server/dist ./apps/server/dist COPY --from=build /app/apps/server/package.json ./apps/server/package.json # T05: run as the image's non-root `node` user (uid/gid 1000, shipped with the # official Node image) so the app container does not run with root privileges. # The server binds port 3000 (>= 1024, no privileged port needed) and only # reads the root-owned application files copied above, so no extra user # creation or ownership changes are required. USER is the last instruction # before EXPOSE so every COPY above lands before the privilege drop. USER node EXPOSE 3000 CMD ["node", "apps/server/dist/index.js"]