Docker Compose Production: Batas Memori, Health Check, Restart
Docker Compose untuk production yang aman: restart policy, batas memori yang dipasangkan dengan heap Node, health check, dan port yang hanya di localhost.
Oleh Matthews WongDitinjau pada
Poin utama
Docker Compose di production adalah satu file YAML yang mendeklarasikan cara sebuah container berjalan di server: image-nya, restart policy, batas memori dan CPU, health check, rotasi log, serta port mana yang dibuka. Setiap baris itu menjawab satu pertanyaan: apa yang terjadi saat aplikasi crash, bocor memori, atau berhenti merespons.
Setelah pelajaran ini, Anda bisa
- Membaca file Compose production dan menjelaskan fungsi setiap baris saat aplikasi crash, bocor memori, atau hang.
- Memasangkan batas memori container dengan batas heap Node, sehingga OOM killer kernel menjadi jalan terakhir.
- Mem-publish port hanya di 127.0.0.1, dan memahami apa yang dilakukan dan tidak dilakukan health check.
Ikuti diagramnya langkah demi langkah
Tekan Putar, atau melangkah dengan panah. Setiap langkah juga ditulis di bawah diagram.
Satu container di bawah tekanan memori
Container dengan batas 512 MB bocor memori di luar heap V8, kernel meng-OOM-kill-nya dengan exit code 137, restart policy menyalakannya lagi, health check mengubah statusnya dari starting ke healthy, dan aplikasi yang hang menjadi unhealthy tanpa di-restart.
Tombol: ← → langkah · Home / End · K putar atau jeda
Satu container dengan batas memori. Kernel menegakkan batas itu, Docker engine menjalankan restart policy, dan health check memeriksa aplikasinya.
NODE_OPTIONS membatasi heap V8 di 384 MB, di bawah limit 512 MB. Mendekati batas itu, V8 makin sering menjalankan garbage collection, sehingga ada ruang untuk memori di luar heap.
ENV NODE_OPTIONS="--max-old-space-size=384" mem_limit: 512mBuffer dan pemrosesan gambar berada di luar heap, jadi batas heap tidak bisa menahannya. Total memori container terus naik sampai menyentuh limit 512 MB.
Tebak langkah berikutnya Total memori menyentuh limit 512 MB. Apa yang terjadi berikutnya? (V8 membersihkan memori di luar heap · Kernel mematikannya: exit 137 · Docker menaikkan limitnya)Limit tersentuh dan tidak ada lagi memori yang bisa diambil kembali, jadi OOM killer kernel menghentikan proses dengan SIGKILL. Docker mencatat exit code 137: 128 ditambah 9.
restart: unless-stopped menyalakan container lagi setelah exit apa pun, kecuali Anda sendiri yang menghentikannya. Start baru ini mengembalikan status health ke starting.
restart: unless-stoppedSetiap 30 detik Docker menjalankan wget ke /robots.txt di dalam container. Pemeriksaan pertama yang lolos mengubah statusnya dari starting menjadi healthy.
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:3000/robots.txt"] interval: 30s timeout: 5s retries: 3 start_period: 20sTebak langkah berikutnya Aplikasi hang dan gagal tiga kali pemeriksaan. Apakah Docker me-restart-nya? (Ya, unless-stopped me-restart-nya · Tidak, tetap berjalan sebagai unhealthy)Jika aplikasi hang, tiga pemeriksaan gagal berturut-turut membuatnya unhealthy. Restart policy hanya bereaksi pada exit, jadi Docker membiarkan container unhealthy tetap berjalan.
retries: 3
Apa saja yang dideklarasikan file Docker Compose untuk production?
File Compose adalah YAML yang mendeklarasikan cara container berjalan: image, environment, port, batas resource, health check, dan logging. Situs ini punya satu service, yaitu web. CI mem-build dan mem-push image-nya, menyalin file ini ke server, lalu menulis .env di sebelahnya, jadi server hanya menjalankan docker compose pull dan up, tanpa pernah mem-build apa pun.
Baca selengkapnya (1)Tutup
restart: unless-stopped me-restart container apa pun exit code-nya, tetapi membiarkannya mati jika Anda sendiri yang menghentikannya, bahkan setelah Docker daemon di-restart. container_name mempertahankan nama container yang sudah ada, jadi setiap deploy menggantinya di tempat. Referensi Docker menyebut konsekuensinya: Compose tidak akan men-scale service lebih dari satu container jika file-nya menyetel container_name.
Bagaimana mem_limit dan batas heap Node bekerja sama?
Secara default container tidak punya batas resource, jadi satu kebocoran bisa menghabiskan memori yang dibutuhkan semua service lain di server. mem_limit: 512m memasang batas keras yang ditegakkan kernel lewat cgroup container. Saat memori menyentuh batas itu dan tidak bisa diambil kembali, OOM killer dipanggil di dalam cgroup tersebut. Prosesnya mati oleh SIGKILL, dan Docker melaporkan exit code 137, yaitu 128 ditambah nomor sinyal 9.
Baca selengkapnya (2)Tutup
Heap Node adalah separuh lainnya. --max-old-space-size membatasi old space milik V8, dan dokumentasi Node menyebut efeknya: saat pemakaian memori mendekati batas, V8 menghabiskan lebih banyak waktu untuk garbage collection. Batas ini hanya berlaku untuk heap. Buffer dan memori native, misalnya buffer yang dipakai saat next/image me-resize cover, berada di luarnya, jadi batas heap harus jauh di bawah batas container.
Situs ini mempelajarinya pada Juli 2026. Batas heap waktu itu 512 MB, seluruh jatah container, jadi saat tekanan memori benar-benar terjadi, kernel akan mematikan container sebelum batas heap sempat berbuat apa-apa. Kondisi itu belum pernah terjadi, tetapi batasnya akan sia-sia tepat saat dibutuhkan. Perbaikannya memasang 384 MB, sekitar 75 persen dari limit. Pemakaian stabil di production terukur 130 MB, jadi batas ini adalah katup pengaman, bukan pengekang.
Apa yang dilakukan health check Docker, dan apa yang tidak?
Health check adalah perintah yang dijalankan Docker di dalam container secara terjadwal. Health check situs ini menjalankan wget dari BusyBox ke /robots.txt setiap 30 detik dengan timeout 5 detik. Statusnya dimulai sebagai starting, dan pemeriksaan pertama yang lolos membuatnya healthy. Tiga kegagalan berturut-turut membuatnya unhealthy, dan kegagalan selama start period 20 detik tidak dihitung.
Baca selengkapnya (1)Tutup
Yang tidak dilakukannya adalah memperbaiki apa pun. Restart policy bereaksi saat proses exit, dan proses yang hang belum exit. Dalam uji coba di Docker Engine 29.8.1, container yang health check-nya selalu gagal tetap berjalan dalam status unhealthy, tanpa satu kali pun restart. Harus ada pihak lain yang bertindak atas status itu: uptime monitor yang mengirim peringatan, atau deploy yang menunggu status healthy sebelum menyatakan dirinya selesai.
Kenapa port container diikat ke 127.0.0.1?
Port yang di-publish tanpa alamat host akan terikat ke semua interface, dan dokumentasi Docker menyebut publish port tidak aman secara default: port itu bisa dijangkau dari luar, bukan hanya dari host. Port itu juga melewati firewall yang mungkin Anda kira melindunginya. Dokumentasi Docker menyatakan Docker dan ufw tidak kompatibel, karena trafik ke port yang di-publish dirutekan sebelum aturan ufw diterapkan.
Baca selengkapnya (1)Tutup
Karena itu situs ini mem-publish 127.0.0.1:${APP_PORT:-3002}:3000. Hanya proses di server itu sendiri yang bisa menjangkau aplikasi, dan nginx, yang menangani TLS, menjadi satu-satunya pintu masuk. ${APP_PORT:-3002} adalah interpolasi Compose: nilai APP_PORT dari .env jika sudah diisi dan tidak kosong, selain itu 3002.
Konfigurasi nyata di balik pelajaran ini
Kutipan dari repositori yang membangun dan men-deploy situs ini, apa adanya.
# Matches the existing container so deploys UPDATE it in place (no duplicate).
container_name: matthewswong-portfolio
restart: unless-stopped
env_file: .envRestart policy dan trade-off di balik container_name. Mengganti di tempat berarti tidak ada container duplikat setelah deploy, tetapi juga berarti Compose tidak bisa menjalankan salinan kedua dari service itu. Secret runtime datang dari .env, yang ditulis CI di server dan tidak pernah masuk git.
ports:
# Bind to loopback only — nginx terminates TLS and proxies to this port.
- "127.0.0.1:${APP_PORT:-3002}:3000"
mem_limit: 512m
cpus: "1.0"Publish khusus loopback, lalu batas resource-nya. mem_limit adalah batas keras yang ditegakkan kernel. cpus: "1.0" membatasi container pada waktu proses setara satu CPU, berapa pun jumlah CPU di host.
# This must stay meaningfully BELOW the `mem_limit: 512m` in docker-compose.yml.
# Resident set is heap plus non-heap — the Node runtime, and libvips buffers
# whenever next/image resizes a cover — and V8 only runs its final aggressive
# GC as it approaches this ceiling. Setting it to the full 512 MB (as this
# briefly did) means the kernel OOM-kills the container before the cap ever
# takes effect, which is the opposite of the intent. 384 MB is ~75% of the
# limit, leaving ~128 MB of headroom for everything off-heap.
#
# Measured steady state in production: 130 MB RSS.
ENV NODE_OPTIONS="--max-old-space-size=384"Separuh lain dari batas memori ada di image, bukan di Compose. Komentarnya mencatat kesalahan Juli 2026 dan hitungan perbaikannya: sekitar 384 MB untuk heap dan sekitar 128 MB untuk sisanya. Jika limit di docker-compose.yml diubah, baris ini harus ikut diubah.
healthcheck:
test:
[
"CMD",
"wget",
"--no-verbose",
"--tries=1",
"--spider",
"http://127.0.0.1:3000/robots.txt",
]
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"Probe dan rotasi log-nya. wget tersedia karena base Alpine membawa BusyBox; base distroless butuh probe yang berbeda. Blok logging membatasi log json-file menjadi tiga file masing-masing 10 MB, total sekitar 30 MB, padahal default Docker adalah ukuran tanpa batas dan satu file.
Kesalahan yang benar-benar membuang waktu
Batas heap sama dengan batas memori
Kesalahan situs ini sendiri pada Juli 2026. Dengan --max-old-space-size 512 dan mem_limit 512m, heap bisa mengambil seluruh jatah, dan memori di luar heap akan membuat container melewati batas lebih dulu. Jaga batas heap jauh di bawah limit, dan ubah keduanya bersamaan.
Yang akan saya ubah: deploy yang tidak menunggu healthy
Situs ini men-deploy dengan docker compose up -d, yang langsung selesai begitu container start. Rilis yang tidak pernah lolos health check tetap berakhir dengan workflow hijau. docker compose up --wait menunggu sampai running dan healthy, dan gagal jika tidak. Opsi ini melaporkan masalah, bukan me-rollback; rollback dibahas di pelajaran 8.
Mem-publish "3000:3000" di VPS
Tanpa alamat host, port terikat ke semua interface, dan trafik ke port yang di-publish dirutekan sebelum aturan ufw berjalan. Aplikasi pun bisa diakses dari internet, melewati nginx dan TLS. Tambahkan 127.0.0.1: di depan setiap port yang seharusnya hanya dijangkau proxy lokal.
Log tanpa rotasi
Driver json-file secara default memakai max-size -1, tanpa batas, dan max-file hanya berlaku jika max-size diisi. Container yang banyak menulis log akan terus membesarkan file log-nya sampai disk penuh. Isi keduanya, per service atau di default daemon.
Volume di atas mount point yang tidak ada
Situs ini memasang named volume di /app/.next/cache/images agar cache next/image bertahan antar deploy, dan komentarnya berharap Docker memberi volume itu uid 1001. Riset untuk pelajaran ini menemukan bahwa mount itu kemungkinan besar tidak pernah berfungsi: volume kosong hanya mewarisi pemilik dari direktori di image jika direktori itu ada, dan stage runner tidak pernah membuatnya. Uji coba di Docker Engine 29.8.1 menunjukkan volume itu dimiliki root dan tidak bisa ditulis uid 1001. Aturannya: buat mount point di image, dimiliki user runtime, sebelum USER.
| Policy | Setelah crash | Setelah exit normal | Anda hentikan, lalu daemon di-restart |
|---|---|---|---|
| no (default) | Tetap mati | Tetap mati | Tetap mati |
| on-failure | Di-restart | Tetap mati | Tetap mati |
| always | Di-restart | Di-restart | Menyala lagi |
| unless-stopped | Di-restart | Di-restart | Tetap mati |
Lakukan di komputer Anda sendiri
Anda hanya butuh Docker dan curl. Jalankan server kecil dengan batas 128 MB, buat sengaja bocor memori, lalu amati reaksi kernel dan Docker.
Buat server dengan route health dan kebocoran yang disengaja, serta Dockerfile yang menjalankannya sebagai user node bawaan image.
// server.js const { createServer } = require("node:http"); const chunks = []; createServer((req, res) => { if (req.url === "/health") return res.end("ok\n"); if (req.url === "/leak") { // Deliberately broken: 16 MB more every 100 ms, outside the V8 heap. setInterval(() => chunks.push(Buffer.alloc(16 * 1024 * 1024, 1)), 100); return res.end("leaking...\n"); } res.end("hello\n"); }).listen(3000, "0.0.0.0"); # Dockerfile FROM node:22-alpine WORKDIR /app COPY server.js . USER node CMD ["node", "server.js"]Tulis compose.yaml dengan restart policy, port loopback, batas resource, health check, dan rotasi log.
services: web: build: . restart: unless-stopped ports: - "127.0.0.1:8080:3000" mem_limit: 128m cpus: "0.5" healthcheck: test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:3000/health"] interval: 5s timeout: 3s retries: 3 start_period: 5s logging: driver: json-file options: max-size: "1m" max-file: "3"Jalankan dan tunggu sampai healthy. ps menampilkan (healthy), dan stats menampilkan limit 128 MiB.
docker compose up -d --build --wait docker compose ps docker stats --no-stream curl http://127.0.0.1:8080/Tunggu sepuluh detik, picu kebocorannya, lalu baca event-nya. Hasilnya oom, lalu die 137, lalu start, dan RestartCount 1.
curl http://127.0.0.1:8080/leak docker events --since 2m --until 0s --filter container=$(docker compose ps -q web) \ --format '{{.Action}} {{index .Actor.Attributes "exitCode"}}' docker inspect -f 'restarts={{.RestartCount}}' $(docker compose ps -q web)Buktikan bahwa unhealthy tidak di-restart: container yang health check-nya selalu gagal tetap hidup, unhealthy, tanpa restart.
docker run -d --name sick --restart unless-stopped \ --health-cmd false --health-interval 2s alpine:3 sleep 600 # about 15 seconds later: docker inspect -f '{{.State.Health.Status}} restarts={{.RestartCount}}' sick docker rm -f sick
Tiga pertanyaan sebelum lanjut
Pilih jawaban untuk melihat kenapa benar atau salah. Tidak ada yang disimpan atau dinilai, jadi tebak saja dengan bebas.
Jawab dalam hati, lalu cek
1.Memori aplikasi menyentuh limit 512 MB dan tidak bisa diambil kembali. Apa yang terjadi, dan apa yang menyalakannya lagi?Lihat jawabannya
Jawaban:OOM killer kernel menghentikan proses dengan SIGKILL, Docker mencatat exit code 137 (128 ditambah 9), dan restart: unless-stopped menyalakannya lagi. docker events menampilkan oom, die 137, lalu start.
2.Health check sudah menyatakan unhealthy selama sepuluh menit. Apakah Docker sudah me-restart container-nya?Lihat jawabannya
Jawaban:Tidak. Health check yang gagal mengubah status, tetapi restart policy hanya bertindak saat proses exit. Monitor, peringatan, atau deploy yang menunggu status healthy yang harus menindaklanjutinya.
3.Kenapa --max-old-space-size diisi 384, bukan 512, padahal limitnya 512 MB?Lihat jawabannya
Jawaban:Karena proses juga memakai memori di luar heap V8. Jika batas heap sama dengan limit, kernel akan mematikan container sebelum garbage collection V8 di bawah tekanan sempat membantu.
Bahasan mendalam terkait di situs ini
Sumber
Sumber primer yang diperiksa pada tanggal riset. Ikuti tautannya untuk versi terbaru.
- Dokumentasi Docker: referensi service Compose (restart, mem_limit, healthcheck, ports, container_name)
- Dokumentasi Docker: menjalankan container otomatis (restart policy)
- Dokumentasi Docker: batas resource
- Dokumentasi kernel Linux: Control Group v2 (memory.max)
- Dokumentasi Node.js: --max-old-space-size
- Dokumentasi Node.js: process.memoryUsage() (heap dan Buffer)
- Dokumentasi Docker: HEALTHCHECK
- Dokumentasi Docker: docker compose up (--wait)
- Dokumentasi Docker: publish port
- Dokumentasi Docker: packet filtering dan firewall (ufw)
- Dokumentasi Docker: logging driver JSON File
- Dokumentasi Docker: volume