Publish Port Loopback di Compose Ternyata Nggak Cukup
Port di-publish loopback tapi klien di host yang sama tetap ditolak. Ternyata Docker nyekip pintu per alamat tujuan, bukan per proses.
Jumat sore saya nambahin satu service ke docker-compose di mesin kerja: headroom, proxy kompresi token buat AI. Image-nya ghcr.io/headroomlabs-ai/headroom. Tugasnya duduk di depan trafik LLM dan menyusutkan context sebelum sampai ke provider, semuanya diproses lokal [5]. Router yang pakai jalan native di host, jadi keputusannya gampang: service ini cuma boleh dilewati dari host. Port-nya saya publish loopback-only.
Test pertama lancar. curl ke loopback dari terminal host, jawaban normal. Pas router LLM-nya diarahkan ke proxy dan nyodorkan satu path, /v1/compress, permintaannya ditolak. Yang bikin kesal: client-nya emang ada di host yang sama. Bukan pindah mesin, bukan dari internet, bukan VPN aneh-aneh.
Dugaan pertama: salah config aplikasi. Saya curiga ke arah sana duluan. Typo di URL, path salah, config belum ke-reload, semua saya cek. Aman semua. Baru deh saya buka dokumentasi jaringan Docker, dan ketemu fakta yang mengubah cara saya pandang port publishing.
Yang dicek Docker bukan prosesnya, tapi pintunya
Default publish port Docker itu terbuka. Kalo kita publish tanpa nempel-in alamat host, kayak -p 8080:80, port-nya dipublish ke semua alamat host, IPv4 dan IPv6 [2] [3]. Docker sendiri nyebut perilaku default ini insecure by default: port yang di-publish bisa diakses bukan cuma dari Docker host, tapi juga dari luar [6].
Nempel-in alamat loopback di depan itu filter. Docs-nya blak-blakan: sertakan alamat loopback di flag publish, dan cuma Docker host yang bisa akses port itu [6]. Nah, frasa "dari Docker host" di sini yang tadinya saya salah baca. Yang dicek bukan "prosesnya jalan di host atau nggak", tapi alamat yang dituju dan jalur yang dipakai traffic itu buat nyampai.
Client saya nyari host lewat interface bridge-nya; dari sisi container, host kelihatan sebagai alamat gateway jaringan bridge-nya [1], dan ini jadi titik baliknya. Traffic yang masuk lewat pintu itu bukan traffic loopback. Rule publish loopback saya cuma nampung traffic yang dialamatkan ke loopback host. Sisanya nyangkut, ditolak dua lapisan yang sama-sama bilang "kamu bukan lokal".
Bonus yang bikin merinding dikit: di rilis lama, sebelum Docker 28.0.0, host lain dalam satu segmen L2 bahkan bisa nyentuh port yang di-publish ke loopback [6]. Udah dipatch, tapi jadi pengingat kalo "loopback" bukan mantra sakti yang bikin semua aman otomatis.
Tiga pintu keluar, saya pilih yang paling sempit
Ada tiga jalan umum buat kasus kayak gini. Pertama, publish ke semua interface terus pasrah ke auth aplikasi. Kedua, sambungin client dan proxy ke satu user-defined bridge network, terus panggil pakai nama service; container di network yang sama emang bisa saling akses langsung [2]. Ketiga, biarin publish tetap loopback-only, dan buka jalurnya di layer aplikasi.
Saya ambil yang ketiga. Di service compose-nya [4], cuma dua blok yang disentuh:
headroom:
image: ghcr.io/headroomlabs-ai/headroom:latest
ports:
- "127.x.x.x:8787:8787" # loopback-only: host saja
environment:
HEADROOM_HOST: "0.0.0.0"
# buka SATU route buat klien yang datang lewat bridge:
HEADROOM_COMPRESS_ALLOW_REMOTE: "1"Env var terakhir itu punya headroom, bukan Docker: dia nyuruh aplikasi ngizinkan klien remote di route /v1/compress aja. Port di sisi host tetap kekunci di loopback, sisanya ngikutin rule publish yang udah ada [6].
Kenapa nggak yang paling gampang? Karena yang saya mau bukan "bisa diakses", tapi "bisa diakses sekecil mungkin". Permukaan yang kebuka cuma satu path, auth tetap jalan normal, dan gak ada port baru yang nongol di interface lain. Kalo besok ada route lain yang butuh akses dari bridge, tinggal ditambahin eksplisit, satu-satu, dengan sadar.
Sekarang kalo ada koneksi ditolak di setup Docker saya, daftar curiga saya udah beda. Bukan cuma "aplikasinya error", tapi juga: traffic ini lewat pintu mana, dan pintu itu ngarepnya nampung siapa. Pertanyaan yang kelihatannya sepele, tapi di Docker jawabannya nentuin banget.
Sumber
Sources:
[1] https://docs.docker.com/engine/network
[2] https://docs.docker.com/engine/network/drivers/bridge
[3] https://docs.docker.com/reference/cli/docker/container/run
[4] https://docs.docker.com/reference/compose-file/services
[5] https://github.com/headroomlabs-ai/headroom
[6] https://docs.docker.com/engine/network/port-publishing