# syntax=docker/dockerfile:1 # Prisma 7 supports Node 20.19+, 22.12+ and 24.x and prints an unsupported # warning on newer majors, so this tracks the current LTS rather than latest. ARG NODE_VERSION=24-slim ARG PNPM_VERSION=11.25.0 # --------------------------------------------------------------------------- # base - shared runtime, with pnpm baked in so no stage fetches it later # --------------------------------------------------------------------------- FROM node:${NODE_VERSION} AS base ARG PNPM_VERSION ENV PNPM_HOME=/pnpm \ PATH=/pnpm:$PATH \ CHECKPOINT_DISABLE=1 RUN corepack enable && corepack prepare pnpm@${PNPM_VERSION} --activate WORKDIR /app # --------------------------------------------------------------------------- # build - install, generate the Prisma client, compile all three packages # --------------------------------------------------------------------------- FROM base AS build # better-sqlite3's install step is `prebuild-install || node-gyp rebuild`, so a # toolchain has to exist for the fallback. It never reaches a runtime stage. RUN apt-get update \ && apt-get install -y --no-install-recommends python3 make g++ \ && rm -rf /var/lib/apt/lists/* # Manifests first: dependencies only reinstall when the lockfile actually moves. COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ COPY apps/backend/package.json apps/backend/ COPY apps/frontend/package.json apps/frontend/ COPY packages/database/package.json packages/database/ RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ pnpm install --frozen-lockfile COPY . . # `prisma generate` resolves datasource.url through prisma7.config.ts, so the # variable must exist - but generate only writes the client, it never connects. ENV DATABASE_URL="file:/tmp/build.db" RUN pnpm --filter database db:generate \ && pnpm --filter database build \ && pnpm --filter backend build \ && pnpm --filter frontend build # Everything is compiled, so drop devDependencies in place. Safe here because # every runtime import in both apps resolves to a production dependency; the # one devDependency reference, `import("vite")` in server.js, sits in the dev # branch that --prod never takes. RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ pnpm install --prod --frozen-lockfile # --------------------------------------------------------------------------- # backend - Express API. Not published to the host; the frontend proxies to it. # --------------------------------------------------------------------------- FROM base AS backend ENV NODE_ENV=production COPY --from=build /app /app # Applying migrations on boot keeps a fresh volume usable. `migrate deploy` only # applies migrations it has not already recorded, so restarts are a no-op. COPY <<'EOF' /usr/local/bin/blog-backend #!/bin/sh set -e cd /app/packages/database ./node_modules/.bin/prisma migrate deploy cd /app/apps/backend exec node dist/index.js EOF RUN chmod +x /usr/local/bin/blog-backend # The SQLite file lives on a volume mounted here. RUN mkdir -p /data && chown node:node /data USER node WORKDIR /app/apps/backend # src/index.ts calls app.listen(3000) directly - env.PORT is parsed but unused. EXPOSE 3000 HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ CMD ["node", "-e", "fetch('http://127.0.0.1:3000/').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"] CMD ["blog-backend"] # --------------------------------------------------------------------------- # frontend - SSR server: renders / and /post/:slug, proxies /api to the backend # --------------------------------------------------------------------------- FROM base AS frontend ENV NODE_ENV=production \ PORT=5173 \ BACKEND_ORIGIN=http://backend:3000 COPY --from=build /app /app USER node WORKDIR /app/apps/frontend EXPOSE 5173 HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ CMD ["node", "-e", "fetch('http://127.0.0.1:'+(process.env.PORT||5173)+'/').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"] CMD ["node", "server.js", "--prod"]