# Cara Deploy Docker Compose di Production untuk Pemula

> Panduan pemula cara deploy Docker Compose di production: restart policy, .env, port loopback, batas memori, health check, update aman, dan tabel perbaikan.

URL: https://www.matthewswong.com/id/blog/cara-deploy-docker-compose-production-vps/

- Author: Matthews Wong (https://www.matthewswong.com/id/about/)
- Published: 2026-10-17
- Updated: 2026-10-17
- Category: DevOps

> **Key takeaway:** Cara deploy Docker Compose di production: tulis service dengan restart: unless-stopped, ikat port ke 127.0.0.1, simpan konfigurasi di file .env, pasang mem_limit dan health check, lalu jalankan dengan docker compose up -d --wait. Untuk update, ganti tag image, pull, dan up lagi. Untuk rollback, kembalikan tag lama.

Aplikasimu jalan di laptop dengan docker compose up. Kamu salin file-nya ke server, jalankan perintah yang sama, dan semuanya tampak beres. Sampai terminal ditutup dan aplikasinya ikut mati, atau server reboot dan tidak ada yang hidup lagi, atau kamu memindai server dan menemukan port aplikasi terbuka ke seluruh internet. File Compose yang cukup untuk development masih kekurangan sekitar lima baris yang dibutuhkan production.

Panduan ini membangun file itu dalam enam langkah, dengan pilihan yang sama seperti docker-compose.yml di balik situs ini: restart policy, port yang hanya di loopback, batas memori yang dipasangkan dengan batas heap Node, health check, dan rotasi log. Contoh di bawah ditulis ulang dengan nama placeholder, jadi tidak ada host atau secret asli. Setiap flag dicek ke dokumentasi Docker yang tercantum di akhir. Sisi CI (mem-build image dan mem-push ke registry) ada di panduan deploy Next.js ke VPS:

cara deploy Next.js ke VPS dengan Docker dan GitHub Actions

. Ikuti langkahnya berurutan, lalu pakai tabel error di akhir kalau ada yang tidak sesuai.

## Ingin paham konsepnya dulu?

Tulisan ini panduan praktiknya. Lesson gratis kami menjelaskan kenapa tiap baris ada, lengkap dengan diagram interaktif container yang menyentuh batas memorinya:

Docker Compose Production: Batas Memori, Health Check, Restart

Lesson itu juga punya kuis singkat untuk mengecek yang kamu ingat.

## Yang kamu butuhkan

Siapkan empat hal ini sebelum mulai. Semua nama di bawah adalah placeholder, ganti dengan milikmu.

Server Linux dengan Docker Engine dan plugin Compose terpasang, bisa diakses lewat SSH (contoh memakai user bernama deploy dan alamat 203.0.113.10).

Image yang sudah di-push ke registry, misalnya ghcr.io/example-user/my-app dengan tag yang tidak pernah berubah, seperti hash commit pendek. Kalau registry-nya private, jalankan docker login di server satu kali.

Aplikasi Node.js yang listen di port 3000 di dalam container dan menjawab /robots.txt atau URL ringan lainnya. Sesuaikan angkanya untuk bahasa lain.

Satu folder di server untuk project ini, misalnya /home/deploy/my-app, yang berisi tepat dua file: docker-compose.yml dan .env.

Output yang diharapkan

## Langkah 1: tulis service dengan restart policy dan port loopback

Mulai dari tiga baris yang biasanya dilewati file development. restart: unless-stopped menyalakan container lagi setelah exit apa pun dan setelah server reboot, tetapi membiarkannya mati kalau kamu sendiri yang menghentikannya. Baris ports menentukan siapa yang bisa menjangkau aplikasi: kalau alamat host dihilangkan, Docker mengikat ke semua interface, dan dokumentasinya sendiri memperingatkan hal ini bisa melewati aturan firewall host.

Mengikat ke 127.0.0.1 berarti hanya server itu sendiri yang bisa menjangkau aplikasi, jadi harus ada reverse proxy di depannya. Itu tugas nginx, dibahas di 

panduan nginx reverse proxy untuk Docker dan Cloudflare

. Sebelum nginx siap, uji dari server itu sendiri dengan curl.

docker compose ps menampilkan web dengan pemetaan 127.0.0.1:3002->3000/tcp, bukan 0.0.0.0. curl ke 127.0.0.1:3002 di server menjawab, sedangkan port yang sama di alamat publik server tidak menjawab dari luar.

Dokumentasi port publishing Docker memperingatkan bahwa port tanpa alamat host bisa dijangkau dari luar host. Aturan firewall buatanmu sendiri mungkin tidak menghentikannya, karena trafik ke port yang di-publish dirutekan sebelum aturan itu. Perbaiki baris ports-nya, jangan mengandalkan aturan.

## Langkah 2: pindahkan konfigurasi ke .env dan cek hasil resolusinya

Jangan mengedit YAML di setiap rilis. Taruh nilai yang berubah (tag image, port host, secret aplikasi) di file .env di sebelah docker-compose.yml. File itu mengerjakan dua hal yang berbeda. Compose membaca .env di folder project untuk mengisi placeholder berbentuk dolar-kurung-kurawal di dalam YAML, dan baris env_file meneruskan variabel dari file yang sama ke dalam container. Pada sintaks interpolasi, tanda titik dua dan minus memberi nilai default: nilai dipakai kalau sudah diisi dan tidak kosong, kalau tidak, default yang berlaku.

Selalu jalankan docker compose config sebelum start pertama. Perintah itu mencetak file setelah variabelnya terisi, jadi salah ketik terlihat sebagai nilai kosong, bukan container yang berperilaku aneh. Jaga .env tetap di luar git, dan baca 

panduan menyimpan secret Docker dengan aman

 sebelum memasukkan password asli: dokumentasi Docker sendiri menyarankan untuk tidak meneruskan nilai sensitif lewat environment variable.

Output config menampilkan image dengan tag yang kamu pin dan baris ports sebagai 127.0.0.1:3002:3000. Kalau ada variabel yang tidak ada di .env, Compose memperingatkan bahwa variabel itu belum di-set dan mengganti dengan nilai kosong.

## Langkah 3: pasang batas memori, dan batas heap yang lebih rendah di image

Secara default container tidak punya batas resource, jadi satu kebocoran memori bisa membuat semua yang lain di server kekurangan. mem_limit memasang batas keras, dan saat container menyentuhnya kernel mematikan prosesnya. Docker lalu melaporkan exit code 137. Untuk aplikasi Node, pasangkan dengan --max-old-space-size yang membatasi heap V8. Pasangan ini penting karena heap bukan satu-satunya memori yang dipakai proses: runtime dan buffer native ada di luarnya.

Situs ini memasang mem_limit 512m dan batas heap 384, sekitar 75 persen, dan komentar di Dockerfile-nya mencatat pemakaian stabil terukur 130 MB. Batas heap itu pernah disetel sama dengan limit, yang akan membuat kernel mematikan container sebelum V8 sempat bereaksi. Pakai rasio yang sama sebagai titik awal, lalu amati angka sebenarnya dengan docker stats. Kebiasaan lain di sisi image ada di 

Docker containerization best practices

.

docker stats --no-stream menampilkan web dengan kolom MEM USAGE / LIMIT, dan kolom limit menunjukkan batas yang kamu pasang. Tepat setelah start, pemakaian harus jauh di bawahnya.

## Langkah 4: tambahkan health check dan batasi ukuran log

Health check adalah perintah yang dijalankan Docker di dalam container secara terjadwal. Statusnya dimulai sebagai starting, menjadi healthy saat pemeriksaan pertama lolos, dan menjadi unhealthy setelah gagal berturut-turut sebanyak yang dikonfigurasi. Pahami apa yang tidak dilakukannya: restart policy bereaksi pada proses yang sudah exit, dan proses yang hang belum exit, jadi container yang unhealthy tetap berjalan. Pemeriksaan ini adalah sinyal untukmu dan untuk --wait di langkah berikutnya, bukan perbaikan.

Blok logging memperbaiki masalah yang lebih sepi. Driver json-file tidak punya batas ukuran secara default, jadi container yang banyak menulis log pelan-pelan memenuhi disk. Dengan max-size dan max-file, jumlah file log yang dirotasi menjadi terbatas; situs ini menyimpan tiga file masing-masing 10 MB.

Setelah start berikutnya, docker compose ps menampilkan (health: starting) dulu dan (healthy) setelah pemeriksaan pertama lolos. Kalau tidak pernah keluar dari starting atau berubah unhealthy, lihat tabel di bawah.

Perintah probe berjalan di dalam image, jadi harus ada di sana. wget bisa dipakai di image berbasis Alpine karena BusyBox membawanya; image minimal tanpa wget atau curl butuh probe lain, kalau tidak health check gagal terus padahal aplikasinya sehat.

## Langkah 5: validasi, lalu jalankan pertama kali

Validasi dulu, baru jalankan. config --quiet hanya memvalidasi dan tidak mencetak apa pun kalau filenya benar. up -d berjalan di background. Menambahkan --wait membuat perintah menunggu sampai service running dan healthy, dengan --wait-timeout sebagai batas atas dalam detik. Tanpa itu, up -d selesai begitu container start, jadi rilis yang tidak pernah healthy tetap terlihat berhasil.

Kalau perintahnya gagal, jangan menebak. Baca dulu log container itu sendiri; baris berikut menunjukkan caranya. Restart policy bisa menyembunyikan crash loop karena container terus hidup kembali, jadi di lognya error yang sebenarnya.

Perintah pertama tidak mencetak apa pun. Perintah kedua selesai ketika web sudah healthy, atau berakhir dengan error kalau belum healthy dalam batas waktu. docker compose logs menampilkan baris startup aplikasi, misalnya alamat yang di-listen.

## Langkah 6: update tanpa merusak, dan cara rollback

Update itu tiga langkah: arahkan IMAGE ke tag baru, pull, lalu jalankan up dengan --wait. Compose membuat ulang container karena image-nya berubah. Inilah alasan tag tidak boleh latest: dengan tag yang tidak berubah seperti hash commit, nilai lama di .env sekaligus menjadi rollback-mu. Catat tag lama sebelum menggantinya.

Jujur soal satu batasan. Dengan satu container, yang lama berhenti lalu yang baru start, jadi ada jeda singkat. Untuk banyak aplikasi kecil itu bisa diterima; kalau tidak, baca 

zero-downtime rolling deploy

. Perlu diingat container_name juga membuat Compose tidak bisa menjalankan lebih dari satu salinan service itu, seperti yang dinyatakan dokumentasi Docker.

up --wait selesai setelah container baru healthy, dan docker compose ps menampilkan tag baru di kolom IMAGE. Setelah rollback, tag lama muncul lagi.

Jangan memakai docker compose down -v sebagai cara restart. down menghentikan dan menghapus container, jeda yang lebih panjang daripada up di tempat, dan flag -v juga menghapus volume, tempat database menyimpan datanya.

## Error umum dan cara memperbaikinya

Cocokkan gejala di kolom pertama, pastikan penyebabnya, lalu terapkan perbaikannya. Baca selalu log container sebelum mengubah file.

Gejala

Penyebab

Perbaikan

Port aplikasi terbuka dari internet lewat alamat IP

Baris ports tanpa alamat host, jadi terikat ke semua interface

Ubah menjadi 127.0.0.1:3002:3000, jalankan up -d lagi, cek docker compose ps

Compose memperingatkan variabel belum di-set, atau nama image kosong

File .env tidak ada di folder project, atau nama variabel salah ketik

Jalankan dari folder yang berisi docker-compose.yml, perbaiki namanya, pastikan dengan docker compose config

Container exit dengan kode 137 lalu restart berulang kali

Memorinya menyentuh mem_limit dan kernel mematikannya

Pastikan dengan docker inspect, turunkan batas heap di bawah limit atau naikkan limit-nya, dan cari kebocoran

Status tetap unhealthy tetapi container masih berjalan

Restart policy bereaksi pada exit, bukan pada pemeriksaan yang gagal

Baca hasil probe dengan docker inspect, perbaiki aplikasi atau URL probe, dan pakai up --wait agar deploy gagal dengan jelas

Selalu unhealthy, padahal curl di server berhasil

Tool probe, misalnya wget, tidak ada di dalam image

Jalankan probe manual dengan docker compose exec, lalu pindah ke tool yang benar-benar ada di image

Status menunjukkan Restarting dan tidak pernah tenang

Aplikasi crash saat boot, sering karena nilai di .env hilang atau salah

Baca docker compose logs --tail 50 web, perbaiki nilainya, jalankan up -d --wait lagi

Disk pelan-pelan penuh dan ada file log yang sangat besar di /var/lib/docker

json-file tidak punya batas ukuran secara default, dan max-file hanya berlaku kalau max-size diisi

Tambahkan blok logging dengan kedua opsi, lalu buat ulang container agar setelan baru berlaku

Conflict: nama container sudah dipakai

Container lama yang dibuat di luar Compose sudah memegang container_name itu

Hapus satu container itu sekali dengan docker rm -f, lalu jalankan up -d; sejak itu Compose yang memegang namanya

up --scale web=2 menolak berjalan

container_name diisi, jadi Compose tidak bisa menjalankan salinan kedua

Hapus container_name kalau butuh lebih dari satu salinan, dan pasang proxy di depannya

Kalau tidak ada baris yang cocok, periksa dari dalam ke luar. Empat perintah ini menjawab sebagian besar pertanyaan, dengan urutan ini:

Untuk cara lain membaca output container, lihat 

panduan pemula membaca Docker logs

.

## Checklist production

restart: unless-stopped sudah dipasang, dan saya tahu ia tidak bertindak pada status unhealthy.

Setiap entri ports diawali 127.0.0.1: kalau hanya proxy yang boleh menjangkaunya.

Tag image berasal dari .env dan tidak berubah, bukan latest, dan tag sebelumnya sudah dicatat.

mem_limit sudah dipasang, dan batas heap Node jelas di bawahnya.

Health check sudah ada, tool-nya ada di image, dan deploy memakai up --wait.

Rotasi log sudah diatur dengan max-size dan max-file, dan .env tidak ada di git.

## Belajar lebih dalam

Sekarang kamu punya file Compose yang selamat dari reboot, menyembunyikan portnya, membatasi memorinya, melaporkan kesehatannya, dan bisa di-rollback. Lesson gratis menelusuri file yang sama baris demi baris, lengkap dengan explainer yang bisa dijalankan langkah demi langkah dan kuis.

Buka lesson: Docker Compose Production

Berikutnya, pasang nginx di depan port loopback itu, lalu otomatiskan update-nya dengan CI.

## Sumber dan dokumentasi resmi

- Docker Docs: referensi services di Compose file (restart, ports, mem_limit, healthcheck, container_name)
- Docker Docs: docker compose up (--wait, --wait-timeout, --remove-orphans)
- Docker Docs: interpolasi di file Compose (nilai default, .env)
- Docker Docs: start container otomatis (restart policy)
- Docker Docs: docker compose config

## Source links

- https://docs.docker.com/reference/compose-file/services/
- https://docs.docker.com/reference/cli/docker/compose/up/
- https://docs.docker.com/compose/how-tos/environment-variables/variable-interpolation/
- https://docs.docker.com/engine/containers/start-containers-automatically/
- https://docs.docker.com/reference/cli/docker/compose/config/

## FAQ

### Apa fungsi restart: unless-stopped di Docker Compose?

Container dinyalakan lagi setelah exit apa pun dan setelah Docker daemon atau server restart, tetapi dibiarkan mati kalau kamu sendiri yang menghentikannya. Policy ini bereaksi pada proses yang exit, bukan pada health check yang gagal. Container yang hang dan berstatus unhealthy tetap berjalan.

### Kenapa port Docker Compose saya terbuka ke internet?

Entri ports tanpa alamat host, seperti 3000:3000, terikat ke semua interface, dan dokumentasi Docker memperingatkan hal ini bisa melewati aturan firewall host. Tulis 127.0.0.1:3000:3000 supaya hanya server itu sendiri yang bisa menjangkaunya. Lalu pasang reverse proxy seperti nginx di depannya.

### Apakah docker compose up -d menunggu aplikasi sampai healthy?

Tidak. up -d selesai begitu container start, jadi rilis yang tidak pernah healthy tetap terlihat berhasil. Tambahkan --wait agar menunggu sampai service running dan healthy, dan pakai --wait-timeout untuk mengatur batasnya dalam detik.

### Bagaimana cara update aplikasi Docker Compose tanpa merusaknya?

Ganti tag image di .env, jalankan docker compose pull, lalu docker compose up -d --wait. Pakai tag yang tidak berubah seperti hash commit, jangan latest. Dengan satu container ada jeda singkat saat yang lama berhenti dan yang baru start.

### Kenapa container saya exit dengan kode 137?

Exit code 137 adalah 128 ditambah sinyal 9, artinya proses dimatikan, biasanya karena menyentuh mem_limit dan OOM killer kernel turun tangan. Jaga batas heap Node jelas di bawah batas memori dan amati pemakaian nyata dengan docker stats. Lalu cari kebocorannya.

---

- HTML version: https://www.matthewswong.com/id/blog/cara-deploy-docker-compose-production-vps/
- English: https://www.matthewswong.com/en/blog/cara-deploy-docker-compose-production-vps/
- Site index for agents: https://www.matthewswong.com/llms.txt
- Sitemap: https://www.matthewswong.com/sitemap.xml
- API description (OpenAPI): https://www.matthewswong.com/openapi.json
