A

Blog Post

Deploy Next.js ke Server Ubuntu dengan Docker dan Nginx

Panduan praktis untuk menjalankan aplikasi Next.js di Ubuntu memakai Docker dan Nginx dengan perhatian pada build reproducibility, cache, proxy header, dan rollback yang cepat.

6 min read
Deploy Next.js ke Server Ubuntu dengan Docker dan Nginx

TL;DR

  • Deploy Next.js yang sehat memisahkan build, runtime, dan edge proxy dengan boundary yang jelas.
  • Docker membantu reproducibility; Nginx membantu kestabilan HTTP, TLS, dan cache policy.
  • Rollback cepat lebih penting daripada deployment yang terlihat pintar tetapi sulit dibalik.

Pendahuluan

Deploy Next.js ke Ubuntu sering terlihat mudah karena secara teknis kita hanya perlu image, container, dan reverse proxy. Masalahnya, aplikasi Next.js modern membawa detail yang sering diabaikan: SSR, asset caching, environment variable build-time versus runtime, dan kebutuhan restart yang tidak merusak sesi pengguna. Jika deployment dilakukan tanpa disiplin, hasilnya memang hidup, tetapi rapuh setiap kali ada release.

Pendekatan yang lebih sehat adalah memperlakukan deploy sebagai jalur perubahan yang bisa diulang. Docker memberi reproducibility, sedangkan Nginx memberi boundary HTTP yang stabil. Namun dua komponen ini hanya membantu jika konfigurasi caching, health check, dan image build dibuat secara sadar.

Prerequisites

  • Ubuntu 22.04 LTS atau setara.
  • Akses sudo ke host.
  • Docker Engine dan Docker Compose plugin sudah terpasang.
  • Domain sudah mengarah ke IP server.
  • Port 80 dan 443 terbuka.
  • Repo Next.js sudah memiliki package-lock.json atau lockfile lain yang konsisten.
  • Anda memahami perbedaan environment variable build-time dan runtime pada Next.js.

Problem di Production

Kegagalan paling umum di produksi bukan container tidak mau start, tetapi asset cache yang salah, environment variable yang tertanam saat build, atau memory usage SSR yang tidak dipahami. Saat release baru bermasalah, tim sering sadar bahwa server mereka sebenarnya menjalankan proses manual yang sulit direkonstruksi.

Mental Model

Gunakan mental model artifact promotion. Server seharusnya menerima image yang sudah jadi, bukan membangun ulang aplikasi dari source setiap kali deploy. Dengan cara ini, perbedaan antara staging dan production jauh lebih kecil dan rollback menjadi tindakan mengganti artifact, bukan debugging di host.

Konsep Utama

Build image harus deterministik

Masalah klasik pada Next.js di Docker adalah image yang berubah perilaku karena dependency tidak terkunci, base image berubah diam-diam, atau proses build menarik env yang salah. Sistem yang sehat memakai lockfile, multi-stage build, dan artifact yang benar-benar sama antara staging dan production.

Reverse proxy bukan sekadar port forwarding

Nginx menentukan bagaimana TLS, header asli klien, compression, cache policy, dan timeout diperlakukan. Untuk aplikasi SSR, salah konfigurasi proxy_set_header atau timeout dapat menghasilkan redirect aneh, log IP yang salah, atau halaman yang terasa lambat padahal bottleneck ada pada jalur proxy.

Rollback harus lebih mudah daripada hotfix manual

Saat release baru memicu memory leak atau bug caching, tim tidak boleh bergantung pada edit container langsung di server. Jalur rollback paling waras adalah mengganti image tag dan me-restart deployment dengan state yang minimal.

Concept diagram

Architecture / Context

Sistem yang dibangun terdiri dari tiga komponen inti. CI atau workstation build menghasilkan image aplikasi. Docker menjalankan image itu di host Ubuntu. Nginx berada di depan sebagai reverse proxy dan TLS terminator. Dalam produksi, Nginx adalah edge HTTP, sedangkan container Next.js tetap berada pada network internal dan tidak terekspos langsung.

Model ini cocok untuk server tunggal, VM kecil, atau edge deployment yang belum membutuhkan orchestrator seperti Kubernetes.

Arsitektur / Cara Kerja

Model sederhana yang cukup cocok untuk satu server Ubuntu:

  1. CI membangun image Next.js
  2. image didorong ke registry
  3. server menarik image versi baru
  4. container aplikasi berjalan pada network internal
  5. Nginx meneruskan traffic publik ke container
  6. log dan metric dikumpulkan di level host atau sidecar sederhana

Dengan pola ini, surface area produksi tetap kecil. Server tidak perlu menginstal Node.js toolchain penuh untuk setiap deploy. Build dilakukan di luar host, sedangkan host berfokus menjalankan image yang sudah diketahui isinya.

Architecture flow

Practical Implementation

Step 1: Buat Dockerfile multi-stage

Dockerfile ini memisahkan tahap build dan runtime.

FROM node:20-alpine AS builder
WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production

COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/package*.json ./

RUN npm ci --omit=dev

EXPOSE 3000
CMD ["npm", "start"]

Step 2: Siapkan compose file di server

Compose memudahkan restart, rollback, dan pengelolaan environment runtime.

services:
  web:
    image: registry.example.com/blog-awwal:2026-04-25
    container_name: blog-awwal
    restart: unless-stopped
    env_file:
      - .env
    ports:
      - "127.0.0.1:3000:3000"
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/"]
      interval: 30s
      timeout: 5s
      retries: 3

