เรื่องนี้เริ่มจากสิ่งที่ดูธรรมดามาก: ติดตั้ง Umami แบบ self-host บน Coolify สร้าง website ใน dashboard ก๊อป tracking code มาวาง แล้วรอดูตัวเลข — แต่ผ่านไปทั้งวัน ทุกช่องขึ้น 0 Visitors, Views, Bounce rate ไม่มีอะไรเข้ามาเลย
ตอนไล่หาสาเหตุ ผมพบว่ามันไม่ใช่บั๊กเดียว แต่เป็น 3 บั๊กซ้อนกัน และแต่ละตัวบังตัวถัดไปไว้ ถ้าแก้แค่ตัวเดียวอาการก็ยังเหมือนเดิม จึงเขียนบันทึกไว้เป็นลำดับการตรวจ เผื่อใครเจอแบบเดียวกัน
เว็บหลัก kritsana.dev เป็น Next.js 16 deploy บน Coolify ด้วย Dockerfile (multi-stage, output: 'standalone' )
Umami 3.0.3 รันเป็น Service Stack ใน Coolify มาพร้อม PostgreSQL 16 (แยกอีกตัวจากเว็บหลัก)
DNS อยู่ที่ Cloudflare: kritsana.dev เป็น CNAME ผ่าน proxy, ส่วน umami.kritsana.dev เป็น A record แบบ DNS only (ไม่ผ่าน proxy)
เว็บหลักโหลด script ของ Umami ผ่าน next/script ใน layout.tsx โดยอ่านค่าจาก env 2 ตัว: NEXT_PUBLIC_UMAMI_URL และ NEXT_PUBLIC_UMAMI_WEBSITE_ID
โค้ดตรงนั้นเขียนไว้ว่า ถ้าตัวใดตัวหนึ่งไม่มีค่า จะไม่ render อะไรเลย เพื่อไม่ให้ dev หรือ deploy ที่ยังไม่ตั้งค่ามี request ไปหา third-party:
tsx copy const umamiSrc = process.env. NEXT_PUBLIC_UMAMI_URL ;
const umamiWebsiteId = process.env. NEXT_PUBLIC_UMAMI_WEBSITE_ID ;
{umamiSrc && umamiWebsiteId && (
< Script src = { umamiSrc } data-website-id = { umamiWebsiteId } strategy = "afterInteractive" />
)}
ดีไซน์นี้ปลอดภัยดี แต่มีด้านที่ต้องระวัง: ถ้าค่าหายไป มันจะเงียบ ไม่มี error ไม่มี warning
ก่อนจะถึงเรื่อง analytics เจอปัญหาที่ใหญ่กว่า — เปิด kritsana.dev แล้วได้ HTTP 503 ทั้ง kritsana.dev และ www :
bash copy $ curl -i https://kritsana.dev/
HTTP/2 503
server: cloudflare
no available server
ข้อความ no available server ไม่ได้มาจาก Cloudflare และไม่ใช่ Next.js แต่เป็นคำตอบของ Traefik (reverse proxy ที่ Coolify ใช้) เมื่อไม่มี router ไหนตรงกับ request
เมื่อเปิดหน้า Configuration ของ app เว็บหลัก ช่อง Domains ถูกตั้งเป็น https://umami.kritsana.dev — คือผมเผลอใส่ domain ของ Umami ลงใน app ผิดตัว
Traefik ทำงานโดยดู Host header ของ request แล้วหา router ที่ตรง ตอนนั้น app เว็บหลักมีกฎแค่ Host(umami.kritsana.dev) ดังนั้น request ที่มา Host: kritsana.dev จึงไม่ตรงกับอะไรเลย ทั้งที่ container ตัว app เองรันอยู่และ healthy
อีกเบาะแสที่ยืนยัน: URL แบบ sslip.io ของ app ก็ตอบ 404 เช่นกัน แปลว่าไม่มี router ที่ match จริง ๆ ส่วน DNS กับ Cloudflare ปกติดี
วิธีแก้: ที่ app เว็บหลัก ตั้ง Domains เป็น https://kritsana.dev,https://www.kritsana.dev แล้ว Save และ Redeploy
บทเรียน: เมื่อเจอ 503 no available server บน Coolify ให้ไปดู Domains ของ resource ก่อนเสมอ ไม่ต้องเริ่มที่ DNS หรือ container
พอเว็บกลับมา (200) แต่ Umami ยังเป็น 0 ลองดู HTML ที่ production ส่งออกมา:
bash copy curl -s https://kritsana.dev/ | grep -i umami
ผลคือ ไม่เจออะไรเลย — ไม่มี <script> ของ Umami ในหน้าเลย แปลว่าปัญหาอยู่ก่อนที่ browser จะได้ส่งข้อมูลด้วยซ้ำ
สาเหตุ: ตัวแปรที่ขึ้นต้นด้วย NEXT_PUBLIC_ ถูก ฝังลงใน bundle ตอน next build ไม่ใช่อ่านตอน runtime และการที่เราใส่ใน Coolify Environment Variables ยังไม่พอ เพราะ Dockerfile แบบ multi-stage ต้องรับค่าผ่าน ARG ถึงจะส่งต่อเข้าไปใน stage ที่รัน pnpm build
Dockerfile ของผมประกาศ ARG ให้เฉพาะ NEXT_PUBLIC_BASE_URL เท่านั้น ส่วน Umami 2 ตัวไม่เคยถูกประกาศ ผลคือ pnpm build เห็นทั้งคู่เป็น undefined เงื่อนไขใน layout.tsx เป็น false และไม่ render script
วิธีแก้: เพิ่มก่อนบรรทัด RUN pnpm build :
dockerfile copy # Read at prerender by src/app/layout.tsx; without ARG the vars never reach `pnpm build`.
ARG NEXT_PUBLIC_UMAMI_URL
ARG NEXT_PUBLIC_UMAMI_WEBSITE_ID
ENV NEXT_PUBLIC_UMAMI_URL=$NEXT_PUBLIC_UMAMI_URL
ENV NEXT_PUBLIC_UMAMI_WEBSITE_ID=$NEXT_PUBLIC_UMAMI_WEBSITE_ID
RUN pnpm build
แล้วที่ Coolify → Environment Variables ของ app ตรวจว่าตัวแปร 2 ตัวนี้ติ๊ก Build Variable เปิดอยู่:
NEXT_PUBLIC_UMAMI_URL = https://umami.kritsana.dev/script.js
NEXT_PUBLIC_UMAMI_WEBSITE_ID = ค่า Website ID จากหน้า Settings ของเว็บใน Umami
ข้อควรจำ: เพราะค่าถูกฝังตอน build การกด Restart ไม่ช่วยอะไร ต้อง Redeploy ให้ build ใหม่
ส่วนวิธีตรวจ ไม่ต้องรอดูตัวเลขใน Umami เลย แค่ curl ดู HTML:
bash copy curl -s https://kritsana.dev/ | grep -io '<script[^>]*umami[^>]*>'
ถ้าเห็น tag พร้อม data-website-id แปลว่าชั้นนี้ผ่านแล้ว
ต่อให้ HTML มี tag แล้ว ข้อมูลก็ยังส่งไปไม่ถึง เพราะเมื่อลอง curl ฝั่ง Umami:
bash copy curl -I https://umami.kritsana.dev/script.js # 503
curl -X POST https://umami.kritsana.dev/api/send # 503
ทั้งหน้า script และ endpoint ที่รับข้อมูล (/api/send ) ตอบ 503 อาการเดียวกับบั๊กที่ 1 — Traefik ไม่มีกฎสำหรับ host นี้
เปิดหน้า Service ของ Umami ใน Coolify พบว่า Domains ยังเป็นค่าเริ่มต้นแบบ http://umami-xxxx.sslip.io:3000 ไม่มี umami.kritsana.dev อยู่เลย (ส่วน Postgres ที่มาด้วยกัน running healthy ปกติ)
ใน Coolify มีกล่อง warning เตือนไว้ว่า service นี้ ต้องใช้ port 3000 และทุก domain ต้องใส่ port นั้นด้วย ซึ่งเป็นพอร์ตของ container ที่ Coolify ใช้ route ไม่ใช่พอร์ตที่เปิดออกภายนอก
วิธีแก้: ตั้ง Domains ของ Umami เป็น
text copy https://umami.kritsana.dev:3000
ต้องคง :3000 ไว้
ต้องเป็น https เพื่อให้ Traefik ขอใบรับรอง Let's Encrypt ให้เอง
กด Save แล้ว Restart service
ที่ฝั่ง Cloudflare record umami เป็น DNS only อยู่แล้ว ซึ่งทำให้ Let's Encrypt ออกใบรับรองได้เอง ข้อแลกเปลี่ยนคือ IP ของเซิร์ฟเวอร์จะมองเห็นได้จาก DNS (ถ้าเปิด proxy ต้องตั้ง SSL mode เป็น Full)
เรื่องที่พลาดได้อีกอย่างคือ tracking code ที่ Umami สร้างให้ใน dashboard ใช้ URL แบบ http://…sslip.io/script.js ถ้านำไปใช้ตรง ๆ กับเว็บที่เป็น https browser จะบล็อกเป็น mixed content เราจึงต้องใช้ URL แบบ https://umami.kritsana.dev/script.js แทน
ตรวจชั้นนี้ด้วย:
bash copy curl -I https://umami.kritsana.dev/script.js # ต้องได้ 200
ถ้า Umami ขึ้น 0 ให้ไล่จาก "ต้นทางของข้อมูล" ไปปลายทาง ใช้ curl อย่างเดียวก็จบ ไม่ต้องรอตัวเลข:
1. เว็บหลักตอบ 200 ไหม — curl -I https://เว็บของคุณ/ ถ้า 503 ไปดู Domains ของ app ใน Coolify
2. HTML มี script tag ไหม — curl -s https://เว็บของคุณ/ | grep -i umami ถ้าไม่มี ไปดู ARG ใน Dockerfile และ Build Variable
3. Umami ส่ง script ได้ไหม — curl -I https://umami.ของคุณ/script.js ถ้า 503 ไปดู Domains ของ Umami และอย่าลืม :3000
4. เปิดเว็บจริงใน incognito ปิด extension ที่บล็อก tracker แล้วดูใน dashboard ว่ามี visitor เข้ามา
ถ้าครบ 4 ข้อแล้วยังไม่เข้า ค่อยสงสัย adblock ฝั่งผู้ใช้ หรือ Cloudflare Rocket Loader ที่ไปแก้ script
503 no available server บน Coolify = Traefik ไม่มี router ที่ตรง Host ให้ดู Domains ของ resource ก่อน
NEXT_PUBLIC_* เป็นค่า build-time ใน multi-stage Dockerfile ต้องมี ARG + ENV และติ๊ก Build Variable ใน Coolify ไม่งั้นค่าถูกทิ้งเงียบ ๆ และต้อง Redeploy ไม่ใช่ Restart
Umami บน Coolify ต้องใช้ :3000 ต่อท้าย domain และใช้ https เพื่อให้ได้ใบรับรอง
เลข 0 ใน dashboard บอกอะไรไม่ได้ เพราะมีหลายชั้นที่พังเงียบ ๆ ให้ตรวจด้วย curl ทีละชั้นเร็วกว่าเดา
เมื่อมีบั๊กหลายตัวซ้อนกัน การแก้ตัวแรกแล้วอาการไม่หาย ไม่ได้แปลว่าแก้ผิด แค่ตัวถัดไปยังบังอยู่