Beberapa waktu lalu saya sempat dapat project pembuatan website pendaftaran dan pengumpulan berkas lomba untuk salah satu kegiatan di departemen kampus saya di ITS. Statusnya solo developer, pengerjaan serba mepet, mana bayarannya kecil wkwkwk cuma 2jt... π Jadi arsitekturnya dibuat se-sat-set dan seringkas mungkin: satu VPS Linux murah, satu file docker-compose.yml, dan aplikasi dibungkus rapi ke dalam Docker.
Alur deploy yang saya pakai adalah alur klasik sejuta umat: push kode dari laptop, SSH ke VPS, jalankan git pull, build image baru dengan docker compose build, lalu hajar docker compose up -d. Sederhana, cepat, dan selama masa persiapan rasanya aman-aman saja, sampai akhirnya masuk ke hari-hari terakhir pendaftaran...
Kamu yang pernah mengurus website event atau lomba pasti paham betul kelakuan peserta: 90% baru mendaftar di jam-jam terakhir menjelang batas akhir pendaftaran mepet wkwkwk π Mana form pendaftaran lomba itu inputnya banyak sekali. Bukan cuma nama dan email, tapi ada belasan field: nama tim, data ketua, data anggota, asal instansi, plus rentetan unggahan berkas seperti scan Kartu Tanda Mahasiswa (KTM), bukti follow akun sponsor, bukti share poster ke IG story, sampai slip bukti transfer pembayaran.
Nah, pas trafik lagi ramai-ramainya dan peserta lagi khusyuk mengunggah berkas, ada bug validasi kecil yang dilaporkan panitia dan harus segera saya perbaiki. Tanpa pikir panjang, saya push perbaikannya ke server lalu jalankan docker compose up -d. Tepat di detik itu juga: DUARRR! Layar peserta langsung memunculkan pesan 502 Bad Gateway π Request upload berkas yang sedang berjalan 80% langsung gagal seketika, form tereset ke awal, peserta panik dan mengamuk di helpdesk panitia, dan ujung-ujungnya developer yang kena todong wkwkwk.

Kalau di project hobi pribadi yang pengunjungnya cuma kita sendiri, jeda mati 5-10 detik mungkin bukan masalah besar. Tapi begitu sistemnya dipakai oleh pengguna riil, menerima request webhook, atau menangani bot dan trafik pendaftaran yang masuk terus-menerus di detik-detik kritis, downtime beberapa detik saja langsung jadi bencana operasional.
Sebenarnya saya tahu solusinya: ya pasang Kubernetes. Ada fitur rolling update bawaan yang bikin peralihan container berjalan mulus tanpa kedip. Tapi mari realistis: masa untuk side project kampus dan dikerjakan sendirian harus setup Kubernetes? Ga worth it banget jir, mana bayarannya kecil wkwkwk cuma 2jt! Yang ada RAM VPS 20 ribu perak habis cuma buat menjalankan control plane K8s, belum lagi pusing mengurus file YAML yang panjang bgt π€£

Dari situlah muncul pertanyaan benang merahnya: "Masa harus pasang Kubernetes cuma buat menghilangkan jeda downtime 5 detik?"
Setelah mencari jalan tengah yang tidak membebani server dan pikiran, akhirnya saya menemukan solusinya: docker-rollout. Cuma modal satu plugin Docker CLI kecil, kita bisa menikmati zero-downtime deployment di Docker Compose tanpa perlu pasang Docker Swarm apalagi Kubernetes, lengkap dengan fitur draining agar request yang sedang berjalan tidak putus di tengah jalan! π₯
Kenapa docker compose up -d bikin downtime
Banyak developer mengira opsi -d (detached mode) pada Docker Compose otomatis membuat pergantian container berjalan mulus di latar belakang. Anggapan ini keliru wkwkwk! Perintah docker compose up -d pada service yang sudah berjalan melakukan mekanisme recreate (bikin ulang), bukan rolling replace.
Urutan kejadian aslinya di balik layar adalah seperti ini:

Downtime yang dialami pengguna adalah akumulasi dari waktu mematikan container lama ditambah proses booting aplikasi baru. Di bahasa seperti Go mungkin cepat, hanya 1 sampai 2 detik. Tetapi di framework seperti Next.js, Node.js (Express/Nest), atau Laravel, proses runtime init, inisialisasi connection pool database, dan parsing rute bisa memakan waktu 5 hingga 20 detik! Selama rentang waktu itulah port aplikasi tertutup, dan reverse proxy di depannya akan langsung melempar pesan 502 Bad Gateway atau Connection Refused ke browser pengguna.
Kondisi ini memakan dua jenis korban:
- Incoming request: Pengguna baru yang baru membuka halaman atau mengirim data saat container sedang mati akan langsung disambut error 502.
- In-flight request: Pengguna yang request-nya sedang diproses (misalnya sedang mengunggah file scan KTM atau slip bayar 15MB) akan terputus paksa di tengah jalan karena proses di container lama keburu dimatikan sebelum selesai menulis data π
Berbagai cara deploy tanpa downtime
Sebelum memilih docker-rollout, mari kita tinjau beberapa pendekatan umum yang biasa dipakai para engineer untuk menyelesaikan masalah ini di server tunggal:
Blue-green manual
Pendekatan ini menyiapkan dua definisi service identik di dalam compose.yml, misalnya web_blue di port 3001 dan web_green di port 3002. Saat versi blue aktif, kita menyalakan green. Begitu green siap, kita mengubah upstream di konfigurasi Nginx lalu menjalankan nginx -s reload, baru kemudian mematikan blue. Cara ini bekerja dengan baik, tetapi menuntut banyak otomasi manual, rawan salah edit konfigurasi saat panik, dan membuat file compose menjadi kembung dengan duplikasi service.
Orchestrator (Swarm / Kubernetes)
Ini adalah standar industri yang sesungguhnya. Baik Docker Swarm maupun Kubernetes memiliki abstraksi rolling update secara native: container baru dinyalakan, dicek kesehatannya, lalu container lama dihapus bertahap. Namun, memasang Swarm atau Kubernetes untuk satu VPS kecil ibarat membawa truk tronton untuk mengantar satu bungkus nasi goreng wkwkwk. Overhead memorinya terlalu besar (bisa memakan 1 sampai 3 GB RAM cluster sendiri) dan kompleksitas perawatannya tidak sebanding untuk project skala kecil hingga menengah.
Deploy tool (Kamal, Dokku, dsb)
Platform seperti Kamal (garapan 37signals) atau Dokku sudah mendukung zero-downtime out-of-the-box. Kekurangannya, tool-tool ini biasanya cukup opinionated dan meminta kita mengikuti aturan main mereka: mulai dari konfigurasi khusus, keharusan memiliki Docker Registry remote, hingga alur SSH khusus. Jika tujuan kita hanya ingin script deploy sederhana yang bisa dijalankan langsung di server dengan file compose yang sudah ada, adopsi platform baru kadang terasa terlalu merepotkan.
docker-rollout
Pendekatan ini mengambil jalan tengah yang paling pragmatis. Cukup satu perintah pengganti, tanpa daemon tambahan, tanpa mengubah format compose.yml secara drastis, dan bekerja langsung di atas engine Docker yang sudah terpasang.
| Metode | Setup | Butuh proxy | Cocok untuk |
|---|---|---|---|
compose up -d |
Tidak ada | Tidak | Dev / hobby |
| Blue-green manual | Sedang | Ya | VPS tunggal + script sendiri |
| Swarm / K8s | Berat | Bawaan | Multi node, enterprise |
| Kamal / Dokku | Sedang | Bawaan | Project baru dengan CI/CD |
| docker-rollout | Ringan | Ya | 1 VPS / homelab, hemat budget |
Kenalan dengan docker-rollout
docker-rollout adalah plugin Docker CLI open-source yang dikembangkan oleh Piotr Gaczkowski (wowu). Plugin ini ditulis murni menggunakan Shell script mandiri, berlisensi MIT, memiliki sekitar 3 ribu bintang di GitHub, dan aktif dirawat (saat artikel ini ditulis sudah mencapai versi 0.14). Tool ini bekerja mulus baik dengan docker compose (plugin v2) maupun docker-compose versi lawas.
Konsep intinya sangat sederhana tetapi cerdik. Alih-alih mematikan container lama lalu membuat yang baru, docker-rollout memanfaatkan fitur bawaan Docker Compose untuk melakukan scaling instance secara dinamis dalam 3 langkah:
- Scale ke 2x: Menggandakan jumlah instance service target menjadi dua kali lipat (misalnya dari 1 menjadi 2). Container baru mulai booting secara paralel, sementara container lama tetap aktif melayani pengguna.
- Tunggu Sehat: Menunggu container baru mencapai status
healthy(jika healthcheck didefinisikan) atau menunggu timer tertentu sampai aplikasi dipastikan siap menerima traffic. - Hapus yang Lama: Menghentikan dan menghapus container versi lama secara bertahap, menyisakan container versi baru yang kini sepenuhnya mengambil alih beban kerja.

