diff --git a/docker/Dockerfile b/docker/Dockerfile index e49d097..bfae3dc 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,7 +1,4 @@ -# Build from the repo root, not from docker/: -# docker build -f docker/Dockerfile -t shhh . -# -# Node 24 is the Active LTS the project targets. Alpine keeps the runtime image small. +# Build from the repo root: docker build -f docker/Dockerfile -t shhh . FROM node:24-alpine AS base ENV PNPM_HOME=/pnpm ENV PATH=$PNPM_HOME:$PATH @@ -10,27 +7,27 @@ WORKDIR /app FROM base AS deps -# Manifests only, so this layer is reused until dependencies change; the app source changes far more often than pnpm-lock.yaml. +# Manifests only, so this layer survives every source change. COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ COPY apps/app/package.json apps/app/ COPY apps/docs/package.json apps/docs/ -# `--filter app...` skips the Docus workspace, a separate site never served by this image. `--frozen-lockfile` turns a stale lockfile into a build failure rather than silent drift. RUN pnpm install --frozen-lockfile --filter app... FROM deps AS build COPY . . -# Nuxt's own binary, not `pnpm build`: that script routes through scripts/nuxt.mjs and its Infisical wrapper, a local convenience. In a container the environment already comes from compose or the orchestrator. +# Not `pnpm build`: that script wraps Nuxt in an Infisical call this image has no use for. RUN pnpm --filter app exec nuxt build -# Not `FROM base`: the runtime needs neither corepack nor a package manager, and inheriting them is what drags their vulnerabilities in. -# Every CVE Docker Scout reports against this image comes from the npm CLI bundled in node:alpine, never from our own layers. +# Not `FROM base`: inheriting the package managers is what drags their CVEs in. FROM node:24-alpine AS runtime WORKDIR /app -# The container only runs `node .output/server/index.mjs`, so npm, npx, yarn, pnpm and corepack are dead weight. Removing them takes the image from 18 reported vulnerabilities to 0. -# It does not shrink it — the files stay in the base layer, whited out — but scanners read the final filesystem, which is also what an attacker in the container gets. +# The base froze its Alpine packages on its build date; openssl has moved since. +RUN apk upgrade --no-cache + +# Nothing here runs a package manager, and scanners read the final filesystem. RUN rm -rf /usr/local/lib/node_modules/npm /usr/local/lib/node_modules/corepack \ /usr/local/bin/npm /usr/local/bin/npx /usr/local/bin/corepack \ /usr/local/bin/pnpm /usr/local/bin/pnpx \ @@ -41,18 +38,16 @@ ENV NUXT_PORT=3000 ENV NUXT_HOST=0.0.0.0 ENV MIGRATIONS_DIR=/app/migrations -# Nitro's output is self-contained, so nothing is installed in this stage. Migrations run at boot from server/plugins/migrate.ts, so no drizzle-kit CLI and no pnpm store follow us here. COPY --from=build /app/apps/app/.output ./.output -# The .sql files themselves are read from disk at runtime, so they are copied in separately. +# Read from disk at boot by server/plugins/migrate.ts, so not part of Nitro's output. COPY --from=build /app/apps/app/server/database/migrations ./migrations -# node:alpine ships an unprivileged `node` user; the server needs no write access anywhere. USER node EXPOSE 3000 -# The app's own health endpoint, not a bare TCP check: an instance that lost its database reports unhealthy. +# The app's own endpoint, so an instance that lost its database reports unhealthy. HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ CMD node -e "fetch('http://127.0.0.1:3000/api/health').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))" diff --git a/docker/docs.Dockerfile b/docker/docs.Dockerfile index a9dc207..1e58032 100644 --- a/docker/docs.Dockerfile +++ b/docker/docs.Dockerfile @@ -1,8 +1,4 @@ -# Build from the repo root, not from docker/: -# docker build -f docker/docs.Dockerfile -t shhh-docs . -# -# The documentation site. Separate image from the app: it shares no runtime, no database and no -# release cadence reason to be bundled together, and an instance operator has no use for it. +# Build from the repo root: docker build -f docker/docs.Dockerfile -t shhh-docs . FROM node:24-alpine AS base ENV PNPM_HOME=/pnpm ENV PATH=$PNPM_HOME:$PATH @@ -14,22 +10,23 @@ FROM base AS deps COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ COPY apps/app/package.json apps/app/ COPY apps/docs/package.json apps/docs/ -# `--filter docs...` is the mirror of the app image: the Nuxt application is never served here. RUN pnpm install --frozen-lockfile --filter docs... FROM deps AS build COPY . . -# Nuxt's own binary rather than `pnpm build:docs`, which routes through scripts/nuxt.mjs and its -# Infisical wrapper. The docs need no secrets at all. +# Not `pnpm build:docs`: that script wraps Nuxt in an Infisical call the docs have no use for. RUN pnpm --filter docs exec nuxt build -# Same reasoning as the app image: the runtime needs no package manager, and inheriting one is what -# drags its vulnerabilities in. +# Not `FROM base`: inheriting the package managers is what drags their CVEs in. FROM node:24-alpine AS runtime WORKDIR /app +# The base froze its Alpine packages on its build date; openssl has moved since. +RUN apk upgrade --no-cache + +# Nothing here runs a package manager, and scanners read the final filesystem. RUN rm -rf /usr/local/lib/node_modules/npm /usr/local/lib/node_modules/corepack \ /usr/local/bin/npm /usr/local/bin/npx /usr/local/bin/corepack \ /usr/local/bin/pnpm /usr/local/bin/pnpx \ @@ -39,20 +36,15 @@ ENV NODE_ENV=production ENV NUXT_PORT=3000 ENV NUXT_HOST=0.0.0.0 -# Nitro's output carries its own node_modules, native bindings included — @nuxt/content's SQLite and -# the sharp build for this image's architecture. Nothing is installed in this stage. COPY --from=build /app/apps/docs/.output ./.output -# @nuxt/content opens its SQLite database read-write at boot, so unlike the app this image does need -# one writable directory. The relative path set in nuxt.config resolves from Nitro's server -# directory, hence this location rather than /app/.data. Everything else stays root-owned. +# @nuxt/content opens its SQLite read-write at boot; the path resolves from Nitro's server directory. RUN mkdir -p /app/.output/server/.data/content && chown -R node:node /app/.output/server/.data USER node EXPOSE 3000 -# No database and no /api/health here, so the site's own root is the only meaningful signal. HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ CMD node -e "fetch('http://127.0.0.1:3000/').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"