Next.js #
Next.js adalah framework React yang membawa opini tentang rendering — Server Components, Server Actions, file-based routing, hingga ISR (Incremental Static Regeneration) menjadi bagian dari default project. Justru karena opini sekuat ini, setup local development harus mengikuti kontrak yang sama: server Node.js untuk runtime SSR, atau static export + nginx untuk SSG. Mengabaikan salah satu sisi membuat Docker container cepat rusak atau hot reload tidak jalan.
Artikel ini membahas setup lengkap Next.js dengan Docker Compose untuk local development, termasuk Dockerfile multi-stage yang memisahkan build dan runtime, integrasi dengan Postgres/Redis, strategi dev vs production image, dan best practice yang sering diabaikan di tutorial standar.
Prasyarat #
Pastikan sudah terinstall di host:
- Docker dan Docker Compose versi terbaru
- Node.js 20+ (opsional, hanya untuk menjalankan tooling di host seperti
create-next-app) - Git untuk version control
Project Next.js standar (bootstrap dengan create-next-app):
my-next-app/
├── app/ # App Router (Next.js 13+)
│ ├── layout.tsx
│ ├── page.tsx
│ └── api/
├── public/
├── components/
├── package.json
├── package-lock.json
├── next.config.js
├── Dockerfile
├── docker-compose.yml
└── .env.local
Artikel ini mengasumsikan App Router (Next.js 13+). Jika project masih pakai Pages Router, strukturpages/menggantikanapp/, dan beberapa konvensi seperti Server Components tidak berlaku.
Mengapa Butuh Dockerfile Multi-Stage #
Next.js menghasilkan dua jenis output:
- Standalone Node server (
output: 'standalone') — untuk SSR/ISR. Butuh runtime Node. - Static export (
output: 'export') — untuk SSG murni. Bisa di-serve dengan nginx.
Dockerfile multi-stage membuat image akhir hanya berisi artefak yang relevan, tanpa node_modules development atau source code asli. Ini juga yang membedakan image dev (dengan Fast Refresh) dari image production (hanya artefak build).
Dockerfile Multi-Stage #
Buat dua stage: deps untuk install dependencies, builder untuk build Next.js, dan runner untuk image production yang ramping.
# syntax=docker/dockerfile:1.6
# ---- Stage 1: Install dependencies ----
FROM node:20-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Copy lock file untuk reproducible install
COPY package.json package-lock.json* ./
RUN npm ci
# ---- Stage 2: Build Next.js ----
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Disable telemetry saat build
ENV NEXT_TELEMETRY_DISABLED=1
# Build Next.js dengan output standalone
RUN npm run build
# ---- Stage 3: Production runner ----
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
# Buat user non-root
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Copy artefak standalone (server.js + minimal node_modules)
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME=0.0.0.0
CMD ["node", "server.js"]
Agar output: 'standalone' aktif, tambahkan di next.config.js:
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
reactStrictMode: true,
};
module.exports = nextConfig;
Mode standalone menghasilkan server.js yang sudah termasuk routing Next.js lengkap, dengan subset node_modules yang dibutuhkan. Image akhir hanya ~150MB (Alpine) dibanding ~500MB+ untuk image non-standalone.
docker-compose.yml untuk Development #
Untuk dev, image berbeda dari production. Kamu tidak butuh builder stage; cukup node:20-alpine dengan npm run dev + bind mount.
# docker-compose.yml
services:
web:
build:
context: .
dockerfile: Dockerfile.dev
image: my-next-app:dev
container_name: nextjs-dev
command: npm run dev
ports:
- "3000:3000"
volumes:
- ./:/app
- /app/node_modules
- /app/.next
environment:
- NODE_ENV=development
- NEXT_TELEMETRY_DISABLED=1
- DATABASE_URL=postgresql://app:dev@db:5432/myapp
- REDIS_URL=redis://cache:6379
depends_on:
db:
condition: service_healthy
cache:
condition: service_healthy
db:
image: postgres:16-alpine
environment:
- POSTGRES_USER=app
- POSTGRES_PASSWORD=dev
- POSTGRES_DB=myapp
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d myapp"]
interval: 10s
timeout: 5s
retries: 5
ports:
- "5432:5432"
cache:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
ports:
- "6379:6379"
volumes:
db-data:
Dockerfile.dev:
FROM node:20-alpine
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]
Penjelasan Service #
web — service Next.js dev server. Pakai Dockerfile.dev yang sederhana — semua dependensi dev (TypeScript, ESLint, Prettier) ikut ter-install. Volume .:/app untuk sinkronisasi source code, anonymous volume /app/node_modules untuk mengisolasi node_modules dari host, dan /app/.next untuk cache build Next.js.
db — PostgreSQL untuk database. Healthcheck pg_isready memastikan web start setelah Postgres siap menerima koneksi.
cache — Redis untuk caching, session, atau rate limiting. Healthcheck redis-cli ping.
Jangan gunakan image production (multi-stage standalone) untuk development. Image tersebut tidak punyanode_moduleslengkap, source code, ataunext devbinary. Hot reload akan mati total dan error cryptic akan muncul.
Hot Reload dengan Fast Refresh #
Next.js punya Fast Refresh bawaan yang bekerja melalui file watcher. Untuk memastikannya jalan di Docker:
// next.config.js
module.exports = {
// Izinkan Fast Refresh untuk HMR
reactStrictMode: true,
// Watch folder ekstra jika monorepo
outputFileTracingRoot: process.cwd(),
};
Dockerfile Compose yang kita tulis di atas sudah bind-mount source code, jadi setiap save di IDE langsung trigger Fast Refresh. Cache .next di-isolasi di anonymous volume agar tidak konflik dengan host.
# Workflow harian
docker compose up -d
# Edit file di app/ atau components/
# Browser auto-update via Fast Refresh
docker compose logs -f web # lihat log request
App Router dan Server Components #
App Router (default sejak Next.js 13) membawa konsep baru yang penting untuk Docker:
flowchart LR
A[Browser] --> B[Next.js Server]
B --> C[Server Component]
B --> D[Client Component]
C --> E[(Database)]
C --> F[(Cache)]
D --> G[Browser Hydration]
B --> H[Server Action]
H --> E
Server Component (default, tanpa 'use client'):
// app/page.tsx — Server Component
import { db } from '@/lib/db';
export default async function HomePage() {
// Bisa langsung query database, tidak perlu API route
const posts = await db.post.findMany();
return (
<main>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</main>
);
}
Client Component (opt-in dengan 'use client'):
// components/Counter.tsx
'use client';
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
);
}
Server Action (mutasi langsung dari form):
// app/posts/new/page.tsx
import { revalidatePath } from 'next/cache';
import { db } from '@/lib/db';
export default function NewPostPage() {
async function createPost(formData: FormData) {
'use server';
const title = formData.get('title') as string;
await db.post.create({ data: { title } });
revalidatePath('/posts');
}
return (
<form action={createPost}>
<input name="title" required />
<button type="submit">Buat Post</button>
</form>
);
}
Di local development, Server Actions jalan melalui endpoint internal Next.js. Di production dengan standalone output, endpoint ini otomatis ter-bundle di server.js. Tidak perlu konfigurasi tambahan di Docker.
Routing dengan App Router #
App Router pakai file-based routing berbasis folder:
app/
├── layout.tsx # Root layout (wajib)
├── page.tsx # / (root)
├── about/
│ └── page.tsx # /about
├── posts/
│ ├── page.tsx # /posts
│ └── [id]/
│ └── page.tsx # /posts/:id
├── dashboard/
│ ├── layout.tsx # Nested layout
│ └── settings/
│ └── page.tsx # /dashboard/settings
└── api/
└── users/
└── route.ts # /api/users endpoint
Dynamic route dengan params:
// app/posts/[id]/page.tsx
export default async function PostPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const post = await db.post.findUnique({ where: { id } });
if (!post) notFound();
return <article>{post.content}</article>;
}
API Route dengan handler:
// app/api/users/route.ts
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';
export async function GET() {
const users = await db.user.findMany();
return NextResponse.json(users);
}
export async function POST(request: Request) {
const body = await request.json();
const user = await db.user.create({ data: body });
return NextResponse.json(user, { status: 201 });
}
Rendering Strategy: SSR vs SSG vs ISR #
Next.js mendukung empat mode rendering — pilihan memengaruhi Dockerfile dan strategi serve:
| Mode | Trigger | Output | Runtime |
|---|---|---|---|
| SSR | Default (no export const dynamic) |
Render per request | Node server |
| SSG | export const dynamic = 'force-static' |
HTML saat build | Static hosting (nginx) |
| ISR | export const revalidate = 60 |
HTML + revalidate | Node server |
| CSR | 'use client' + useEffect |
HTML kosong | Static hosting |
Contoh masing-masing:
// app/page.tsx — SSR (default)
export const dynamic = 'force-dynamic';
export default async function Page() {
const data = await fetch('https://api.example.com', { cache: 'no-store' });
return <div>{(await data.json()).title}</div>;
}
// app/blog/page.tsx — SSG
export const dynamic = 'force-static';
export default async function BlogPage() {
const posts = await db.post.findMany();
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}
// app/products/[id]/page.tsx — ISR
export const revalidate = 3600; // Regenerate setiap 1 jam
export default async function ProductPage({ params }) {
const { id } = await params;
const product = await db.product.findUnique({ where: { id } });
return <h1>{product.name}</h1>;
}
Untuk project yang hanya SSG (misalnya blog atau landing page), tambahkanoutput: 'export'dinext.config.js. Image hasil build bisa di-serve dengan nginx tanpa Node runtime. Ini sangat berbeda dari mode default (SSR/ISR) yang butuh Node.
Static Export dengan Nginx #
Jika project full SSG, build menghasilkan folder out/ berisi HTML statis. Tidak butuh Node di production:
# syntax=docker/dockerfile:1.6
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build
# Serve static export dengan nginx
FROM nginx:1.27-alpine
COPY --from=builder /app/out /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
nginx.conf:
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Cache static assets agresif
location /_next/static/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# SPA fallback untuk client-side routing
location / {
try_files $uri $uri/ /index.html;
}
}
Mode output: 'export' punya keterbatasan: tidak ada ISR, tidak ada Server Actions, tidak ada API Routes yang jalan di server. Pilih mode ini hanya jika project memang fully static.
Build dan Run #
# Development
docker compose up --build
# Akses: http://localhost:3000
# Lihat log Next.js
docker compose logs -f web
# Production build
docker build -t my-next-app:prod -f Dockerfile .
docker run -p 3000:3000 my-next-app:prod
# Stop semua
docker compose down
Untuk reset total termasuk database:
docker compose down -v # Hapus volume (termasuk db-data)
docker compose up -d
Best Practice #
1. Pisahkan Dockerfile Dev dan Prod #
Gunakan Dockerfile.dev untuk development (image besar, full source, dev server) dan Dockerfile (multi-stage) untuk production. Hindari satu Dockerfile yang menangani keduanya — konfliknya banyak (cache layer, CMD, user permission).
2. Gunakan output: 'standalone'
#
Mode standalone menghasilkan image production yang ramping. Tanpa ini, image butuh node_modules lengkap yang ukurannya ratusan MB.
3. Jangan Build .next di Container Dev
#
Di dev, biarkan next dev yang generate cache .next di anonymous volume. Jangan jalankan next build di container dev — itu tugas image production.
4. Matikan Telemetry #
Set NEXT_TELEMETRY_DISABLED=1 di Dockerfile dan .env.local. Next.js mengirim data anonymous ke Vercel saat build, dan ini memperlambat build di network terbatas.
5. Healthcheck dan Resource Limits #
Tambahkan healthcheck dan memory limit:
services:
web:
deploy:
resources:
limits:
memory: 512M
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000"]
interval: 30s
timeout: 10s
retries: 3
6. Environment Variable dengan .env.local
#
Next.js otomatis baca .env.local (yang harus di-.gitignore). Di Compose, mount file ini:
volumes:
- ./.env.local:/app/.env.local:ro
7. Cache .next di Volume
#
Anonymous volume untuk .next mempercepat restart container:
volumes:
- /app/.next
Tanpa volume, setiap docker compose restart akan re-build cache .next, memperlambat startup.
Troubleshooting #
Fast Refresh Tidak Jalan #
Edit file → browser tidak auto-update
Pastikan volume bind mount aktif dan cache .next tidak konflik. Cek dengan docker compose exec web ls -la .next — folder harus berisi cache.
Build Gagal: “Cannot find module” #
Biasanya karena node_modules di host ikut ter-copy. Pastikan anonymous volume /app/node_modules di-set, atau tambahkan node_modules ke .dockerignore.
Port 3000 Sudah Dipakai #
lsof -i :3000 # macOS/Linux
Ubah port mapping di Compose: "3001:3000". Akses via http://localhost:3001.
Server Action Tidak Eksekusi #
Pastikan output: 'standalone' di next.config.js. Server Actions butuh runtime Node, bukan static export.
Ringkasan #
- Next.js ideal untuk SSR/ISR development — Postgres, Redis, dan service lain berjalan sebagai container terpisah.
- Dockerfile multi-stage dengan
output: 'standalone'menghasilkan image production ramping. Tahapdepsinstall dependencies,builderbuild Next.js,runnerserve dengannode server.js.- Pisahkan Dockerfile dev (sederhana,
npm run dev) dari production (multi-stage). Hot reload Fast Refresh hanya jalan di image dev.- App Router dengan Server Components + Server Actions memungkinkan query database langsung dari komponen, tanpa API Route tradisional.
- Mode rendering (SSR/SSG/ISR/CSR) menentukan strategi serve: SSG pakai nginx statis, SSR/ISR butuh Node server.
- Anonymous volume untuk
node_modulesdan.nextmencegah konflik dengan host dan mempercepat restart container.- Healthcheck dan resource limits wajib untuk service
db,cache, danweb. Set memory limit yang realistis (512MB cukup untuk dev Next.js).- Environment variable lewat
.env.localdanenvironmentCompose. MatikanNEXT_TELEMETRY_DISABLEDuntuk build yang lebih cepat.- Troubleshooting umum: Fast Refresh mati (cek volume), port conflict (ubah mapping), Server Action gagal (cek
outputconfig).- Best practice untuk dev: bind mount source, anonymous volume cache, healthcheck,
.env.local, non-root user di production.