Catatan: Karena mekanisme scaling ini memanfaatkan penamaan bawaan Docker Compose, nomor pada nama container akan bertambah terus setiap kali deploy (misalnya dariweb-1keweb-2, laluweb-3, dan seterusnya). Itu perilaku normal dan memang cara Docker Compose membedakan instance yang berbeda.
Persyaratan
Sebelum mulai menerapkan docker-rollout, pastikan lingkungan server kamu memenuhi prasyarat arsitektur berikut:
- VPS / mesin dengan Docker dan Docker Compose plugin: Disarankan menggunakan Docker Compose modern (perintah
docker compose, bukandocker-composelama, walau dua-duanya tetap didukung). - Reverse proxy dinamis: Karena selama masa transisi akan ada dua container yang hidup bersamaan, kamu wajib menggunakan reverse proxy yang bisa mendeteksi container Docker secara otomatis lewat label atau Docker network, seperti Traefik, Nginx Proxy (nginx-proxy), atau Caddy Docker Proxy. Di artikel ini kita akan memakai Traefik sebagai contoh utama, sisanya cukup disesuaikan.
- Service target tidak boleh punya
container_name:: Docker melarang keras dua container memiliki nama yang persis sama. Hapus bariscontainer_name:di service kamu agar Docker Compose bisa mengelola penamaan otomatis (web-1,web-2). - Service target tidak boleh punya
ports:langsung: Hapus baris pemetaan port host sepertiports: ["3000:3000"]pada service web. Jika dua container mencoba membinding port yang sama di host secara bersamaan, container kedua akan langsung gagal start dengan error address already in use. Semua traffic dari luar harus dialirkan melalui reverse proxy. - Endpoint healthcheck yang murah: Aplikasi sebaiknya menyediakan endpoint pengecekan kondisi kesehatan (misalnya
GET /healthatauGET /api/health) yang cepat dan hemat resource. - (Opsional) Graceful shutdown: Aplikasi sebaiknya menangani sinyal
SIGTERMuntuk menyelesaikan proses yang sedang aktif sebelum proses dimatikan.
Peringatan: Bagian berikutnya sangat teknis. Diasumsikan kamu sudah nyaman mengoperasikan terminal Linux, Docker Compose, dan konsep dasar reverse proxy.
Instalasi docker-rollout
Pemasangan docker-rollout sangat ringkas karena berupa script mandiri yang langsung dipasang ke folder plugin CLI Docker:
# Buat direktori plugin docker jika belum ada
mkdir -p ~/.docker/cli-plugins
# Unduh script docker-rollout langsung dari repository resminya
curl https://raw.githubusercontent.com/wowu/docker-rollout/main/docker-rollout \
-o ~/.docker/cli-plugins/docker-rollout
# Berikan izin eksekusi
chmod +x ~/.docker/cli-plugins/docker-rollout
Jika kamu sering menjalankan Docker menggunakan sudo, atau ingin plugin ini dapat diakses oleh seluruh pengguna di server, pasang script tersebut ke path global /usr/local/lib/docker/cli-plugins/docker-rollout.
Cek apakah sudah terpasang:
docker rollout --help
Jika instalasi berhasil, terminal akan menampilkan opsi bantuan dari perintah docker rollout. Kalau yang muncul pesan "docker: 'rollout' is not a docker command", jangan lanjut dulu! Cek kembali apakah path foldernya sudah tepat dan izin eksekusi (chmod +x) sudah diberikan.
Setup
Proses setup terdiri dari empat bagian utama. Pastikan kamu menyelesaikan checkpoint di setiap langkahnya sebelum melangkah ke bagian berikutnya:
1. Pasang reverse proxy
Siapkan reverse proxy (di sini kita gunakan Traefik) yang terhubung ke satu Docker network eksternal bersama bernama proxy. Berikut contoh ringkas file compose.yml untuk Traefik:
services:
traefik:
image: traefik:v3.0
restart: always
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entryPoints.web.address=:80"
ports:
- "80:80"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- proxy
networks:
proxy:
name: proxy
Cek: Jalankan Traefik dengan docker compose up -d. Pastikan container Traefik berstatus running dan network proxy muncul saat kamu mengetikkan docker network ls.
2. Siapkan compose.yml service
Sekarang buka file compose.yml aplikasi kamu. Buang baris container_name dan pemetaan ports, lalu pasang routing label Traefik serta daftarkan service ke network proxy.
Sebelum (konfigurasi lama yang rawan 502):
services:
web:
build: .
container_name: web-app # WAJIB DIHAPUS!
ports:
- "3000:3000" # WAJIB DIHAPUS!
Sesudah (siap rollout):
services:
web:
build: .
restart: always
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.http.routers.web.rule=Host(`lomba.kampus.ac.id`)"
- "traefik.http.routers.web.entrypoints=web"
- "traefik.http.services.web.loadbalancer.server.port=3000"
healthcheck:
test: curl -f http://localhost:3000/health || exit 1
interval: 5s
timeout: 3s
retries: 3
start_period: 10s
networks:
proxy:
external: true
Cek: Jalankan aplikasi sekali dengan docker compose up -d web. Akses domain kamu lewat browser atau curl; pastikan sudah merespons dengan kode HTTP 200.
3. Tambahkan healthcheck
Healthcheck adalah kunci paling krusial dalam mekanisme zero-downtime. Tanpa healthcheck, docker-rollout tidak punya cara untuk mengetahui apakah aplikasi baru sudah selesai inisialisasi koneksi database atau masih cold start. Akibatnya, rollout hanya akan menunggu pasif selama sleep 10 detik default, yang belum tentu cukup bagi aplikasi berat.
healthcheck:
test: curl -f http://localhost:3000/health || exit 1
interval: 5s
timeout: 3s
retries: 3
start_period: 10s
Jika image aplikasi kamu berbasis minimal tanpa curl (seperti Alpine murni atau Distroless), gunakan wget -qO- http://localhost:3000/health || exit 1 atau sertakan binary healthcheck kecil.
Cek: Jalankan perintah docker ps di terminal. Kolom STATUS pada container kamu harus menampilkan keterangan (healthy) setelah durasi start_period terlewati.
4. Rollout pertama
Saatnya menguji coba. Sekarang, gantikan perintah lama docker compose up -d web dengan perintah rollout:
docker rollout web
Tempelkan perhatianmu ke log output terminal yang dihasilkan:
Scaling service web to 2 instances...
Waiting for new container to become healthy...
Container web-2 is healthy!
Stopping and removing old container web-1...
Rollout completed successfully in 12s.
Untuk membuktikan bahwa tidak ada downtime sama sekali, buka tab terminal kedua sebelum kamu menjalankan perintah rollout, lalu tembak domain kamu secara terus-menerus setiap 0.5 detik:
while true; do
curl -s -o /dev/null -w "%{http_code}\n" https://lomba.kampus.ac.id
sleep 0.5
done
Lihat hasilnya: seluruh baris output di terminal kedua akan tetap stabil menampilkan angka 200 dari awal proses scaling sampai container lama terhapus sempurna!
Beberapa opsi rollout yang sering dipakai:
-f | --file PATH: Menentukan path ke file compose jika namanya bukancompose.ymlstandar.-t | --timeout DETIK: Batas waktu menunggu container baru menjadi healthy (default: 60 detik).-w | --wait DETIK: Waktu tunggu jika container tidak memiliki healthcheck (default: 10 detik).--wait-after-healthy DETIK: Jeda waktu tambahan setelah container baru sehat sebelum container lama mulai dimatikan.--env-file PATH: Menentukan file environment khusus.
Draining: jangan buang request yang sedang jalan
Berhasil mendapatkan kode HTTP 200 terus-menerus baru menyelesaikan separuh masalah. Masih ada masalah kedua yang tidak kalah menyakitkan: in-flight request.
Bayangkan ada peserta lomba yang sedang mengunggah berkas zip portofolio sebesar 20MB di container web-1. Tepat ketika web-2 dinyatakan healthy oleh Docker, docker-rollout secara default akan langsung mematikan web-1. Traefik memang pintar langsung mengarahkan pengunjung baru ke web-2, tetapi koneksi upload yang sedang berjalan di web-1 akan terputus seketika di tengah jalan!
Solusi resminya adalah menerapkan mekanisme connection draining. Caranya adalah dengan membuat healthcheck yang sengaja gagal jika ada file penanda /tmp/drain:
services:
web:
# ... konfigurasi lainnya ...
healthcheck:
test: test ! -f /tmp/drain && curl -f http://localhost:3000/health
interval: 5s
retries: 1
labels:
- "docker-rollout.pre-stop-hook=touch /tmp/drain && sleep 10"
Urutan kejadian yang berlangsung di balik layar:

