Ringkasan Utama
Cara setting nginx reverse proxy untuk aplikasi Docker: ikat container ke 127.0.0.1, tulis server block dengan proxy_pass ke port itu, teruskan header Host dan X-Forwarded-For, lalu uji dengan nginx -t sebelum reload. Di belakang Cloudflare, pakai mode SSL Full (strict) dan set_real_ip_from agar tidak ada redirect loop dan IP pengunjung tetap asli.
Aplikasimu sudah jalan di container dan bisa dibuka lewat 203.0.113.10:3000. Lalu kamu mengarahkan domain, dan yang muncul adalah halaman Welcome to nginx!, 502 Bad Gateway, atau halaman yang berputar sampai browser menyerah. Di titik ini kebanyakan pemula sadar bahwa memasang nginx baru separuh pekerjaan. Separuh lainnya adalah menyambungkan nginx ke container, ke domain, dan ke Cloudflare dengan benar.
Panduan ini memakai pola yang sama dengan situs ini: docker-compose.yml mengikat aplikasi ke 127.0.0.1 dengan komentar bahwa nginx yang menangani TLS dan meneruskan request ke port itu. Konfigurasi nginx situs ini sendiri tidak ada di repository, jadi semua blok di bawah bersumber dari dokumentasi nginx.org dan Cloudflare, bukan salinan file server saya. Ikuti enam langkahnya berurutan, lalu pakai tabel error di akhir kalau ada yang tidak jalan.
Ingin paham konsepnya dulu?
Panduan ini fokus pada langkah dan error. Kalau kamu lebih suka belajar urutan dari konsep ke praktik, lesson gratis kami membahas reverse proxy, header, dan cache di Cloudflare: Reverse Proxy Nginx dan Docker Compose di Balik Cloudflare. Lesson itu juga punya latihan yang bisa dijalankan di laptop tanpa server.
Yang kamu butuhkan
Siapkan empat hal ini sebelum mulai. Semua nama di bawah adalah placeholder, ganti dengan milikmu.
- Sebuah server Linux (Debian atau Ubuntu) dengan akses sudo, misalnya dengan user bernama deploy dan alamat 203.0.113.10.
- Docker dan Docker Compose sudah terpasang, dan aplikasimu sudah punya image yang bisa dijalankan (di sini bernama my-app:latest, mendengarkan di port 3000).
- Sebuah domain, di sini example.com, yang DNS-nya dikelola di Cloudflare.
- Port 80 dan 443 terbuka di firewall server.
Langkah 1: Jalankan app di container, tapi hanya di loopback
Aturan pertama: container tidak boleh bisa dijangkau langsung dari internet. Di docker-compose.yml situs ini, port aplikasi diikat ke 127.0.0.1 supaya satu-satunya jalan masuk dari luar adalah lewat nginx. Pola yang sama berlaku untuk appmu. Di sisi kiri titik dua pada ports kamu menentukan alamat dan port di host, di sisi kanan port di dalam container.
# docker-compose.yml
services:
web:
image: my-app:latest
restart: unless-stopped
ports:
# Wrong: "3000:3000" publishes on every interface, so anyone can skip nginx
# Right: left side = host address + port, right side = port in the container
- "127.0.0.1:3000:3000"
# Start it, then check the PORTS column and the app itself
docker compose up -d
docker compose ps
curl -I http://127.0.0.1:3000Output yang diharapkan: Output yang diharapkan dari docker compose ps: kolom PORTS menampilkan 127.0.0.1:3000->3000/tcp, bukan 0.0.0.0:3000. Lalu curl -I ke alamat itu membalas status 200 (atau 3xx kalau app-mu me-redirect).
Kesalahan paling umum di langkah ini adalah menulis 3000:3000 tanpa alamat. Port jadi terbuka di semua interface, siapa pun bisa melewati nginx dan Cloudflare, dan semua pengaturan keamanan yang kamu buat di langkah berikutnya tidak berlaku untuk jalur itu.
Langkah 2: Pasang nginx dan pastikan hidup
Di Debian atau Ubuntu, nginx tersedia dari repository bawaan. Setelah terpasang, service-nya langsung berjalan dan melayani halaman sambutan di port 80. Itu justru berguna: kalau halaman itu muncul, berarti nginx sehat dan sisa masalah ada di konfigurasimu.
sudo apt install -y nginx
nginx -v
sudo systemctl status nginx
curl -I http://127.0.0.1Output yang diharapkan: Output yang diharapkan: nginx -v mencetak nginx version: nginx/ diikuti nomor versi yang terpasang, systemctl status menampilkan active (running), dan curl ke 127.0.0.1 membalas 200 dengan isi halaman Welcome to nginx!.
Langkah 3: Tulis server block dengan proxy_pass
Reverse proxy di nginx pada dasarnya satu directive: proxy_pass, yang menentukan protokol dan alamat server tujuan. Letakkan di dalam location di sebuah server block. File konfigurasi utama nginx.conf biasanya berada di /etc/nginx, dan paket Debian atau Ubuntu memuat file di conf.d dari sana. Pastikan dengan nginx -T seperti di bawah, karena tata letak bisa berbeda antar distro.
# See which files this nginx really loads (layout differs per distro)
sudo nginx -T | grep include
# /etc/nginx/conf.d/example.com.conf
server {
listen 80;
server_name example.com www.example.com;
location / {
# No trailing slash and no path: the client's URI is passed on unchanged
proxy_pass http://127.0.0.1:3000;
}
}
# Test first, reload second. A failed reload keeps the old config running.
sudo nginx -t
sudo nginx -s reload
# Fake the domain locally to prove the server block matches
curl -I -H "Host: example.com" http://127.0.0.1Selalu jalankan nginx -t sebelum reload. Perintah itu memeriksa sintaks dan mencoba membuka file yang disebut di konfigurasi. Saat reload, master process memeriksa konfigurasi baru dulu; kalau gagal, nginx membatalkan perubahan dan tetap memakai konfigurasi lama, jadi reload yang salah tidak menjatuhkan situsmu.
Output yang diharapkan: Output yang diharapkan: nginx -t mencetak dua baris yang berakhir dengan syntax is ok dan test is successful. Setelah reload, curl dengan header Host example.com ke 127.0.0.1 membalas persis seperti app-mu, bukan halaman sambutan.
Langkah 4: Teruskan header yang benar
Secara default nginx tidak meneruskan header Host dari request asli. Ia mengirim Host berisi alamat upstream-nya sendiri, yaitu 127.0.0.1:3000. Akibatnya app membuat link absolut, cookie, atau redirect ke alamat itu, dan semuanya rusak di browser pengunjung. Tambahkan empat baris proxy_set_header agar app tahu domain, IP, dan protokol yang sebenarnya.
location / {
proxy_pass http://127.0.0.1:3000;
# Wrong: leave these out and the app sees Host: 127.0.0.1:3000
# Right: pass what the visitor asked for
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Trailing slash trap
# Wrong (if the app expects /api/users): /api/users becomes /users
location /api/ {
proxy_pass http://127.0.0.1:3000/;
}
# Right: no URI after the port, the path is forwarded as sent
location /api/ {
proxy_pass http://127.0.0.1:3000;
}Dua jebakan lagi. Pertama, proxy_set_header hanya diwarisi dari level di atasnya kalau tidak ada proxy_set_header sama sekali di level saat ini, jadi jangan menyebar header di server dan location sekaligus. Kedua, garis miring di akhir proxy_pass mengubah arti: tanpa path, URI dari klien diteruskan apa adanya; dengan path atau garis miring, bagian yang cocok dengan location diganti.
Langkah 5: Sambungkan domain lewat Cloudflare dan pilih mode SSL yang benar
Di dashboard Cloudflare, buat record A untuk example.com ke 203.0.113.10 dengan proxy aktif. Lalu buka pengaturan SSL/TLS dan pilih mode. Menurut dokumentasi Cloudflare, mode Flexible mengenkripsi pengunjung ke Cloudflare saja, sedangkan Cloudflare ke origin tetap HTTP. Full (strict) memvalidasi sertifikat di origin, yang bisa dari CA publik seperti Let's Encrypt atau dari Cloudflare Origin CA. Cloudflare menyarankan Full atau Full (strict) jika memungkinkan.
Artinya nginx perlu mendengarkan di 443 dengan sertifikat. Cara memasangnya otomatis dibahas di perbandingan Caddy dan nginx untuk HTTPS otomatis. Kalau kamu tidak mau membuka port ke internet sama sekali, ada jalur lain lewat Cloudflare Tunnel. Di sini kita tetap memakai nginx dengan port terbuka.
# Cloudflare dashboard
# DNS: A example.com -> 203.0.113.10 (proxy on)
# SSL/TLS: Full (strict)
# /etc/nginx/conf.d/example.com.conf
server {
listen 80;
server_name example.com www.example.com;
# Safe with Full (strict). With Flexible this line causes a redirect loop.
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name example.com www.example.com;
# Placeholder paths: use the certificate you issued (public CA or Cloudflare Origin CA)
ssl_certificate /etc/ssl/example.com/fullchain.pem;
ssl_certificate_key /etc/ssl/example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Mode Flexible plus redirect HTTP ke HTTPS di nginx adalah penyebab klasik ERR_TOO_MANY_REDIRECTS. Cloudflare mengirim HTTP, nginx menjawab redirect ke HTTPS, Cloudflare mengirim HTTP lagi, dan seterusnya. Pilih satu: Full (strict) dengan redirect, atau tanpa redirect di nginx.
Langkah 6: Pulihkan IP asli pengunjung
Di belakang Cloudflare, setiap koneksi ke nginx datang dari alamat Cloudflare, jadi log, rate limit, dan firewall hanya melihat proxy. Cloudflare menaruh IP asli pengunjung di header CF-Connecting-IP. Modul realip di nginx menukarnya: real_ip_header menyebut nama header, dan set_real_ip_from mendaftar alamat yang dipercaya untuk mengirimnya.
# 1. Is the realip module built in? (it is not on by default)
nginx -V 2>&1 | grep -o with-http_realip_module
# 2. /etc/nginx/conf.d/cloudflare-realip.conf (http context)
# One line per range from https://www.cloudflare.com/ips/ , nothing else.
set_real_ip_from <range-1-from-cloudflare>;
set_real_ip_from <range-2-from-cloudflare>;
real_ip_header CF-Connecting-IP;
# Wrong: trusts the header from the whole internet, so anyone can fake their IP
# set_real_ip_from 0.0.0.0/0;
# 3. Reload, then watch the log
sudo nginx -t && sudo nginx -s reload
sudo tail -n 5 /var/log/nginx/access.logDaftar tepercaya itu bagian keamanannya. Isi hanya dengan rentang yang dipublikasikan Cloudflare di halaman IP mereka, dan perbarui secara rutin, seperti yang disarankan dokumentasinya. Modul realip tidak ikut di-build secara default, jadi cek dulu apakah nginx-mu punya with-http_realip_module.
Output yang diharapkan: Output yang diharapkan: perintah pengecekan mencetak with-http_realip_module. Setelah reload, baris baru di access.log menampilkan IP pengunjung, bukan alamat dari rentang Cloudflare.
Error umum dan cara memperbaikinya
Hampir semua masalah reverse proxy jatuh ke sembilan gejala ini. Cari gejalamu di kolom pertama, lalu cek sebabnya sebelum mengubah apa pun.
| Gejala | Penyebab | Perbaikan |
|---|---|---|
| 502 Bad Gateway dengan halaman bertuliskan nginx | nginx hidup tetapi tidak bisa menyambung ke app: container mati, belum siap, atau port di proxy_pass salah | Jalankan docker compose ps lalu curl -I http://127.0.0.1:3000. Samakan port di proxy_pass dengan angka sebelum titik dua pada ports. Lihat baris terakhir error.log untuk pesan upstream-nya. |
| 502 atau 504 dengan halaman bermerek Cloudflare | Menurut dokumentasi Cloudflare, ini kasus yang paling umum: error datang dari origin, dan Cloudflare hanya meneruskannya | Debug seperti baris pertama: yang bermasalah nginx atau app di server, bukan Cloudflare. Halaman kosong tanpa merek Cloudflare adalah kasus lain, misalnya kompresi gzip yang rusak di origin. |
| 504 Gateway Timeout setelah menunggu lama | App butuh lebih dari 60 detik untuk menjawab; proxy_read_timeout bawaan nginx adalah 60s | Perbaiki endpoint yang lambat lebih dulu. Kalau memang harus lama, naikkan proxy_read_timeout hanya di location itu, bukan di seluruh server. |
| ERR_TOO_MANY_REDIRECTS di browser | Mode SSL Cloudflare Flexible mengirim HTTP ke origin, lalu nginx me-redirect HTTP ke HTTPS, dan kedua sisi saling memantulkan request | Pasang sertifikat di nginx dan ganti mode ke Full (strict). Atau, kalau tetap Flexible, hapus redirect HTTP ke HTTPS di nginx. |
| Error 521: web server is down | Cloudflare tidak bisa menyambung ke origin; biasanya nginx mati, atau port 80 dan 443 tertutup firewall | Di server jalankan sudo systemctl status nginx dan curl -I http://127.0.0.1. Pastikan firewall membuka port 80 dan 443. Dokumentasi Cloudflare juga menyarankan memeriksa log proxy atau firewall di antaranya. |
| Yang muncul halaman Welcome to nginx! | File konfigurasimu tidak dimuat, server_name tidak cocok dengan domain, atau kamu lupa reload | Jalankan sudo nginx -T dan cari nama domainmu di outputnya. Kalau tidak ada, file-nya ada di folder yang tidak di-include. Setelah memperbaiki, sudo nginx -s reload. |
| Link atau redirect di app mengarah ke 127.0.0.1:3000 | Header Host tidak diteruskan, jadi app menerima nilai bawaan nginx, yaitu alamat upstream-nya sendiri | Tambahkan proxy_set_header Host $host; di location yang memakai proxy_pass, lalu reload. |
| Log nginx hanya berisi alamat IP Cloudflare | Semua koneksi datang dari Cloudflare, dan IP asli pengunjung ada di header CF-Connecting-IP | Aktifkan modul realip: set_real_ip_from untuk setiap rentang Cloudflare dan real_ip_header CF-Connecting-IP (Langkah 6). |
| 404 di path yang di app jelas ada | proxy_pass memakai URI (ada garis miring di akhir), sehingga nginx mengganti bagian path yang cocok dengan location | Tulis proxy_pass tanpa garis miring dan tanpa path kalau path dari klien harus diteruskan apa adanya. |
Kalau gejalamu tidak ada di tabel, debug dari dalam ke luar. Urutan ini memisahkan app, nginx, dan Cloudflare satu per satu, jadi kamu tahu lapisan mana yang salah.
# 1. Is the app alive on its own?
docker compose ps
curl -I http://127.0.0.1:3000
# 2. Is nginx healthy, and which config did it load?
sudo systemctl status nginx
sudo nginx -t
# 3. Does nginx reach the app? Read what it says about the upstream
curl -I -H "Host: example.com" http://127.0.0.1
sudo tail -n 20 /var/log/nginx/error.log
# 4. Only now look outside: DNS record, SSL mode, then the real URL
curl -I https://example.comKalau langkah 1 gagal, masalahnya di app atau container. Kalau langkah 1 lolos tetapi langkah 3 gagal, masalahnya di konfigurasi nginx. Kalau keduanya lolos tetapi domain tetap error, lihat pengaturan DNS dan mode SSL di Cloudflare.
Checklist sebelum kamu menyebut selesai
- Port container hanya terikat ke 127.0.0.1 dan docker compose ps membuktikannya.
- sudo nginx -t lulus sebelum setiap reload.
- proxy_set_header Host $host dan X-Forwarded-Proto ada di location yang memakai proxy_pass.
- Mode SSL Cloudflare adalah Full (strict) dan tidak ada redirect loop.
- set_real_ip_from hanya berisi rentang Cloudflare, dan log menampilkan IP pengunjung.
- Kamu tahu urutan debug: app, nginx, lalu Cloudflare.
Belajar lebih dalam
Panduan ini sengaja berupa resep. Kalau kamu ingin tahu kenapa Cloudflare menyimpan file ber-hash selama setahun tetapi tidak pernah menyimpan HTML, dan apa arti DYNAMIC pada header cf-cache-status, lanjutkan ke lesson gratis berikut:
Reverse Proxy Nginx dan Docker Compose di Balik Cloudflare
Lesson itu menjalankan nginx dan app di laptop dengan Docker Compose, jadi kamu bisa mengulang percobaannya tanpa menyentuh server produksi.
Sumber dan dokumentasi resmi

