Menjalankan aplikasi Nuxt di lingkungan produksi tidak cukup hanya dengan build lalu menjalankan server. Untuk kebutuhan deploy Nuxt Docker yang stabil dan efisien, kita perlu memikirkan ukuran image, cara aplikasi dijalankan, konfigurasi environment variable, reverse proxy, logging, sampai proses validasi setelah rilis. Artikel ini membahas pola deploy yang umum dipakai untuk hosting Nuxt VPS dengan fokus pada setup yang ringan namun tetap layak produksi.

Pendekatan yang dibahas di sini menggunakan output Nitro production, karena Nuxt modern membangun server production melalui Nitro. Hasil akhirnya biasanya berupa artefak di folder .output yang berisi server runtime dan aset yang siap dijalankan. Ini penting karena strategi Docker yang baik seharusnya hanya membawa artefak runtime yang benar-benar dibutuhkan, bukan seluruh source code development.

Kenapa Nuxt Modern Cocok Dideploy via Docker

Docker memberi beberapa keuntungan praktis saat menjalankan Nuxt di VPS:

  • Lingkungan konsisten: versi Node, dependency, dan command runtime tidak berubah antar server.
  • Proses rilis lebih sederhana: Anda membangun image sekali, lalu menjalankannya di server mana pun.
  • Rollback lebih mudah: cukup jalankan image versi sebelumnya.
  • Isolasi aplikasi: aplikasi tidak mengotori sistem host dengan banyak dependency runtime.

Untuk Nuxt, ini sangat relevan karena runtime produksinya sebenarnya cukup kecil jika dibandingkan dengan seluruh dependency saat development. Dengan memanfaatkan multi-stage build, kita bisa memisahkan tahap instalasi, build, dan runtime sehingga image akhir lebih ramping.

Memahami Output Nitro untuk Produksi

Pada Nuxt modern, hasil build server biasanya berada di .output. Folder ini umumnya berisi:

  • .output/server untuk kode server runtime.
  • .output/public untuk aset statis hasil build.
  • File entry seperti .output/server/index.mjs untuk menjalankan aplikasi.

Alasan pendekatan ini efisien adalah karena container runtime tidak perlu membawa seluruh source project, file konfigurasi development, atau tool build. Cukup salin artefak Nitro yang sudah jadi.

Kesalahan umum adalah menyalin seluruh project ke image final lalu menjalankan npm run start. Ini sering membuat image lebih besar, memperpanjang waktu build, dan menambah permukaan masalah keamanan.

Struktur Dockerfile dengan Multi-Stage Build

Berikut contoh Dockerfile yang praktis untuk produksi. Contoh ini memakai pendekatan tiga tahap: dependency, build, dan runtime.

FROM node:20-alpine AS base
WORKDIR /app
RUN apk add --no-cache libc6-compat

FROM base AS deps
COPY package.json package-lock.json* pnpm-lock.yaml* yarn.lock* ./
RUN if [ -f package-lock.json ]; then npm ci; \
  elif [ -f pnpm-lock.yaml ]; then corepack enable && pnpm install --frozen-lockfile; \
  elif [ -f yarn.lock ]; then yarn install --frozen-lockfile; \
  else npm install; fi

FROM base AS build
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NITRO_PORT=3000
ENV NITRO_HOST=0.0.0.0
COPY --from=build /app/.output ./.output
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
  CMD wget -qO- http://127.0.0.1:3000/ || exit 1
CMD ["node", ".output/server/index.mjs"]

Beberapa poin penting dari Dockerfile di atas:

  • Multi-stage build memisahkan dependency dan runtime agar image akhir lebih kecil.
  • Image Alpine membantu menekan ukuran image, walau pada beberapa dependency native bisa butuh perhatian ekstra.
  • Hanya folder .output yang disalin ke image final. Ini inti optimasi ukuran image untuk Nuxt.
  • HEALTHCHECK memberi sinyal ke Docker apakah proses masih sehat.

Jika Anda menggunakan package manager selain npm, sesuaikan command install dan build. Jika proyek memakai modul native tertentu, terkadang image berbasis Debian lebih mudah daripada Alpine karena kompatibilitas library lebih luas. Trade-off-nya adalah ukuran image sedikit lebih besar.