Selain itu, pastikan juga aplikasi backend kamu menangani sinyal SIGTERM dengan baik untuk melakukan graceful shutdown (misalnya menutup server HTTP dan menunggu koneksi aktif selesai diproses sebelum benar-benar keluar):
process.on('SIGTERM', async () => {
console.log('Menerima sinyal SIGTERM, bersiap shutdown...');
server.close(() => {
console.log('Seluruh koneksi HTTP aktif selesai diproses. Keluar.');
process.exit(0);
});
});
Cek: Jalankan request yang memakan waktu lama (misalnya endpoint simulasi sleep 5 detik) tepat saat kamu mengeksekusi rollout. Request tersebut harus tetap selesai dengan selamat dan menghasilkan kode 200.
Script deploy lengkap
Untuk menyatukan seluruh alur ini ke dalam alur kerja produksi harian di server, bungkus ke dalam satu file script bash sederhana, misalnya deploy.sh:
#!/usr/bin/env bash
set -euo pipefail
echo "==> Mengambil kode terbaru..."
git pull
echo "==> Membangun image baru..."
docker compose build web # atau: docker compose pull web
echo "==> Menjalankan migrasi database..."
docker compose run --rm web npm run db:migrate
echo "==> Melakukan zero-downtime rollout..."
docker rollout web --timeout 60
echo "==> Selesai! Deploy sukses tanpa downtime."
Catatan: Saat rollout berlangsung, sempat ada dua versi aplikasi yang berjalan bersamaan di waktu yang sama. Oleh karena itu, migrasi database kamu harus bersifat backward-compatible. Menambah kolom baru adalah tindakan aman. Tetapi menghapus kolom, mengubah nama kolom, atau mengubah tipe data kolom harus dipecah ke dalam fase rilis terpisah agar versi aplikasi lama tidak crash saat rollout masih berlangsung.
Opsional: Script ini bisa dipanggil secara otomatis dari pipeline CI/CD seperti GitHub Actions via SSH atau webhook server pribadi.
Catatan dan batasan
Agar ekspektasi tetap realistis, ada beberapa batasan teknis dari docker-rollout yang wajib diperhatikan:
- Wajib proxy, tidak bisa ports: langsung: Tanpa reverse proxy dinamis, kamu tidak bisa menjalankan dua instance container di host yang sama.
- Satu service per perintah: Perintah
docker rollouthanya menerima satu service dalam sekali jalan. Jika memiliki beberapa service web yang saling berhubungan, lakukan pemanggilan berurutan di script. - Bukan rollback otomatis: Jika container baru gagal melewati pengecekan healthcheck dalam batas waktu timeout, container lama memang tidak dimatikan (layanan tetap hidup). Namun proses deploy akan berhenti dengan status error dan kamu tetap harus memeriksa log secara manual.
- Bukan untuk stateful service: Jangan pernah menggunakan rollout pada database seperti PostgreSQL, MySQL, Redis, atau antrean RabbitMQ. Scaling pada database tunggal akan merusak integritas data lock.
- RAM/CPU sementara naik 2x saat deploy: Pastikan VPS kamu memiliki sisa headroom memori yang cukup agar container baru tidak terkena bunuh oleh Out-of-Memory (OOM) Killer.
- Kalau sudah multi node, ini saatnya lirik Swarm / K8s beneran: docker-rollout dirancang khusus untuk satu mesin Docker Compose. Jika arsitektur kamu sudah berkembang ke beberapa server cluster, beralihlah ke orchestrator multi-node sejati.
Penutup

Kombinasi antara reverse proxy dinamis, healthcheck yang tepat, dan docker-rollout membuktikan bahwa kita bisa mencapai deployment tanpa jeda 502 tanpa harus mengorbankan kesederhanaan stack Docker Compose yang sudah ada.
Bagi saya pribadi, workflow deployment di server side project dan homelab kini jauh lebih tenang. Tidak ada lagi rasa was-was saat harus menekan tombol deploy di jam-jam sibuk, dan peserta lomba bisa menyelesaikan pendaftaran mereka dengan nyaman tanpa terganggu error teknis.
Silakan coba terapkan di salah satu service web kamu terlebih dahulu, perhatikan bagaimana transisi scaling-nya bekerja, lalu tambahkan konfigurasi draining saat kamu sudah siap masuk ke lingkungan produksi.
