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 init jika 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 folder app/ sebagai entry point (sejak Nuxt 4 stable). Jika project masih di Nuxt 3.x awal, struktur root mungkin pakai pages/, 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-importscomponents/, 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 di nuxt build adalah node-server. Untuk deploy ke platform lain (Vercel, Cloudflare, Netlify), ubah NITRO_PRESET atau set di nuxt.config.ts dengan nitro.preset. Untuk local development dengan Docker, node-server adalah 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 .nuxt berisi 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 .nuxt sendiri, 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].tsposts/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), tambahkan nitro: { preset: 'static' } di nuxt.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 pnpm mempercepat install. Tahap deps install dependencies, builder build dengan nuxt build, runner serve .output/server/index.mjs.
  • Nitro preset node-server menghasilkan output portable. Untuk static-only, preset static menghasilkan folder .output/public/ yang dilayani nginx.
  • File-based routing di pages/, server routes di server/api/, auto-imports untuk components/composables/utils.
  • Hybrid rendering dengan routeRules memungkinkan halaman berbeda pakai strategi berbeda (SSG untuk about, ISR untuk home, SPA untuk dashboard).
  • Anonymous volume untuk node_modules, .nuxt, .output mencegah konflik dengan host.
  • State management dengan useState untuk sederhana, Pinia untuk kompleks. Modul @pinia/nuxt integrasi otomatis.
  • Healthcheck wajib untuk db dan cache agar web start setelah dependency siap.
  • Mode dev pakai Dockerfile.dev sederhana, 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.

← Sebelumnya: Next.js   Berikutnya: React →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact