Reverse Proxy Nginx dan Docker Compose di Balik Cloudflare
Pasang nginx di depan aplikasi Docker Compose dan Cloudflare di depan nginx, lalu tentukan per header apa yang boleh di-cache di edge dan apa yang tidak.
Oleh Matthews WongDitinjau pada
Poin utama
Reverse proxy adalah server yang menerima setiap request lebih dulu lalu meneruskannya ke aplikasi Anda. Di sini nginx menangani TLS dan meneruskan request ke container yang hanya terikat ke loopback, sementara Cloudflare berada di depannya sebagai CDN. Header Cache-Control dari aplikasi dan aturan di edge menentukan apa yang boleh disimpan Cloudflare: file ber-hash selama setahun, HTML sama sekali tidak.
Setelah pelajaran ini, Anda bisa
- Mengatur nginx agar meneruskan request dengan proxy_pass ke container di loopback, sambil membawa Host asli dan IP asli pengunjung.
- Membaca cf-cache-status dan Cache-Control untuk membedakan apa yang dilayani edge dari cache dan apa yang sampai ke server Anda.
- Menjelaskan kenapa aset ber-hash boleh di-cache setahun, sedangkan HTML yang berbeda menurut header Accept tidak boleh disimpan di edge.
Ikuti diagramnya langkah demi langkah
Tekan Putar, atau melangkah dengan panah. Setiap langkah juga ditulis di bawah diagram.
Satu request melewati Cloudflare, nginx, dan aplikasi
Request halaman melewati Cloudflare tanpa pengecekan cache dan nginx meneruskannya ke aplikasi di loopback; file ber-hash diambil sekali, disimpan di edge, dan setiap request berikutnya berstatus HIT.
Tombol: ← → langkah · Home / End · K putar atau jeda
Sebuah kunjungan melewati edge Cloudflare, lalu nginx di VPS, lalu container aplikasi yang hanya mendengarkan di loopback. Edge punya cache sendiri.
Request halaman sampai di edge. HTML tidak termasuk yang boleh di-cache di edge situs ini, jadi request langsung diteruskan ke nginx tanpa pengecekan cache: cf-cache-status DYNAMIC.
cf-cache-status: DYNAMIC cache-control: s-maxage=21600, stale-while-revalidate=31514400nginx menangani TLS lalu meneruskan request dengan proxy_pass ke aplikasi di 127.0.0.1. Dengan modul realip, nginx juga memulihkan IP pengunjung dari CF-Connecting-IP.
proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; real_ip_header CF-Connecting-IP;Tebak langkah berikutnya URL yang sama diminta dengan Accept: text/markdown. Apa yang dikembalikan? (Halaman HTML yang sama · Markdown, dari URL yang sama)URL yang sama mengembalikan Markdown kalau header Accept memintanya. Response HTML tidak membawa Vary: Accept, jadi edge yang meng-cache-nya bisa mengirim HTML ke klien yang meminta Markdown.
Accept: text/html → no Vary: Accept Accept: text/markdown → Vary: AcceptChunk di /_next/static berbeda: namanya membawa hash dari isinya. Request pertama berstatus MISS; edge mengambilnya dari aplikasi lalu menyimpan response-nya.
cache-control: public, max-age=31536000, immutableTebak langkah berikutnya Chunk yang sama diminta lagi. Siapa yang menjawab kali ini? (Edge, dari cache-nya · nginx, setelah bertanya lagi ke aplikasi)Setiap request berikutnya dijawab langsung oleh edge: cf-cache-status HIT. nginx dan aplikasi tidak pernah melihatnya, dan deploy baru mengirim nama file baru.
cf-cache-status: HIT
Apa tugas reverse proxy nginx di depan aplikasi Docker Compose?
Reverse proxy menerima setiap request lebih dulu lalu meneruskannya ke aplikasi Anda. Di nginx, itu cukup satu directive, proxy_pass, yang menentukan protokol dan alamat server tujuan. docker-compose.yml situs ini mengikat aplikasi ke 127.0.0.1 di host, dengan komentar bahwa nginx menangani TLS lalu meneruskan request ke port itu, jadi tidak ada yang dari luar server bisa menjangkau container secara langsung.
Baca selengkapnya (2)Tutup
Ada dua header yang perlu diperhatikan. Secara default nginx mengirim Host berisi $proxy_host, yaitu alamat upstream itu sendiri, jadi aplikasi di belakang proxy_pass http://web:3000 melihat web:3000, bukan domain Anda. proxy_set_header Host $host meneruskan nama yang diminta pengunjung. X-Forwarded-For yang diisi $proxy_add_x_forwarded_for menambahkan alamat yang dilihat nginx ke nilai yang sudah dikirim klien.
Konfigurasi nginx milik situs ini tidak ada di repository, jadi blok di pelajaran ini adalah versi umum dari nginx.org yang sudah diuji di lokal. Periksa setiap perubahan dengan nginx -t sebelum nginx -s reload. Saat reload, nginx memeriksa konfigurasi baru, menyalakan worker baru, dan membiarkan worker lama menyelesaikan klien yang sedang dilayaninya.
Bagaimana mendapatkan IP asli pengunjung di balik Cloudflare?
Di balik Cloudflare, setiap koneksi ke nginx datang dari alamat Cloudflare, jadi log dan rate limit melihat proxy, bukan pengunjung. Cloudflare menambahkan header CF-Connecting-IP berisi alamat pengunjung, dan modul realip di nginx menukarnya: real_ip_header menyebut nama header-nya, dan set_real_ip_from mendaftar alamat yang dipercaya untuk mengirimnya.
Baca selengkapnya (1)Tutup
Daftar alamat tepercaya itulah bagian keamanannya. Daftarkan setiap rentang yang dipublikasikan Cloudflare di cloudflare.com/ips dan jangan yang lain, karena header yang dipercaya dari alamat mana pun membuat siapa saja bisa memilih IP-nya sendiri. Cloudflare menyebut daftar itu perlu diperbarui secara rutin. Modul ini tidak ikut di-build secara default, jadi periksa nginx -V untuk --with-http_realip_module sebelum mengandalkannya.
Apa yang boleh di-cache Cloudflare, dan apa yang tidak boleh?
Cloudflare melakukan cache berdasarkan ekstensi file, bukan MIME type, dan secara default tidak meng-cache HTML maupun JSON. Di dalam batas itu, Cache-Control dari origin yang menentukan. next.config.ts situs ini mengirim public, max-age=31536000, immutable untuk /images/* dan file gambar di root, serta untuk /_next/static/* khusus di production.
Baca selengkapnya (2)Tutup
Setahun hanya aman karena URL-URL itu memakai cache busting: nama ber-hash berubah setiap kali isinya berubah, jadi URL lama tidak pernah butuh isi baru. Kalau sebuah nama tidak bisa berubah, konfigurasinya menolak berjanji selamanya. Film hanya immutable kalau URL-nya membawa stempel render ?v=, dan manifest.json milik game, satu-satunya file yang namanya tidak pernah berubah, dikecualikan.
HTML adalah kebalikannya. HTML berada di URL tetap yang isinya berubah di setiap deploy, jadi situs ini tidak menyimpannya di edge: satu request tambahan ke origin per tampilan halaman, sebagai ganti tidak pernah mengirim hasil deploy yang basi. Tabel di bawah menunjukkan header live pada 2026-10-13. Ulangi baris mana pun dengan curl -sI ke situs ini.
Kenapa Vary: Accept membuat HTML tidak boleh di-cache di edge?
Setiap halaman di situs ini menjawab dalam dua representasi di satu URL. middleware.ts menulis ulang request yang mengirim Accept: text/markdown ke route Markdown, jadi agent mendapat Markdown dan browser mendapat HTML. Vary adalah cara response memberi tahu cache header request mana yang membentuknya, sehingga cache menyimpan salinan terpisah untuk setiap varian.
Baca selengkapnya (1)Tutup
Response Markdown mengirim Vary: Accept. Response HTML tidak bisa, karena page handler App Router milik Next mengganti header Vary saat render, seperti dicatat di komentar middleware.ts. Header Vary pada response HTML yang live berisi header router dan Accept-Encoding, tetapi tidak berisi Accept. Edge yang meng-cache HTML itu bisa mengirimkannya ke request Markdown berikutnya, atau sebaliknya.
Konfigurasi nyata di balik pelajaran ini
Kutipan dari repositori yang membangun dan men-deploy situs ini, apa adanya.
ports:
# Bind to loopback only — nginx terminates TLS and proxies to this port.
- "127.0.0.1:${APP_PORT:-3002}:3000"Inilah seluruh kontrak antara proxy dan aplikasi. Port container 3000 hanya dipublikasikan di loopback host, pada APP_PORT, jadi satu-satunya jalan masuk dari luar adalah lewat nginx. Nomor port-nya berasal dari .env yang ditulis proses deploy.
{
source: "/images/(.*)",
headers: [
{
key: "Cache-Control",
value: "public, max-age=31536000, immutable",
}, // Cache images for 1 year
],
},Aplikasi, bukan nginx, yang menentukan kebijakan cache, lalu proxy dan CDN meneruskannya. Perhatikan apa yang dicocokkan aturan ini: setiap path di bawah /images/, termasuk yang ternyata 404. Bagian What breaks di bawah membahas kenapa itu penting.
// Production only. Build chunks are content-hashed there, so a year of
// immutable caching is safe. Turbopack's dev chunks keep the same name
// across edits, and this header in development made browsers keep
// running stale game code for as long as their cache lived — the
// "sometimes WASD stops working" reports were a mix of old and new
// modules, not the controller.
...(process.env.NODE_ENV === "production"
? [
{
source: "/_next/static/(.*)",
headers: [
{
key: "Cache-Control",
value: "public, max-age=31536000, immutable",
}, // Cache static assets for 1 year
],
},
]
: []),immutable adalah janji bahwa isi di balik sebuah URL tidak akan pernah berubah. Di production itu benar, karena nama chunk membawa hash isinya. Di development itu salah, dan browser terus menjalankan kode lama; komentarnya mencatat bug yang mengajarkan hal ini.
{
// The films, their posters and captions. A re-render keeps the file
// names, so only a request carrying the render stamp
// (lib/film-manifest.ts prints `?v=` on every URL) may be cached as
// immutable; Cloudflare then serves the films from its edge. A bare
// URL, from an old link, keeps revalidating against the origin.
source: "/videos/:path*",
has: [{ type: "query", key: "v" }],
headers: [
{
key: "Cache-Control",
value: "public, max-age=31536000, immutable",
},
],
},Cache busting saat nama file tidak bisa berubah. Kondisi has membuat header setahun hanya berlaku untuk URL yang membawa stempel ?v=, jadi render ulang mendapat URL baru, sementara tautan lama tanpa stempel tetap melakukan revalidasi.
/**
* Every page answers in two representations at the same URL: HTML for
* browsers, Markdown for agents that ask for it with `Accept: text/markdown`
* (rendered by app/agent-md from the site's own data). Markdown requests are
* rewritten before the locale middleware, so they get 200 at the URL they
* asked for instead of a locale redirect, and carry `Vary: Accept`.
*
* Everything else goes through next-intl as before. Its locale redirects get
* `Vary: Accept` here too; a rendered HTML page does not, because Next's App
* Router page handler replaces the Vary header at render time (setHeader in
* build/templates/app-page.js). Nothing caches in front of the site today;
* if a shared cache is ever added, add `Vary: Accept` in nginx instead.
*/
export default function middleware(request: NextRequest): NextResponse {
if (prefersMarkdown(request.headers.get("accept"))) {
const url = request.nextUrl.clone();
url.pathname = `${MARKDOWN_ROUTE}${request.nextUrl.pathname}`;
return NextResponse.rewrite(url);
}
const response = intlMiddleware(request);
response.headers.append("Vary", "Accept");
return response;
}Content negotiation di satu URL. Request Markdown ditulis ulang ke route-nya sendiri dan membawa Vary: Accept; halaman HTML tidak bisa membawanya. Satu baris di sini sudah usang: Cloudflare mem-proxy www sejak 2026-10-12, jadi sekarang memang ada yang meng-cache di depan situs. Kesimpulannya tetap berlaku, karena edge tidak pernah menyimpan HTML.
Kesalahan yang benar-benar membuang waktu
Cache everything di situs yang berbeda menurut Accept
Aturan yang membuat HTML bisa di-cache akan mengikuti s-maxage dari Next dan membekukan halaman lintas deploy. Karena HTML tidak membawa Vary: Accept, aturan itu juga bisa mengirim satu representasi ke klien yang meminta representasi lain. Jangan simpan HTML di edge kecuali setiap variannya dideklarasikan.
Header setahun di URL yang isinya bisa berubah
immutable memberi tahu setiap cache bahwa isi di URL itu tidak pernah berubah. Di situs ini, header itu pernah berlaku untuk /_next/static saat development, ketika nama chunk tetap sama walaupun kodenya diubah, dan browser menjalankan kode game yang basi. Berikan umur panjang hanya untuk URL yang ber-hash atau berstempel.
Memercayai CF-Connecting-IP dari siapa saja
real_ip_header tanpa set_real_ip_from yang ketat membuat klien mana pun bisa menentukan alamatnya sendiri hanya dengan mengirim header itu, sehingga log, rate limit, dan pemblokiran tidak ada artinya. Daftarkan hanya rentang yang dipublikasikan Cloudflare, dan perbarui secara rutin.
Header cache panjang pada response error
Aturan /images mengirim public, max-age=31536000, immutable bahkan saat file-nya tidak ada dan status-nya 404. Pada 2026-10-13, edge menjawab 404 itu dengan BYPASS, jadi tidak disimpan. Edge yang menyimpan error akan membuat file tetap terlihat hilang lama setelah Anda mengunggahnya, jadi jangan pernah meng-cache response 4xx atau 5xx.
Membuka port aplikasi dan hanya mengandalkan CDN
Kalau container memublikasikan port-nya di semua interface, siapa pun yang menemukan alamat server bisa melewati Cloudflare dan nginx sepenuhnya. Ikat ke 127.0.0.1 seperti file compose situs ini, atau jangan publikasikan port sama sekali dan biarkan nginx menjangkau aplikasi lewat network Compose.
Yang akan saya ubah: dua komentar yang sudah usang
middleware.ts masih menyebut tidak ada yang meng-cache di depan situs, dan next.config.ts menyebut Cloudflare tidak meng-cache /_next/image. Keduanya ditulis sebelum www berada di balik Cloudflare; hari ini request ke /_next/image mengembalikan HIT. Komentar yang menjelaskan caching butuh review yang sama dengan konfigurasi tempat ia berada.
| Request | cf-cache-status | Cache-Control | Pelajarannya |
|---|---|---|---|
| /en/ (HTML) | DYNAMIC | s-maxage=21600, stale-while-revalidate=31514400 | Next mengizinkan shared cache, tetapi edge tidak pernah menyimpan HTML |
| /en/ dengan Accept: text/markdown | DYNAMIC | public, max-age=300, ditambah Vary: Accept | URL sama, isi berbeda |
| Chunk /_next/static | HIT | public, max-age=31536000, immutable | File ber-hash dilayani dari edge |
| Gambar teroptimasi /_next/image | HIT | public, max-age=31536000, must-revalidate | Varian teroptimasi juga di-cache di edge |
| File /images yang tidak ada (404) | BYPASS | public, max-age=31536000, immutable | Error tidak disimpan, meski header-nya setahun |
| /api/v1/ | DYNAMIC | public, max-age=3600 | API tidak di-cache di edge |
Lakukan di komputer Anda sendiri
Pasang nginx di depan aplikasi devops-lab dari pelajaran Docker, dengan Docker Compose, di laptop Anda. Aplikasinya tidak memublikasikan port sama sekali; hanya nginx yang bisa menjangkaunya. Anda hanya butuh Docker.
Buat aplikasinya melaporkan apa yang ia terima, dan kirim Cache-Control setahun untuk semua yang ada di /assets/, seperti next.config.ts untuk file ber-hash. Build ulang image-nya.
// src/server.js require("node:http") .createServer((req, res) => { if (req.url.startsWith("/assets/")) { // A hashed file name never changes content: cache it for a year. res.setHeader("cache-control", "public, max-age=31536000, immutable"); res.end("console.log('hi');\n"); return; } res.setHeader("cache-control", "no-cache"); res.end(`host=${req.headers.host} real-ip=${req.headers["x-real-ip"]}\n`); }) .listen(3000, "0.0.0.0"); # then rebuild the image docker build -t devops-lab:proxy .Tulis konfigurasi nginx. Baris realip hanya memercayai CF-Connecting-IP dari 127.0.0.1, yaitu container proxy itu sendiri; di production, daftar itu berisi rentang IP Cloudflare.
# nginx.conf # Trust CF-Connecting-IP only from addresses you list. In production that # is each Cloudflare range; in this lab it is the proxy container itself. set_real_ip_from 127.0.0.1; real_ip_header CF-Connecting-IP; server { listen 80; location / { proxy_pass http://web: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; } }Jalankan keduanya dengan Compose. web tidak punya bagian ports, dan nginx hanya dipublikasikan di 127.0.0.1:8081, aturan loopback-only yang sama dengan situs ini.
# compose.proxy.yaml services: web: image: devops-lab:proxy healthcheck: test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:3000/"] interval: 5s retries: 3 proxy: image: nginx:stable-alpine ports: - "127.0.0.1:8081:80" volumes: - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro depends_on: web: condition: service_healthy # then start both docker compose -f compose.proxy.yaml up -d --waitPeriksa apa yang lewat. Halaman membalas no-cache dan host=127.0.0.1, aset membalas immutable, web tidak punya port yang dipublikasikan, dan nginx -t menyetujui konfigurasinya.
curl -si http://127.0.0.1:8081/ | grep -iE '^(server|cache-control)|host=' curl -sI http://127.0.0.1:8081/assets/app.3f9a1c.js | grep -i cache-control docker compose -f compose.proxy.yaml port web 3000 docker compose -f compose.proxy.yaml exec proxy nginx -tUji aturan kepercayaannya. Dari laptop Anda, header itu diabaikan karena alamat Anda tidak dipercaya; dari dalam proxy, header itu menggantikan alamat klien. Lalu hapus baris proxy_set_header Host, restart proxy, dan aplikasi akan melihat host=web:3000.
curl -s -H 'CF-Connecting-IP: 2001:db8::7' http://127.0.0.1:8081/ docker compose -f compose.proxy.yaml exec proxy wget -qO- --header 'CF-Connecting-IP: 2001:db8::7' http://127.0.0.1/ # delete the proxy_set_header Host line in nginx.conf, then docker compose -f compose.proxy.yaml restart proxy curl -s http://127.0.0.1:8081/ docker compose -f compose.proxy.yaml down
Tiga pertanyaan sebelum lanjut
Pilih jawaban untuk melihat kenapa benar atau salah. Tidak ada yang disimpan atau dinilai, jadi tebak saja dengan bebas.
Jawab dalam hati, lalu cek
1.Kenapa chunk /_next/static boleh di-cache setahun, tetapi halaman HTML yang memuatnya tidak?Lihat jawabannya
Jawaban:Nama chunk membawa hash isinya, jadi isi baru mendapat URL baru dan URL lama tidak pernah perlu diperbarui. HTML berada di URL tetap yang isinya berubah di setiap deploy, dan di situs ini juga berubah menurut header Accept.
2.Log nginx Anda hanya berisi alamat Cloudflare. Apa perbaikan yang aman?Lihat jawabannya
Jawaban:Modul realip dengan real_ip_header CF-Connecting-IP dan satu baris set_real_ip_from untuk setiap rentang Cloudflare. Memercayai header itu dari alamat mana pun membuat siapa saja bisa memilih IP-nya sendiri.
3.Aplikasi di belakang proxy_pass http://web:3000 membuat tautan yang mengarah ke web:3000. Baris apa yang kurang?Lihat jawabannya
Jawaban:proxy_set_header Host $host. Tanpa baris itu, nginx mengirim Host berisi $proxy_host, yaitu nama upstream itu sendiri, jadi aplikasi tidak pernah tahu domain mana yang diminta pengunjung.
4.Sebuah halaman menampilkan cf-cache-status: DYNAMIC. Apakah itu cache miss?Lihat jawabannya
Jawaban:Bukan. MISS berarti response itu boleh di-cache tetapi belum tersimpan. DYNAMIC berarti Cloudflare memutuskan response itu sama sekali tidak boleh di-cache, jadi request langsung ke origin tanpa pengecekan cache.
Bahasan mendalam terkait di situs ini
Sumber
Sumber primer yang diperiksa pada tanggal riset. Ikuti tautannya untuk versi terbaru.
- Dokumentasi nginx: ngx_http_proxy_module (proxy_pass, proxy_set_header)
- Dokumentasi nginx: ngx_http_realip_module
- Dokumentasi nginx: mengendalikan nginx (reload)
- Dokumentasi Cloudflare: perilaku cache default
- Dokumentasi Cloudflare: response cache (cf-cache-status)
- Dokumentasi Cloudflare: memulihkan IP asli pengunjung
- Rentang IP Cloudflare
- MDN: Vary
- MDN: Cache-Control