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
2import type { NextConfig } from "next";
3
4const nextConfig: NextConfig = {
5 output: "standalone",
6};
7
8export default nextConfig;

3. Dockerfile

1# syntax=docker/dockerfile:1.7
2FROM node:24-alpine AS base
3RUN apk add --no-cache libc6-compat
4WORKDIR /app
5
6# === deps stage ===
7FROM base AS deps
8COPY package.json yarn.lock ./
9RUN yarn install --frozen-lockfile --production=false
10
11# === builder stage ===
12FROM base AS builder
13COPY --from=deps /app/node_modules ./node_modules
14COPY . .
15ENV NEXT_TELEMETRY_DISABLED=1
16RUN yarn build
17
18# === runner stage ===
19FROM base AS runner
20ENV NODE_ENV=production \
21 NEXT_TELEMETRY_DISABLED=1 \
22 PORT=3000 \
23 HOSTNAME=0.0.0.0
24
25RUN addgroup --system --gid 1001 nodejs \
26 && adduser --system --uid 1001 nextjs
27
28COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
29COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
30COPY --from=builder --chown=nextjs:nodejs /app/public ./public
31
32USER nextjs
33EXPOSE 3000
34
35HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
36 CMD wget --quiet --spider http://localhost:3000/api/health || exit 1
37
38CMD ["node", "server.js"]

4. .dockerignore

Keep your build context small. Anything that won't affect the final image should be excluded.

1node_modules
2.next
3.git
4.env*
5!.env.example
6**/*.log
7**/.DS_Store
8README.md
9docs/
10e2e/
11playwright-report/
12coverage/
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.0

6. 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
2name: CI
3
4on:
5 push: { branches: [main] }
6 pull_request: { branches: [main] }
7
8jobs:
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 PORT from the platform env.

Built with Next.js, deployed from a couch in Indonesia.

© 2025 Erik Yuntantyo · turmerrific