Tambahkan .dockerignore

Agar konteks build tetap kecil dan cepat, gunakan .dockerignore:

node_modules
.nuxt
.output
.git
.gitignore
Dockerfile
npm-debug.log
.env
.env.*
README.md

Ini mencegah file yang tidak perlu ikut terkirim ke Docker daemon saat build.

Build dan Menjalankan Container di VPS

Setelah Dockerfile siap, build image secara lokal atau langsung di server:

docker build -t nuxt-app:latest .

Jalankan container dengan restart policy agar otomatis hidup kembali setelah reboot atau crash:

docker run -d \
  --name nuxt-app \
  --restart unless-stopped \
  -p 3000:3000 \
  -e NODE_ENV=production \
  -e NUXT_PUBLIC_API_BASE=https://api.contoh.com \
  nuxt-app:latest

Untuk skenario hosting Nuxt VPS, praktik umum adalah tidak langsung mengekspos aplikasi ke internet pada port acak, melainkan meletakkannya di belakang Nginx sebagai reverse proxy. Dengan begitu, TLS, cache aset, header forwarding, dan pembatasan akses lebih mudah dikelola.

Environment Variable: Mana yang Aman Dipublikasikan

Di Nuxt, Anda perlu membedakan variable yang hanya digunakan di server dan variable yang boleh tampil di sisi klien. Secara umum:

  • Private: token internal, kredensial database, secret key. Jangan expose ke browser.
  • Public: base URL API publik, flag fitur klien, URL CDN.

Di level aplikasi, pengaturan biasanya diletakkan di runtimeConfig. Contoh sederhana:

export default defineNuxtConfig({
  runtimeConfig: {
    apiSecret: process.env.API_SECRET,
    public: {
      apiBase: process.env.NUXT_PUBLIC_API_BASE || 'http://localhost:3000'
    }
  }
})

Mengapa ini penting? Karena variabel pada bagian public dapat dikirim ke browser. Jika Anda salah menaruh secret di sana, data sensitif bisa bocor ke klien.

Untuk produksi, hindari menyalin file .env ke image jika berisi kredensial. Lebih aman mengirimkannya saat runtime melalui:

  • flag -e pada docker run,
  • file --env-file,
  • atau secret manager jika infrastruktur Anda mendukung.

Nginx sebagai Reverse Proxy untuk Nuxt

Menaruh Nginx di depan aplikasi Nuxt memberi beberapa manfaat:

  • terminasi HTTPS/TLS,
  • forward header yang benar,
  • cache aset statis,
  • kompresi respons,
  • isolasi trafik dari process Node.

Contoh konfigurasi server block Nginx:

server {
    listen 80;
    server_name contoh.com www.contoh.com;

    location /_nuxt/ {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        expires 30d;
        add_header Cache-Control "public, max-age=2592000, immutable";
    }

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 60s;
    }
}

Direktori /_nuxt/ biasanya berisi aset build yang dapat diberi cache lebih agresif. Ini membantu performa karena browser dan proxy tidak perlu terus meminta ulang file JavaScript/CSS yang sudah dipakai. Namun, jangan asal memberi cache panjang pada halaman HTML dinamis jika kontennya sering berubah.

Kapan Perlu HTTPS

Untuk produksi, gunakan HTTPS, misalnya dengan Let's Encrypt. Nginx menangani sertifikat dan meneruskan request ke container Nuxt di port internal. Ini adalah pola yang lebih aman dan lebih mudah dipelihara dibanding membiarkan aplikasi Node menangani sertifikat secara langsung.

Health Check, Logging, dan Restart Policy

Setup produksi yang baik tidak hanya soal aplikasi bisa jalan, tetapi juga bisa dipantau dan dipulihkan otomatis.

Health Check

Contoh Dockerfile tadi sudah memakai HEALTHCHECK. Jika aplikasi Anda memiliki endpoint khusus seperti /health, lebih baik gunakan itu karena hasilnya lebih eksplisit:

HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
  CMD wget -qO- http://127.0.0.1:3000/health || exit 1

Endpoint health idealnya memeriksa bahwa proses server benar-benar siap menerima request, bukan hanya port terbuka.

Restart Policy

