- packages/config: add src/startup.ts exposing assertValidConfig (builds on the T01 TypeBox/Ajv schema) and the field-specific startup errors (MissingRequiredSettingError names the missing field; ConfigStartupError names each violating field); re-export from the package boundary - apps/server: validate the startup configuration (including the required EPPP_SESSION_SECRET) before the server binds, so a missing required setting crashes the process at startup naming the field; depends on @personal-blog/config - compose.yaml: provide EPPP_SESSION_SECRET for the app service (dev-only >= 32 char default; override via .env / shell) - Dockerfile: ship the compiled packages/config in the image (build source + runtime dist), matching the server's new workspace dependency - pnpm-lock.yaml: apps/server importer gains @personal-blog/config
108 lines
6.1 KiB
Docker
108 lines
6.1 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` on port 3000. Since T06 (E00-S03) the health
|
|
# endpoint is the readiness probe: the app runs the startup migrations (the
|
|
# `MigrationRunner` from `@personal-blog/database-postgres`) before reporting
|
|
# ready — `GET /health` answers 503 `{"status":"not ready"}` while the run is
|
|
# in flight and flips to 200 `{"status":"ok"}` only after it completes — so
|
|
# the app container does not report ready before migrations complete. 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).
|
|
#
|
|
# The app now depends on the `config` and `database-postgres` workspace
|
|
# packages (the configuration service — E00-S04-T02 validates the required
|
|
# settings at startup — and the single owner of the pg/Kysely driver,
|
|
# E00-S03-T02). The build stage therefore also installs/builds those
|
|
# packages — the server's `build`/`typecheck` scripts build their workspace
|
|
# dependencies first (`pnpm --filter @personal-blog/config build` and
|
|
# `pnpm --filter @personal-blog/database-postgres build`), and the runtime
|
|
# stage ships the compiled `packages/config/dist` and
|
|
# `packages/database-postgres/dist` next to the copied workspace node_modules
|
|
# links so the server's `@personal-blog/config` and
|
|
# `@personal-blog/database-postgres` imports resolve at run time.
|
|
#
|
|
# 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, packages/config,
|
|
# packages/database-postgres, 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 packages/config/package.json packages/config/package.json
|
|
COPY packages/database-postgres/package.json packages/database-postgres/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/). The
|
|
# server's build script builds its workspace dependencies first (the `config`
|
|
# and `database-postgres` packages, whose compiled dists the server imports),
|
|
# so a single command produces all dists in the right order.
|
|
COPY apps/server apps/server
|
|
COPY packages/config packages/config
|
|
COPY packages/database-postgres packages/database-postgres
|
|
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, the compiled config
|
|
# and database-postgres outputs the server imports, and the package manifests.
|
|
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
|
|
COPY --from=build /app/packages/config/dist ./packages/config/dist
|
|
COPY --from=build /app/packages/config/package.json ./packages/config/package.json
|
|
COPY --from=build /app/packages/database-postgres/dist ./packages/database-postgres/dist
|
|
COPY --from=build /app/packages/database-postgres/package.json ./packages/database-postgres/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"]
|