Step 3: Jalankan container aplikasi

sudo docker compose pull
sudo docker compose up -d

Jika Anda belum memakai registry, build lokal masih mungkin, tetapi untuk produksi lebih baik image final datang dari CI yang sudah dikontrol.

Step 4: Konfigurasi Nginx

Simpan konfigurasi site ke /etc/nginx/sites-available/blog-awwal.conf.

server {
  listen 80;
  server_name blog.example.com;

  location / {
    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;
    proxy_read_timeout 60s;
  }
}

Aktifkan site:

sudo ln -s /etc/nginx/sites-available/blog-awwal.conf /etc/nginx/sites-enabled/blog-awwal.conf
sudo nginx -t
sudo systemctl reload nginx

Step 5: Tambahkan TLS

Jika memakai Let’s Encrypt:

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d blog.example.com

Step 6: Siapkan rollback sederhana

Simpan tag image yang sedang aktif dan satu versi sebelumnya. Rollback hanya perlu mengganti image di compose file lalu restart service.

sudo docker compose down
sudo docker compose up -d

Verification

sudo docker ps
sudo docker logs blog-awwal --tail=50
curl -I http://127.0.0.1:3000
curl -I https://blog.example.com
sudo nginx -t

Tanda berhasil:

  • container berjalan dan tidak restart berulang.
  • response lokal dan publik mengembalikan 200 atau redirect HTTPS yang sesuai.
  • nginx -t valid.
  • asset dan page render sesuai domain production.

Troubleshooting

Container restart loop

Biasanya image gagal start, env kurang, atau memory terlalu kecil.

sudo docker logs blog-awwal --tail=100
sudo docker inspect blog-awwal

Aplikasi hidup, tetapi URL redirect salah

Periksa X-Forwarded-Proto, domain, dan config runtime yang dipakai Next.js.

Asset tidak ter-load setelah deploy

Sering terjadi karena cache lama atau image build lama masih aktif. Pastikan image yang berjalan memang tag terbaru dan browser/CDN tidak memegang asset lama.

Production Notes

  • Pantau memory dan restart count container, terutama untuk SSR-heavy route.
  • Jangan campur secret runtime ke proses build di CI tanpa kebutuhan yang jelas.
  • Tambahkan log shipping untuk nginx dan container stdout agar incident tidak bergantung pada akses shell ke host.
  • Untuk traffic lebih tinggi, pindahkan static asset ke CDN dan tetap gunakan origin Nginx yang sederhana.

Studi Kasus / Masalah Nyata

Env salah masuk ke build

Tim memperbarui URL API di production tetapi lupa bahwa sebagian konfigurasi sudah tertanam saat next build. Aplikasi terlihat seperti memakai config lama walaupun container baru sudah berjalan.

Nginx cache terlalu agresif

Static asset memang cocok di-cache lama, tetapi HTML atau SSR response yang tidak seharusnya dicache bisa menyebabkan user melihat data lama. Ini sering terjadi saat semua response diperlakukan sama.

Deploy berhasil tetapi rollback lambat

Image baru menyebabkan penggunaan memory melonjak. Karena tidak ada tag versi yang jelas dan proses deploy dilakukan manual di server, rollback memakan waktu lebih lama dari outage itu sendiri.

Trade-offs

  • Build image di CI menambah langkah pipeline, tetapi jauh lebih aman daripada build langsung di server produksi.
  • Nginx memberi kontrol edge yang bagus, tetapi berarti ada satu komponen lagi yang perlu dipantau dan diupgrade.
  • SSR memberi fleksibilitas render, tetapi biasanya lebih boros memory daripada site yang sepenuhnya static.

Best Practices

Pakai multi-stage Docker build

Pisahkan tahap install, build, dan runtime agar image lebih kecil dan lebih mudah diaudit. Runtime image yang ramping juga mengurangi attack surface.

Jadikan Nginx sebagai boundary yang konsisten

Simpan TLS termination, redirect HTTP ke HTTPS, dan header proxy di satu tempat yang jelas.

Rancang deploy dengan rollback satu langkah

Simpan tag image secara eksplisit, dokumentasikan perintah rollback, dan jangan bergantung pada container yang dimodifikasi manual.

Failure Modes

  • Image baru membawa env build-time yang salah sehingga perilaku aplikasi berbeda dari ekspektasi production.
  • Nginx cache atau header proxy salah konfigurasi dan membuat user melihat state yang tidak konsisten.
  • Container restart terus-menerus karena memory limit terlalu ketat atau startup probe tidak realistis.

Kesalahan Umum

  • membangun image langsung di server produksi tanpa kontrol versi yang jelas
  • mencampur secret runtime dan config build-time
  • meng-cache semua response seperti static asset
  • tidak memberi memory limit atau observability dasar pada container
  • mengedit container live lalu lupa perubahan itu tidak persisten

Kesimpulan

Deploy Next.js ke Ubuntu dengan Docker dan Nginx akan terasa stabil jika kita memaksa jalur build, runtime, dan proxy tetap bersih. Fokus utamanya bukan sekadar membuat aplikasi tampil, tetapi membuat release bisa diulang, dipantau, dan dibalik dengan cepat saat perilaku produksi tidak sesuai harapan.

Kalau artikel ini membantu, kamu bisa support eksperimen berikutnya.

Apresiasi di Trakteer