Nuxt 3 #
Nuxt 3 adalah meta-framework Vue yang menghadirkan opini lengkap: file-based routing otomatis, server routes, auto-imports, hingga hybrid rendering (campuran SSR, SSG, ISR, dan CSR dalam satu project). Mesin render-nya, Nitro, menghasilkan output universal yang bisa jalan di Node, Bun, Workers, atau sebagai static site. Karena kompleksitas ini, Docker untuk local development harus memisahkan mode dev (Vite + Nitro dev) dari production (Nitro preset Node).
Artikel ini membahas setup Nuxt 3 dengan Docker Compose untuk local development, lengkap dengan Dockerfile multi-stage, integrasi Postgres/Redis, strategi Nitro preset, dan best practice untuk hybrid rendering.
Prasyarat #
Pastikan sudah terinstall:
- Docker dan Docker Compose versi terbaru
- Node.js 20+ (opsional, untuk tooling di host)
- pnpm atau npm (untuk
nuxi initjika scaffolding manual)
Project Nuxt 3 standar (bootstrap dengan npx nuxi@latest init):
my-nuxt-app/
├── app/
│ ├── app.vue
│ ├── pages/ # File-based routing
│ ├── components/ # Auto-imported
│ ├── composables/ # Auto-imported
│ ├── layouts/
│ ├── middleware/
│ └── assets/
├── server/
│ ├── api/ # Server routes (/api/...)
│ ├── routes/ # Server middleware
│ └── utils/
├── public/
├── nuxt.config.ts
├── package.json
├── Dockerfile
├── docker-compose.yml
└── .env
Berbeda dengan Nuxt 2, Nuxt 3 pakai folderapp/sebagai entry point (sejak Nuxt 4 stable). Jika project masih di Nuxt 3.x awal, struktur root mungkin pakaipages/,components/, dll langsung di root.
Arsitektur Nuxt 3 #
Sebelum menulis Dockerfile, pahami dulu arsitektur internal Nuxt 3:
flowchart TB
Browser[Browser]
NuxtServer[Nuxt Server / Nitro]
VueApp[Vue 3 App]
Vite[Vite Dev Server]
API[Server Routes /api]
Storage[Storage Layer]
DB[(Database)]
Cache[(Redis)]
Browser <-->|HTTP/WS| NuxtServer
NuxtServer --> VueApp
Vite -.->|HMR| Browser
NuxtServer --> API
API --> Storage
Storage --> DB
Storage --> Cache
- Nitro — server engine universal, menghasilkan output untuk banyak runtime
- Vite — bundler dan dev server dengan HMR
- Vue 3 — reactivity engine dengan Composition API
- Server Routes — endpoint API otomatis di folder
server/api/ - Auto-imports —
components/,composables/,utils/di-import otomatis
Dockerfile Multi-Stage #
# syntax=docker/dockerfile:1.6
# ---- Stage 1: Install dependencies ----
FROM node:20-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Pakai pnpm untuk instalasi cepat dan deterministic
COPY package.json pnpm-lock.yaml* ./
RUN corepack enable && pnpm install --frozen-lockfile
# ---- Stage 2: Build Nuxt ----
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NITRO_PRESET=node-server
ENV NODE_ENV=production
# Disable telemetry saat build
ENV NUXT_TELEMETRY_DISABLED=1
RUN corepack enable && pnpm build
# ---- Stage 3: Production runner ----
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NUXT_TELEMETRY_DISABLED=1
ENV HOST=0.0.0.0
ENV PORT=3000
# Buat user non-root
RUN addgroup --system --gid 1001 nuxt
RUN adduser --system --uid 1001 nuxt
# Copy output Nitro
COPY --from=builder --chown=nuxt:nuxt /app/.output ./.output
USER nuxt
EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]
Stage builder menghasilkan folder .output/ berisi server Node + client assets. Folder ini portable dan bisa dijalankan di mana saja dengan Node 20+.
Nitro preset default dinuxt buildadalahnode-server. Untuk deploy ke platform lain (Vercel, Cloudflare, Netlify), ubahNITRO_PRESETatau set dinuxt.config.tsdengannitro.preset. Untuk local development dengan Docker,node-serveradalah pilihan paling straightforward.
nuxt.config.ts untuk Production #
// nuxt.config.ts
export default defineNuxtConfig({
compatibilityDate: '2025-01-01',
devtools: { enabled: true },
// SSR enabled untuk hybrid rendering
ssr: true,
nitro: {
preset: 'node-server',
storage: {
// Redis storage adapter
cache: {
driver: 'redis',
url: process.env.REDIS_URL || 'redis://localhost:6379',
},
},
},
runtimeConfig: {
databaseUrl: process.env.DATABASE_URL,
redisUrl: process.env.REDIS_URL,
public: {
apiBase: process.env.NUXT_PUBLIC_API_BASE || '/api',
},
},
});
docker-compose.yml untuk Development #
# docker-compose.yml
services:
web:
build:
context: .
dockerfile: Dockerfile.dev
image: my-nuxt-app:dev
container_name: nuxt-dev
command: npm run dev -- --host 0.0.0.0
ports:
- "3000:3000"
volumes:
- ./:/app
- /app/node_modules
- /app/.nuxt
- /app/.output
environment:
- NODE_ENV=development
- NUXT_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
# Enable pnpm via corepack
RUN corepack enable
COPY package.json pnpm-lock.yaml* ./
RUN pnpm install
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]
Penjelasan Service #
web — service Nuxt dev server. Pakai Dockerfile.dev yang sederhana. Anonymous volume untuk node_modules, .nuxt (cache build), dan .output (output Nitro) mencegah konflik dengan host.
db dan cache — Postgres dan Redis sebagai dependency, dengan healthcheck.
Kenapa butuh anonymous volume untuk.nuxt? Folder.nuxtberisi TypeScript types yang di-generate oleh Nuxt. Jika di-mount dari host, type generation sering konflik dengan versi yang di-generate container. Dengan anonymous volume, container selalu generate.nuxtsendiri, konsisten dengan versi Node dan package yang dipakai.
File-Based Routing #
Nuxt 3 otomatis membuat route dari struktur folder di pages/:
pages/
├── index.vue # /
├── about.vue # /about
├── blog/
│ ├── index.vue # /blog
│ └── [slug].vue # /blog/:slug (dynamic)
├── products/
│ ├── [id].vue # /products/:id
│ └── [[category]].vue # /products/:category? (optional)
└── [...notFound].vue # Catch-all 404
Contoh page component:
<!-- pages/blog/[slug].vue -->
<script setup lang="ts">
const route = useRoute();
const { data: post } = await useFetch(`/api/posts/${route.params.slug}`);
</script>
<template>
<article v-if="post">
<h1>{{ post.title }}</h1>
<p>{{ post.content }}</p>
</article>
</template>
Dynamic route dengan validasi:
<!-- pages/products/[id].vue -->
<script setup lang="ts">
const route = useRoute();
const id = computed(() => Number(route.params.id));
// Validasi
if (isNaN(id.value)) {
throw createError({ statusCode: 400, statusMessage: 'Invalid ID' });
}
const { data: product } = await useFetch(`/api/products/${id.value}`);
</script>
<template>
<div v-if="product">
<h1>{{ product.name }}</h1>
<p>Rp {{ product.price.toLocaleString('id-ID') }}</p>
</div>
</template>
Server Routes #
Folder server/api/ otomatis menjadi endpoint API:
// server/api/posts/index.get.ts
import { db } from '~/server/utils/db';
export default defineEventHandler(async () => {
const posts = await db.post.findMany();
return posts;
});
// server/api/posts/[id].get.ts
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id');
const post = await db.post.findUnique({ where: { id: Number(id) } });
if (!post) {
throw createError({ statusCode: 404, statusMessage: 'Post not found' });
}
return post;
});
// server/api/posts/index.post.ts
export default defineEventHandler(async (event) => {
const body = await readBody(event);
const post = await db.post.create({ data: body });
setResponseStatus(event, 201);
return post;
});
Konvensi penamaan file: [name].[method].ts — posts/index.get.ts untuk GET /api/posts, posts/index.post.ts untuk POST /api/posts.
Composables dan Auto-Imports #
Salah satu kekuatan Nuxt 3 adalah auto-imports. Komponen, composables, dan utilitas tidak perlu di-import manual:
// composables/useAuth.ts — auto-imported sebagai useAuth()
export function useAuth() {
const user = useState<User | null>('user', () => null);
async function login(email: string, password: string) {
const result = await $fetch('/api/auth/login', {
method: 'POST',
body: { email, password },
});
user.value = result.user;
}
function logout() {
user.value = null;
}
return { user, login, logout };
}
Pakai di komponen tanpa import:
<script setup lang="ts">
const { user, login, logout } = useAuth();
</script>
<template>
<div v-if="user">
<p>Halo, {{ user.name }}!</p>
<button @click="logout">Logout</button>
</div>
<button v-else @click="login('[email protected]', 'pass')">Login</button>
</template>
State Management dengan useState dan Pinia
#
Nuxt 3 punya useState untuk shared state lintas komponen, atau pakai Pinia untuk state management yang lebih terstruktur.
useState untuk state sederhana:
// composables/useCounter.ts
export const useCounter = () => useState<number>('counter', () => 0);
Pinia untuk state kompleks:
npm install @pinia/nuxt pinia
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@pinia/nuxt'],
});
// stores/cart.ts
import { defineStore } from 'pinia';
export const useCartStore = defineStore('cart', {
state: () => ({
items: [] as Array<{ id: number; name: string; price: number; qty: number }>,
}),
getters: {
total: (state) => state.items.reduce((sum, i) => sum + i.price * i.qty, 0),
itemCount: (state) => state.items.reduce((sum, i) => sum + i.qty, 0),
},
actions: {
add(item: { id: number; name: string; price: number }) {
const existing = this.items.find(i => i.id === item.id);
if (existing) {
existing.qty++;
} else {
this.items.push({ ...item, qty: 1 });
}
},
remove(id: number) {
this.items = this.items.filter(i => i.id !== id);
},
clear() {
this.items = [];
},
},
});
Pakai di komponen:
<script setup lang="ts">
const cart = useCartStore();
</script>
<template>
<div>
<p>Items: {{ cart.itemCount }} — Total: Rp {{ cart.total.toLocaleString('id-ID') }}</p>
<button @click="cart.clear">Kosongkan</button>
</div>
</template>
Hybrid Rendering #
Salah satu fitur paling powerful Nuxt 3: kamu bisa campur SSR, SSG, ISR, dan SPA dalam satu project, cukup dengan routeRules:
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
// Homepage: ISR, revalidate tiap 60 detik
'/': { isr: 60 },
// Halaman statis: SSG
'/about': { prerender: true },
'/blog/**': { prerender: true },
// API: SSR (default)
'/api/**': { ssr: true },
// Dashboard: client-side only
'/dashboard/**': { ssr: false },
// Product detail: SSR dengan cache
'/products/**': { ssr: true, headers: { 'cache-control': 's-maxage=60' } },
},
});
| Rule | Mode | Cocok untuk |
|---|---|---|
prerender: true |
SSG | Halaman yang jarang berubah |
isr: 60 |
ISR | Halaman yang di-cache 60 detik |
ssr: true |
SSR | Konten dinamis, user-specific |
ssr: false |
SPA | Dashboard, halaman yang butuh auth |
Untuk project SSG murni (semua halaman static), tambahkannitro: { preset: 'static' }dinuxt.config.ts. Build menghasilkan folder.output/public/yang bisa di-serve dengan nginx.
Static Export dengan Nginx #
# syntax=docker/dockerfile:1.6
FROM node:20-alpine AS builder
WORKDIR /app
RUN corepack enable
COPY package.json pnpm-lock.yaml* ./
RUN pnpm install --frozen-lockfile
COPY . .
ENV NUXT_TELEMETRY_DISABLED=1
RUN pnpm generate # menghasilkan .output/public/
FROM nginx:1.27-alpine
COPY --from=builder /app/.output/public /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
nuxt generate (atau nuxi generate) melakukan prerender untuk semua halaman yang ditandai SSG/ISR. Outputnya HTML statis + assets.
Build dan Run #
# Development
docker compose up --build
# Akses: http://localhost:3000
# Production build
docker build -t my-nuxt-app:prod -f Dockerfile .
docker run -p 3000:3000 my-nuxt-app:prod
# Lihat log
docker compose logs -f web
# Stop
docker compose down
Best Practice #
1. Gunakan pnpm untuk Install Cepat
#
Nuxt 3 sendiri merekomendasikan pnpm karena content-addressable storage yang mempercepat install. Aktifkan via corepack enable.
2. Pakai NITRO_PRESET=node-server di Docker
#
Preset ini menghasilkan output yang bisa dijalankan di container Linux mana saja dengan Node 20+. Hindari preset platform-specific (Vercel, Netlify) di local development.
3. Matikan Telemetry #
NUXT_TELEMETRY_DISABLED=1 di Dockerfile dan .env. Build jadi lebih cepat dan tidak ada panggilan keluar.
4. Auto-Import Hanya untuk yang Universal #
Jangan letakkan logic spesifik di composables/ jika hanya dipakai di satu halaman. Simpan di komponen atau utils/.
5. Healthcheck untuk Service Dependent #
Pakai healthcheck agar web start setelah db dan cache siap:
depends_on:
db:
condition: service_healthy
cache:
condition: service_healthy
6. Volume untuk .nuxt dan .output
#
volumes:
- /app/.nuxt
- /app/.output
Tanpa volume, type generation dan Nitro output konflik dengan host.
7. Mode Dev Harus Punya --host 0.0.0.0
#
Tanpa flag ini, dev server hanya bind ke localhost di dalam container, dan tidak bisa diakses dari host.
Troubleshooting #
Port 3000 Sudah Dipakai #
Ubah port mapping di Compose atau stop proses yang menggunakan port:
lsof -i :3000
HMR Tidak Bekerja #
Pastikan volume bind-mount aktif dan folder .nuxt di-isolasi. Cek dengan docker compose exec web ls -la .nuxt.
“Cannot find module” Setelah Tambah Package #
Restart container dengan rebuild:
docker compose up --build
Atau restart saja:
docker compose restart web
Server Route Return 404 #
Cek konvensi penamaan file: server/api/[name].[method].ts. File tanpa suffix method dianggap generic handler.
Ringkasan #
- Nuxt 3 ideal untuk hybrid rendering — SSR/SSG/ISR/SPA dalam satu project dengan file-based routing.
- Dockerfile multi-stage dengan
pnpmmempercepat install. Tahapdepsinstall dependencies,builderbuild dengannuxt build,runnerserve.output/server/index.mjs.- Nitro preset
node-servermenghasilkan output portable. Untuk static-only, presetstaticmenghasilkan folder.output/public/yang dilayani nginx.- File-based routing di
pages/, server routes diserver/api/, auto-imports untuk components/composables/utils.- Hybrid rendering dengan
routeRulesmemungkinkan halaman berbeda pakai strategi berbeda (SSG untuk about, ISR untuk home, SPA untuk dashboard).- Anonymous volume untuk
node_modules,.nuxt,.outputmencegah konflik dengan host.- State management dengan
useStateuntuk sederhana, Pinia untuk kompleks. Modul@pinia/nuxtintegrasi otomatis.- Healthcheck wajib untuk
dbdancacheagarwebstart setelah dependency siap.- Mode dev pakai
Dockerfile.devsederhana, mode prod pakai multi-stage. Jangan campur.- Best practice: pnpm, NUXT_TELEMETRY_DISABLED, –host 0.0.0.0, anonymous volume cache, healthcheck.
- Troubleshooting: port conflict (ubah mapping), HMR mati (cek volume), server route 404 (cek naming convention).
- Static export untuk blog/landing page yang fully static. Build dengan
nuxt generate, serve dengan nginx.