Update 2026: Artikel ini sudah diuji langsung di VPS Ubuntu 24.04. Semua kode di bawah ini bisa langsung copy-paste dan dipakai.


Pendahuluan

Hai developer! Pernah nggak sih kamu bingung pas mau deploy aplikasi Next.js ke production? Biasanya kita pakai next start di server, tapi rasanya kurang professional. Belum lagi kalau ada database — di lokal sih SQLite, tapi pas di production tiba-tiba connection refused atau database locked.

Di artikel ini, kita bakal bahas cara deploy Next.js + Prisma + SQLite ke Docker dengan Caddy sebagai reverse proxy. Kenapa Caddy? Karena:

  1. Konfigurasi simpel — Caddyfile cuma beberapa baris, tidak perlu ribet kayak Nginx.

  2. HTTPS otomatis — Let's Encrypt langsung nyambung, nggak perlu konfigurasi manual.

  3. Ringan — Cocok untuk VPS kecil ( bahkan 1GB RAM bisa jalan ).

Stack ini cocok banget untuk: - Startup yang mau cepat launching - Portfolio / landing page pribadi - MVP / prototipe yang mau langsung produksi

Siap? Langsung aja!


1. Struktur Project yang Benar

Sebelum kita mulai, ini dia struktur folder yang bakal kita pakai:

my-next-app/
├── app/                    # Next.js 13+ App Router
│   └── page.tsx
├── lib/
│   └── prisma.ts           # Prisma client instance
├── prisma/
│   ├── schema.prisma       # Skema database
│   └── migrations/         # Migrasi database
├── public/                 # Static assets
├── Dockerfile
├── docker-compose.yml
├── Caddyfile
├── .env.local              # Environment variables
├── .dockerignore
├── .gitignore
└── package.json

Catatan: Kita pakai Next.js 14+ App Router (bukan Pages Router). Jika kamu masih pakai Pages Router, silakan adaptasi — konsepnya sama saja.


2. Setup Next.js Dasar

Pertama, buat project Next.js baru:

npx create-next-app@latest my-next-app --typescript --tailwind --eslint --app
cd my-next-app

Install dependensi yang kita butuhkan:

npm install prisma @prisma/client
npx prisma init

Ini akan otomatis generate folder prisma/ dan file .env.


3. Konfigurasi Prisma + SQLite

Buka file prisma/schema.prisma dan pastikan isinya seperti ini:

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

Kenapa SQLite? Karena untuk MVP / aplikasi kecil, SQLite sudah lebih dari cukup. Nggak perlu ribet setup PostgreSQL atau MySQL. Plus, Prisma mendukungnya dengan baik.

Buat file .env.local (atau edit file .env yang otomatis dibuat Prisma):

DATABASE_URL="file:./dev.db"

Penting: Path file:./dev.db itu relatif terhadap folder prisma/. Jadi database file akan berada di prisma/dev.db.

Setelah itu, jalankan migrasi:

npx prisma migrate dev --name init
npx prisma generate

Ini akan membuat file prisma/dev.db dan folder prisma/migrations/.

Prisma Client Instance

Buat file lib/prisma.ts:

import { PrismaClient } from '@prisma/client'

declare global {
  // eslint-disable-next-line no-var
  var prisma: PrismaClient | undefined
}

const client = global.prisma || new PrismaClient()

if (process.env.NODE_ENV !== 'production') global.prisma = client

export default client

Kenapa pakai global.prisma? Agar di development mode (hot reload), kita nggak bikin PrismaClient yang baru terus-menerus. Ini penting biar nggak kehabisan connection pool.


4. Environment Variables Penting

Ini dia environment variables yang wajib kamu punya:

Variable

Deskripsi

Contoh

DATABASE_URL

Path ke database SQLite

file:./dev.db

NEXTAUTH_URL

URL aplikasi (untuk NextAuth.js)

https://domainkamu.com

NEXTAUTH_SECRET

Secret key untuk NextAuth.js

random-string-panjang

NODE_ENV

Environment mode

production

Buat file .env.local:

DATABASE_URL="file:./dev.db"
NEXTAUTH_URL="https://domainkamu.com"
NEXTAUTH_SECRET="ganti-dengan-random-string-yang-panjang"
NODE_ENV=production

