ปัญหาที่เจอ
แอปของผมหลายตัวอยู่ใต้โดเมนเดียวกัน คือ Cloudflare zone เดียว และวิ่งผ่าน cloudflared tunnel ตัวเดียวกัน งานที่ต้องทำกับ Cloudflare ซ้ำ ๆ มีอยู่ไม่กี่อย่าง
- หลัง deploy ต้องล้าง cache ของไฟล์ CSS/JS ชุดใหม่ ถ้ากด Purge Everything ก็จะล้าง cache ของแอปอื่นใน zone ไปด้วย
- ขึ้นแอปใหม่ต้องเพิ่ม CNAME ชี้ไปที่ tunnel
- อยากให้หน้า
/adminมี challenge กันบอทที่ edge ก่อนถึงเซิร์ฟเวอร์
ทั้งหมดทำใน dashboard ได้ แต่ต้องจำขั้นตอนเอง และไม่มีอะไรบันทึกไว้ใน repo ของแอป เลยรวมไว้เป็นแพ็กเกจ ให้แต่ละแอปประกาศของที่ตัวเองเป็นเจ้าของไว้ใน config/cloudflare.php แล้วสั่งด้วย artisan

แต่ละแอปสั่งได้เฉพาะของที่อยู่ใน config ของตัวเอง
ติดตั้ง
composer require phattarachai/laravel-cloudflare
php artisan vendor:publish --tag=cloudflare-config
ใส่ zone id ใน .env และใส่ tunnel id ด้วยถ้าจะใช้คำสั่ง DNS ทั้งสองค่าเป็นตัวระบุสาธารณะ ไม่ใช่ความลับ commit ไว้ได้
CLOUDFLARE_ZONE_ID=...
CLOUDFLARE_TUNNEL_ID=...
API token
CLOUDFLARE_API_TOKEN เป็นความลับ ไม่ต้องใส่ใน repo ของแอป แพ็กเกจจะอ่าน config ก่อน ถ้าไม่เจอจะอ่านจาก environment ของ process ตอนรัน ถ้าใช้ self-hosted runner ใส่ token ไว้ใน .env ของ runner ครั้งเดียว ทุกแอปที่ deploy ผ่าน runner นั้นก็ใช้ได้
ที่ต้องอ่านจาก environment ตอนรัน เพราะขั้นตอน deploy มักรัน php artisan optimize ก่อน ถ้าตอนนั้นยังไม่มี token ใน .env ของแอป config ที่ cache ไว้จะได้ค่า null
สร้าง token ใน Cloudflare ให้มีสิทธิ์เท่าที่ใช้
| คำสั่ง | สิทธิ์ที่ต้องมี |
|---|---|
cloudflare:purge |
Zone → Cache Purge |
cloudflare:dev-mode |
Zone → Zone Settings Edit |
cloudflare:dns |
Zone → DNS Edit |
cloudflare:waf |
Zone → Zone WAF Edit |
ถ้าไม่มี token หรือไม่มี zone id ทุกคำสั่งจะแค่เตือนแล้วจบด้วย exit 0 โดยไม่เรียก API บนเครื่อง dev ใน test และใน repo ที่ยังไม่ได้ตั้งค่า CI จึงยังผ่านตามปกติ
ล้าง cache หลัง deploy
php artisan cloudflare:purge
คำสั่งนี้อ่าน Vite manifest (public/build/manifest.json) แล้วล้าง cache ทีละ URL ทั้งหน้า / และไฟล์ JS/CSS ทุกตัวที่ build ออกมา โดยใช้ host จาก APP_URL (หรือ CLOUDFLARE_HOST) ไม่ใช้ Purge Everything จึงไม่กระทบแอปอื่นใน zone ถ้า URL เกิน 100 ตัว ซึ่งเป็นเพดานต่อ request ของแพลน Free, Pro และ Business จะแบ่งส่งเป็นหลาย batch
ใส่เป็นขั้นตอนสุดท้ายของ deploy
- name: Purge Cloudflare cache
run: php artisan cloudflare:purge || true
ที่ต่อท้ายด้วย || true ก็เพื่อให้ deploy ไม่ถูกนับว่าล้มเหลวเมื่อ Cloudflare มีปัญหาชั่วคราว เพราะถึงขั้นนี้แอปขึ้นไปแล้ว
ถ้ามีหน้าอื่นที่ต้องล้างด้วย ให้ระบุ URL เพิ่ม ใส่ --url ซ้ำได้หลายครั้ง
php artisan cloudflare:purge --url=https://example.app/sitemap.xml
ถ้าจำเป็นต้องล้างทั้ง zone จริง ๆ ต้องใส่ --force คู่กับ --everything เสมอ ถ้าไม่ใส่ คำสั่งจะถามยืนยันก่อน
php artisan cloudflare:purge --everything --force
ล้างทั้ง host ของแอป
ถ้า cache rule ของ zone เก็บหน้า HTML ไว้ด้วย การล้างแค่ / กับ assets จะไม่พอ ตั้งแต่ v2.0 สั่งล้างทุก URL ที่ cache ไว้ของ host แอปนี้ได้ใน request เดียว โดยไม่แตะแอปอื่นใน zone
php artisan cloudflare:purge --host
ถ้าอยากให้ deploy ใช้แบบนี้ทุกครั้ง ตั้งใน .env แทน คำสั่ง cloudflare:purge ใน CI ก็ไม่ต้องแก้
CLOUDFLARE_PURGE_MODE=host
การล้างตาม hostname เคยใช้ได้เฉพาะแพลน Enterprise แต่ตั้งแต่ 1 เม.ย. 2025 Cloudflare เปิดการล้างทุกแบบให้ทุกแพลน รวมถึงแพลน Free ซึ่งล้างได้ 5 request ต่อนาทีต่อบัญชี (รายละเอียดและ rate limit, การล้างตาม hostname) deploy หนึ่งครั้งใช้แค่ request เดียว
Development Mode
ตอนไล่แก้ปัญหาบนเครื่องจริง บางครั้งต้องการให้ request วิ่งตรงถึงเซิร์ฟเวอร์โดยไม่ผ่าน cache ของ edge Development Mode ทำแบบนั้นได้ประมาณ 3 ชั่วโมง แล้วปิดเอง
php artisan cloudflare:dev-mode # ดูสถานะ และเวลาที่เหลือ
php artisan cloudflare:dev-mode on
php artisan cloudflare:dev-mode off
DNS สำหรับแอปใหม่บน tunnel
คำสั่ง DNS ทำงานแบบ upsert คือสร้างใหม่ถ้ายังไม่มี และอัปเดตถ้ามีอยู่แล้ว (เทียบจากชื่อและ type) ไม่เคยลบ record ที่ไม่ได้ประกาศไว้
php artisan cloudflare:dns --name=new-app.example.app --tunnel
--tunnel จะสร้าง CNAME แบบ proxied ชี้ไปที่ <CLOUDFLARE_TUNNEL_ID>.cfargotunnel.com ถ้าอยากให้ record อยู่กับแอป ก็ประกาศไว้ใน config แล้วรันคำสั่งเปล่า ๆ
'dns' => [
'tunnel_id' => env('CLOUDFLARE_TUNNEL_ID'),
'records' => [
['name' => 'new-app.example.app', 'tunnel' => true],
],
],
php artisan cloudflare:dns # upsert ทุก record ที่ประกาศไว้
php artisan cloudflare:dns --list # ดู record ทั้ง zone
record แบบอื่นก็สร้างได้ เช่น TXT แบบไม่ผ่าน proxy
php artisan cloudflare:dns --name=example.app --type=TXT --content='v=spf1 -all' --dns-only
การลบต้องระบุชื่อ และต้องใส่ --type ถ้าไม่ใช่ CNAME เพราะค่าเริ่มต้นของ type คือ CNAME
php artisan cloudflare:dns --name=old.example.app --type=TXT --delete --force
WAF: challenge หน้า /admin
กรณีที่ใช้บ่อยที่สุดคือใส่ Managed Challenge ให้ /admin บอทและสคริปต์ที่ยิงหน้า login จะเจอ challenge ที่ edge ส่วนคนที่ใช้เบราว์เซอร์จริงแทบไม่รู้ตัวว่าผ่าน challenge ไป
'waf' => [
'rules' => [
['tag' => 'admin-challenge', 'paths' => ['/admin']],
],
],
php artisan cloudflare:waf # sync กฎที่ประกาศไว้
php artisan cloudflare:waf --list # ดูกฎ custom ทั้ง zone
php artisan cloudflare:waf --remove=admin-challenge --force
ถ้าประกาศแค่ paths แพ็กเกจจะสร้าง expression ให้ และผูกกับ host ของแอปนี้ให้อัตโนมัติ กฎแต่ละตัวมี tag ซึ่งจะเก็บไว้ในคำอธิบายของกฎเป็น [admin-challenge] ตอน sync แพ็กเกจหากฎจาก tag นี้ แล้วอัปเดตเฉพาะกฎของตัวเอง
ถ้าแอปเดียวกันมีหลาย environment ใน zone เดียว เช่น prod กับ qas ให้เขียน expression เองแล้วใส่ host ให้ครบ ทุก environment จะ upsert กฎตัวเดียวกัน แทนที่จะแย่ง tag เดียวกันไปมา
['tag' => 'admin-challenge', 'expression' => '(starts_with(http.request.uri.path, "/admin")) and (http.host in {"app.example.app" "app-qas.example.app"})'],
วิธีเช็กว่ากฎทำงานแล้ว
curl -sI https://app.example.app/admin | grep -i cf-mitigated
ถ้าได้ cf-mitigated: challenge กับสถานะ 403 แปลว่ากฎทำงาน (curl ผ่าน JavaScript challenge ไม่ได้ จึงได้ผลนี้ทุกครั้ง) ส่วนในเบราว์เซอร์ หลังผ่าน challenge แล้วจะไม่มี header นี้ และจะเห็น cookie cf_clearance ใน DevTools → Application → Cookies
ข้อควรรู้ก่อนใช้
- ค่าเริ่มต้นของ purge ล้างแค่หน้า
/กับไฟล์ในbuild/ถ้า cache rule ของ zone เก็บหน้า HTML อื่นไว้ด้วย ให้ใช้--hostหรือส่ง--urlเพิ่ม - ถ้าบาง batch ล้มเหลว batch ที่ส่งไปก่อนหน้าถูกล้างไปแล้ว คำสั่งจะจบด้วย error และบอกว่า batch ไหนล้มเหลว
- host มาจาก
APP_URLถ้าแอปเสิร์ฟหลายโดเมน ตั้งCLOUDFLARE_HOSTให้ตรงกับโดเมนที่ต้องการ ถ้าไม่มีทั้งสองค่า purge จะจบด้วย error - ถ้าอ่านข้อมูลจาก Cloudflare ไม่สำเร็จ คำสั่งจะไม่เขียนอะไรเลย เช่น
cloudflare:wafอ่าน ruleset ไม่ได้เพราะ API ตอบ 5xx หรือ token ไม่มีสิทธิ์ จะจบด้วย error ว่าไม่มีอะไรเปลี่ยน ส่วน--tunnelที่ยังไม่ได้ตั้งCLOUDFLARE_TUNNEL_IDก็จะหยุดก่อนสร้าง record
เทียบกับ CLI ของ Cloudflare
Cloudflare มี CLI ใหม่ชื่อ cf ซึ่งเปิด beta เมื่อ 28 ก.ย. 2026 และครอบคลุม API ของ Cloudflare ทั้งหมด ถ้าต้องจัดการเรื่องที่แพ็กเกจนี้ไม่มี หรือทำงานที่ทำครั้งเดียว cf หรือ dashboard ตอบโจทย์กว่า
แพ็กเกจนี้เหมาะกว่าตอนที่งานผูกอยู่กับแอป Laravel
- รู้จักแอป อ่าน Vite manifest และ host จาก
APP_URLเอง ไม่ต้องเขียนสคริปต์หา URL ของ assets - config อยู่ใน repo ของแอป DNS และกฎ WAF ที่แอปต้องมี ถูก review และ deploy ไปพร้อมกับโค้ด
- ปลอดภัยใน zone ที่ใช้ร่วมกัน ทุกคำสั่งเป็น upsert ที่แตะเฉพาะของที่ประกาศไว้ และเมื่อไม่ได้ตั้งค่าไว้ก็จะไม่ทำอะไรเลย
- ใช้แค่ PHP runner ที่ deploy แอป Laravel มี PHP อยู่แล้ว ไม่ต้องติดตั้ง Node หรือ binary เพิ่ม
เวอร์ชันที่รองรับ
- PHP 8.3 ขึ้นไป
- Laravel 12 และ 13
แอปที่ยังใช้ Laravel 11 หรือ PHP 8.2 ใช้เวอร์ชัน 1.x (phattarachai/laravel-cloudflare:^1.1) ได้ แต่ --host และฟีเจอร์ในโพสต์นี้บางส่วนมีเฉพาะใน v2
GitHub: phattarachai/laravel-cloudflare


