From cab91fb80f0331890cb722674831b4a90752cc53 Mon Sep 17 00:00:00 2001 From: SJCoetz Date: Sun, 19 Jul 2026 21:05:04 +0200 Subject: [PATCH] build(docker): add multi-stage docker build and .dockerignore sets up production-grade multi-stage Docker build for the Next.js app, and configures .dockerignore to exclude dev artifacts, logs, env files, and unnecessary assets from the build context. --- .dockerignore | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++ Dockerfile | 60 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..99581e3b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,64 @@ +# ─── Build artifacts / state ─────────────────────────────────────────────────── +node_modules +.next +.next/** +out +dist +.turbo +.tsbuildinfo +*.tsbuildinfo +.cache +coverage + +# ─── Git / IDE ──────────────────────────────────────────────────────────────── +.git +.gitignore +.gitattributes +.DS_Store +.idea +.vscode +*.swp +*.swo +*~ + +# ─── Env / secrets (Coolify sets these at runtime, never bake them in) ───── +.env +.env.local +.env.*.local + +# ─── Logs / tmp ─────────────────────────────────────────────────────────────── +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +tmp +tmp/** + +# ─── Test / dev artifacts ────────────────────────────────────────────────────── +__snapshots__ +**/*.test.ts +**/*.test.tsx +**/*.spec.ts +**/*.spec.tsx +**/__mocks__/** + +# ─── Docs (not needed at runtime) ───────────────────────────────────────────── +*.md +!README.md +AGENTS.md +docs/** + +# ─── Migration / setup scripts (run from local, not in container) ────────────── +scripts/setup-pb.mjs +scripts/migrate*.py +scripts/migrate*.mjs +scripts/diag.mjs +scripts/screenshot.mjs +scripts/shoot-*.mjs +scripts/render-pdfs.mjs +diag.mjs + +# ─── Public assets that aren't needed at runtime ────────────────────────────── +public/*.bak +public/*.tmp diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..cd9709f8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,60 @@ +# syntax=docker/dockerfile:1.7 +# Multi-stage build for the Next.js 16 app. +# +# Build context: apps/web/ (set "Base Directory" in Coolify) +# Required env at runtime: PORT (default 4004), HOSTNAME=0.0.0.0, +# POCKETBASE_URL, RESEND_API_KEY, RESEND_FROM + +# ─── Stage 1: install deps ──────────────────────────────────────────────────── +FROM node:22-bookworm-slim AS deps +WORKDIR /app + +# pnpm via corepack — matches the dev environment (pnpm 10.x) +RUN corepack enable && corepack prepare pnpm@10.30.3 --activate + +# Copy only the manifests so the install layer caches well +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ +RUN pnpm install --frozen-lockfile + +# ─── Stage 2: build ──────────────────────────────────────────────────────────── +FROM node:22-bookworm-slim AS builder +WORKDIR /app +RUN corepack enable && corepack prepare pnpm@10.30.3 --activate + +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +ENV NEXT_TELEMETRY_DISABLED=1 +RUN pnpm build + +# ─── Stage 3: runtime ────────────────────────────────────────────────────────── +FROM node:22-bookworm-slim AS runner +WORKDIR /app + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 +# Next.js defaults to binding localhost, which doesn't work in a container. +# Coolify's Traefik reverse-proxies to this port. +ENV HOSTNAME=0.0.0.0 +ENV PORT=4004 + +RUN corepack enable && corepack prepare pnpm@10.30.3 --activate + +# Non-root user for the runtime +RUN groupadd --system --gid 1001 nodejs \ + && useradd --system --uid 1001 --gid nodejs nextjs + +# Copy the build artifacts +COPY --from=builder --chown=nextjs:nodejs /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next +COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules +COPY --from=builder --chown=nextjs:nodejs /app/package.json ./package.json + +USER nextjs +EXPOSE 4004 + +# Health check: hit the login page (no auth required) +HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ + CMD node -e "require('http').get('http://127.0.0.1:'+process.env.PORT+'/login',r=>process.exit(r.statusCode<500?0:1)).on('error',()=>process.exit(1))" + +CMD ["pnpm", "start"]