# syntax=docker/dockerfile:1 # --------------------------------------------------------------------------- # Base image pin # --------------------------------------------------------------------------- # Pinned to a specific minor (20.19) so rebuilds are reproducible and satisfy # the package.json `engines` constraint (">=20.19 <22 || >=22.12"). # A digest pin is PREFERRED for full immutability, e.g.: # FROM node:20.19-alpine@sha256: # Add the real sha256 (from `docker buildx imagetools inspect node:20.19-alpine`) # when you have network access. We do NOT invent a fake digest here. # =========================================================================== # Stage 1 — builder: install ALL deps, generate Prisma client, build the app # =========================================================================== FROM node:20.19-alpine AS builder # openssl is required by Prisma's engines. RUN apk add --no-cache openssl WORKDIR /app # Install the full dependency tree (incl. devDependencies needed by the # Vite / React Router build toolchain). NODE_ENV is intentionally left unset # here so `npm ci` does not prune devDependencies. COPY package.json package-lock.json* ./ RUN npm ci # Copy the rest of the source and produce the production build + Prisma client. COPY . . RUN npx prisma generate \ && npm run build # =========================================================================== # Stage 2 — runtime: pruned prod deps + only the artifacts needed to run # =========================================================================== FROM node:20.19-alpine AS runtime # openssl for Prisma engines at runtime (migrate deploy / query engine). RUN apk add --no-cache openssl WORKDIR /app ENV NODE_ENV=production # Keep npm + Prisma incidental writes off the (dev) read-only root filesystem: # both are redirected to the tmpfs-backed /tmp mount. ENV NPM_CONFIG_CACHE=/tmp/.npm ENV CHECKPOINT_DISABLE=1 # Install ONLY production dependencies. COPY package.json package-lock.json* ./ RUN npm ci --omit=dev && npm cache clean --force # Prisma schema + migrations are needed for `prisma generate` (below) and for # `prisma migrate deploy` on container start. COPY --from=builder /app/prisma ./prisma # Bake the generated Prisma client into the image so the container start # command only has to run migrations (no generate at runtime → compatible with # a read-only root filesystem). RUN npx prisma generate # Application artifacts required at runtime. COPY --from=builder /app/build ./build COPY --from=builder /app/public ./public COPY --from=builder /app/server.js ./server.js # Run as the unprivileged, pre-existing `node` user (uid/gid 1000). Ensure the # app tree is owned by it. NOTE: the SQLite DB is written to the /data bind # mount — the HOST directory mounted at /data MUST be chown'd to uid 1000 # (see deploy/README.md), otherwise migrations/writes will fail as non-root. RUN chown -R node:node /app USER node EXPOSE 3000 # Container-level health probe hitting the unauthenticated /healthz route. # busybox wget ships with node:alpine. HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ CMD wget -qO- http://127.0.0.1:3000/healthz || exit 1 # Functionally equivalent to today's start: runs DB migrations then the server. # (`docker-start` no longer needs `prisma generate` — the client is baked above.) CMD ["npm", "run", "docker-start"]