Dokploy adalah panel deployment open-source yang berjalan di VPS kamu sendiri. Pengalaman pakainya mirip Vercel (connect repo GitHub, push, auto-deploy), tapi semua berjalan di server milikmu dengan biaya bulanan yang jauh lebih murah. Di panduan ini kita deploy aplikasi Next.js ke VM IDCloudHost dari nol sampai punya domain dengan HTTPS.
Contoh aplikasi di panduan ini memakai stack Next.js 15 + Prisma + PostgreSQL + Better Auth (stack yang sama dengan boilerplate yang saya bangun live di YouTube), tapi langkah-langkahnya berlaku untuk hampir semua aplikasi Next.js.
Gambaran alur
VM IDCloudHost (Ubuntu) → hardening → install Dokploy → hubungkan repo GitHub → PostgreSQL di Dokploy → deploy aplikasi → environment variables → setup database → domain + HTTPS → verifikasi.
1. Siapkan VM di IDCloudHost
- Login ke
console.idcloudhost.com→ buat VM baru. - Pilih OS Ubuntu 24.04 LTS.
- Spek minimal untuk Dokploy: 2 vCPU / 2 GB RAM / 30 GB disk, tapi saya sarankan 2 vCPU / 4 GB RAM. Build Next.js cukup berat di RAM: dengan 2 GB, RAM nyaris mentok saat build (Dokploy + Traefik + PostgreSQL sudah memakan sekitar 1 GB sebelum build dimulai).
- Set username + password (atau SSH key), lalu create.
- Pastikan port 22, 80, 443, dan 3000 bisa diakses.
2. Hardening VM
Jangan lewati bagian ini: VM yang baru dibuat dengan login password itu sasaran empuk bot brute-force. Lakukan minimal ini sebelum install Dokploy:
# SSH masuk sebagai user biasa, lalu masuk ke shell root
# karena semua perintah di bawah butuh root:
sudo su -
# a. Update sistem
apt update && apt upgrade -y
# b. Pakai SSH key, lalu matikan login password & root.
# Edit config SSH: vim /etc/ssh/sshd_config
# lalu set dua baris berikut:
# PasswordAuthentication no
# PermitRootLogin prohibit-password
systemctl restart ssh
# c. Firewall
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw allow 3000/tcp # panel Dokploy, tutup lagi setelah setup selesai
ufw enable
# cek rule yang aktif
ufw status # atau: verbose / numbered
# hapus rule (misal tutup port 3000 setelah setup):
# ufw status numbered → ufw delete <nomor>
# d. Anti brute-force SSH
apt install -y fail2ban Dua catatan penting yang sering kelewat:
- Docker mem-bypass UFW. Container yang publish port (misal PostgreSQL dengan
-p 5432:5432) tetap terekspos ke publik walaupun UFW tidak membuka port itu, karena trafik ke container lewat chainFORWARD(chainDOCKER/DOCKER-USERmilik Docker), bukan chainINPUTtempat rule UFW berada. Solusinya bukan di UFW, tapi: jangan expose port database di Dokploy sama sekali. Biarkan aplikasi connect lewat network internal Docker.ufw statusjuga tidak menampilkan port yang dipublish Docker; untuk cek port yang benar-benar listening, pakaisudo ss -tlnp. - Panel Dokploy di port 3000 adalah akses admin penuh ke server. Setelah setup selesai, beri panel domain sendiri + HTTPS (misal
panel.domainkamu.com) lalu tutup port 3000 dari publik, atau minimal pakai password kuat + aktifkan 2FA di Dokploy.
3. Install Dokploy
Masih di shell root yang sama dari langkah 2 (hasil sudo su -), jalankan installer resminya:
curl -sSL https://dokploy.com/install.sh | sh (Kalau sesi SSH sempat terputus: SSH lagi sebagai user biasa lalu sudo su -, karena login root langsung sudah dimatikan di langkah 2.)
Setelah selesai, buka http://<IP-VM>:3000 di browser → buat akun admin Dokploy pertama.
4. Hubungkan GitHub ke Dokploy
Pastikan kode aplikasimu sudah ada di repo GitHub (private juga bisa). Lalu di Dokploy: Settings → Git → GitHub → Create GitHub App.
- Toggle Organization?: matikan (OFF) kalau repo ada di akun pribadimu. Toggle ini menentukan di mana GitHub App terdaftar; kalau ON, butuh approval owner organization.
- Nama app bebas, misal
dokploy-namakamu. - Setelah app dibuat, statusnya masih “Action Required”, artinya app terdaftar tapi belum di-install. Klik → install → Only select repositories → pilih repo aplikasimu.
Soal keamanan: GitHub App itu semacam “akun robot” ber-izin terbatas: akses kode read-only (write hanya untuk pull request, dipakai fitur preview deployment), scope dibatasi ke repo yang dipilih, dan bisa dicabut kapan saja dari GitHub Settings → Installations. Kredensial app tersimpan di panel Dokploy, satu alasan lagi kenapa port 3000 harus diamankan.
5. Buat project + database PostgreSQL
Create Project → beri nama sesuai aplikasimu.
Di dalam project: Create Service → Database → PostgreSQL. Isi form:
- Name: label di UI saja, bebas.
- App Name: (auto). Penting: ini jadi hostname internal database untuk
DATABASE_URL. - Database Name: nama database aplikasimu.
- Database User: kosongkan (default
postgres). - Database Password: pakai yang di-generate → salin dan simpan.
- Docker image: samakan dengan versi PostgreSQL yang kamu pakai lokal, misal
postgres:16-alpine.
Deploy database-nya. Connection string internalnya:
postgresql://postgres:<password>@<app-name-db>:5432/<nama-database>?schema=publicAplikasi dan database berada di satu network Docker internal, jadi jangan expose port 5432 ke publik (ingat catatan Docker vs UFW di atas).
6. Buat application untuk Next.js
- Create Service → Application → source: GitHub → pilih repo + branch
main. - Build type: Nixpacks (auto-detect pnpm + Next.js, tidak perlu Dockerfile). Kolom Publish Directory kosongkan (itu hanya untuk static site).
- Kalau kamu pakai Prisma dengan
postinstall: prisma generatedipackage.json, script itu jalan otomatis saatpnpm install. Prosesnya tidak butuh koneksi database, aman saat build, dan tidak perlu setting apa pun.
⚠️ Gotcha: versi Node
Nixpacks default memilih Node 18, sedangkan Prisma 7 butuh Node 20.19+ / 22.12+ / 24+, sehingga pnpm install gagal dengan error Prisma only supports Node.js versions 20.19+.... Pilih salah satu fix:
Permanen (disarankan): tambah di
package.json:"engines": { "node": "22.x" }Cepat tanpa commit: env var
NIXPACKS_NODE_VERSION=22di tab Environment.
7. Set environment variables
Di tab Environment aplikasi, isi sesuai .env.example project-mu. Untuk stack contoh (Prisma + Better Auth):
DATABASE_URL=postgresql://postgres:<password>@<app-name-db>:5432/<nama-database>?schema=public
BETTER_AUTH_SECRET=<hasil: openssl rand -base64 32>
BETTER_AUTH_URL=https://app.domainkamu.com # pakai http://<IP> dulu kalau belum ada domain Lalu klik Deploy dan tunggu build selesai.
8. Pasang domain + HTTPS
- Buka tab Domains → Add Domain. Isi:
- Host: domain yang mengarah ke IP VM (A record). Belum punya domain? Pakai trik sslip.io:
app.<IP-VM>.sslip.iootomatis resolve ke IP itu tanpa setting DNS. Dokploy juga punya tombol dice 🎲 untuk generate domain*.traefik.megratis. - Path:
/ - Container Port:
3000 - HTTPS: aktifkan, certificate provider Let’s Encrypt (Traefik bawaan Dokploy yang mengurus sertifikatnya).
- Host: domain yang mengarah ke IP VM (A record). Belum punya domain? Pakai trik sslip.io:
- Save. Traefik langsung merutekan domain ke container, tidak perlu redeploy untuk domainnya.
- Kalau kamu pakai Better Auth (atau auth lain yang cek origin): update
BETTER_AUTH_URLdi tab Environment menjadi persis URL tadi → redeploy. Kalau tidak sinkron, login ditolak karena origin mismatch.
9. Setup database (sekali saja)
Setelah container aplikasi jalan, buka terminal container dari UI Dokploy (menu aplikasi → Terminal), lalu jalankan migrasi + seed sesuai project-mu, misalnya:
pnpm db:setup Tanpa langkah ini, login pasti gagal karena tabelnya belum ada.
Urutannya penting: deploy dulu supaya container-nya ada, baru jalankan setup database dari terminal container.
10. Verifikasi + auto-deploy
Buka domainnya → pastikan aplikasi jalan dan kamu bisa login. Bonus yang bikin setup ini terasa seperti Vercel: trigger On Push sudah aktif secara default: setiap git push ke main, Dokploy otomatis rebuild dan redeploy.
Dua langkah penutup yang saya sarankan:
- Amankan panel Dokploy: beri domain sendiri + HTTPS, lalu tutup port 3000 dari publik.
- Ganti password admin pertama aplikasimu kalau tadi memakai password seed.
Troubleshooting
Build error
Prisma only supports Node.js versions 20.19+→ Nixpacks memilih Node 18. Fix:engines.node: "22.x"dipackage.jsonatau envNIXPACKS_NODE_VERSION=22(lihat langkah 6).VM 2 GB RAM bisa OOM saat
next build. Cek jejaknya:sudo dmesg | grep -i -E "oom|killed process", atau build log yang berhenti mendadak / exit code 137. Fix: upgrade ke 4 GB (8 GB tidak perlu untuk satu aplikasi), dan/atau tambah swap:sudo fallocate -l 2G /swapfile sudo chmod 600 /swapfile sudo mkswap /swapfile sudo swapon /swapfile echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstabSwap = disk, build jadi 2–3× lebih lambat, tapi gratis. Kombinasi paling anti-gagal: 4 GB RAM + swap 1–2 GB.
Login gagal setelah deploy → cek tiga hal berurutan: (1) setup database sudah jalan? (2)
BETTER_AUTH_URLsama persis dengan URL yang dibuka? (3) sudah redeploy setelah ganti env?Mau membuktikan Docker mem-bypass UFW? Jalankan
docker run -d -p 8080:80 nginx:alpinedanpython3 -m http.server 8081di VM, dua-duanya tidak di-allow UFW. Curl dari luar: port 8080 tembus, 8081 diblokir. Buktinya ada disudo iptables -t nat -L DOCKER -n.Kalau mau image produksi lebih kecil dan build lebih deterministik: buat
Dockerfilesendiri +output: "standalone"dinext.config.ts. Untuk kebanyakan kasus, Nixpacks sudah cukup.
Proses deploy seperti ini juga saya praktikkan langsung di channel YouTube Pixel Developer. Kalau kamu lebih suka format nonton sambil praktik, mampir ke arsip episodenya.
