# 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"]