Gunakan --restart unless-stopped atau always agar service kembali hidup setelah reboot server atau crash. Ini penting pada VPS yang tidak memiliki orkestrator seperti Kubernetes.

Logging Dasar

Untuk deployment sederhana, log ke stdout/stderr sudah cukup karena Docker dapat menangkapnya. Beberapa perintah yang berguna:

docker logs -f nuxt-app
docker inspect nuxt-app
docker ps

Jika log tumbuh terlalu besar, atur driver logging atau rotasi log di Docker daemon. Kesalahan umum adalah membiarkan log menumpuk hingga memenuhi disk VPS.

Optimasi Ukuran Image dan Performa Runtime

Beberapa langkah praktis untuk menjaga image tetap ringan:

  • Pakai multi-stage build agar dependency build tidak ikut ke image final.
  • Salin hanya .output ke runtime image.
  • Gunakan .dockerignore untuk mengecilkan konteks build.
  • Jangan masukkan file rahasia atau cache lokal ke image.
  • Pertimbangkan base image: Alpine lebih kecil, Debian sering lebih kompatibel.

Dari sisi runtime, Nuxt berbasis Nitro cukup efisien untuk SSR maupun hybrid rendering, tetapi performa tetap dipengaruhi oleh hal lain seperti ukuran bundle, jumlah request ke API eksternal, dan strategi cache. Jika aplikasi banyak mengambil data dari backend lain, bottleneck sering justru ada pada latensi API, bukan pada server Nuxt-nya.

Langkah Validasi Pasca-Deploy

Setelah container berjalan dan Nginx dikonfigurasi, lakukan validasi berikut:

  1. Periksa status container
    docker ps dan pastikan container berstatus healthy jika health check aktif.
  2. Cek log startup
    docker logs --tail 100 nuxt-app. Pastikan tidak ada error runtimeConfig, missing module, atau port binding.
  3. Uji endpoint lokal
    curl -I http://127.0.0.1:3000 dari VPS untuk memastikan app merespons.
  4. Uji melalui Nginx
    curl -I http://contoh.com dan jika HTTPS aktif, pastikan redirect dan sertifikat benar.
  5. Periksa asset loading
    Buka browser devtools dan pastikan file di /_nuxt/ termuat tanpa 404.
  6. Verifikasi environment variable
    Pastikan URL API yang dipakai sesuai environment production, bukan localhost development.
  7. Uji halaman penting
    Homepage, halaman dinamis, login, dan halaman yang bergantung pada SSR atau API.

Jika ada masalah, urutkan debugging dari lapisan paling bawah:

  • apakah process Node berjalan,
  • apakah port 3000 merespons,
  • apakah Nginx meneruskan request dengan benar,
  • apakah DNS dan firewall VPS sudah benar.

Kesalahan Umum Saat Deploy Nuxt ke Docker dan VPS

  • Binding hanya ke localhost: jika server hanya listen ke 127.0.0.1 di dalam container, request dari luar tidak akan masuk. Gunakan host 0.0.0.0.
  • Secret bocor ke public runtime config: pastikan hanya data aman yang ada di bagian public.
  • Image terlalu besar: biasanya karena seluruh source code dan node_modules development ikut ke image final.
  • Nginx tidak mengirim header forwarding: ini bisa memengaruhi redirect, URL absolut, atau deteksi protokol.
  • Cache terlalu agresif untuk HTML: aset statis boleh lama, konten dinamis perlu hati-hati.

Penutup

Untuk deploy Nuxt Docker yang layak produksi, fokus utamanya adalah memanfaatkan output Nitro production, membangun image dengan multi-stage build, dan menjalankan aplikasi di balik Nginx pada VPS. Pendekatan ini membuat proses hosting Nuxt VPS lebih rapi, image lebih kecil, dan operasional lebih mudah dipelihara.

Jika kebutuhan Anda masih sederhana, satu VPS dengan Docker dan Nginx sudah cukup untuk banyak aplikasi. Ketika trafik meningkat, fondasi ini tetap relevan karena mudah dikembangkan ke pipeline CI/CD, registry image, atau orkestrasi yang lebih kompleks. Yang terpenting, bangun deployment yang mudah dipahami, mudah di-debug, dan tidak membawa komponen yang tidak benar-benar diperlukan.