Deploy Next.js ke Cloudflare Workers dengan OpenNext: dari Build Gagal sampai Live
Portofolio ini dibangun dengan Next.js 16 dan Hygraph sebagai CMS. Awalnya saya ragu memindahkannya dari hosting lama ke Cloudflare Workers, karena Next.js identik dengan Vercel. Ternyata dengan adapter OpenNext, aplikasinya bisa jalan di Workers lengkap dengan ISR, dan sekarang mihdan.my.id dilayani dari sana.
Tulisan ini merangkum langkah-langkahnya, mulai dari konfigurasi sampai header keamanan, plus beberapa jebakan yang sempat membuat build gagal. Semua contoh di bawah diambil dari konfigurasi situs ini.
Kenapa Cloudflare Workers?
- Domain sudah dikelola di Cloudflare, jadi DNS, proxy, dan sertifikat ada di satu tempat.
- Aset statis (JavaScript, CSS, gambar di folder public) dilayani langsung dari edge tanpa lewat Worker.
- ISR tetap jalan: halaman disimpan di R2 dan diperbarui di latar belakang.
- Untuk situs pribadi, paket gratisnya sudah cukup.
Satu hal yang perlu diingat: di paket gratis, satu request hanya boleh memakai CPU sekitar 10 ms. Render halaman yang berat bisa berakhir dengan error 1102. Karena itu hampir semua halaman di sini dirender sekali, lalu disajikan dari cache.
Gambaran arsitekturnya
OpenNext mengubah hasil next build menjadi satu Worker (.open-next/worker.js) ditambah folder aset statis. Alurnya kira-kira seperti ini:
Pengunjung → Cloudflare (edge)
├─ /_next/static, /public → aset statis (binding ASSETS)
└─ halaman → Worker (OpenNext)
├─ cache ISR → R2
├─ revalidasi → Durable Object
├─ next/image → Cloudflare Images
└─ data → Hygraph (GraphQL)Langkah 1: Pasang OpenNext dan Wrangler
npm install @opennextjs/cloudflare
npm install -D wranglerSaya memakai Next.js 16.4 dan @opennextjs/cloudflare 1.20. Setelah itu tambahkan tiga script di package.json:
{
"scripts": {
"preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
"deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
"upload": "opennextjs-cloudflare build && opennextjs-cloudflare upload"
}
}previewuntuk mencoba hasil build di mesin lokal.deployuntuk build sekaligus menerbitkan versi baru ke produksi.uploaduntuk mengunggah versi tanpa langsung menjadikannya produksi, cocok untuk branch selain main.
Langkah 2: Konfigurasi wrangler.jsonc
Ini konfigurasi yang dipakai situs ini:
{
"name": "portfolio-mihdan",
"main": ".open-next/worker.js",
"compatibility_date": "2026-10-01",
"compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
"assets": { "directory": ".open-next/assets", "binding": "ASSETS" },
"services": [
{ "binding": "WORKER_SELF_REFERENCE", "service": "portfolio-mihdan" }
],
"images": { "binding": "IMAGES" },
"r2_buckets": [
{ "binding": "NEXT_INC_CACHE_R2_BUCKET", "bucket_name": "portfolio-mihdan-cache" }
],
"durable_objects": {
"bindings": [{ "name": "NEXT_CACHE_DO_QUEUE", "class_name": "DOQueueHandler" }]
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["DOQueueHandler"] }]
}- ASSETS: hasil build statis dilayani langsung oleh Cloudflare.
- WORKER_SELF_REFERENCE: Worker bisa memanggil dirinya sendiri, dipakai OpenNext saat revalidasi.
- IMAGES: optimasi next/image lewat Cloudflare Images (paket gratis dapat 5.000 transformasi unik per bulan).
- NEXT_INC_CACHE_R2_BUCKET: tempat menyimpan halaman ISR. Buat bucket R2-nya dulu lewat dasbor atau
wrangler r2 bucket create. - NEXT_CACHE_DO_QUEUE: antrean revalidasi berbasis Durable Object.
Langkah 3: open-next.config.ts untuk ISR
Tanpa berkas ini, export const revalidate = 60 di halaman tidak akan berjalan seperti di Vercel. OpenNext perlu tahu di mana cache disimpan dan bagaimana antrean revalidasinya:
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";
import doQueue from "@opennextjs/cloudflare/overrides/queue/do-queue";
export default defineCloudflareConfig({
incrementalCache: r2IncrementalCache,
queue: doQueue,
});Langkah 4: Data Hygraph tanpa membebani build
Hygraph membatasi request tanpa cache (di paket gratis sekitar 5 request per detik). Kalau semua halaman artikel dibangun saat build, build bisa kena error 429. Jadi halaman detail sengaja tidak dibangun di awal:
export const revalidate = 60;
export async function generateStaticParams() {
// dirender saat pertama dikunjungi, lalu disajikan dari cache
return [];
}Hasilnya, pengunjung pertama memicu render, sedangkan pengunjung berikutnya mendapat halaman dari R2. Setiap 60 detik halaman bisa diperbarui di latar belakang, jadi tulisan baru dari Hygraph muncul tanpa perlu deploy ulang.
Langkah 5: Workers Builds dan variabel
Repo GitHub saya hubungkan ke Workers Builds, sehingga setiap push ke main langsung menjadi deploy produksi. Pengaturannya:
- Build command: dikosongkan, karena script deploy sudah menjalankan build.
- Deploy command:
npm run deploy -- --keep-vars. - Non-production branch deploy command:
npm run upload. - HYGRAPH_API: endpoint Hygraph, disimpan sebagai Secret dan diisi untuk build maupun runtime.
- NEXT_PUBLIC_SITE_URL: https://mihdan.my.id. Variabel berawalan NEXT_PUBLIC_ ditanam saat build, jadi wajib tersedia ketika build berjalan.
Untuk domain, record A lama yang masih mengarah ke hosting sebelumnya harus dihapus dulu, baru kemudian domain bisa ditambahkan sebagai Custom Domain di Worker.
Jebakan yang Saya Temui
1. Build gagal: could not determine executable to run
Build pertama di Workers Builds langsung gagal di perintah npx opennextjs-cloudflare build. Ternyata ada dua penyebab yang datang bersamaan:
- Saat impor repo tidak ada pilihan branch, jadi yang dibangun adalah main, padahal konfigurasi OpenNext masih ada di branch lain.
- Karena dependensinya tidak ada di main, npx mencari paket
opennextjs-cloudflare(tanpa scope) di registry npm. Paket itu hanya placeholder milik pihak ketiga dan tidak punya program yang bisa dijalankan.
Solusinya: gabungkan konfigurasi ke main, lalu gunakan script npm run deploy alih-alih npx. Dengan begitu yang dipakai selalu paket @opennextjs/cloudflare yang terpasang di proyek.
Pelajaran: di CI, panggil alat lewat script npm, jangan lewat npx dengan nama paket tanpa scope. Kalau paket lokal tidak ada, npx bisa diam-diam mengambil paket lain dari internet.
2. www.mihdan.my.id error 522
Record www masih mengarah ke hosting lama yang sudah tidak aktif, sehingga Cloudflare menampilkan error 522. Saya membuat Redirect Rule dari template Redirect from WWW to Root, jadi semua akses www dialihkan ke domain utama.
3. http tidak otomatis pindah ke https
Awalnya, membuka http://mihdan.my.id tetap menampilkan halaman tanpa dialihkan ke https. Perbaikannya: aktifkan Always Use HTTPS di SSL/TLS → Edge Certificates, lalu tambahkan header HSTS dari aplikasi supaya browser mengingat untuk selalu memakai https.
4. CSP, skrip inline, dan Cloudflare Web Analytics
Next.js menyisipkan beberapa skrip inline di setiap halaman. CSP yang ketat biasanya memakai nonce, tetapi nonce memaksa setiap halaman dirender ulang per request, sehingga ISR tidak berlaku lagi. Saya memilih 'unsafe-inline' untuk script-src dan tetap mengetatkan bagian lain: object-src none, frame-ancestors none, base-uri self.
Kejutan kecil: Cloudflare otomatis menyisipkan skrip Web Analytics dari static.cloudflareinsights.com. Kalau domain itu tidak diizinkan di script-src, dan cloudflareinsights.com di connect-src, skripnya akan diblokir oleh CSP.
5. Uji lokal: cache fetch ikut tersimpan
Saat mencoba build lokal dengan data tiruan yang berganti-ganti, hasilnya tidak berubah. Ternyata halaman dengan revalidate menyimpan hasil fetch di .next/cache/fetch-cache, dan folder itu ikut terbawa antar-build. Hapus folder tersebut setiap kali menguji dengan data yang berbeda.
Langkah 6: Header Keamanan
Header untuk halaman cukup ditulis di next.config.ts. OpenNext menerapkannya ke semua respons yang dirender Worker. Versi ringkasnya:
const nextConfig = {
poweredByHeader: false,
async headers() {
return [
{
source: "/:path*",
headers: [
{ key: "Strict-Transport-Security", value: "max-age=31536000" },
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "X-Frame-Options", value: "DENY" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{ key: "Content-Security-Policy", value: "default-src 'self'; script-src 'self' 'unsafe-inline' https://static.cloudflareinsights.com; object-src 'none'; frame-ancestors 'none'" },
],
},
];
},
};Aset statis tidak lewat Worker, jadi header di next.config tidak berlaku untuknya. Untuk itu ada berkas public/_headers:
/_next/static/*
Cache-Control: public,max-age=31536000,immutable
/*
Strict-Transport-Security: max-age=31536000
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-originHasilnya
- Cache ISR terbukti jalan. Header
x-nextjs-cacheberisi HIT selama halaman masih segar, STALE setelah lewat 60 detik, lalu kembali HIT begitu halaman selesai diperbarui di latar belakang. - Semua alamat www dan http dialihkan dengan 301 ke https://mihdan.my.id, lengkap dengan path-nya.
- File di /_next/static disajikan dengan cache satu tahun (immutable).
- Ukuran Worker sekitar 6,1 MiB (1,3 MiB setelah gzip). Batas ukuran Worker sekarang 64 MiB, dihitung tanpa kompresi, untuk paket gratis maupun berbayar, jadi masih jauh dari batas.
Kalau kamu punya situs Next.js yang datanya dari CMS dan domainnya sudah di Cloudflare, kombinasi Workers dan OpenNext layak dicoba. Siapkan R2 untuk ISR, panggil alat lewat script npm, dan cek header keamanan sejak awal, supaya tidak mengulang jebakan yang sama seperti saya.
Referensi: dokumentasi OpenNext untuk Cloudflare.