Docker
Multi-stage Dockerfile (deps/builder/runner), Node 24 Alpine base, non-root user, healthcheck, .dockerignore, and GitHub Actions CI pipeline with versioned zip artifacts.
1. Why Multi-Stage
Turmerrific ships a three-stage Dockerfile: deps → builder → runner. Each stage starts fresh from a minimal base image. Final image contains only the standalone Next.js output and runtime dependencies — no source, no build tools, no dev packages.
- deps — install production deps only
- builder — install all deps, compile, generate standalone output
- runner — copy standalone output, run as non-root
2. Enable Standalone Output
Next.js standalone mode bundles only the files needed for production into .next/standalone/. Reduces image size by 70%+ vs copying the full project.
1 // next.config.ts 2 import type { NextConfig } from "next"; 3 4 const nextConfig: NextConfig = { 5 output: "standalone", 6 }; 7 8 export default nextConfig;
3. Dockerfile
1 # syntax=docker/dockerfile:1.7 2 FROM node:24-alpine AS base 3 RUN apk add --no-cache libc6-compat 4 WORKDIR /app 5 6 # === deps stage === 7 FROM base AS deps 8 COPY package.json yarn.lock ./ 9 RUN yarn install --frozen-lockfile --production=false 10 11 # === builder stage === 12 FROM base AS builder 13 COPY --from=deps /app/node_modules ./node_modules 14 COPY . . 15 ENV NEXT_TELEMETRY_DISABLED=1 16 RUN yarn build 17 18 # === runner stage === 19 FROM base AS runner 20 ENV NODE_ENV=production \ 21 NEXT_TELEMETRY_DISABLED=1 \ 22 PORT=3000 \ 23 HOSTNAME=0.0.0.0 24 25 RUN addgroup --system --gid 1001 nodejs \ 26 && adduser --system --uid 1001 nextjs 27 28 COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ 29 COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static 30 COPY --from=builder --chown=nextjs:nodejs /app/public ./public 31 32 USER nextjs 33 EXPOSE 3000 34 35 HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \ 36 CMD wget --quiet --spider http://localhost:3000/api/health || exit 1 37 38 CMD ["node", "server.js"]
4. .dockerignore
Keep your build context small. Anything that won't affect the final image should be excluded.
1 node_modules 2 .next 3 .git 4 .env* 5 !.env.example 6 **/*.log 7 **/.DS_Store 8 README.md 9 docs/ 10 e2e/ 11 playwright-report/ 12 coverage/ 13 .vscode/ 14 .idea/
5. Build & Run
# Build production image
docker build -t turmerrific:latest .
# Run locally
docker run --rm -p 3000:3000 \
-e DATABASE_URL=postgresql://user:pass@host.docker.internal:5432/db \
-e JWT_SECRET=changeme \
turmerrific:latest
# Check healthcheck status
docker inspect --format='{{.State.Health.Status}}' <container-id>
# Tag and push
docker tag turmerrific:latest registry.example.com/turmerrific:1.0.0
docker push registry.example.com/turmerrific:1.0.06. GitHub Actions CI
The default pipeline runs lint, format check, and build, then uploads a versioned zip artifact. Tag your release commits to get versioned bundles.
1 # .github/workflows/ci.yml 2 name: CI 3 4 on: 5 push: { branches: [main] } 6 pull_request: { branches: [main] } 7 8 jobs: 9 build: 10 runs-on: ubuntu-latest 11 steps: 12 - uses: actions/checkout@v4 13 - uses: actions/setup-node@v4 14 with: 15 node-version: "24" 16 cache: "yarn" 17 - run: yarn install --frozen-lockfile 18 - run: yarn lint 19 - run: yarn format:check 20 - run: yarn build 21 - name: Compute version 22 id: vers 23 run: | 24 TAG=${{ github.ref_name }} 25 SHA=${GITHUB_SHA::7} 26 BUILD=${{ github.run_number }} 27 echo "v=${TAG:-0.0.1}-${BUILD}-${SHA}" >> $GITHUB_OUTPUT 28 - run: zip -r turmerrific-${{ steps.vers.outputs.v }}.zip .next/standalone .next/static public 29 - uses: actions/upload-artifact@v4 30 with: 31 name: turmerrific-${{ steps.vers.outputs.v }} 32 path: turmerrific-${{ steps.vers.outputs.v }}.zip
7. Deploy Targets
- Self-host — Docker Compose, Kubernetes, Fly.io, Railway. Healthcheck endpoint already wired.
- Vercel — skip Docker. Push to GitHub, Vercel builds and deploys natively.
- Cloud Run / Lambda — standalone output is already container-ready. Set
PORTfrom the platform env.