Multi-stage build on node:22-alpine, standalone output, non-root user, healthcheck on /api/health, and migrations applied by the entrypoint before the first request. A failed migration stops the container rather than serving an inconsistent database. Getting the Prisma CLI into the runtime image took three attempts and the reasoning is recorded in the Dockerfile. Copying it out of the build stage leaves its transitive dependencies behind; patching them in one at a time is a losing game. It now gets its own stage and its own tree, with the schema and prisma.config.ts beside it, and the entrypoint runs from there so every import resolves locally. The version is read from our own package.json so it cannot drift from the generated client. Two things had to change to build without a database, which a build container rightly does not have. prisma.config.ts no longer reads the URL through prisma's env() helper, which throws on a missing variable even for `generate`. And lib/db.ts creates the client on first use rather than on import: Next imports every route module while collecting page data, so a module that threw on import failed the build with an error naming whichever route was analysed first, which says nothing useful. The failure now lands on the first query, where it belongs. Verified by running the image against a real database: migrations applied, cron scheduled in Europe/Zurich, a device paired, and the panel image served as a genuine 1-bit 800x480 BMP — so satori, resvg and the vendored fonts all work on musl. The image hash came out identical to the one produced on the glibc host, which is the reproducibility the vendored fonts were for. The production overlay publishes through an existing Traefik, drops the host port, mounts the filesystem read-only, and adds a nightly dump kept for a fortnight. README and DEPLOY are in French and cover what actually bites: the panel receives nothing and only updates when it wakes; the issuer must match to the character; the captive portal URL takes no trailing slash; a rollback across a migration needs the dump, because Prisma does not undo one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd
87 lines
3.6 KiB
Docker
87 lines
3.6 KiB
Docker
# syntax=docker/dockerfile:1
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# deps — install once, cached on the lockfile alone
|
|
# ---------------------------------------------------------------------------
|
|
FROM node:22-alpine AS deps
|
|
WORKDIR /app
|
|
COPY package.json package-lock.json ./
|
|
RUN npm ci
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# build — generate the Prisma client, then compile
|
|
# ---------------------------------------------------------------------------
|
|
FROM node:22-alpine AS build
|
|
WORKDIR /app
|
|
ENV NEXT_TELEMETRY_DISABLED=1
|
|
COPY --from=deps /app/node_modules ./node_modules
|
|
COPY . .
|
|
# The client is generated into lib/generated and imported by the app, so it
|
|
# must exist before the build traces its dependencies.
|
|
RUN npx prisma generate && npm run build
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# migrator — the Prisma CLI, on its own, with its own dependency tree
|
|
#
|
|
# The CLI cannot simply be copied out of the build stage: it needs transitive
|
|
# dependencies that are scattered through a hoisted node_modules, and copying
|
|
# the whole tree would double the image. Installing it alone keeps it small and
|
|
# keeps it working. The version is read from our own package.json so it cannot
|
|
# drift from the client the application was built against. dotenv comes along
|
|
# because prisma.config.ts imports it; in a container the environment is
|
|
# already populated, but the file is shared with local development.
|
|
# ---------------------------------------------------------------------------
|
|
FROM node:22-alpine AS migrator
|
|
WORKDIR /cli
|
|
COPY package.json ./
|
|
RUN PRISMA_VERSION="$(node -p "require('./package.json').dependencies.prisma")" \
|
|
&& rm package.json \
|
|
&& npm init -y > /dev/null \
|
|
&& npm install --no-audit --no-fund --omit=dev "prisma@${PRISMA_VERSION}" dotenv
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# runner — standalone output plus what the migrations need
|
|
# ---------------------------------------------------------------------------
|
|
FROM node:22-alpine AS runner
|
|
WORKDIR /app
|
|
|
|
ENV NODE_ENV=production \
|
|
NEXT_TELEMETRY_DISABLED=1 \
|
|
PORT=3010 \
|
|
HOSTNAME=0.0.0.0
|
|
|
|
# curl for the healthcheck, tzdata so Europe/Zurich is a real zone rather than
|
|
# a name Intl quietly falls back to UTC for.
|
|
RUN apk add --no-cache curl tzdata
|
|
|
|
COPY --from=build /app/.next/standalone ./
|
|
COPY --from=build /app/.next/static ./.next/static
|
|
COPY --from=build /app/public ./public
|
|
|
|
# `prisma migrate deploy` runs at startup, so the CLI, the schema, the
|
|
# migrations and the config travel with the image — all of them together under
|
|
# .migrator, which the entrypoint uses as its working directory.
|
|
#
|
|
# Keeping the config beside the CLI rather than at the application root is
|
|
# what makes the isolation hold: `prisma.config.ts` imports `prisma/config`
|
|
# and `dotenv`, and both resolve from the tree next to it. Splitting them
|
|
# would mean copying the CLI's dependencies into the application's own
|
|
# node_modules one failure at a time.
|
|
COPY --from=migrator /cli/node_modules ./.migrator/node_modules
|
|
COPY --from=build /app/prisma ./.migrator/prisma
|
|
COPY --from=build /app/prisma.config.ts ./.migrator/prisma.config.ts
|
|
|
|
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
|
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
|
|
|
# The image ships with a non-root user; use it.
|
|
USER node
|
|
|
|
EXPOSE 3010
|
|
|
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
|
CMD curl -fsS http://127.0.0.1:3010/api/health || exit 1
|
|
|
|
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
|
CMD ["node", "server.js"]
|