Cara Menyiapkan VPS untuk Pemula, dari Seorang Pemula (Bagian 4: Traefik dan HTTPS)
Bagian 4 dari penyiapan VPS pertama saya di Debian 13. Traefik sebagai reverse proxy rootless, dengan port yang diserahkan systemd, sertifikat Let's Encrypt otomatis, HTTP/3, security header, dan rute yang berasal dari label kontainer.
Seri 5 bagian.
Sampai Mana Kita
Setelah Bagian 3, server bisa menjalankan kontainer yang bertahan setelah reboot. Tapi belum ada yang bisa menjangkaunya dari luar, dan belum ada HTTPS.
Itu tugas reverse proxy. Ia satu-satunya yang mendengarkan di port 80 dan 443. Ia mengambil sertifikat dari Let’s Encrypt, dan meneruskan setiap request ke kontainer yang tepat berdasarkan domainnya.
Kenapa Traefik
Saya pilih Traefik karena ia bisa menemukan aplikasi sendiri. Ia memantau Podman, dan begitu ada kontainer yang menyala dengan beberapa label yang bilang “saya app.example.com, saya mendengarkan di port 3000”, Traefik menambahkan rutenya dan mengambil sertifikat untuknya. Begitu kontainernya berhenti, rutenya hilang.
Itu penting untuk Bagian 5, di mana aplikasi di-deploy di setiap git push. Saya tidak mau mengedit berkas config proxy manual setiap kali ada aplikasi baru.
Membuat Traefik Bisa Melihat Kontainer
Traefik mengenal kontainer lewat Docker API. Podman bisa melayani API yang sama, dari socket milik pengguna saya:
systemctl --user enable --now podman.socket
ls $XDG_RUNTIME_DIR/podman/podman.sock/run/user/1000/podman/podman.sockPerlu diingat, siapa pun yang bisa bicara ke socket ini bisa mengendalikan semua kontainer saya. Traefik butuh socket itu, jadi di bawah nanti kontainer Traefik sendiri saya kunci serapat mungkin.
Jaringan untuk Aplikasi Publik
Traefik dan aplikasi yang dilayaninya butuh jaringan bersama. Quadlet juga bisa membuat jaringan:
nano ~/.config/containers/systemd/web.network[Network]
NetworkName=webHanya kontainer yang memang publik yang masuk ke web. Database tidak pernah, jadi Traefik tidak bisa menjangkaunya walaupun tidak sengaja.
Sertifikat harus bertahan melewati restart, jadi ia dapat named volume:
nano ~/.config/containers/systemd/traefik-acme.volume[Volume]
VolumeName=traefik-acmeConfig Traefik
Traefik punya dua jenis config. Yang statis dibaca sekali saat start: port, tempat mencari kontainer, cara mengambil sertifikat.
mkdir -p ~/.config/traefik/dynamic
nano ~/.config/traefik/traefik.ymlglobal:
checkNewVersion: false
sendAnonymousUsage: false
log:
level: WARN
format: json
entryPoints:
web:
address: ':80'
http:
redirections:
entryPoint:
to: websecure
scheme: https
permanent: true
aliasHeadersStrategy: delete
encodedCharacters:
allowEncodedBackSlash: false
allowEncodedNullCharacter: false
websecure:
address: ':443'
http3: {}
http:
tls:
certResolver: le
middlewares:
- secure@file
aliasHeadersStrategy: delete
encodedCharacters:
allowEncodedBackSlash: false
allowEncodedNullCharacter: false
providers:
docker:
endpoint: 'unix:///var/run/docker.sock'
exposedByDefault: false
network: web
watch: true
file:
directory: /etc/traefik/dynamic
watch: true
certificatesResolvers:
le:
acme:
email: 'you@example.org'
storage: /acme/acme.json
httpChallenge:
entryPoint: web
api:
dashboard: falseBagian pentingnya:
- Dua entry point.
webdi port 80 cuma mengalihkan ke HTTPS (dan menjawab pengecekan Let’s Encrypt).websecuredi 443 tempat semuanya dilayani, dengan HTTP/3 menyala. - Setiap rute HTTPS memakai middleware
secure, yang didefinisikan di bawah. Aplikasi baru tidak bisa lupa security header-nya karena ia memang tidak perlu menambahkannya. exposedByDefault: false. Kontainer hanya dapat rute kalau ia memintanya dengantraefik.enable=true.leadalah setup Let’s Encrypt. Isi dengan e-mail asli; Let’s Encrypt memakainya untuk memperingatkan sertifikat yang hampir kedaluwarsa.aliasHeadersStrategydanencodedCharactersmembuang beberapa trik dengan header aneh dan karakter ter-encode di URL yang bisa membingungkan aplikasi di belakang proxy.- Tanpa dashboard. Saya tidak butuh, dan itu satu hal yang tidak perlu terbuka.
Selama masih mencoba-coba, tambahkan caServer: 'https://acme-staging-v02.api.letsencrypt.org/directory' di bawah acme. Let’s Encrypt punya rate limit, dan beberapa percobaan gagal berturut-turut bisa bikin kita diblokir sementara. Server staging memberi sertifikat palsu tanpa batas. Hapus barisnya begitu semuanya jalan.
Config dinamis adalah jenis yang satunya. Traefik memuatnya ulang setiap kali berkasnya berubah:
nano ~/.config/traefik/dynamic/00-defaults.ymlhttp:
middlewares:
secure:
chain:
middlewares:
- security-headers
- rate-limit
- compress
security-headers:
headers:
stsSeconds: 31536000
stsIncludeSubdomains: true
contentTypeNosniff: true
customFrameOptionsValue: SAMEORIGIN
referrerPolicy: strict-origin-when-cross-origin
permissionsPolicy: 'camera=(), microphone=(), geolocation=(), payment=()'
customResponseHeaders:
Server: ''
X-Powered-By: ''
rate-limit:
rateLimit:
average: 100
burst: 200
period: 1s
compress:
compress:
minResponseBodyBytes: 1024
tls:
options:
default:
minVersion: VersionTLS12
sniStrict: true
cipherSuites:
- TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
- TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
- TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305
- TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305secure itu tiga middleware yang dirangkai:
- Security header. HSTS menyuruh browser selalu memakai HTTPS untuk domain ini. Yang lain mencegah MIME sniffing dan framing oleh situs lain, membatasi apa yang bocor lewat referrer, dan mematikan akses kamera, mikrofon, dan lokasi.
ServerdanX-Powered-Bydihapus supaya response tidak mengumumkan apa yang sedang berjalan. - Rate limit. 100 request per detik per IP klien, dengan lonjakan sampai 200. Lebih dari cukup untuk manusia, dan memperlambat apa pun yang menggempur server.
- Kompresi untuk response di atas 1 KB.
Bagian tls mengizinkan TLS 1.2 ke atas dengan cipher modern saja. sniStrict menolak koneksi yang tidak menyebut salah satu domain saya, jadi orang yang memindai IP-nya langsung tidak dapat apa-apa.
Membiarkan systemd yang Membuka Port
Bagian ini yang paling lama saya kerjakan. Saya mau Traefik melihat IP asli setiap pengunjung, karena rate limit dan log bergantung padanya.
Jadi Traefik tidak membuka port 80 dan 443 sama sekali. Yang membukanya systemd milik pengguna saya (diizinkan sejak perubahan ip_unprivileged_port_start di Bagian 3), lalu socket yang sudah terbuka itu diserahkan ke Traefik saat ia menyala. Koneksi langsung sampai ke Traefik, tanpa ada yang di tengah yang mengubah asal-usulnya. Traefik mendukung ini langsung: ia mencocokkan setiap socket dengan entry point berdasarkan namanya.
nano ~/.config/systemd/user/traefik-http.socket[Unit]
Description=Traefik :80 (HTTP → HTTPS redirect, ACME challenges)
[Socket]
ListenStream=80
FileDescriptorName=web
Service=traefik.service
[Install]
WantedBy=sockets.targetnano ~/.config/systemd/user/traefik-https.socket[Unit]
Description=Traefik :443 (HTTPS over TCP, HTTP/3 over UDP)
[Socket]
ListenStream=443
ListenDatagram=443
FileDescriptorName=websecure
Service=traefik.service
[Install]
WantedBy=sockets.targetFileDescriptorName harus sama dengan nama entry point di traefik.yml, yaitu web dan websecure. ListenDatagram=443 adalah socket UDP untuk HTTP/3.
Kontainer Traefik
nano ~/.config/containers/systemd/traefik.container[Unit]
Description=Traefik reverse proxy (socket-activated)
Requires=traefik-http.socket traefik-https.socket podman.socket
After=traefik-http.socket traefik-https.socket podman.socket
[Container]
Image=docker.io/library/traefik:v3.7
ContainerName=traefik
Network=web.network
Notify=true
AutoUpdate=registry
NoNewPrivileges=true
ReadOnly=true
DropCapability=ALL
PidsLimit=256
PodmanArgs=--memory=256m
Volume=%t/podman/podman.sock:/var/run/docker.sock
Volume=%h/.config/traefik/traefik.yml:/etc/traefik/traefik.yml:ro
Volume=%h/.config/traefik/dynamic:/etc/traefik/dynamic:ro
Volume=traefik-acme.volume:/acme
[Service]
Sockets=traefik-http.socket traefik-https.socket
Restart=always
TimeoutStartSec=300
[Install]
WantedBy=default.targetSatu per satu:
Sockets=adalah tempat systemd menyerahkan dua socket tadi.- Socket Podman di-mount di tempat Traefik mengharapkan socket Docker.
%tadalah direktori runtime pengguna saya dan%hdirektori home saya. - Berkas config di-mount read-only.
ReadOnly,DropCapability=ALL,NoNewPrivileges, serta batas memori dan proses mengunci kontainernya. Karena ia bisa menjangkau socket Podman, saya mau ia sekotak mungkin.Notify=truebikin systemd menunggu sampai Traefik bilang siap sebelum start-nya dianggap selesai.AutoUpdate=registrybersamatraefik:v3.7berarti Podman mengecek setiap hari apakah ada imagev3.7yang lebih baru, yaitu rilis patch, lalu me-restart Traefik dengannya. Lompatan ke 3.8 tetap keputusan saya.
Nyalakan pengecekan update hariannya:
systemctl --user enable --now podman-auto-update.timerMenjalankannya
Pastikan tidak ada program lain yang memakai port-nya. Beberapa image provider datang dengan Apache atau nginx yang sudah jalan:
sudo ss -ltnup '( sport = :80 or sport = :443 )'Seharusnya tidak ada yang tercetak selain header. Lalu cek berkas Quadlet, muat semuanya, dan jalankan:
/usr/libexec/podman/quadlet -dryrun -user > /dev/null
systemctl --user daemon-reload
systemctl --user enable --now traefik-http.socket traefik-https.socket
podman pull docker.io/library/traefik:v3.7
systemctl --user start traefik
systemctl --user status traefik● traefik.service - Traefik reverse proxy (socket-activated)
Loaded: loaded (/home/admin/.config/containers/systemd/traefik.container; generated)
Active: active (running) since Tue 2026-10-14 10:12:40 WIB; 3s ago
TriggeredBy: ● traefik-https.socket
● traefik-http.socketImage-nya saya tarik dulu supaya start pertama tidak diam lama karena mengunduh. Kalau gagal, log-nya bilang kenapa:
journalctl --user -u traefik -n 30DNS
Sebelum Let’s Encrypt bisa mengeluarkan sertifikat, domainnya harus mengarah ke server. Di penyedia DNS, tambahkan record A untuk domainnya dengan IP server. Saya juga menambahkan wildcard (*.example.com), supaya subdomain baru apa pun yang saya buat nanti langsung jalan tanpa harus ke pengaturan DNS lagi.
Kalau server punya IPv6, tambahkan record AAAA juga.
Tes dengan Domain Sungguhan
Kontainer whoami yang sama dari Bagian 3, sekarang dengan label, bukan port yang dipublikasikan:
nano ~/.config/containers/systemd/whoami.container[Container]
ContainerName=whoami
Image=docker.io/traefik/whoami
Network=web.network
Label=traefik.enable=true
Label=traefik.http.routers.whoami.rule=Host(`whoami.example.com`)
Label=traefik.http.routers.whoami.entrypoints=websecure
Label=traefik.http.routers.whoami.tls.certresolver=le
Label=traefik.http.services.whoami.loadbalancer.server.port=80
[Install]
WantedBy=default.targetLabel-label itu adalah seluruh config proxy untuk aplikasi ini: domain yang mana, entry point yang mana, ambil sertifikat dari le, dan kirim lalu lintas ke port 80 di dalam kontainer.
systemctl --user daemon-reload
systemctl --user start whoamiBeri beberapa detik untuk mengambil sertifikat, lalu dari laptop:
curl -I https://whoami.example.comHTTP/2 200
alt-svc: h3=":443"; ma=2592000
content-type: text/plain; charset=utf-8
permissions-policy: camera=(), microphone=(), geolocation=(), payment=()
referrer-policy: strict-origin-when-cross-origin
strict-transport-security: max-age=31536000; includeSubDomains
x-content-type-options: nosniff
x-frame-options: SAMEORIGINItu sertifikat yang valid, semua security header dari middleware secure, dan alt-svc yang memberi tahu browser bahwa HTTP/3 tersedia. http://whoami.example.com dialihkan ke HTTPS.
Lalu cek IP aslinya. Tanpa -I, whoami mencetak request yang ia terima:
curl -s https://whoami.example.com | grep X-Real-IpX-Real-Ip: 203.0.113.41Itu alamat publik laptop saya, bukan alamat internal kontainer. Penyerahan socket-nya jalan.
Kalau sertifikatnya tidak kunjung datang, hampir selalu karena DNS belum mengarah ke server, atau port 80 terblokir. Let’s Encrypt mengecek lewat HTTP biasa di port 80. journalctl --user -u traefik menunjukkan error persisnya.
Bersihkan tesnya:
systemctl --user stop whoami
rm ~/.config/containers/systemd/whoami.container
systemctl --user daemon-reloadPosisi Server Sekarang
Traefik memegang port 80 dan 443 lewat systemd dan melihat IP klien yang asli. Sertifikat otomatis, HTTP/3 menyala, setiap situs dapat security header dan rate limit yang sama, dan aplikasi baru cuma butuh beberapa label untuk online. Update patch Traefik terpasang sendiri.
Yang tersisa adalah memasukkan aplikasi saya sendiri ke server tanpa harus SSH dan menyalin berkas ke sana kemari. Itu Bagian 5: deploy dari GitHub di setiap push, dan skrip yang mengerjakan kelima bagian ini sekaligus.