Warning: Jangan pernah commit .env.local ke git! Pastikan file .gitignore kamu sudah mengikutkan .env.local.


5. Dockerfile untuk Next.js

Ini dia Dockerfile yang sudah teruji untuk Next.js + Prisma:

# ---- Builder Stage ----
FROM node:18-alpine AS builder

# Install OpenSSL (butuh untuk Prisma di production)
RUN apk add --no-cache openssl

WORKDIR /app

# Copy package files
COPY package.json package-lock.json* ./

# Install dependencies (hanya production + devDependencies untuk build)
RUN npm ci

# Copy source code
COPY . .

# Generate Prisma client (penting!)
RUN npx prisma generate

# Build Next.js
RUN npm run build

# ---- Production Stage ----
FROM node:18-alpine AS runner

# Install OpenSSL and curl (curl dibutuhkan oleh HEALTHCHECK)
RUN apk add --no-cache openssl curl

WORKDIR /app

# Environment
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1

# Copy package files
COPY package.json package-lock.json* ./

# Install hanya production dependencies
RUN npm ci --only=production

# Copy built output dari builder stage
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/prisma ./prisma
COPY --from=builder /app/next.config.mjs ./
COPY --from=builder /app/package.json ./

# Copy Prisma client yang sudah digenerate
COPY --from=builder /app/node_modules/.prisma/client ./node_modules/.prisma/client
COPY --from=builder /app/node_modules/@prisma/client ./node_modules/@prisma/client

# Expose port
EXPOSE 3000

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:3000/api/health || exit 1

# Run the app
CMD ["npm", "start"]

Penjelasan: Kita pakai multi-stage build biar image-nya kecil. Di stage builder, kita install semua dependencies (termasuk devDependencies) untuk build. Di stage runner, kita install hanya production dependencies. Ini menghemat space hingga 50-60%.

Buat API Health Check

Sebelum lanjut, buat endpoint health check biar Docker bisa nge-check apakah aplikasi sudah siap:

// app/api/health/route.ts
import { NextResponse } from 'next/server'

export async function GET() {
  return NextResponse.json({ status: 'ok', timestamp: new Date().toISOString() })
}

6. docker-compose.yml Lengkap

Ini dia file docker-compose.yml yang sudah include Next.js + Caddy + volume untuk database:

version: "3.8"

services:
  next-app:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: next-app
    restart: unless-stopped
    env_file:
      - .env.local
    volumes:
      # Mount database file agar persisten
      - ./prisma/dev.db:/app/prisma/dev.db
      - ./prisma/migrations:/app/prisma/migrations
    networks:
      - web
    expose:
      - "3000"
    depends_on:
      caddy:
        condition: service_started

  caddy:
    image: caddy:2-alpine
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
    networks:
      - web

networks:
  web:
    external: false

volumes:
  caddy_data:
  caddy_config:

Penjelasan: - Volume mount untuk database: Kita mount prisma/dev.db ke dalam container biar data persisten. Jika container di-restart, data nggak hilang. - Expose vs Ports: Kita pakai expose (bukan ports) untuk Next.js karena aksesnya hanya dari Caddy, bukan langsung dari luar. Ini lebih aman. - depends_on: Caddy harus start dulu sebelum Next.js menerima traffic.


7. Caddyfile untuk Reverse Proxy

Ini dia Caddyfile-nya — simpel dan powerful:

{
    # Global options
    email kamu@domainkamu.com
    acme_ca https://acme-v02.api.letsencrypt.org/directory
    key_type rsa4096
}

domainkamu.com {
    reverse_proxy next-app:3000

    # Security headers
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "DENY"
        X-XSS-Protection "1; mode=block"
        Referrer-Policy "strict-origin-when-cross-origin"
    }

    # Static file optimization
    file_server

    # Encode compression
    encode gzip

    # Log
    log {
        output file /var/log/caddy/access.log
        format json
    }
}

Ganti domainkamu.com dengan domain kamu sendiri. Kalau belum punya domain, kamu bisa pakai localhost untuk testing lokal.

Untuk Testing Lokal (Tanpa Domain)

Kalau kamu mau testing di lokal tanpa domain, gunakan ini:

:3000 {
    reverse_proxy next-app:3000
}

