Files
PersonalBlog/apps/server/Dockerfile
T
implementer 0938da8a73 feat: keep secrets out of the app image (E00-S02-T08)
- .dockerignore: exclude env + credential files (.npmrc, .netrc, .aws, .ssh,
  secrets/, *.pem, *.key, *.p12, *.pfx, *.jks, id_rsa, id_ed25519, ...) from
  the build context so a local secret file cannot be embedded in the image
- Dockerfile: document the T08 guarantee (no secret ARG/ENV, fixed non-secret
  COPY paths, runtime credentials via Compose environment)
- compose.yaml: T08 in scope; runtime credentials stay in service environment,
  never in the image
2026-08-29 01:28:26 +00:00

77 lines
4.0 KiB
Docker

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