# syntax=docker/dockerfile:1.7 # # Production image for MEGA PLANT — designed for Coolify. # # Multi-stage build: # 1. deps — fetch and cache npm dependencies # 2. builder — compile the Next.js standalone bundle # 3. runner — minimal runtime image with just the standalone server # # The container listens on ${PORT:-3000} and binds 0.0.0.0 so Coolify's # Traefik proxy can reach it. /api/health is the liveness probe. # # IMPORTANT — NEXT_PUBLIC_PB_URL must be passed as a BUILD ARG, not just a # runtime variable. Next.js inlines NEXT_PUBLIC_* into the client bundle at # build time; setting it only at runtime leaves the browser pointing at # whatever was baked in. See COOLIFY.md. # --------------------------------------------------------------------------- # 1. deps # --------------------------------------------------------------------------- FROM node:20-alpine AS deps RUN apk add --no-cache libc6-compat WORKDIR /app # Copy only the manifest + lockfile so this layer stays cached for as long # as dependencies don't change. COPY package.json package-lock.json* ./ RUN npm ci --no-audit --no-fund # --------------------------------------------------------------------------- # 2. builder # --------------------------------------------------------------------------- FROM node:20-alpine AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . # The PocketBase URL is compiled into the client bundle here. Fail loudly # rather than shipping an image that silently talks to the wrong database. ARG NEXT_PUBLIC_PB_URL ENV NEXT_PUBLIC_PB_URL=${NEXT_PUBLIC_PB_URL} RUN test -n "$NEXT_PUBLIC_PB_URL" \ || (echo "" \ && echo "ERROR: build arg NEXT_PUBLIC_PB_URL is required." \ && echo " Coolify -> Build -> Build Arguments -> NEXT_PUBLIC_PB_URL=https://pb.mega.plant.example" \ && echo " Local: docker build --build-arg NEXT_PUBLIC_PB_URL=https://... ." \ && echo "" && exit 1) ENV NEXT_TELEMETRY_DISABLED=1 ENV NODE_ENV=production # `next build --webpack` is deliberate: @ducanh2912/next-pwa is Webpack-only # and is what generates public/sw.js + public/workbox-*.js. Dropping the flag # silently ships an app with no service worker and no offline install. RUN npm run build # --------------------------------------------------------------------------- # 3. runner # --------------------------------------------------------------------------- FROM node:20-alpine AS runner WORKDIR /app ENV NODE_ENV=production ENV NEXT_TELEMETRY_DISABLED=1 ENV PORT=3000 ENV HOSTNAME=0.0.0.0 RUN addgroup --system --gid 1001 nodejs \ && adduser --system --uid 1001 nextjs # Static assets — includes the manifest, icons, and the service worker that # next-pwa generated during the build. COPY --from=builder /app/public ./public # `output: "standalone"` produces server.js plus a trimmed node_modules, but # does NOT include .next/static — that has to be copied in separately. COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static # Next's output tracer cannot see pdfjs-dist's worker. The only reference to it # is a createRequire(...).resolve() call, which the tracer does not follow, so # the standalone bundle ships pdf.mjs WITHOUT pdf.worker.mjs — and every parse # then dies with "Setting up fake worker failed: Cannot find module # '/app/node_modules/pdfjs-dist/legacy/build/pdf.worker.mjs'". Copy the package # in whole: the worker has to sit next to pdf.mjs at runtime, and so do the # standard fonts, which the parser needs because it passes useSystemFonts. COPY --from=deps --chown=nextjs:nodejs /app/node_modules/pdfjs-dist ./node_modules/pdfjs-dist USER nextjs EXPOSE 3000 HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ CMD wget -q --spider http://127.0.0.1:3000/api/health || exit 1 CMD ["node", "server.js"]