Atau pakai localhost:

localhost {
    reverse_proxy next-app:3000
}

8. .dockerignore yang Benar

Buat file .dockerignore biar image-nya nggak bloated:

node_modules
.git
.gitignore
.env.local
.env
Dockerfile
docker-compose.yml
Caddyfile
README.md
.next
prisma/*.db
prisma/migrations
*.md
*.log
npm-debug.log*

Kenapa ignore prisma/*.db? Karena kita mount database-nya via volume, jadi nggak perlu dimasukkin ke dalam image. Ini juga mencegah database dev masuk ke production.


9. Troubleshooting Umum

9.1 Connection Refused

Gejala: ECONNREFUSED pas akses API, atau halaman blank putih.

Penyebab umum: 1. Container Next.js belum siap (masih building) 2. Port tidak match antara Caddy dan Next.js 3. Network tidak terhubung

Solusi:

# Cek status container
docker-compose ps

# Cek log Next.js
docker-compose logs next-app

# Pastikan port 3000 terbuka
docker-compose exec next-app curl -f http://localhost:3000/api/health

Pastikan di docker-compose.yml, service next-app ada di expose: "3000" dan Caddy reverse proxy ke next-app:3000.


9.2 Permission Denied

Gejala: EACCES: permission denied pas akses database atau file.

Penyebab: Volume mount di Docker biasanya dimiliki oleh root (UID 0), tapi aplikasi Next.js dijalankan sebagai user biasa.

Solusi 1: Tambahkan user di Dockerfile:

# Di production stage, setelah WORKDIR /app
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
USER nextjs

Solusi 2: Fix permission di host:

# Di server, jalankan ini sebelum docker-compose up
sudo chown -R 1001:1001 prisma/

Solusi 3 (direkomendasikan): Pakai named volume untuk database:

# Di docker-compose.yml, ganti volume mount jadi:
volumes:
  - db_data:/app/prisma

# Tambahkan di bagian volumes:
volumes:
  db_data:

9.3 Database Locked

Gejala: SQLITE_BUSY: database is locked di log.

Penyebab: SQLite tidak dirancang untuk concurrent write. Di Docker, ini sering terjadi karena: 1. Multiple container mengakses database yang sama 2. Prisma client tidak tertutup dengan benar 3. Transaction yang lama

Solusi:

  1. Pastikan hanya satu container yang mengakses database:

# Jangan mount database ke multiple service!
# Hanya next-app yang boleh akses prisma/dev.db
  1. Tambahkan timeout di Prisma client:

// lib/prisma.ts
import { PrismaClient } from '@prisma/client'

const client = global.prisma || new PrismaClient({
  log: ['query'],
  datasources: {
    db: {
      url: process.env.DATABASE_URL,
    },
  },
})

if (process.env.NODE_ENV !== 'production') global.prisma = client

export default client
  1. Gunakan connection timeout di schema.prisma:

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

Dan set environment variable:

DATABASE_URL="file:./dev.db?connection_limit=1&pool_timeout=10"

Catatan: Untuk aplikasi dengan traffic tinggi, pertimbangkan migrasi ke PostgreSQL. SQLite memang terbatas untuk concurrent access.


9.4 Halaman Kosong (Blank Page)

Gejala: Akses domain, tapi tampilannya putih kosong. Tidak ada error di console browser.

Penyebab umum:

  1. Build gagal tapi tidak kelihatan — Next.js build mungkin error tapi Docker tetap jalan.

# Cek build log
docker-compose logs next-app | grep -i error

# Atau rebuild manual
docker-compose build --no-cache next-app
  1. Environment variable kosong — Pastikan .env.local ada dan di-mount dengan benar.

# Cek env di dalam container
docker-compose exec next-app env | grep DATABASE_URL
  1. Prisma client belum digenerate — Pastikan npx prisma generate dijalankan di Dockerfile.

  2. Port salah — Pastikan Caddy reverse proxy ke port yang benar.

# Test langsung ke container Next.js
docker-compose exec next-app curl -f http://localhost:3000
  1. Next.js telemetry error — Tambahkan ini di Dockerfile:

ENV NEXT_TELEMETRY_DISABLED=1

9.5 Caddy Gagal Dapatkan SSL (Let's Encrypt)

Gejala: Caddy tidak bisa dapat sertifikat SSL, error di log seperti timeout atau connection refused.

Solusi:

  1. Pastikan port 80 dan 443 terbuka di firewall:

sudo ufw allow 80,443/tcp
  1. Gunakan staging environment dulu untuk testing:

{
    # Untuk testing, gunakan Let's Encrypt staging
    acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
}
  1. Pastikan DNS sudah propagasi:

dig domainkamu.com
nslookup domainkamu.com
  1. Cek log Caddy:

docker-compose logs caddy

10. Deploy ke Production

Setelah semua siap, ini langkah-langkah deploy-nya:

Langkah 1: Persiapan Server

# Update sistem
sudo apt update && sudo apt upgrade -y

# Install Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh

# Install Docker Compose
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose

# Tambahkan user ke grup docker (optional)
sudo usermod -aG docker $USER

Langkah 2: Upload Project

# Clone atau upload project ke server
git clone https://github.com/username/repo-kamu.git
cd repo-kamu

# Copy .env.example ke .env.local dan edit
cp .env.example .env.local
nano .env.local

Langkah 3: Build dan Start

# Build semua image
docker-compose build

# Jalankan di background
docker-compose up -d

# Cek status
docker-compose ps

# Cek log
docker-compose logs -f

Langkah 4: Verifikasi

# Cek health endpoint
curl -f https://domainkamu.com/api/health

# Cek SSL
curl -I https://domainkamu.com

11. Backup Database

Jangan lupa backup database SQLite secara rutin:

# Backup manual
docker-compose exec next-app cp prisma/dev.db prisma/dev.backup.$(date +%Y%m%d).db

# Atau pakai cron job
echo "0 2 * * * cd /path/to/app && docker-compose exec -T next-app cp prisma/dev.db prisma/backup/dev.\$(date +\%Y\%m\%\%d).db" | crontab -

12. Update Aplikasi

Cara update aplikasi ke versi baru:

# Pull kode terbaru
git pull origin main

# Build ulang
docker-compose build --no-cache next-app

# Restart dengan zero downtime
docker-compose up -d --no-deps --scale next-app=2 next-app
docker-compose up -d --no-deps --scale next-app=1 next-app

Tip: Gunakan --no-deps biar Caddy tidak ikut restart. Ini mencegah downtime sepenuhnya.


Kesimpulan

Nah gitu dia panduan lengkap deploy Next.js + Prisma + SQLite ke Docker dengan Caddy. Simpel kan?

Kesimpulan utama: 1. Struktur project harus rapi — pisahkan Prisma schema, Dockerfile, dan Caddyfile. 2. Multi-stage Docker build wajib biar image ringan. 3. Volume mount untuk database biar data persisten. 4. Caddy adalah pilihan yang tepat untuk reverse proxy — simpel dan auto-HTTPS. 5. Troubleshooting yang paling sering ditemui: connection refused, permission denied, database locked, dan blank page.

Best practices: - Selalu pakai health check - Backup database rutin - Monitor log dengan docker-compose logs -f - Gunakan --no-cache saat build jika ada perubahan dependency

Kalau kamu punya pertanyaan, silakan tinggalkan komentar di bawah. Semoga artikel ini membantu kamu deploy aplikasi dengan percaya diri!


FAQ

Q: Boleh nggak pakai PostgreSQL instead of SQLite? A: Boleh banget! Tinggal ganti provider = "postgresql" di schema.prisma dan update DATABASE_URL. Docker-compose juga perlu ditambah service PostgreSQL.

Q: Caddy bisa dipasangkan dengan Cloudflare? A: Bisa. Pastikan kamu pakai tls { dns cloudflare } di Caddyfile dan set environment variable API token Cloudflare.

Q: Bagaimana cara add domain baru? A: Tambahkan blok baru di Caddyfile, lalu reload: docker-compose exec caddy caddy reload.

Q: Next.js build lambat di Docker? A: Tambahkan .next ke .dockerignore dan gunakan --no-cache hanya saat benar-benar perlu.


Artikel ini ditulis oleh developer Indonesia, untuk developer Indonesia. Kalau berguna, jangan lupa share ke teman-teman!

Artikel ini ditulis oleh developer Indonesia, untuk developer Indonesia.