Skip to main content
DevOps dari nol · Pelajaran 5 dari 8 · 13 menit

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.
Lihat cara kerjanya

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.

Langkah 1 dari 7. Satu container dengan batas memori. Kernel menegakkan batas itu, Docker engine menjalankan restart policy, dan health check memeriksa aplikasinya.

Tombol: ← → langkah · Home / End · K putar atau jeda

  1. Satu container dengan batas memori. Kernel menegakkan batas itu, Docker engine menjalankan restart policy, dan health check memeriksa aplikasinya.

  2. 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: 512m
  3. Buffer 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)
  4. 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.

  5. 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-stopped
  6. Setiap 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)
  7. 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)

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)

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)

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)

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.

Cara situs ini melakukannya

Konfigurasi nyata di balik pelajaran ini

Kutipan dari repositori yang membangun dan men-deploy situs ini, apa adanya.

docker-compose.yml:8-11
    # Matches the existing container so deploys UPDATE it in place (no duplicate).
    container_name: matthewswong-portfolio
    restart: unless-stopped
    env_file: .env

Restart 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.

docker-compose.yml:24-28
    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.

Dockerfile:47-56
# 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.

docker-compose.yml:29-47
    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.

Yang sering rusak

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.

Restart policy mana melakukan apa
no (default)Tetap matiTetap matiTetap mati
on-failureDi-restartTetap matiTetap mati
alwaysDi-restartDi-restartMenyala lagi
unless-stoppedDi-restartDi-restartTetap mati
Coba sendiri

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.

  1. 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"]
  2. 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"
  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/
  4. 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)
  5. 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
Kuis singkat

Tiga pertanyaan sebelum lanjut

Pilih jawaban untuk melihat kenapa benar atau salah. Tidak ada yang disimpan atau dinilai, jadi tebak saja dengan bebas.

  1. 1.Container Anda terus mati dengan exit code 137, padahal heap V8 tidak pernah mendekati batas 384 MB-nya. Apa penyebab yang paling mungkin?
  2. 2.Sebuah container memakai restart: unless-stopped. Anda menghentikannya dengan docker stop untuk maintenance, lalu server di-reboot. Apakah container itu berjalan sesudahnya?
  3. 3.Anda mengisi max-file dengan 3 pada log json-file sebuah container yang banyak menulis log, tetapi max-size tidak diisi. Beberapa minggu kemudian disk penuh. Kenapa?
Pikirkan dulu

Jawab dalam hati, lalu cek

  1. 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. 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. 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.

Pelajari lebih dalam

Bahasan mendalam terkait di situs ini