Self-hosting Plane tanpa satu pun port publik
Cerita deploy Plane 13 service di VPS tunggal: cloudflared sidecar jadi satu-satunya pintu publik, state bind-mount, dan host tanpa port publik.
Ringkasan
VPS gue udah sesak container, eh Plane malah maksa buka port 80 dan 443 lewat proxy defaultnya. Gue akalin dengan hapus semua ports, jalanin 13 service internal aja dan public-nya cuma lewat cloudflared tunnel ke proxy bukan port 8000. Enak banget firewall ketutup rapat cuma SSH, nggak usah ngurus certbot dan tetap jalan mulus di lokal.
Satu per satu service-nya naik: web, api, worker, sampai minio. Pas file compose resmi Plane saya buka ulang, di service proxy ada dua baris yang bikin alis berkerut, mapping ke port 80 dan 443 di host. Di VPS yang sama ada belasan kontainer lain, dan aturan main saya emang sederhana: nggak ada port publik sama sekali.
Awalnya saya kira cukup ganti mapping di bagian proxy ke 8080, beres. Saya juga sempat mengira skrip setup.sh edisi komunitas bakal menangani perpindahan port dengan rapi. Ternyata dugaan kedua salah besar.
Alur setup.sh edisi komunitas memang mengunci LISTEN_HTTP_PORT ke 80 dan LISTEN_HTTPS_PORT ke 443 [1]. Nggak ada opsi \"tanpa port\". Dari sisi vendor ini masuk akal sih, instalasinya memang didesain langsung expose dan tinggal diarahkan ke domain. Cuma buat VPS kecil yang udah penuh service lain, model begitu rasanya kayak nyewa kamar tapi dipaksa pasang pintu baru di tembok yang nggak perlu.
Sedikit konteks: Plane itu platform manajemen proyek open-source, alternatif Jira, Linear, sampai ClickUp, lisensinya AGPLv3 [3]. Kebutuhan resminya 2 core CPU dan 4 GB RAM, dengan rekomendasi 8 GB buat produksi [1]. Dokumentasinya juga mengingatkan supaya self-host produksi pakai database dan storage eksternal, karena mengandalkan disk lokal aja berisiko kehilangan data pas mesin bermasalah [1]. Dengan resource yang udah dimakan segitu, nambah reverse proxy di level host rasanya makin berat aja.
Stack 13 service tanpa port host
Solusinya bukan melawan default-nya, tapi menghapus kebutuhan port host-nya. Stack yang saya adaptasi dari deployment komunitas makeplane/plane terdiri dari 13 service: web, space, admin, live, api, worker, beat worker, migrator, postgres, redis, rabbitmq, minio, dan proxy. Semua state yang harus awet di-bind mount ke direktori data dengan user id tetap, postgres 70, redis dan rabbitmq 999, uploads 1000, biar izin file nggak berantakan pas kontainer di-recreate.
cloudflared jalan sebagai sidecar di compose profile terpisah bernama tunnel. Image-nya saya bikin sendiri: binary-nya disalin dari image distroless resmi ke image alpine mungil, lalu di-pin pakai digest. Enaknya, container ini bisa idle tanpa TUNNEL_TOKEN. File compose yang sama tetap jalan normal di lokal tanpa mengaktifkan profile tunnel.
services:
proxy:
image: makeplane/plane-proxy
# service ini mainnya di jaringan compose aja,
# tanpa mapping port ke host sama sekali
cloudflared:
image: cloudflare/cloudflared
profiles: ["tunnel"]
command: tunnel --no-autoupdate run
env_file:
- .env
# nggak ada blok ports: yang datang ke dia malah tunnel
Nggak ada satu pun blok ports di service cloudflared. Satu-satunya port yang nempel di host cuma port debug buat triage, diikat ke antarmuka loopback. Jalur publiknya murni lewat tunnel: di dashboard Cloudflare, tunnel dibuat, command instalasinya dijalanin di server, terus route diterbitkan dengan Service URL yang menunjuk ke service proxy di dalam jaringan compose [2].
Jebakan port 8000
Ini bagian yang hampir bikin saya salah forward. Contoh Service URL di dokumentasi Cloudflare menunjuk sebuah service di port 8000 [2]. Angka 8000 di stack Plane ternyata bukan pintu depan: itu port internal container api, dan compose resminya bahkan nggak nerbitkan port itu ke luar sama sekali [4]. Kalau contoh dokumentasi diteladani mentah-mentah, tunnel cuma nembak API, sementara halaman web, auth, dan static gagal semua. Service URL yang benar menuju proxy, satu-satunya service yang emang didesain jadi pintu masuk semua rute.
Model ini yang saya pertahankan
Keputusan saya tegas: buat VPS dengan admin tunggal, tunnel sebagai satu-satunya pintu publik itu pilihan paling masuk akal. Firewall host bisa ditutup buat inbound, cuma SSH yang lolos. Arah koneksinya dari server menuju edge Cloudflare, bukan sebaliknya. Nggak ada certbot yang perlu diurus, nggak ada config nginx tambahan di host yang rawan salah ketik, TLS beres di edge.
Buktinya gampang dicek:
docker compose ps --format "table {{.Name}}\t{{.Ports}}"
Kolom Ports kosong, atau cuma ada satu binding loopback, artinya stack-nya bersih. Kalau muncul port lain yang nempel di semua interface, ada yang kelewat.
Dulu saya ragu pendekatan ini malah nambah kompleksitas. Kenyataannya kebalikannya: aturan firewall makin pendek, komponen yang expose sesuatu makin sedikit, dan file compose yang sama tetap bisa dipakai di laptop tanpa tunnel. Kalau besok ada service lain yang mau naik ke VPS ini, templatenya udah siap: compose tanpa blok ports, tunnel opsional, satu port debug di loopback buat triage.