From a4cf365098b9dd8e4c3f82239de24d02c299359e Mon Sep 17 00:00:00 2001 From: implementer Date: Sat, 29 Aug 2026 00:41:10 +0000 Subject: [PATCH] feat: run the app image as a non-root user (E00-S02-T05) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The runtime stage of apps/server/Dockerfile now drops root privileges with 'USER node' — the non-root user (uid/gid 1000) the official Node image ships with — so the app container does not run with root privileges. The server binds port 3000 (>= 1024) and only reads the root-owned files copied above, so no extra user creation or ownership changes are required. compose.yaml header updated: T05 is in scope; T06 (read-only rootfs) and T07 (multi-arch) remain out of scope. --- apps/server/Dockerfile | 14 ++++++++++++-- compose.yaml | 11 ++++++++--- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/apps/server/Dockerfile b/apps/server/Dockerfile index 4cc2d96..a42ccb8 100644 --- a/apps/server/Dockerfile +++ b/apps/server/Dockerfile @@ -9,8 +9,10 @@ # 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) is a Compose-level concern (see compose.yaml — -# this image is unchanged), while non-root/read-only hardening (T05/T06) and -# multi-arch targets remain later E00-S02 tasks — all out of scope here. +# this image is unchanged), while read-only root filesystem (T06) and +# multi-arch build targets (T07) remain later E00-S02 tasks — all out of scope +# here. Since T05 the runtime stage drops root privileges (runs as the +# image's non-root `node` user). # # 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 @@ -50,5 +52,13 @@ 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"] diff --git a/compose.yaml b/compose.yaml index ebcdf89..4cb33c8 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,4 +1,4 @@ -# EPPP Docker Compose baseline — [E00-S02-T01/T02/T03/T04] +# EPPP Docker Compose baseline — [E00-S02-T01..T05] # # `docker compose up -d` starts both the database (PostgreSQL) and the # application (@personal-blog/server). Rollback: `docker compose down`. @@ -18,8 +18,13 @@ # `docker compose down` + `docker compose up -d` (recreate, which discards the # container filesystem). Reset the data with `docker compose down -v`. # -# Explicitly out of scope for T01..T04 (land in later E00-S02 tasks): -# - non-root execution (T05), read-only root filesystem (T06) +# Non-root execution (T05): the app image's runtime stage runs as the official +# Node image's non-root `node` user (see apps/server/Dockerfile — `USER node`), +# so the app container does not run with root privileges. No Compose-level +# `user:` override is needed: the image's USER is inherited by the container. +# +# Explicitly out of scope for T01..T05 (land in later E00-S02 tasks): +# - read-only root filesystem (T06), multi-arch build targets (T07) # # All values have defaults so `docker compose up -d` works from a clean clone # without a .env file (a committed .env.example template lands in E00-S04).