คู่มือติดตั้งแพลตฟอร์ม
คู่มือนี้สำหรับผู้ดูแลระบบวิทยุออนไลน์ (ชื่อทางเทคนิค radio) ติดตั้งด้วย Docker: คำสั่งเดียวติดตั้งทุกอย่างบนเซิร์ฟเวอร์เครื่องเดียว ในโฟลเดอร์ /opt/radio ถ้าเป็นคลัสเตอร์ใช้ Helm chart ทุกแบบรันโค้ดชุดเดียวกัน ต่างกันแค่ค่าตั้งใน .env (หรือค่าของ Helm) การติดตั้งโดยตรงแบบไม่ใช้ Docker ยังมีอธิบายไว้ในภาคผนวกที่ไม่แนะนำ
เพิ่งเริ่มดูแลเซิร์ฟเวอร์? คู่มือติดตั้งบน Ubuntu ทีละขั้น พาทำทุกขั้นด้วยแพ็กเกจ radio_<version>_all.deb: เตรียมเซิร์ฟเวอร์ DNS ใช้ SSH กับโปรแกรมอ่านหน้าจอ sudo radio-setup ตั้งค่าครั้งแรกในเว็บ สำรองข้อมูล อัปเดต และแก้ปัญหา คู่มือนี้เป็นเอกสารอ้างอิง
สิ่งที่ติดตั้ง
| ส่วนประกอบ | หน้าที่ |
|---|---|
| app | เว็บไซต์ หน้าผู้ฟัง หน้าผู้ดูแล API การเข้าสู่ระบบ และรับการเรียกกลับจาก Icecast และ Liquidsoap ปรับฐานข้อมูล (migration) เองเมื่อเริ่มทำงาน |
| worker | ระบบออกอากาศ: Liquidsoap หนึ่งโปรเซสต่อสถานี รับสัญญาณสดของดีเจ (พอร์ตดีเจ) งานเบื้องหลัง รายการและโฆษณาตามเวลา การแจ้งเตือน สถิติผู้ฟัง และปรับค่าตั้งของ Icecast ให้ตรงกับสถานีเสมอ |
| icecast | Icecast 2.4 ส่งสตรีมให้ผู้ฟังผ่าน Caddy ที่ <ที่อยู่>/stream/<สถานี> ถามแอปก่อนรับผู้ฟังทุกคน (โควตา จำกัดประเทศ สถิติ) |
| db | PostgreSQL 16 แยกข้อมูลแต่ละสถานีด้วย row-level security |
| caddy | Reverse proxy: HTTPS พร้อมใบรับรองอัตโนมัติ (รวมโดเมนย่อยของสถานีและโดเมนของสถานีเอง) หรือ HTTP ธรรมดาในเครือข่ายภายใน |
| backup | สำรองข้อมูลแบบเข้ารหัสทุกวัน (restic) ลงเครื่องนี้ และ S3 ถ้าตั้งไว้ |
| asterisk (ไม่บังคับ) | ฟังทางโทรศัพท์ผ่าน SIP trunk |
ความต้องการขั้นต่ำ
- เซิร์ฟเวอร์ Linux 64 บิต (x86_64/amd64 หรือ arm64) ที่มี Docker: ทดสอบแล้วกับ Ubuntu 22.04, 24.04 และ Debian 12 ตัวติดตั้งติดตั้ง Docker ให้บนระบบเหล่านี้ ใช้ NAS (Synology, QNAP) หรือ container LXC ของ Proxmox ได้ด้วย (ดูด้านล่าง)
- 2 คอร์ หน่วยความจำ 4 GB ดิสก์ 30 GB สำหรับไม่เกิน 5 สถานี (โปรไฟล์ small) บวกคลังเสียงและข้อมูลสำรอง
- โดเมนที่ชี้มาที่เซิร์ฟเวอร์ (แนะนำ) หรือใช้แค่หมายเลข IP ก็ได้ (ดู "เลือกวิธีเข้าถึงแพลตฟอร์ม")
ไม่มีขนาดเครื่องตายตัว ขึ้นกับจำนวนสถานี และที่สำคัญที่สุดคือจำนวนผู้ฟัง:
- สถานีที่เปิดอยู่แต่ละสถานีรัน Liquidsoap หนึ่งโปรเซสใน worker ซีพียูและหน่วยความจำเพิ่มตามจำนวนสถานี
- แบนด์วิดท์ของผู้ฟังประมาณจำนวนผู้ฟัง × bitrate เช่น ผู้ฟัง 500 คนที่ 128 kbps ต้องใช้ upload ประมาณ 64 Mbit/s
- เกณฑ์ที่ทดสอบ:
npm run bench -- <base URL> <slug สถานี>ตรวจว่าหน้าสถานีตอบที่ p95 ต่ำกว่า 300 ms และ API สถานะที่ p95 ต่ำกว่า 100 ms เมื่อมีผู้ใช้พร้อมกัน 50 คน สคริปต์รายงาน p97.5 ซึ่งเข้มกว่า ต้องมีแพ็กเกจสำหรับพัฒนา (npm ci)
วัดเครื่องของคุณเองด้วยสคริปต์ bench และจำนวนผู้ฟังจริงก่อนรับปากเรื่องความจุ
โปรไฟล์ขนาด
ตัวติดตั้งเลือกโปรไฟล์จากหน่วยความจำและซีพียูของเครื่อง แล้วให้คุณยืนยัน โปรไฟล์กำหนดเพดานหน่วยความจำและซีพียูของทุก container และหน่วยความจำของ PostgreSQL ส่วนที่ทำงานหนักจึงไม่แย่งทรัพยากรของส่วนอื่นจนหมด
| โปรไฟล์ | เครื่อง | สถานี | shard ของระบบออกอากาศ | Connection pooling (PgBouncer) |
|---|---|---|---|---|
| small | 2 vCPU, RAM 4 GB | ไม่เกิน 5 | 1 | ไม่ใช้ |
| medium | 4 vCPU, RAM 8 GB | ไม่เกิน 15 | 2 | ใช้ |
| large | 8 vCPU, RAM 16 GB | ไม่เกิน 40 | 4 | ใช้ |
วัดจริงด้วย 5 สถานีที่เล่นเล่นเพลงอัตโนมัติบนโปรไฟล์ small: ใช้หน่วยความจำรวมประมาณ 780 MB ในนั้นระบบออกอากาศใช้ประมาณ 575 MB และซีพียูหนึ่งในสามถึงครึ่งคอร์ แบนด์วิดท์ของผู้ฟังคิดเพิ่มต่างหาก (ผู้ฟัง × bitrate)
เลือกวิธีเข้าถึงแพลตฟอร์ม
ตัวติดตั้งถามเรื่องนี้เป็นอย่างแรก เปลี่ยนภายหลังได้โดยรันตัวติดตั้งอีกครั้ง (รหัสลับและข้อมูลทั้งหมดยังอยู่)
| แบบ | ที่อยู่ | ต้องมีอะไร | อะไรใช้ไม่ได้ |
|---|---|---|---|
โดเมน (domain แนะนำ) |
https://radio.example.th |
โดเมนที่ DNS ชี้มาที่เซิร์ฟเวอร์ และพอร์ต 80 กับ 443 ที่อินเทอร์เน็ตเข้าถึงได้ | ไม่มี ใช้ได้ทุกความสามารถ |
IP แบบ HTTPS (ip-https) |
https://203.0.113.10 |
IP สาธารณะที่อินเทอร์เน็ตเข้าถึงพอร์ต 80 และ 443 ได้ Caddy ขอใบรับรองของ Let's Encrypt สำหรับหมายเลข IP (อายุประมาณ 6 วัน ต่ออายุอัตโนมัติ) | โดเมนย่อยของสถานี, พาสคีย์ (ต้องใช้ชื่อโดเมน), LINE และ Telegram อาจไม่รับที่อยู่ที่ไม่มีชื่อโดเมน |
IP แบบ HTTPS ด้วย IP ส่วนตัว (ip-https ใบรับรองจาก Caddy เอง) |
https://192.168.1.20 |
ไม่ต้องมีอะไร (ใช้แบบนี้ด้วยเมื่อ Let's Encrypt ออกใบรับรอง IP ให้ไม่ได้) | เหมือนแถวบน และเบราว์เซอร์เตือนว่า "ไม่ปลอดภัย" จนกว่าจะติดตั้งใบรับรองหลักของ Caddy ในแต่ละเครื่อง การแจ้งเตือนแบบพุช LINE Telegram และเครื่องเล่นที่ฝังในเว็บไซต์อื่นใช้ไม่ได้ |
IP แบบ HTTP ธรรมดา ในเครือข่ายภายใน (ip-http) |
http://192.168.1.20 |
ไม่ต้องมีอะไร | ไม่มีการเข้ารหัสเลย เบราว์เซอร์ไม่ให้ใช้ไมโครโฟน (ห้องจัดรายการบนเว็บ) แขกรับเชิญทางไกล (WebRTC) การแจ้งเตือนแบบพุช และพาสคีย์ LINE และ Telegram ส่งข้อความเข้ามาไม่ได้ ส่วนการฟัง หน้าผู้ดูแล และดีเจที่ใช้ BUTT/OBS ใช้ได้ ใช้ในเครือข่ายที่ไว้ใจได้เท่านั้น |
ทุกแบบ สตรีมของผู้ฟังอยู่ที่ <ที่อยู่>/stream/<สถานี> Caddy ส่งต่อให้ Icecast จึงเปิดแค่พอร์ต 80 และ 443 (และพอร์ตดีเจ) โดเมนแยกสำหรับสตรีม (เช่น stream.radio.example.th) จะใช้หรือไม่ก็ได้
โดเมนไม่ได้ตายตัวตั้งแต่ติดตั้ง หลังติดตั้งแล้วผู้ดูแลระบบเพิ่มและเปลี่ยนโดเมนได้ในเว็บที่ การจัดการแพลตฟอร์ม → โดเมนและที่อยู่ (โดเมนของสถานีเอง ที่อยู่หลัก ที่อยู่สตรีม) Caddy ขอใบรับรองของชื่อใหม่ได้เองโดยไม่ต้องเริ่มใหม่ ค่าที่ตัวติดตั้งเขียนลง .env (BASE_URL, BASE_DOMAIN, ICECAST_PUBLIC_URL, HARBOR_PUBLIC_HOST) เป็นแค่ค่าเริ่มต้นเท่านั้น
DNS (แบบโดเมน)
สร้างระเบียนเหล่านี้ก่อนหรือหลังติดตั้งทันที (ตัวติดตั้งตรวจให้ และให้ติดตั้งต่อได้ถ้ายังไม่พร้อม radioctl doctor ตรวจซ้ำได้ภายหลัง) ใบรับรองจะออกทันทีที่ชื่อชี้มาที่เซิร์ฟเวอร์
| ชื่อ | ชนิด | ใช้ทำอะไร |
|---|---|---|
radio.example.th |
A (และ AAAA สำหรับ IPv6) → IP ของเซิร์ฟเวอร์ | เว็บไซต์ สตรีม (/stream/…) และที่อยู่สำหรับดีเจ |
*.radio.example.th |
A → IP ของเซิร์ฟเวอร์ | เฉพาะเมื่อต้องการโดเมนย่อยของสถานี (<สถานี>.radio.example.th) ระเบียน wildcard ตอบทุกชื่อใต้โดเมนนี้ |
stream.radio.example.th |
A → IP ของเซิร์ฟเวอร์ | เฉพาะเมื่อเลือกใช้โดเมนแยกสำหรับสตรีม |
ติดตั้งด้วยคำสั่งเดียว
บนเซิร์ฟเวอร์ ด้วยสิทธิ์ root:
curl -fsSL https://github.com/<owner>/<repo>/releases/latest/download/install.sh | sudo bash
<owner>/<repo> คือคลัง GitHub ที่ออกรุ่นนั้น สคริปต์ดาวน์โหลดรุ่นนั้นมาไว้ที่ /opt/radio แล้วรันตัวติดตั้งจากที่นั่น ถ้ามีไฟล์อยู่แล้ว (ไฟล์รุ่น radio-<version>.tar.gz ที่แตกแล้ว หรือ git clone) ให้วางไว้ที่ /opt/radio แล้วรัน:
cd /opt/radio
sudo bash scripts/install.sh
บน Ubuntu วิธีที่ง่ายที่สุดคือแพ็กเกจ Debian: sudo apt install ./radio_<version>_all.deb วางโฟลเดอร์เดียวกันไว้ที่ /opt/radio และเพิ่มคำสั่ง radio-setup (เหมือน sudo bash /opt/radio/scripts/install.sh) ดูคู่มือติดตั้งบน Ubuntu ทีละขั้น
ใช้ได้กับ Ubuntu 22.04 หรือ 24.04 และ Debian 12 บน x86_64 หรือ arm64 คำถามแรกคือภาษา (ไทยหรืออังกฤษ) ทุกคำถามเป็นข้อความธรรมดา พิมพ์คำตอบแล้วกด Enter ถ้าไม่พิมพ์อะไรจะใช้ค่าเริ่มต้นที่แสดงไว้ ตัวติดตั้งจะ:
- ตรวจระบบ ซีพียู หน่วยความจำ พื้นที่ดิสก์ และนาฬิกา แล้วแนะนำโปรไฟล์ขนาด
- ติดตั้ง Docker Engine และ Compose plugin จากคลัง apt ของ Docker ถ้ายังไม่มี และตั้งการหมุนเวียน log ของ Docker
- ถามวิธีเข้าถึง (โดเมน, IP แบบ HTTPS, IP แบบ HTTP) และรายละเอียดของที่อยู่: แบบโดเมนถามโดเมน จะใช้โดเมนย่อยของสถานีหรือไม่ โดเมนสตรีมแยก (ไม่บังคับ) แล้วตรวจ DNS ให้ แบบ IP ถามหมายเลข IP (เสนอค่าที่ตรวจพบ ถ้าอยู่หลังเราเตอร์จะบอกทั้ง IP ของเครื่องและ IP สาธารณะ)
- ถามอีเมลผู้ดูแลระบบ (ใช้กับ Let's Encrypt ด้วย) เซิร์ฟเวอร์อีเมล (ไม่บังคับ) โฟลเดอร์เก็บข้อมูลสำรอง และที่เก็บ S3 สำหรับข้อมูลสำรอง (ไม่บังคับ)
- สร้างรหัสผ่านและรหัสลับทั้งหมดด้วย
opensslแล้วเขียนลง/opt/radio/.envซึ่งอ่านได้เฉพาะ root - เปิดพอร์ตใน
ufwถ้าไฟร์วอลล์เปิดใช้อยู่ - ติดตั้งคำสั่ง
radioctl(ดูแลระบบ) และradio(คำสั่งผู้ดูแลของแพลตฟอร์ม) ใน/usr/local/bin - เตรียมอิมเมจ (ดาวน์โหลด หรือสร้างบนเครื่องเมื่อไม่ได้กำหนดที่มาของอิมเมจ หรือดาวน์โหลดไม่ได้และมีซอร์สโค้ดอยู่) เริ่มทุกบริการ แล้วรอจนทุกบริการพร้อม
- แสดงที่อยู่ของหน้าติดตั้ง
จากนั้นเปิด <ที่อยู่>/install ทันที แล้วทำตาม "เริ่มใช้งานครั้งแรก" ด้านล่าง คนแรกที่เปิดหน้านี้เป็นผู้สร้างผู้ดูแลระบบ (หรือสั่ง sudo radio create-admin you@example.th) เก็บสำเนา .env ไว้ที่ปลอดภัยนอกเซิร์ฟเวอร์ ต้องใช้ RESTIC_PASSWORD ในไฟล์นี้เพื่ออ่านข้อมูลสำรอง
แบบไม่ถาม (สำหรับระบบอัตโนมัติ) ให้ใส่คำตอบเป็นตัวเลือก:
sudo bash scripts/install.sh --yes --lang th --mode domain --domain radio.example.th --wildcard \
--email admin@example.th --profile small \
--s3-repository s3:https://s3.ap-southeast-1.amazonaws.com/my-bucket/radio \
--s3-access-key AKIA... --s3-secret-key ... --s3-region ap-southeast-1
sudo bash scripts/install.sh --yes --mode ip-https --ip 203.0.113.10 --email admin@example.th
sudo bash scripts/install.sh --yes --mode ip-http --ip 192.168.1.20 --email admin@example.th
--help แสดงตัวเลือกทั้งหมด เช่น --stream-domain, --dj-host, --ip-tls letsencrypt|internal, --domain-tls internal (ใบรับรองของ Caddy เอง สำหรับชื่อที่รู้จักเฉพาะในเครือข่ายขององค์กร), --version, --image-prefix ghcr.io/<owner> (ที่มาของอิมเมจ), --build (สร้างอิมเมจบนเครื่อง), --offline ไฟล์, --http-port/--https-port, --dj-port-start, --dry-run (ตรวจเครื่องและเขียน .env อย่างเดียว) รันตัวติดตั้งซ้ำได้อย่างปลอดภัย: เก็บรหัสลับเดิม เพิ่มรหัสที่ขาด ดึงอิมเมจใหม่ และเริ่มส่วนที่เปลี่ยนใหม่ ใช้ซ่อม อัปเกรด หรือเปลี่ยนวิธีเข้าถึงได้
พอร์ตที่ต้องเปิด (ตัวติดตั้งเปิดให้ถ้า ufw เปิดใช้อยู่):
| พอร์ต | ใช้โดย |
|---|---|
| 80/tcp, 443/tcp, 443/udp | Caddy (เว็บไซต์ สตรีม ใบรับรอง HTTP/3) แบบ ip-http ใช้แค่ 80 |
| 8100–8119/tcp (small), 8100–8139 (medium), 8100–8199 (large) | สัญญาณสดของดีเจ (BUTT/OBS) สถานีละหนึ่งพอร์ต ช่วงคือ HARBOR_PORT_START ถึง HARBOR_PORT_END |
| 5060/udp, 10000–10100/udp | Asterisk เฉพาะเมื่อใช้การฟังทางโทรศัพท์ |
อย่าเปิด 3000, 5432, 6432, 8000 หรือ 9100 ขึ้นไป เป็นพอร์ตภายใน และ Docker ไม่ได้เปิดออกนอกเครื่อง
ค่าที่ตัวติดตั้งเขียน
ค่าใน .env |
แบบโดเมน | แบบ IP |
|---|---|---|
PUBLIC_MODE |
domain |
ip-https หรือ ip-http (Caddy ใช้ deploy/caddy/<แบบ>.Caddyfile) |
BASE_URL |
https://radio.example.th |
https://203.0.113.10 หรือ http://192.168.1.20 (มี :พอร์ต ถ้าไม่ใช่ 443/80) |
BASE_DOMAIN |
โดเมน เฉพาะเมื่อใช้โดเมนย่อยของสถานี | ว่าง |
STREAM_PATH |
/stream |
/stream |
ICECAST_PUBLIC_URL |
BASE_URL/stream หรือ https://<โดเมนสตรีม> |
BASE_URL/stream |
STREAM_DOMAIN |
โดเมนสตรีมแยก (ถ้ามี) | ว่าง |
HARBOR_PUBLIC_HOST |
โดเมน (หรือ --dj-host) |
IP |
PUBLIC_IP, PUBLIC_IP_HOST, IP_TLS_ISSUER |
ว่าง | IP, IP ในรูปแบบ URL (IPv6 อยู่ในวงเล็บเหลี่ยม), letsencrypt หรือ internal |
DOMAIN_TLS_ISSUER |
public หรือ internal |
ไม่ใช้ |
HTTPS_PORT_SUFFIX |
:<พอร์ต> เมื่อ HTTPS_PORT ไม่ใช่ 443 นอกนั้นว่าง |
เหมือนกัน |
ใบรับรองของ Caddy เอง (IP ส่วนตัว)
เมื่อใช้ใบรับรองจากผู้ออกใบรับรองของ Caddy เอง เบราว์เซอร์จะเตือนจนกว่าแต่ละเครื่องจะเชื่อถือใบรับรองหลักของ Caddy วิธีทำให้เชื่อถือ:
sudo docker compose -f /opt/radio/docker-compose.yml cp caddy:/data/caddy/pki/authorities/local/root.crt ./radio-root.crt
ติดตั้ง radio-root.crt เป็นใบรับรองหลักที่เชื่อถือในคอมพิวเตอร์หรือโทรศัพท์แต่ละเครื่อง (Windows: ดับเบิลคลิก → Install certificate → Local machine → Trusted Root Certification Authorities, macOS: Keychain Access → System → Always trust, Android และ iOS: การตั้งค่า → ความปลอดภัย → ติดตั้งใบรับรอง แล้วบน iOS เปิดใช้ที่ ทั่วไป → เกี่ยวกับ → การตั้งค่าความเชื่อถือใบรับรอง) ทำเฉพาะกับอุปกรณ์ขององค์กรคุณเองเท่านั้น
อิมเมจและการติดตั้งแบบออฟไลน์
อิมเมจคือ ghcr.io/<owner>/radio-app, radio-engine, radio-icecast, radio-backup และ radio-pgbouncer สำหรับ linux/amd64 และ linux/arm64 เผยแพร่โดย GitHub Actions ของคลังโค้ด (<owner> คือเจ้าของคลัง) RADIO_IMAGE_PREFIX ใน .env บอกที่มาของอิมเมจ (ghcr.io/<owner> ไฟล์รุ่นตั้งให้เอง) ถ้าว่าง หรือดาวน์โหลดไม่ได้และมีซอร์สโค้ดในโฟลเดอร์ ตัวติดตั้งจะสร้างอิมเมจบนเครื่อง (RADIO_BUILD=true ครั้งแรกใช้เวลาหลายนาที) ในช่วงเปลี่ยนผ่าน อิมเมจชุดเดียวกันยังเผยแพร่ในชื่อเดิม ghcr.io/<owner>/saint-radio-* ด้วย
แต่ละรุ่นบน GitHub มีไฟล์เหล่านี้:
| ไฟล์ | คืออะไร |
|---|---|
install.sh |
ตัวติดตั้งบรรทัดเดียวของรุ่นนั้น |
radio_<version>_all.deb |
แพ็กเกจ Debian/Ubuntu: โฟลเดอร์เดียวกับไฟล์รุ่นใน /opt/radio พร้อมคำสั่ง radio-setup, radioctl และ radio ไม่มีไฟล์ .env อยู่ในแพ็กเกจ |
radio-<version>.tar.gz |
โฟลเดอร์ติดตั้ง: ไฟล์ Compose, deploy/, scripts/, docs/, .env.example, VERSION, release.env, คู่มือแบบข้อความธรรมดา INSTALL-UBUNTU.*.txt และซอร์สโค้ดที่เซิร์ฟเวอร์ใช้สร้างอิมเมจเอง |
radio-images-<version>-amd64.tar.gz, -arm64.tar.gz |
อิมเมจทั้งหมดของรุ่นสำหรับสถาปัตยกรรมหนึ่ง (รวม PostgreSQL และ Caddy) สำหรับเครื่องที่ไม่มีอินเทอร์เน็ต |
SHA256SUMS |
ค่าตรวจสอบไฟล์ |
ติดตั้งแบบไม่มีอินเทอร์เน็ต (ต้องมี Docker อยู่แล้ว):
sudo mkdir -p /opt/radio
sudo tar xzf radio-1.5.0.tar.gz -C /opt/radio --strip-components=1
cd /opt/radio
sudo bash scripts/install.sh --offline radio-images-1.5.0-amd64.tar.gz
--offline โหลดอิมเมจด้วย docker load และไม่ดาวน์โหลดอะไรเลย การอัปเดตทำแบบเดียวกัน: sudo radioctl update --offline radio-images-1.6.0-amd64.tar.gz (ถ้าไฟล์ในรุ่นใหม่เปลี่ยน ให้แตก radio-1.6.0.tar.gz ทับ /opt/radio ก่อน)
ติดตั้งและอัปเดตจาก GitHub ด้วย git
scripts/radio-git ติดตั้งระบบเป็นสำเนา git ของ repository แล้วอัปเดตจากที่นั่น ใช้กับ repository แบบ private ได้ เพราะเซิร์ฟเวอร์มี deploy key ของตัวเองที่อ่านได้อย่างเดียว และไม่ดาวน์โหลดอะไรจากหน้าเว็บของ GitHub คัดลอกไฟล์ไปที่เซิร์ฟเวอร์ (scp, WinSCP หรือวางเนื้อหาลงไฟล์) แล้วรัน
sudo bash radio-git install
- ติดตั้ง git, SSH client และ curl ถ้ายังไม่มี
- สร้าง deploy key (
/root/.ssh/radio_deploy) แสดง public key แยกบรรทัดพร้อมหน้าที่ต้องไปเพิ่ม (https://github.com/OWNER/REPO/settings/keys/newไม่ต้องติ๊ก Allow write access) และเก็บสำเนาไว้ในโฟลเดอร์บ้านของคุณ (~/radio-deploy-key.pub) ให้ดาวน์โหลดไปวางได้ถ้าคัดลอกจากหน้าจอไม่สะดวก แล้วรอจน GitHub รับ key host key ของ GitHub ได้จาก API ของ GitHub ผ่าน HTTPS และ Ed25519 key ที่ GitHub เผยแพร่ จึงไม่มีคำถาม "are you sure" - clone สาขาลงใน
/opt/radioแล้วรันตัวติดตั้ง (ตัวเลือกหลัง--ส่งต่อให้ตัวติดตั้ง เช่นsudo bash radio-git install -- --mode domain --domain radio.example.th) - หลังจากนั้นใช้
sudo radio-gitเป็นคำสั่งบนเซิร์ฟเวอร์ได้เลย
ถ้าเครื่องติดตั้งไว้แล้วด้วย .deb หรือ .tar.gz คำสั่งนี้จะเปลี่ยนให้อัปเดตผ่าน git: สำรองข้อมูลก่อน ถอนแพ็กเกจ (ไม่ลบ .env วอลุ่มของ Docker และข้อมูลสำรอง) เปลี่ยนโฟลเดอร์เป็นสำเนา git แล้วรันตัวติดตั้งอีกครั้งด้วยการตั้งค่าเดิม
| คำสั่ง | ทำอะไร |
|---|---|
sudo radio-git update |
ดึงสาขาล่าสุด แสดงรายการ commit ใหม่และ migration ฐานข้อมูลที่เพิ่ม ถามยืนยัน แล้วสำรองข้อมูลและย้ายระบบไปโค้ดใหม่ (radioctl update) |
sudo radio-git rollback |
กลับไปใช้โค้ดก่อนการอัปเดตครั้งล่าสุด (ไม่ย้อน migration ฐานข้อมูล ต้องกู้คืนข้อมูลสำรองเอง) |
sudo radio-git status |
เวอร์ชันและ commit ที่ใช้อยู่ และ GitHub มีโค้ดใหม่กว่าหรือไม่ |
sudo radio-git key |
แสดง deploy key อีกครั้งและทดสอบการเชื่อมต่อ |
ตัวเลือก: --lang th|en, --yes, --branch ชื่อสาขา, --pull-images (ใช้อิมเมจที่เผยแพร่บน ghcr.io แทนการสร้างบนเซิร์ฟเวอร์ อิมเมจของ repository แบบ private ต้อง docker login ghcr.io หรือตั้งแพ็กเกจเป็น public), --ssh-443 (เมื่อเครือข่ายปิดพอร์ต 22), --no-backup, --dry-run ค่าที่เลือกเก็บไว้ใน /etc/radio-git.conf การอัปเดตจะหยุดถ้ามีการแก้ไฟล์โปรแกรมใน /opt/radio บนเซิร์ฟเวอร์ หรือถ้าประวัติบน GitHub ไม่ต่อจากโค้ดในเครื่อง
ติดตั้งเอง (ไม่ใช้ตัวติดตั้ง)
ตัวติดตั้งแค่ทำขั้นตอนเหล่านี้ให้อัตโนมัติ คุณทำเองได้:
- ติดตั้ง Docker Engine พร้อม Compose plugin แล้ววางไฟล์ไว้ที่
/opt/radio cp .env.example .env && chmod 600 .envแล้วแก้.env: ค่าที่อยู่ตามตารางด้านบน,ACME_EMAIL(Let's Encrypt),POSTGRES_PASSWORDและDB_APP_PASSWORD,SESSION_SECRET,IP_HASH_SALT,DB_SCOPE_SECRET(สตริงสุ่มยาว เช่นจากopenssl rand -hex 32),ICECAST_SOURCE_PASSWORD,ICECAST_ADMIN_PASSWORD,RESTIC_PASSWORD(เข้ารหัสข้อมูลสำรอง เก็บสำเนาไว้นอกเซิร์ฟเวอร์),SMTP_URL,MAIL_FROM(ถ้าSMTP_URLว่าง อีเมลจะถูกเขียนเป็นไฟล์ ดู "เริ่มใช้งานครั้งแรก"),RADIO_IMAGE_PREFIXและRADIO_VERSION- คัดลอกค่าของโปรไฟล์ขนาดหนึ่งโปรไฟล์
deploy/profiles/small.env(หรือmedium,large) ลงใน.envแทนบรรทัดชื่อเดียวกัน และตั้งHARBOR_PORT_ENDเป็นHARBOR_PORT_START + HARBOR_PORT_COUNT − 1ถ้าใช้มากกว่าหนึ่ง shard ให้รันradioctl scale --shards Nภายหลัง คำสั่งนี้เขียนบริการที่เพิ่มให้ - เริ่มระบบ:
docker compose up -dถ้าจะสร้างอิมเมจเอง:docker compose -f docker-compose.yml -f docker-compose.build.yml buildก่อน
docker-compose.yml ตั้งชื่อโปรเจกต์ Compose ว่า radio ไม่ว่าโฟลเดอร์จะชื่ออะไร วอลุ่มจึงชื่อ radio_db-data, radio_radio-data และอื่น ๆ เสมอ Docker Compose ตั้ง DATABASE_URL, DATABASE_ADMIN_URL, DATA_DIR, ICECAST_HOST, LIQUIDSOAP_CALLBACK_URL, LIQUIDSOAP_CONTROL_HOST, LIQUIDSOAP_HARBOR_HOST, IVR_AGI_PUBLIC_HOST, ICECAST_TRUSTED_PROXIES, ICECAST_CONFIG_FILE, ICECAST_LOG_DIR, ICECAST_STANDBY, WORKER_METRICS_PORT และ INSTANCE_NAME เอง ค่าเหล่านี้ใน .env จะไม่ถูกใช้ DATABASE_ADMIN_URL (บัญชีผู้ดูแลฐานข้อมูล) ให้เฉพาะบริการ app ซึ่งเป็นผู้ปรับฐานข้อมูล รหัสลับของการสำรองข้อมูลให้เฉพาะบริการ backup
เมื่อ NODE_ENV=production (ค่าเริ่มต้นใน image) app และ worker จะไม่ยอมเริ่มทำงานถ้า SESSION_SECRET สั้นกว่า 32 ตัวอักษรหรือยังเป็นค่าตัวอย่าง IP_HASH_SALT ยังเป็น dev-ip-salt หรือค่าตัวอย่าง หรือรหัสผ่าน Icecast ยังเป็น hackme-… หรือ change-me… ข้อความแจ้งข้อผิดพลาดบอกว่าต้องเปลี่ยนค่าใด
การติดตั้งที่ทำก่อนใช้ชื่อ "radio"
การติดตั้งรุ่นก่อนหน้านี้อยู่ที่ /opt/saint-radio หรือโฟลเดอร์ radio-platform และวอลุ่มของ Docker ตั้งชื่อตามโฟลเดอร์นั้น (saint-radio_db-data, …) docker-compose.yml ใหม่ใช้ชื่อโปรเจกต์ radio เสมอ การติดตั้งแบบนี้จึงต้องย้ายหนึ่งครั้ง ไม่อย่างนั้นจะเริ่มด้วยฐานข้อมูลว่าง radioctl และตัวติดตั้งตรวจพบและหยุดพร้อมบอกวิธีแก้ ให้แตกรุ่นใหม่ทับโฟลเดอร์เดิม แล้ว:
cd /opt/saint-radio
sudo bash scripts/radioctl migrate-name --dry-run # แสดงแผน ไม่เปลี่ยนอะไร
sudo bash scripts/radioctl migrate-name
คำสั่งนี้สำรองข้อมูล หยุด container เดิม คัดลอกทุกวอลุ่มไปเป็น radio_* ย้ายโฟลเดอร์ไปที่ /opt/radio (ที่เดิมเหลือเป็นลิงก์) ย้าย /var/backups/saint-radio ไปเป็น /var/backups/radio ตั้งชื่อใหม่ให้อิมเมจที่มีในเครื่อง เริ่มระบบ แล้วเทียบจำนวนสถานี ผู้ใช้ และไฟล์เสียงก่อนและหลังย้าย วอลุ่มเดิมไม่ถูกแก้ไข เมื่อตรวจเว็บและสตรีมแล้ว ลบด้วยคำสั่ง docker volume rm ที่คำสั่งนี้แสดง รันซ้ำก็ไม่ทำอะไรเพิ่ม รายละเอียดในคู่มือดูแลระบบ
NAS (Synology, QNAP)
NAS ใช้ไฟล์ Compose ของตัวเอง deploy/nas/docker-compose.yml: พอร์ตเว็บ 8080 (HTTP) และ 8443 (HTTPS) เพราะ NAS ใช้ 80, 443, 5000 และ 5001 อยู่แล้ว ไม่สร้างอิมเมจบน NAS ไม่ใช้เครือข่ายของเครื่องโฮสต์ ข้อมูลสำรองอยู่ที่ deploy/nas/backups ในโฟลเดอร์โปรเจกต์ ต้องใช้ DSM 7.2 กับ Container Manager หรือ QTS/QuTS hero กับ Container Station 3 รุ่น x86_64 หรือ arm64 และหน่วยความจำอย่างน้อย 4 GB
คัดลอกโฟลเดอร์รุ่นไปไว้บน NAS เช่น
/volume1/docker/radio(Synology) หรือ/share/Container/radio(QNAP)เขียนค่าตั้งผ่าน SSH (Synology: แผงควบคุม → Terminal & SNMP → เปิด SSH):
cd /volume1/docker/radio sudo bash scripts/install.sh --nasคำถามเหมือนกัน (บน NAS ส่วนใหญ่เลือก "IP แบบ HTTP" สำหรับเครือข่ายภายใน) เขียน
deploy/nas/.envและถ้ามีคำสั่งdocker composeก็เริ่มระบบให้ ไม่ติดตั้ง Docker และไม่แก้ไฟร์วอลล์ของ NAS ถ้าไม่มี SSH ให้คัดลอก.env.exampleเป็นdeploy/nas/.envแล้วกรอกค่าตาม "ติดตั้งเอง"ถ้ายังไม่ได้เริ่มระบบ (ไม่มี
docker composeทาง SSH): Synology Container Manager → โปรเจกต์ → สร้าง ตั้งชื่อโปรเจกต์radioพาธ…/radio/deploy/nasที่มา "ใช้ docker-compose.yml ที่มีอยู่" → ถัดไป → เสร็จ บน QNAP ให้เริ่มผ่าน SSH ในโฟลเดอร์นั้น:cd /share/Container/radio/deploy/nas && sudo docker compose up -d(หน้า "สร้างแอปพลิเคชัน" ของ Container Station ใช้โฟลเดอร์แบบอ้างอิงที่ไฟล์นี้ต้องใช้ไม่ได้)เปิด
http://<IP ของ NAS>:8080/install
ถ้าต้องให้เข้าจากอินเทอร์เน็ตด้วยโดเมน: ตั้งเราเตอร์ส่งต่อพอร์ต 80 → 8080 และ 443 → 8443 ไปที่ NAS แล้วรัน sudo bash scripts/install.sh --nas --mode domain (Let's Encrypt ตรวจผ่านพอร์ต 80) ส่งต่อช่วงพอร์ตดีเจ (8100 ขึ้นไป) ด้วยถ้าดีเจเชื่อมต่อจากข้างนอก อิมเมจ: NAS ดาวน์โหลดเอง (RADIO_IMAGE_PREFIX) หรือโหลดไฟล์อิมเมจของรุ่นก่อนด้วย sudo docker load -i radio-images-<version>-<arch>.tar.gz แล้วใช้ --offline สำรอง deploy/nas/backups และ deploy/nas/.env ออกนอก NAS ด้วย (Hyper Backup, HBS 3) ดูแลระบบด้วย sudo bash scripts/radioctl … ผ่าน SSH
Proxmox VE
ใช้เครื่องเสมือน (VM) Ubuntu 24.04 (ง่ายที่สุด ทุกอย่างเหมือนเซิร์ฟเวอร์ทั่วไป) หรือ container LXC:
- สร้าง container แบบ unprivileged ด้วย Ubuntu 24.04 หรือ Debian 12 อย่างน้อย 2 คอร์ หน่วยความจำ 4 GB ดิสก์ 30 GB
- ก่อนเริ่ม container ให้เปิด Options → Features → nesting และ keyctl (หรือใน
/etc/pve/lxc/<id>.conf:features: keyctl=1,nesting=1) ถ้าไม่เปิด Docker จะรัน container ไม่ได้ - เริ่ม container แล้วรันตัวติดตั้งบรรทัดเดียวข้างใน ตัวติดตั้งตรวจพบ LXC และเตือนเรื่องสองตัวเลือกนี้
ถ้า Docker ใน LXC แจ้ง "permission denied" เรื่อง sysctl หรือ overlay ให้ใช้ VM แทน ถ้า container อยู่หลัง NAT ให้ส่งต่อพอร์ต 80, 443 และช่วงพอร์ตดีเจไปที่ที่อยู่ของ container
เพิ่มเติม: ฟังทางโทรศัพท์
คัดลอกตัวอย่าง SIP trunk แล้วกรอกข้อมูลของผู้ให้บริการ:
cp deploy/asterisk/pjsip_trunk.example.conf deploy/asterisk/pjsip_trunk.confใน
.envตั้งIVR_AGI_PORT=4573และเพิ่มivrในCOMPOSE_PROFILES(เช่นCOMPOSE_PROFILES=ivrหรือpgbouncer,ivrสำหรับ medium และ large)สร้างและเริ่ม:
sudo docker compose -f /opt/radio/docker-compose.yml build asterisk && sudo radioctl up
ดู "ฟังทางโทรศัพท์ (Asterisk)" ในหัวข้อตั้งค่าความสามารถเสริม
Kubernetes (Helm)
chart อยู่ที่ deploy/helm/radio CI ตรวจด้วย helm lint, helm template และ kubeconform ยังไม่เคยรันบนคลัสเตอร์จริง ควรลองบนคลัสเตอร์ทดสอบก่อนใช้งานจริง
chart เปลี่ยนชื่อจาก saint-radio เป็น radio แต่ออบเจกต์ Kubernetes ที่สร้างยังใช้ชื่อ <release>-saint-radio-… เหมือนเดิม (ค่า nameOverride เริ่มต้นเป็น saint-radio) เพราะการเปลี่ยนชื่อจะทำให้ release ที่มีอยู่ได้วอลุ่ม Secret และที่อยู่ LoadBalancer ชุดใหม่ อัปเกรด release เดิมด้วยโฟลเดอร์ chart ใหม่ได้ตามปกติ การติดตั้งใหม่ตั้ง nameOverride: radio ได้
สิ่งที่ต้องมี
- PostgreSQL 16 ภายนอก chart (แนะนำฐานข้อมูลแบบ managed)
- ที่เก็บไฟล์แบบ S3 (แนะนำ) ถ้าเก็บในเครื่อง ต้องใช้ volume แบบ ReadWriteMany
- volume แบบ ReadWriteMany (NFS, CephFS, EFS, Filestore ฯลฯ) สำหรับสคริปต์สถานี ไฟล์ HLS ไฟล์ส่งออก และไฟล์ชั่วคราว worker เป็นผู้เขียน app เป็นผู้ให้บริการไฟล์
- ingress controller (ค่าเริ่มต้นใช้ nginx) และใบรับรอง TLS ของ
baseDomain,*.baseDomainและstreamDomainเช่น จาก cert-manager - image ใน registry ที่คลัสเตอร์ดึงได้: ใช้
ghcr.io/<owner>/radio-appและ-engineที่เผยแพร่ไว้ (amd64 และ arm64) หรือสร้างเอง เช่นdocker build --target app -t registry.example.th/radio-app:0.3.0 .และ--target engine -t registry.example.th/radio-engine:0.3.0แล้วdocker push
ค่าที่ต้องตั้ง
| ค่า | ใส่อะไร |
|---|---|
image.repository, image.tag |
บังคับ: ชื่อ image โดยไม่มีส่วนท้าย เช่น ghcr.io/<owner>/radio หรือ registry.example.th/radio chart จะเติม -app (เว็บแอป และ migration job) และ -engine (worker, Icecast, relay) ให้เอง |
nameOverride |
ชื่อที่อยู่ในชื่อออบเจกต์ ว่าง = saint-radio (คงไว้สำหรับ release ที่มีอยู่) การติดตั้งใหม่ใช้ radio ได้ |
baseUrl, baseDomain, streamDomain |
โดเมนของคุณ |
harborPublicHost |
ที่ดีเจเชื่อมต่อ {shard} จะถูกแทนด้วยหมายเลข worker เช่น dj{shard}.radio.example.th |
database.url |
URL ของ role ของ app (ไม่ใช่ superuser) |
database.adminUrl |
URL ของผู้ดูแล ใช้เฉพาะ migration job (pod อื่นไม่ได้รับค่านี้) |
database.appPassword |
ไม่บังคับ รหัสผ่าน role ของ app ถ้าเว้นว่างใช้รหัสผ่านใน database.url (ถ้าตั้ง ต้องตรงกัน) |
secrets.sessionSecret, secrets.ipHashSalt |
ข้อความสุ่มยาว ๆ (บังคับ) |
secrets.icecastSourcePassword, secrets.icecastAdminPassword |
บังคับ |
secrets.icecastRelayPassword |
ต้องมีเมื่อ relays.enabled เป็น true |
secrets.smtpUrl, mailFrom |
อีเมล |
secrets.s3AccessKeyId, secrets.s3SecretAccessKey, storage.* |
ที่เก็บไฟล์ |
secrets.transcribeApiKey, secrets.omiseSecretKey |
ความสามารถเสริม |
secrets.dbScopeSecret |
ไม่บังคับ ใช้ลงลายมือชื่อขอบเขตสถานีในฐานข้อมูล (เว้นว่าง = สร้างจาก secrets.sessionSecret) |
secrets.metricsToken, secrets.alertWebhookSecret |
ไม่บังคับ สำหรับ metrics ของ Prometheus และเว็บฮุกการแจ้งเตือนที่มีลายเซ็น (ดูหัวข้อการติดตามระบบ) |
settings: |
TRANSCRIBE_URL, TRANSCRIBE_MODEL, TRANSCRIBE_COMMAND, TTS_COMMAND, RTC_ICE_SERVERS, LICENSE_ENFORCE, UPDATE_CHECK_URL, WEBHOOK_ALLOW_HTTP, WEBHOOK_ALLOW_PRIVATE, EXTERNAL_OUTPUT_ALLOW_PRIVATE, ALERT_EXTERNAL_OUTPUT_MINUTES, LOGIN_RATE_LIMIT, IVR_AGI_PORT, ALERT_WEBHOOK_URL, ALERT_WEBHOOK_ALLOW_PRIVATE, ALERT_RENOTIFY_HOURS, ALERT_DISK_PERCENT, METRICS_ALLOW_IPS ค่าที่ว่างจะไม่ถูกส่ง |
monitoring.* |
Prometheus metrics: enabled, workerPort (ค่าเริ่มต้น 9464), annotation ของ pod, ServiceMonitor (ดูการติดตามระบบ) |
extraEnv |
ตัวแปรอื่น ๆ เป็นรายการคู่ name/value |
worker.shards, worker.harborPorts |
จำนวน pod ของ worker และจำนวนพอร์ตดีเจต่อ pod (ต้องพอกับจำนวนสถานี) |
icecast.trustedProxies |
IP ของ proxy หน้า Icecast เพื่อให้เห็น IP จริงของผู้ฟัง |
relays.enabled, relays.replicas |
edge relay |
geoip.secretName, geoip.fileName |
ฐานข้อมูล GeoIP จาก Secret ที่คุณสร้าง |
ingress.*, persistence.*, app.autoscaling.* |
ค่าเฉพาะของคลัสเตอร์ |
ถ้าต้องการจัดการ secret เอง ให้สร้าง Secret แล้วตั้ง existingSecret คีย์ที่ใช้คือ SESSION_SECRET, IP_HASH_SALT, DATABASE_URL, DATABASE_ADMIN_URL, DB_APP_PASSWORD, ICECAST_SOURCE_PASSWORD, ICECAST_ADMIN_PASSWORD, ICECAST_RELAY_PASSWORD, SMTP_URL, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, TRANSCRIBE_API_KEY, OMISE_SECRET_KEY และคีย์ที่ไม่บังคับ DB_SCOPE_SECRET, METRICS_TOKEN, ALERT_WEBHOOK_SECRET คีย์ SESSION_SECRET, IP_HASH_SALT, DATABASE_URL, DATABASE_ADMIN_URL, ICECAST_SOURCE_PASSWORD และ ICECAST_ADMIN_PASSWORD จำเป็นต้องมี ถ้า Secret ขาดคีย์ใด pod นั้นจะไม่เริ่มทำงาน (CreateContainerConfigError)
ติดตั้ง
helm install radio deploy/helm/radio -f my-values.yaml
สิ่งที่ chart สร้าง
| ออบเจกต์ | หน้าที่ |
|---|---|
| Migration Job | รัน node dist/src/cli.js migrate ก่อนติดตั้งและอัปเกรดทุกครั้ง pod ของ app และ worker ได้ MIGRATE_ON_START=false จะได้ไม่แย่งกัน migrate มีเฉพาะ job นี้ที่ได้รับ DATABASE_ADMIN_URL |
| app Deployment + Service | เว็บ (ค่าเริ่มต้น 2 replica ตั้ง autoscaling ได้) probe ใช้ /health |
| worker StatefulSet + headless Service | worker ระบบออกอากาศ หมายเลข pod คือหมายเลข shard |
Service …-dj-<n> |
shard ละ 1 ตัว (ค่าเริ่มต้นเป็น LoadBalancer) สำหรับพอร์ตดีเจชี้ชื่อ harborPublicHost แต่ละชื่อไปที่ service ของ shard นั้น |
| icecast Deployment + Service | Icecast หลัก (1 replica) |
| relay StatefulSet + Service | edge relay เมื่อ relays.enabled เป็น true |
| Ingress | baseDomain และ *.baseDomain ไปที่ app, streamDomain ไปที่ Icecast หรือ relay และกัน /internal |
| PersistentVolumeClaim | volume ข้อมูลที่ใช้ร่วมกัน (ยกเว้นตั้ง persistence.existingClaim) |
| Secret | สร้างจาก secrets.* (ยกเว้นตั้ง existingSecret) สร้างเป็น hook ก่อนติดตั้งและอัปเกรด เพื่อให้ migration job อ่านได้ helm uninstall จะไม่ลบ Secret นี้ ให้ลบเองด้วย kubectl delete secret <release>-saint-radio (ถ้าตั้ง nameOverride: <release>-<nameOverride>) |
ingress ครอบคลุมแค่โดเมนหลัก โดเมนย่อย และโดเมนสตรีม (chart ให้บริการสตรีมที่ streamDomain ไม่ใช่ใต้ /stream) ถ้าสถานีใช้โดเมนของตัวเอง ต้องเพิ่มกฎ ingress และใบรับรองเอง
ทุกคอนเทนเนอร์ทำงานเป็นผู้ใช้ 10001 ยกระดับสิทธิ์ไม่ได้ และตัด Linux capabilities ออกทั้งหมด
หลังติดตั้ง เปิด <baseUrl>/install หรือสร้างผู้ดูแลด้วย kubectl exec deploy/radio-saint-radio-app -- node dist/src/cli.js create-admin you@example.th (ชื่อคือ <release>-saint-radio-app) ใน pod ของ app คำสั่ง create-admin ไม่ทำ migration เพราะมีแต่ migration job ที่มีการเชื่อมต่อของผู้ดูแล ถ้ายังมี migration ค้าง คำสั่งจะหยุดพร้อมข้อความแจ้ง
เริ่มใช้งานครั้งแรก
เปิด https://<โดเมนของคุณ>/install หน้าติดตั้งบนเว็บจะ
- ตรวจความพร้อม: PostgreSQL, migration, ที่เก็บไฟล์, FFmpeg (บังคับ) และ Icecast, SMTP (แค่เตือน) เมื่อใช้ Docker Liquidsoap อยู่ใน container ของ worker ไม่ใช่ในเว็บแอป และ migration ทำไปแล้วตอนแอปเริ่มทำงาน
- สร้างผู้ดูแลระบบคนแรก เข้าสู่ระบบด้วยรหัสที่ส่งทางอีเมล ไม่มีรหัสผ่าน
- สร้างองค์กรและสถานีแรก (ไม่บังคับ) องค์กรนี้ได้แพ็กเกจ Unlimited
- พาผู้ดูแลเข้าสู่ระบบทันที แล้วเปิดศูนย์ควบคุมแพลตฟอร์ม
หลังติดตั้งเสร็จ /install จะพากลับหน้าแรก ถ้าต้องการเพิ่มผู้ดูแลระบบภายหลัง ใช้ sudo radio create-admin <อีเมล> (ดูหัวข้อคำสั่งผู้ดูแล)
ทำอะไรต่อ
- อีเมล: ตั้ง
SMTP_URLและMAIL_FROMแล้ว restart app ถ้าไม่ตั้งSMTP_URLอีเมลทุกฉบับ (รวมรหัสเข้าสู่ระบบ) จะถูกเขียนเป็นไฟล์ในDATA_DIR/mail-outbox - แพ็กเกจ: ศูนย์ควบคุมแพลตฟอร์ม → แพ็กเกจ ระบบสร้างไว้ 4 แพ็กเกจ: Community, Standard, Pro และ Unlimited ปรับขีดจำกัดหรือเพิ่มแพ็กเกจเองได้
- องค์กรและสถานี: ศูนย์ควบคุมแพลตฟอร์ม → องค์กรและสถานี
- ตั้งค่าสถานี: ในแต่ละสถานี อัปโหลดเสียง สร้างเพลย์ลิสต์และผังรายการ แล้วเชิญทีมงาน
- ความปลอดภัย: เพิ่มพาสคีย์ให้บัญชีของคุณเอง แล้วพิจารณานโยบายพาสคีย์ของทีมงาน (ดูรายการตรวจความปลอดภัย)
- หน้าสถานะ: ศูนย์ควบคุมแพลตฟอร์ม → สุขภาพระบบ ตั้งเป้าหมายความพร้อมใช้งานและจำนวนนาทีก่อนแจ้งเตือน
ตัวแปรสภาพแวดล้อม (environment variables)
"ว่าง" หมายถึงไม่ได้ตั้งค่า สวิตช์เปิดปิดจะเปิดเฉพาะเมื่อค่าเป็น true ตรงตัว (ยกเว้น MIGRATE_ON_START ที่ปิดเฉพาะเมื่อเป็น false)
ค่าหลัก
| ตัวแปร | ค่าเริ่มต้น | ใช้ทำอะไร |
|---|---|---|
NODE_ENV |
development |
ตั้งเป็น production บนเครื่องใช้งานจริง |
PORT |
3000 |
พอร์ตที่ app ฟัง |
HOST |
0.0.0.0 |
ที่อยู่ที่ app ฟัง ถ้าเครื่องเดียวมี proxy ในเครื่อง ใช้ 127.0.0.1 ถ้าอยู่ใน container ให้คง 0.0.0.0 |
PUBLIC_MODE |
domain |
วิธีเข้าถึงแพลตฟอร์ม: domain, ip-https หรือ ip-http (ดู "เลือกวิธีเข้าถึงแพลตฟอร์ม") Docker Compose ใช้ค่านี้เลือกค่าตั้งของ Caddy |
BASE_URL |
http://localhost:3000 |
URL สาธารณะของแพลตฟอร์ม ใช้ในลิงก์และอีเมลพาสคีย์ใช้โดเมนนี้และต้องเป็น HTTPS เป็นค่าเริ่มต้นเท่านั้น ภายหลังเปลี่ยนได้ที่ การจัดการแพลตฟอร์ม → โดเมนและที่อยู่ |
BASE_DOMAIN |
ว่าง | โดเมนย่อยของสถานี <slug>.BASE_DOMAIN (ต้องมีระเบียน DNS แบบ wildcard) ถ้าว่าง ใช้แค่ลิงก์ /s/<slug> |
STREAM_PATH |
/stream |
พาธใต้ BASE_URL ที่ Caddy ส่งสตรีมต่อให้ Icecast |
SESSION_SECRET |
dev-only-change-me-please-32-bytes-min |
ใช้ลงนาม session และกุญแจภายใน (รวมถึงลิงก์ตรวจสิทธิ์ผู้ฟังของ Icecast) ต้องยาวและสุ่ม เมื่อ NODE_ENV=production app จะไม่เริ่มทำงานถ้าสั้นกว่า 32 ตัวอักษรหรือเป็นค่าตัวอย่าง |
IP_HASH_SALT |
dev-ip-salt |
salt สำหรับแฮช IP ผู้ฟัง ระบบไม่เก็บ IP จริง ห้ามเปลี่ยนบ่อย เมื่อ NODE_ENV=production ระบบไม่ยอมรับค่าเริ่มต้นและค่าตัวอย่าง |
DATA_DIR |
./data |
โฟลเดอร์ข้อมูล: media, stations, hls, mail-outbox, exports, tmp และอื่น ๆ |
LOGIN_RATE_LIMIT |
8 |
จำนวนครั้งที่ขอเข้าสู่ระบบได้ต่อ IP ต่อ 10 นาที การกรอกรหัสได้ 20 ครั้งหรือค่านี้ถ้ามากกว่า เพิ่มค่าได้ถ้าสำนักงานใหญ่ใช้ NAT เดียวกัน |
LOGIN_CODES_PER_HOUR |
5 เมื่อ production, 1000 ในกรณีอื่น |
จำนวนรหัสเข้าสู่ระบบที่ส่งให้อีเมลหนึ่งได้ต่อชั่วโมง |
LOGIN_FAILED_PER_HOUR |
15 |
จำนวนครั้งที่กรอกรหัสผิดได้ต่ออีเมลต่อชั่วโมง นับรวมทุกรหัสของอีเมลนั้น |
TRUST_PROXY |
loopback,linklocal,uniquelocal |
เครื่องที่ต่อเข้ามาโดยตรงเครื่องใดส่ง X-Forwarded-For ได้ (IP ของผู้ใช้ที่ใช้จำกัดจำนวนครั้ง บล็อกตามประเทศ และบันทึก) ค่าเริ่มต้นเชื่อ loopback และเครือข่ายภายใน (proxy บนเครื่องเดียวกัน Docker หรือ Kubernetes) true เชื่อทุกเครื่อง (ใช้เฉพาะเมื่อไม่มีใครเข้าถึง app ได้โดยตรง) false ไม่ใช้ header นี้ หรือระบุรายการ IP ช่วง CIDR และชื่อ loopback, linklocal และ uniquelocal คั่นด้วยจุลภาค proxy ที่อยู่นอกเครือข่ายภายในต้องระบุไว้ในรายการ |
POSTGRES_PASSWORD |
ไม่มี | เฉพาะ Docker Compose: รหัสผ่านบัญชี postgres ใน container db |
STREAM_DOMAIN |
ว่าง | เฉพาะ Docker Compose: โฮสต์แยกสำหรับสตรีม (ไม่บังคับ ทุกอย่างบนโฮสต์นี้ส่งไป Icecast) |
ACME_EMAIL |
ไม่มี | เฉพาะ Docker Compose: อีเมลสำหรับ Let's Encrypt |
PUBLIC_IP, PUBLIC_IP_HOST |
ว่าง | Docker Compose แบบ IP: IP ของเซิร์ฟเวอร์ และค่าเดียวกันในรูปแบบ URL (IPv6 อยู่ในวงเล็บเหลี่ยม) |
IP_TLS_ISSUER |
letsencrypt |
Docker Compose แบบ ip-https: letsencrypt (ใบรับรอง IP โปรไฟล์ "shortlived" ถ้าไม่ได้ใช้ของ Caddy เองแทน) หรือ internal (ของ Caddy เอง) |
DOMAIN_TLS_ISSUER |
public |
Docker Compose แบบ domain: public (Let's Encrypt) หรือ internal (ของ Caddy เอง สำหรับชื่อที่รู้จักเฉพาะในเครือข่ายขององค์กร) |
HTTP_PORT, HTTPS_PORT, HTTPS_PORT_SUFFIX |
80, 443, ว่าง |
Docker Compose: พอร์ตของ Caddy บนเครื่อง ส่วนต่อท้าย (:8443) ทำให้การเปลี่ยนจาก HTTP ไป HTTPS คงพอร์ตที่ไม่ใช่ 443 ไว้ |
RADIO_IMAGE_PREFIX, RADIO_VERSION, ICECAST_VERSION, RADIO_BUILD |
ว่าง, latest, RADIO_VERSION, ว่าง |
Docker Compose: อิมเมจ <prefix>/radio-*:<version> (prefix ว่าง: local/radio-* สร้างบนเครื่องเมื่อ RADIO_BUILD=true) |
ฐานข้อมูล
| ตัวแปร | ค่าเริ่มต้น | ใช้ทำอะไร |
|---|---|---|
DATABASE_URL |
postgres://radio_app:radio_app@127.0.0.1:5432/radio |
การเชื่อมต่อของ app ต้องเป็น role ที่ไม่ใช่ superuser และอยู่ใต้ row-level security |
DATABASE_ADMIN_URL |
ไม่มี | การเชื่อมต่อของผู้ดูแล ใช้เฉพาะ migration ต้องเป็นผู้ใช้คนละคนกับใน DATABASE_URL ถ้าตั้งแค่ DATABASE_URL คำสั่ง migrate (และ app ตอนเริ่มทำงาน) จะแค่ตรวจว่า migration ครบแล้ว ถ้าไม่ตั้งทั้งสองตัว ใช้ postgres://postgres@127.0.0.1:5432/radio |
DB_SCOPE_SECRET |
สร้างจาก SESSION_SECRET |
ใช้ลงลายมือชื่อขอบเขตสถานีของทุกธุรกรรมในฐานข้อมูล SQL ที่รันด้วย role ของ app จึงสลับตัวเองไปใช้สถานีอื่นไม่ได้ (ดูรายการตรวจความปลอดภัย) ไม่บังคับ ถ้าเว้นว่างจะสร้างจาก SESSION_SECRET ถ้าตั้ง ต้องเป็นข้อความสุ่มอย่างน้อย 32 ตัวอักษร app, worker และ migration (ใน Helm คือ migration job) ต้องได้ค่าเดียวกัน |
DATABASE_POOL_URL |
ว่าง | การเชื่อมต่อที่สอง (ไม่บังคับ) สำหรับ pool คำสั่ง query ของแอป เช่นผ่าน PgBouncer (postgres://radio_app:…@pgbouncer:6432/radio โปรไฟล์ medium และ large ตั้งให้) LISTEN/NOTIFY และ migration ใช้ DATABASE_URL เสมอ |
DB_APP_PASSWORD |
รหัสผ่านใน DATABASE_URL |
รหัสผ่านของ role ของ app migration ตั้งให้ตอนสร้าง role และตั้งใหม่เมื่อ app เข้าระบบด้วยรหัสนี้ไม่ได้แล้ว ถ้าตั้ง ต้องตรงกับใน DATABASE_URL |
MIGRATE_ON_START |
เปิด | app อัปเดตฐานข้อมูลตอนเริ่มทำงาน ยกเว้นตั้งเป็น false Helm chart ตั้ง false เพราะมี migration job ทำให้แล้ว |
ที่เก็บไฟล์
| ตัวแปร | ค่าเริ่มต้น | ใช้ทำอะไร |
|---|---|---|
STORAGE_DRIVER |
local |
local (ไฟล์อยู่ใน DATA_DIR/media) หรือ s3 |
S3_ENDPOINT |
ว่าง | endpoint ของ S3 สำหรับ MinIO, Cloudflare R2, Wasabi ฯลฯ ถ้าว่าง ใช้ AWS |
S3_REGION |
auto |
region |
S3_BUCKET |
radio |
ชื่อ bucket |
S3_ACCESS_KEY_ID |
ว่าง | access key |
S3_SECRET_ACCESS_KEY |
ว่าง | secret key |
S3_FORCE_PATH_STYLE |
true |
ใช้ URL แบบ path-style ค่าอื่นที่ไม่ใช่ true คือปิด |
อีเมล
| ตัวแปร | ค่าเริ่มต้น | ใช้ทำอะไร |
|---|---|---|
SMTP_URL |
ว่าง | เช่น smtp://user:pass@smtp.example.th:587 ถ้าว่าง อีเมลถูกเขียนลง DATA_DIR/mail-outbox |
MAIL_FROM |
Online Radio <no-reply@localhost> |
ผู้ส่ง ใน .env ต้องใส่เครื่องหมายคำพูด |
การออกอากาศ
| ตัวแปร | ค่าเริ่มต้น | ใช้ทำอะไร |
|---|---|---|
ICECAST_HOST |
127.0.0.1 |
โฮสต์ที่ Liquidsoap และ app ใช้ติดต่อ Icecast |
ICECAST_PORT |
8000 |
พอร์ต Icecast |
ICECAST_SOURCE_PASSWORD |
hackme-source |
รหัสผ่านที่ Liquidsoap ใช้ส่งสตรีม ต้องเปลี่ยน |
ICECAST_ADMIN_PASSWORD |
hackme-admin |
รหัสผ่านผู้ดูแล Icecast ต้องเปลี่ยน |
ICECAST_PUBLIC_URL |
http://localhost:8000 |
URL ที่ผู้ฟังใช้: BASE_URL/stream เมื่อใช้ Docker Compose หรือโดเมนสตรีม relay หรือ CDN เป็นค่าเริ่มต้นเท่านั้น (ดูโดเมนและที่อยู่) |
ICECAST_TRUSTED_PROXIES |
ว่าง | IP ของ proxy หน้า Icecast (ตรงตัว คั่นด้วยจุลภาค) ที่เชื่อ X-Forwarded-For ได้ Docker Compose ตั้งเป็น IP ของ Caddy 172.28.0.10 |
LIQUIDSOAP_BIN |
liquidsoap |
โปรแกรม Liquidsoap (ชื่อใน PATH หรือพาธเต็ม) |
LIQUIDSOAP_CALLBACK_URL |
http://127.0.0.1:3000 |
URL ที่ Liquidsoap และ Icecast ใช้เรียก app |
LIQUIDSOAP_CONTROL_HOST |
127.0.0.1 |
โฮสต์ที่ app ใช้ติดต่อพอร์ตควบคุม Liquidsoap ของแต่ละสถานี (worker) ใส่ {shard} ได้ |
LIQUIDSOAP_HARBOR_HOST |
127.0.0.1 |
โฮสต์ที่ app ส่งเสียงจากห้องจัดรายการบนเว็บไป (worker) ใส่ {shard} ได้ |
LIQUIDSOAP_UID / LIQUIDSOAP_GID |
ว่าง | ใช้เฉพาะเมื่อ worker ทำงานด้วย root: user id และ group id ที่ Liquidsoap ใช้ ถ้าว่าง ใช้ผู้ใช้ระบบ liquidsoap หรือ nobody ไม่มีผลเมื่อ worker ไม่ใช่ root |
LISTEN_TOKEN_TTL |
300 |
จำนวนวินาทีที่ลิงก์สำหรับฟังจากเครื่องเล่น (/s/<สถานี>/listen.json) ใช้เริ่มฟังได้ (10 ถึง 86400) ถ้าฟังอยู่แล้ว เสียงไม่ถูกตัดเมื่อลิงก์หมดอายุ |
HARBOR_PORT_START |
8100 |
พอร์ตแรกสำหรับเสียงสดจากดีเจสถานีใหม่ได้พอร์ตถัดไป |
CONTROL_PORT_START |
9100 |
พอร์ตควบคุมภายในพอร์ตแรก สถานีใหม่ได้พอร์ตถัดไป |
HARBOR_PUBLIC_HOST |
localhost |
ชื่อโฮสต์ที่ดีเจกรอกใน BUTT หรือ OBS ใส่ {shard} ได้ |
FFMPEG_BIN |
ffmpeg |
โปรแกรม FFmpeg |
FFPROBE_BIN |
ffprobe |
โปรแกรม FFprobe |
ENCODE_ON_DEMAND |
true |
เข้ารหัสสตรีมประหยัดเน็ตและ HLS เฉพาะเมื่อมีคนฟัง low = เฉพาะสตรีมประหยัดเน็ต false = เข้ารหัสตลอด |
ENCODE_IDLE_MINUTES |
5 |
นาทีที่สตรีมแบบตามความต้องการยังทำงานต่อหลังผู้ฟังคนสุดท้ายออก |
ICECAST_FALLBACK |
true |
ทุก mount ของสถานีมีไฟล์เสียงสำรองของตัวเองไว้เล่นขณะระบบออกอากาศเริ่มใหม่ (/fallback/<slug>.mp3, <slug>-low.aac จาก DATA_DIR/ops/icecast-fallback เปลี่ยนไฟล์ของสถานีเพื่อใช้เสียงของตัวเองได้) false = ปิด |
ICECAST_WEBROOT |
/usr/share/icecast2/web |
web root ของ Icecast ใน icecast.xml ที่สร้างขึ้น โฟลเดอร์ fallback ข้างในต้องชี้ไปที่ DATA_DIR/ops/icecast-fallback (อิมเมจ Docker ทำไว้แล้ว) |
ICECAST_STANDBY |
false (Docker Compose เป็น true เสมอ) |
ทุก mount ของสถานีใช้ relay ของตัวเอง (/<slug>.standby) จากสตรีมรอสัญญาณที่อิมเมจ Icecast ป้อนตามเวลาจริง แทนไฟล์ด้านบน (ซึ่ง Icecast ส่งเร็วกว่าเวลาจริงหลายเท่า) |
ICECAST_STANDBY_MOUNT |
/rp-standby |
ชื่อสตรีมรอสัญญาณ (และ <ชื่อ>-low สำหรับ mount ประหยัดเน็ต) |
ICECAST_CONFIG_FILE |
ว่าง | ถ้าตั้งไว้ worker (shard 0) เขียน icecast.xml ที่นี่และเขียนใหม่เมื่อสถานีเปลี่ยน อิมเมจ Icecast ของ Docker อ่านค่าใหม่เอง Docker Compose ตั้งให้แล้ว |
ICECAST_LOG_DIR |
/var/log/icecast2 |
โฟลเดอร์ log ของ Icecast ในไฟล์นั้น |
การขยายระบบและ relay
| ตัวแปร | ค่าเริ่มต้น | ใช้ทำอะไร |
|---|---|---|
ICECAST_RELAY_PASSWORD |
ว่าง | รหัสผ่านที่ edge relay ใช้ดึงสตรีมจาก Icecast หลัก relay ไม่ถูกนับเป็นผู้ฟัง |
ICECAST_RELAYS |
ว่าง | URL ของ edge relay (คั่นด้วยจุลภาค) ระบบอ่านสถานะเพื่อกระทบยอดจำนวนผู้ฟัง |
WORKER_SHARDS |
1 |
จำนวน worker สถานีถูกกระจายตามรหัสสถานี ตั้งค่าเดียวกันทั้งที่ app และ worker ทุกตัว |
WORKER_SHARD_INDEX |
จากชื่อโฮสต์ ถ้าไม่มีเป็น 0 |
หมายเลขของ worker นี้ (0 ถึง N−1) worker 0 รันงานตามเวลาด้วย (ผังรายการ การแจ้งเตือน สถิติ) |
HOSTNAME |
ระบบตั้งให้ | ถ้าไม่ได้ตั้ง WORKER_SHARD_INDEX ระบบใช้ -<ตัวเลข> ท้ายชื่อโฮสต์ (เช่น pod …-worker-2 ใน Kubernetes) เป็นหมายเลข shard |
WORKER_NO_ENGINE |
ปิด | true = worker ทำงานเบื้องหลังและงานตามเวลา แต่ไม่เปิด Liquidsoap |
ความสามารถเสริม
| ตัวแปร | ค่าเริ่มต้น | ใช้ทำอะไร |
|---|---|---|
GEOIP_DB |
ว่าง | พาธไฟล์ .mmdb ของประเทศ เปิดสถิติตามประเทศและการจำกัดพื้นที่ |
TRANSCRIBE_COMMAND |
ว่าง | คำสั่งถอดเสียงในเครื่อง {in} = ไฟล์เสียง {out} = พาธผลลัพธ์ คำสั่งต้องเขียนไฟล์ {out}.vtt |
TRANSCRIBE_URL |
ว่าง | endpoint ถอดเสียงแบบ OpenAI (ใช้เมื่อ TRANSCRIBE_COMMAND ว่าง) |
TRANSCRIBE_API_KEY |
ว่าง | Bearer โทเคนสำหรับ TRANSCRIBE_URL |
TRANSCRIBE_MODEL |
whisper-1 |
ชื่อโมเดลที่ส่งไป TRANSCRIBE_URL |
RTC_ICE_SERVERS |
[{"urls":"stun:stun.l.google.com:19302"}] |
เซิร์ฟเวอร์ STUN/TURN เป็น JSON สำหรับแขกรับเชิญระยะไกลและสายผู้ฟัง ถ้า JSON ผิด ระบบใช้ค่าเริ่มต้น |
WEBHOOK_ALLOW_HTTP |
false |
true = ยอมให้เว็บฮุกปลายทางเป็น http:// |
WEBHOOK_ALLOW_PRIVATE |
false |
true = ยอมให้เว็บฮุกปลายทางอยู่ในเครือข่ายภายในหรือเครือข่ายสงวน |
EXTERNAL_OUTPUT_ALLOW_PRIVATE |
false |
true = ยอมให้สถานีส่ง สัญญาณออกภายนอก ไปยังที่อยู่ในเครือข่ายภายใน loopback หรือเครือข่ายสงวน (เช่น relay ใน LAN ขององค์กร) ปกติจะปฏิเสธเหมือนเว็บฮุก |
RTMP_FONT_FILE |
อัตโนมัติ | ฟอนต์ที่ใช้เขียนชื่อสถานีและชื่อเพลงบนภาพวิดีโอ RTMP ต้องแสดงภาษาไทยได้ ค่าเริ่มต้นคือฟอนต์แรกที่พบในเครื่อง: Noto Sans Thai, Loma (fonts-tlwg-loma), FreeSans หรือ DejaVu Sans |
RTMP_CA_FILE |
ชุดของระบบ | ไฟล์ใบรับรอง CA ที่ FFmpeg ใช้ตรวจใบรับรองของเซิร์ฟเวอร์ rtmps:// (ค่าเริ่มต้น /etc/ssl/certs/ca-certificates.crt หรือชุดแรกที่พบ) เซิร์ฟเวอร์ที่ใบรับรองไม่น่าเชื่อถือจะถูกปฏิเสธ ส่วนปลายทาง Icecast แบบ TLS นั้น Liquidsoap ตรวจกับใบรับรองของระบบ |
LINE_API_URL |
https://api.line.me |
ที่อยู่ Messaging API ของ LINE เปลี่ยนเฉพาะตอนทดสอบ |
TELEGRAM_API_URL |
https://api.telegram.org |
ที่อยู่ Bot API ของ Telegram เปลี่ยนเฉพาะตอนทดสอบ |
การชำระเงินและใบอนุญาต
| ตัวแปร | ค่าเริ่มต้น | ใช้ทำอะไร |
|---|---|---|
OMISE_SECRET_KEY |
ว่าง | secret key ของ Omise (Opn Payments) พร้อมเพย์ QR พร้อมตรวจสลิปใช้ได้โดยไม่ต้องตั้ง |
OMISE_API_URL |
https://api.omise.co |
ที่อยู่ API ของ Omise เปลี่ยนเฉพาะตอนทดสอบ |
ETAX_P12_PATH |
ว่าง | e-Tax Invoice: ที่อยู่ไฟล์ PKCS#12 (.p12/.pfx) ที่มีใบรับรองสำหรับลงลายมือชื่อและกุญแจส่วนตัว ถ้าว่าง เอกสารจะไม่ถูกลงลายมือชื่อ ดูหัวข้อ "e-Tax Invoice" ด้านล่าง |
ETAX_P12_PASSWORD |
ว่าง | รหัสผ่านของไฟล์ PKCS#12 ไม่แสดงในหน้าผู้ดูแลหรือในบันทึกระบบ |
ETAX_P12_PASSWORD_FILE |
ว่าง | ใช้แทน ETAX_P12_PASSWORD: ไฟล์ที่เก็บรหัสผ่าน (secret ของ Docker หรือ Kubernetes) |
ETAX_FONT_DIR |
ว่าง | โฟลเดอร์ไฟล์ฟอนต์ Noto Sans Thai (.woff) สำหรับ PDF ของ e-Tax ถ้าว่าง ใช้ชุดที่ติดตั้งมากับแอป |
LICENSE_ENFORCE |
false |
true = รุ่นติดตั้งเองที่ต้องใช้คีย์ใบอนุญาต |
UPDATE_CHECK_URL |
ว่าง | URL ที่ระบบใช้ถามรุ่นล่าสุด ถ้าว่าง ไม่ตรวจอัปเดต |
โทรศัพท์และเสียงสังเคราะห์
| ตัวแปร | ค่าเริ่มต้น | ใช้ทำอะไร |
|---|---|---|
TTS_COMMAND |
ว่าง | คำสั่งแปลงข้อความเป็นเสียง {text} = ไฟล์ข้อความ {out} = ไฟล์ WAV ที่ต้องเขียน ใช้กับเสียงเมนูโทรศัพท์และประกาศฉุกเฉินที่มาเป็นข้อความ |
IVR_AGI_PORT |
ว่าง (ปิด) | เมื่อตั้งค่า worker 0 เปิดเซิร์ฟเวอร์ FastAGI ที่พอร์ตนี้ และเขียนไฟล์ของ Asterisk ทุกนาที ใช้ 4573 |
IVR_AGI_HOST |
0.0.0.0 |
ที่อยู่ที่เซิร์ฟเวอร์ FastAGI ฟัง |
IVR_AGI_PUBLIC_HOST |
worker |
โฮสต์ที่ Asterisk ใช้ติดต่อเซิร์ฟเวอร์ FastAGI |
IVR_SOUNDS_DIR |
DATA_DIR/ivr-sounds |
โฟลเดอร์เสียงเมนูโทรศัพท์ |
IVR_ASTERISK_CONF_DIR |
DATA_DIR/asterisk |
โฟลเดอร์ที่ worker เขียน extensions_radio.conf และ musiconhold_radio.conf |
การติดตามระบบและการแจ้งเตือน
| ตัวแปร | ค่าเริ่มต้น | ใช้ทำอะไร |
|---|---|---|
METRICS_TOKEN |
ว่าง | โทเคน Bearer สำหรับอ่านค่าวัดแบบ Prometheus (GET /metrics ของ app และพอร์ตค่าวัดของ worker) ว่าง = ปิด (ตอบ 404) ควรเป็นอักขระสุ่ม 32 ตัวขึ้นไป |
METRICS_ALLOW_IPS |
ว่าง | ไม่บังคับ: ให้เฉพาะ IP หรือช่วง CIDR เหล่านี้อ่านค่าวัด (คั่นด้วยจุลภาค เช่น 10.0.0.0/8,192.168.1.5) app ใช้ IP ของผู้เรียกหลังผ่าน TRUST_PROXY |
WORKER_METRICS_PORT |
ว่าง (Docker Compose: 9464) |
พอร์ตที่ worker แต่ละตัวให้บริการ GET /metrics เมื่อใช้ Docker Compose shard ที่ N ใช้ 9464 + N ว่าง = ไม่เปิด |
WORKER_METRICS_HOST |
0.0.0.0 |
ที่อยู่ที่พอร์ตค่าวัดของ worker รอรับ |
ALERT_WEBHOOK_URL |
ว่าง | ส่งทุกเหตุการณ์แจ้งเตือน (เปิด แจ้งซ้ำ คลี่คลาย) ไปที่นี่เป็น JSON ด้วย |
ALERT_WEBHOOK_SECRET |
ว่าง | ลงลายมือชื่อคำขอเหล่านั้น: x-radio-signature: sha256=… จาก timestamp.body แบบเดียวกับเว็บฮุกของสถานี |
ALERT_WEBHOOK_ALLOW_PRIVATE |
ปิด | true = ยอมให้ ALERT_WEBHOOK_URL ชี้ไปที่อยู่ภายในหรือ loopback (ปกติจะปฏิเสธ เหมือนเว็บฮุกของสถานี) |
ALERT_RENOTIFY_HOURS |
4 |
แจ้งซ้ำทุก N ชั่วโมงถ้าเหตุยังไม่คลี่คลาย |
ALERT_DISK_PERCENT |
85 |
แจ้งเมื่อดิสก์ที่เก็บ DATA_DIR ใช้เกินร้อยละนี้ |
ALERT_CERT_DAYS |
30 |
แจ้งเมื่อใบรับรองลงนาม e-Tax จะหมดอายุภายในจำนวนวันนี้ |
ALERT_TICK_FAILURES |
3 |
แจ้งเมื่องานตามรอบของ worker ล้มเหลวติดกันครบจำนวนนี้ |
ALERT_RESTARTS_10M |
3 |
แจ้งเมื่อระบบออกอากาศของสถานีเริ่มใหม่เกินจำนวนนี้ใน 10 นาที |
ALERT_ENGINE_STOPPED_MINUTES |
2 |
แจ้งเมื่อระบบออกอากาศของสถานีที่เปิดใช้งานไม่ทำงานนานเท่านี้ (นาที) |
ALERT_EXTERNAL_OUTPUT_MINUTES |
5 |
แจ้งผู้ดูแลสถานีเมื่อปลายทางถ่ายทอดไปช่องทางอื่น (SHOUTcast, Icecast, RTMP) ที่เปิดอยู่เชื่อมต่อไม่ได้นานเท่านี้ (นาที) |
ALERT_MEMORY_PERCENT, ALERT_MEMORY_MINUTES |
90, 5 |
แจ้งเตือนเมื่อบริการใช้หน่วยความจำเกินสัดส่วนนี้ของขีดจำกัดนานเท่านี้นาที |
ALERT_CPU_PERCENT, ALERT_CPU_MINUTES |
90, 10 |
แจ้งเตือนเมื่อบริการใช้ CPU เกินสัดส่วนนี้นานเท่านี้นาที |
ALERT_DISK_FREE_PERCENT |
10 |
แจ้งเตือนเมื่อพื้นที่ว่างของระบบไฟล์เหลือน้อยกว่านี้ |
ALERT_SERVICE_STALE_MINUTES |
3 |
แจ้งเตือนเมื่อบริการ (app, shard ของ worker) ไม่รายงานนานเท่านี้และไม่มีตัวใหม่มาแทน |
ALERT_RESTARTS_30M |
3 |
แจ้งเตือนเมื่อบริการเริ่มใหม่เกินจำนวนนี้ใน 30 นาที |
ALERT_BACKUP_HOURS |
36 |
แจ้งเตือนเมื่อการสำรองข้อมูลที่สำเร็จครั้งล่าสุด (DATA_DIR/ops/backup-status.json) เก่ากว่านี้ หรือครั้งล่าสุดล้มเหลว |
RESOURCE_HEARTBEAT |
true |
false หยุดการรายงานทรัพยากรของบริการ (ทุก 30 วินาที) สำหรับหน้า "ทรัพยากรเซิร์ฟเวอร์" |
INSTANCE_NAME |
ชื่อเครื่อง | ชื่อของโปรเซสนี้ในหน้านั้น Docker Compose ตั้งเป็น app, worker-0, worker-1, ... |
APP_VERSION |
เวอร์ชันของแพ็กเกจ | เวอร์ชันที่แสดงในหน้านั้น อิมเมจตั้งให้เอง |
LOG_LEVEL |
info |
ระดับ log ของ app และ worker: debug info warn หรือ error |
WORKER_LOG_FORMAT |
JSON | text = ให้ worker เขียน log เป็นข้อความธรรมดาแบบเดิม |
การดีบัก
| ตัวแปร | ค่าเริ่มต้น | ใช้ทำอะไร |
|---|---|---|
DEBUG_IVR |
ปิด | true = บันทึก log ทุกขั้นของสายโทรศัพท์ |
DEBUG_ICECAST_AUTH |
ปิด | true = บันทึก log การตรวจสิทธิ์ผู้ฟังของ Icecast ทุกครั้ง (ซ่อนรหัสผ่าน) |
LICENSE_PUBLIC_KEY |
ว่าง | ใช้ในการทดสอบอัตโนมัติเท่านั้น ไม่มีผลถ้า NODE_ENV ไม่ใช่ test |
กุญแจสาธารณะของใบอนุญาตที่ใช้งานจริงไม่ใช่ตัวแปรสภาพแวดล้อม แต่เป็นค่าคงที่ VENDOR_PUBLIC_KEY ใน src/lib/license.ts
ตั้งค่าความสามารถเสริม
ถอดเสียงเป็นข้อความ
เลือกอย่างใดอย่างหนึ่ง
- คำสั่งในเครื่อง เช่น whisper.cpp:
TRANSCRIBE_COMMAND="whisper-cli -m /models/ggml-large-v3.bin -l th -ovtt -of {out} -f {in}"ตรวจตัวเลือกให้ตรงกับรุ่นของ whisper.cpp ที่ใช้ ระบบแยกคำสั่งด้วยช่องว่าง พาธจึงห้ามมีช่องว่าง - บริการที่รองรับ API แบบ OpenAI:
TRANSCRIBE_URL=https://.../v1/audio/transcriptionsพร้อมTRANSCRIBE_API_KEYและTRANSCRIBE_MODEL
ชั่วโมงถอดเสียงต่อเดือนกำหนดในแพ็กเกจ ถ้าใช้ Docker คำสั่งในเครื่องต้องมีอยู่ใน image
GeoIP และ IP จริงของผู้ฟัง
- ดาวน์โหลดฐานข้อมูลประเทศ (
.mmdb): MaxMind GeoLite2-Country (สมัครบัญชีฟรี) หรือ DB-IP Lite - ตั้ง
GEOIP_DBเป็นพาธของไฟล์ ถ้าใช้ Docker ให้วางไฟล์ใน volume ข้อมูล เช่นdocker compose cp GeoLite2-Country.mmdb app:/data/GeoLite2-Country.mmdbแล้วตั้งGEOIP_DB=/data/GeoLite2-Country.mmdbถ้าใช้ Helm ใช้geoip.secretName - restart app และ worker
ระบบเก็บแค่รหัสประเทศ ไม่เก็บ IP
ถ้ามี proxy อยู่หน้า Icecast ให้ตั้ง ICECAST_TRUSTED_PROXIES เป็น IP ของ proxy (Docker Compose ตั้ง IP ของ Caddy ให้) ไม่อย่างนั้นผู้ฟังทุกคนจะดูเหมือนมาจาก IP เดียว โควตาต่อ IP และการจำกัดพื้นที่จะผิด
Edge relay
relay คือ Icecast เพิ่มเติมที่ดึงสตรีมจาก Icecast หลัก ผู้ฟังทุกคนยังต้องผ่านการตรวจสิทธิ์จากแพลตฟอร์ม โควตา การจำกัดพื้นที่ และสถิติจึงยังถูกต้อง
ตั้ง
ICECAST_RELAY_PASSWORDบนแพลตฟอร์ม สร้างicecast.xmlของเครื่องหลักใหม่ แล้ว restart Icecastบนเครื่อง relay แต่ละเครื่อง (ต้องมีสำเนา
.envของแพลตฟอร์มและต่อฐานข้อมูลได้)node --env-file=.env dist/src/cli.js icecast-relay-config --hostname edge1.radio.example.th --master-host <ที่อยู่เครื่องหลัก> --master-port 8000 --out /etc/icecast2/icecast.xml --user icecast2 --group icecast --watch --pid-file /run/icecast2/icecast2.pid--watchเขียนไฟล์ใหม่เมื่อสถานีเปลี่ยน แล้วสั่ง Icecast โหลดใหม่ relay ต้องเข้าถึงพอร์ต 8000 ของเครื่องหลักได้เพิ่ม relay แต่ละเครื่อง (เช่น
http://edge1.radio.example.th:8000) ในICECAST_RELAYSบนแพลตฟอร์มชี้
ICECAST_PUBLIC_URLและ DNS ของสตรีมไปที่ relay หรือ load balancer ของ relay
ถ้าใช้ Helm ตั้ง relays.enabled: true
ถ้าผู้ฟังมาก ยังเปิด HLS ในแพ็กเกจ แล้วตั้งลิงก์ HLS ผ่าน CDN ในหน้าตั้งค่าสถานีได้ด้วย
แบ่ง worker เป็นหลาย shard
ถ้ามีสถานีมาก ให้รัน worker หลายตัว
- ตั้ง
WORKER_SHARDS=Nที่ app และ worker ทุกตัว - ให้ worker แต่ละตัวมี
WORKER_SHARD_INDEXของตัวเอง (0 ถึง N−1) Kubernetes ใช้ชื่อ pod แทน - ใส่
{shard}ในLIQUIDSOAP_CONTROL_HOST,LIQUIDSOAP_HARBOR_HOSTและHARBOR_PUBLIC_HOSTเช่นHARBOR_PUBLIC_HOST=dj{shard}.radio.example.th - worker ทุกตัวและ app ต้องใช้
DATA_DIRร่วมกัน (worker เขียนไฟล์ HLS และไฟล์ส่งออก app เป็นผู้ให้บริการ)
worker 0 รันงานตามเวลาด้วย ต้องเปิดไว้เสมอ
ถ้าใช้ Docker Compose ให้รัน sudo radioctl scale --shards N แทน คำสั่งนี้ทำทุกข้อให้ ทุก shard ใช้เครือข่ายเดียวกัน จึงไม่ต้องใช้ที่อยู่แบบ {shard} และดีเจใช้ที่อยู่เดียว ดูคู่มือดูแลระบบ
แขกรับเชิญระยะไกลและ TURN
แขกรับเชิญระยะไกลและสายผู้ฟังใช้ WebRTC จำนวนแขกกำหนดในแพ็กเกจ ค่าเริ่มต้นใช้ STUN สาธารณะ ถ้าผู้โทรอยู่หลัง NAT ที่เข้มงวด ให้ติดตั้ง TURN (เช่น coturn) แล้วเพิ่มเข้าไป
RTC_ICE_SERVERS='[{"urls":"stun:stun.l.google.com:19302"},{"urls":"turn:turn.radio.example.th:3478","username":"radio","credential":"SECRET"}]'
proxy ต้องให้ WebSocket ผ่านได้
เว็บฮุก
ปลายทางเว็บฮุกต้องเป็น https:// ปลายทางที่อยู่ในเครือข่ายภายในหรือเครือข่ายสงวนจะถูกปฏิเสธตอนเชื่อมต่อ ถ้าติดตั้งในองค์กรและต้องส่งเข้าระบบภายใน
WEBHOOK_ALLOW_PRIVATE=trueยอมให้ส่งเข้าเครือข่ายภายในWEBHOOK_ALLOW_HTTP=trueยอมให้ใช้http://
ถ่ายทอดไปช่องทางอื่น
สถานีส่งรายการไปยัง SHOUTcast, Icecast ของที่อื่น และบริการ RTMP (YouTube Live, Facebook Live) ได้จากหน้า ถ่ายทอดไปช่องทางอื่น ผู้ดูแลระบบกำหนดจำนวนตามแพ็กเกจ ไม่ต้องตั้งค่าอะไรบนเซิร์ฟเวอร์ แต่ควรรู้ว่า
- CPU: ปลายทาง SHOUTcast และ Icecast เป็นตัวเข้ารหัสเสียงเพิ่มใน Liquidsoap ของสถานี (ใช้ CPU ราวเท่าสตรีมหลักของสถานีต่อปลายทาง AAC ใช้มากกว่าเล็กน้อย) ปลายทาง RTMP แต่ละรายการเป็นโปรเซส FFmpeg หนึ่งตัวบน worker ซึ่งดึงสตรีมของสถานีจาก Icecast ในเครื่อง (ไม่นับเป็นผู้ฟัง) แล้วเข้ารหัสภาพนิ่ง H.264 1280×720 2 เฟรมต่อวินาที กับเสียง AAC 128 kbps วัดได้ราว 3–5% ของ CPU หนึ่งคอร์และหน่วยความจำราว 150 MB ต่อปลายทาง เตรียม worker ให้พอกับจำนวนปลายทาง RTMP ที่แพ็กเกจอนุญาต
- แพ็กเกจ: FFmpeg ที่มี
libx264,libfreetype,libharfbuzz(แพ็กเกจffmpegของ Ubuntu มีครบ) และฟอนต์ภาษาไทย (fonts-tlwg-lomaหรือfonts-noto-core) image ของ Docker มีให้แล้ว RTMPS ต้องใช้ FFmpeg ที่รองรับ TLS (GnuTLS หรือ OpenSSL) ส่วน Icecast แบบ TLS ต้องใช้ Liquidsoap ที่รองรับ SSL - เครือข่าย: ต้องเชื่อมต่อขาออกไปยังปลายทางได้ (ดู "เปิดไฟร์วอลล์") ปลายทางที่เป็นที่อยู่ภายในหรือ loopback จะถูกปฏิเสธ เว้นแต่ตั้ง
EXTERNAL_OUTPUT_ALLOW_PRIVATE=true - การแจ้งเตือน: ปลายทางที่เปิดอยู่แต่เชื่อมต่อไม่ได้นาน
ALERT_EXTERNAL_OUTPUT_MINUTESนาที (ค่าเริ่มต้น 5) จะแจ้งผู้ดูแลสถานี - รหัสผ่านและ Stream key เก็บในฐานข้อมูล (ตาราง
external_output_secretsมี row-level security) เขียนลงเฉพาะไฟล์ตั้งค่าของระบบออกอากาศ (station.jsonที่ผู้ใช้ของ engine อ่านได้คนเดียว) หรือส่งให้ FFmpeg ไม่แสดงในหน้าเว็บและไม่ถูกส่งออก ถ้าเซิร์ฟเวอร์มีผู้ใช้อื่นร่วมใช้ บรรทัดคำสั่งของ FFmpeg (ที่มี Stream key ของ RTMP) จะเห็นได้ในรายการโปรเซส
ข้อความจาก LINE และ Telegram
ไม่ต้องตั้งค่าบนเซิร์ฟเวอร์ แต่ละสถานีใส่คีย์ของตนในหน้า ช่องทางติดต่อดีเจ เงื่อนไขมีดังนี้
BASE_URLต้องเป็นhttps://ที่มีใบรับรองที่เบราว์เซอร์เชื่อถือ (แบบโดเมน หรือใบรับรอง IP ของ Let's Encrypt) และ LINE กับ Telegram เข้าถึงได้จากอินเทอร์เน็ต ใช้ไม่ได้กับแบบip-httpหรือใบรับรองของ Caddy เอง ที่อยู่เว็บฮุกมีรูปแบบBASE_URL/hooks/line/<สถานี>/<คีย์>และBASE_URL/hooks/telegram/<สถานี>/<คีย์>- reverse proxy ต้องส่ง
/hooks/ไปที่แอปโดยไม่แก้เนื้อหา request (LINE ตรวจลายเซ็นจากเนื้อหาเดิม) - เซิร์ฟเวอร์ต้องออกอินเทอร์เน็ตไปที่
api.line.meและapi.telegram.orgได้ (ตอบกลับอัตโนมัติ ดึงชื่อผู้ส่ง และตั้งเว็บฮุก) - คีย์ถูกเก็บในตาราง
station_secrets(มี row-level security แยกสถานี) ไม่อยู่ในไฟล์ส่งออกสถานี สำรองข้อมูลด้วยpg_dumpตามปกติจะรวมตารางนี้ด้วย ให้เก็บไฟล์สำรองอย่างปลอดภัย
การเงินและ Omise
ระบบการเงินปิดไว้เป็นค่าเริ่มต้น เปิดหรือปิดได้ที่ ศูนย์ควบคุมแพลตฟอร์ม → การเงินและใบแจ้งหนี้ การชำระด้วยพร้อมเพย์ QR และแนบสลิปใช้ได้โดยไม่ต้องมีผู้ให้บริการชำระเงิน
ถ้าจะรับบัตรผ่าน Omise
- ตั้ง
OMISE_SECRET_KEYแล้ว restart - ใน Omise Dashboard ตั้งเว็บฮุก URL เป็น
https://<โดเมนของคุณ>/billing/omise/webhook
e-Tax Invoice
ระบบออกใบเสร็จรับเงิน/ใบกำกับภาษีแต่ละฉบับเป็นใบกำกับภาษีอิเล็กทรอนิกส์ได้ คือไฟล์ XML ตามมาตรฐาน ขมธอ. 3-2560 (เวอร์ชัน 2.0) ของ สพธอ. ที่ลงลายมือชื่อดิจิทัลแบบ XAdES-BES และไฟล์ PDF/A-3 ที่มี XML แนบอยู่ภายใน ปิดไว้เป็นค่าเริ่มต้น ผู้ดูแลระบบเปิดได้ที่ ศูนย์ควบคุมแพลตฟอร์ม → การเงินและใบแจ้งหนี้ → e-Tax ใช้ได้เฉพาะเมื่อผู้ขายจดทะเบียนภาษีมูลค่าเพิ่ม
การลงลายมือชื่อต้องใช้ใบรับรองอิเล็กทรอนิกส์ของบริษัท จากผู้ให้บริการออกใบรับรองที่อยู่ภายใต้ NRCA ของประเทศไทย ส่งออกเป็นไฟล์ PKCS#12 พร้อมกุญแจส่วนตัว รองรับเฉพาะกุญแจ RSA
- คัดลอกไฟล์ไปไว้บนเซิร์ฟเวอร์ในที่ที่ผู้ใช้ของแอปอ่านได้คนเดียว เช่น
/etc/radio/etax.p12(chmod 600เจ้าของradio) - ตั้ง
ETAX_P12_PATHเป็นที่อยู่ไฟล์นั้น และตั้งETAX_P12_PASSWORD(หรือETAX_P12_PASSWORD_FILE) เป็นรหัสผ่าน ถ้าใช้ Docker Compose ให้ mount ไฟล์เข้า containerappแล้วเพิ่มตัวแปรใน.envถ้าใช้ Helm ให้ mount จาก Secret แล้วเพิ่มตัวแปรด้วยextraEnv - restart แอปและ worker
- ที่ ศูนย์ควบคุมแพลตฟอร์ม → การเงินและใบแจ้งหนี้ → e-Tax ตรวจสถานะใบรับรอง แล้วกด ทดสอบลงลายมือชื่อ
ข้อควรทราบ
- กรมสรรพากรแนะนำให้เก็บกุญแจใน USB โทเคนหรือ HSM (PKCS#11) รุ่นนี้อ่านได้เฉพาะไฟล์ PKCS#12 จึงต้องดูแลไฟล์และรหัสผ่านเหมือน secret อื่น และต่ออายุใบรับรองก่อนหมดอายุ (หน้าตั้งค่าแสดงจำนวนวันที่เหลือ)
- ระบบไม่ได้นำส่งข้อมูลเข้าระบบ e-Tax Invoice & e-Receipt ของกรมสรรพากร ถ้าได้รับอนุมัติให้ใช้ระบบนั้น ให้นำส่งไฟล์ XML ที่ลงลายมือชื่อแล้วผ่านช่องทางที่ได้รับอนุมัติภายในวันที่ 15 ของเดือนถัดไป
- e-Tax Invoice by Email (Time Stamp): สำหรับผู้จดทะเบียน VAT ที่มีรายได้ไม่เกิน 30 ล้านบาทต่อปี หลังได้รับอนุมัติจากกรมสรรพากร (แบบ ก.อ.01 ที่ rd.go.th) ระบบส่งไฟล์ PDF/A-3 ทางอีเมลถึงผู้ซื้อ พร้อมสำเนาถึงที่อยู่ที่กรอกในหน้าตั้งค่า e-Tax การส่งอีเมลต้องตั้ง
SMTP_URLถ้าไม่ได้ตั้ง อีเมลจะถูกเขียนเป็นไฟล์.emlในDATA_DIR/mail-outbox - ไฟล์ XML ถูกตรวจตามข้อกำหนดของมาตรฐาน แต่ไม่ได้รวมไฟล์ XSD ทางการของ สพธอ. มาด้วย ก่อนใช้งานจริงให้ตรวจไฟล์ตัวอย่างด้วยเครื่องมือของ สพธอ.
คีย์ใบอนุญาตสำหรับรุ่นติดตั้งเอง
สำหรับรุ่นที่ขายให้ลูกค้าติดตั้งเอง ขั้นที่ 1–3 ทำโดย SAINT Technology
สร้างคู่กุญแจครั้งเดียว
node tools/license-issuer/issue.mjs keygen --out ~/.saint-licenseเก็บกุญแจส่วนตัว (
private.pem) ไว้นอกระบบและสำรองไว้ ห้ามแนบไปกับโปรแกรมวางกุญแจสาธารณะที่แสดงออกมาใน
VENDOR_PUBLIC_KEYของsrc/lib/license.tsแล้ว build รุ่นที่จะส่งมอบออกคีย์ให้ลูกค้าแต่ละราย
node tools/license-issuer/issue.mjs issue --key ~/.saint-license/private.pem --licensee "ชื่อลูกค้า" --stations 10 --listeners 2000 --expires 2027-12-31 --updates 2027-12-31 [--domain radio.example.th] [--features emergency,ivr,relays,billing]ลูกค้าตั้ง
LICENSE_ENFORCE=trueแล้ววางคีย์ที่ ศูนย์ควบคุมแพลตฟอร์ม → ใบอนุญาต
node tools/license-issuer/issue.mjs show <คีย์> ใช้ถอดดูข้อมูลในคีย์ ใบอนุญาตจำกัดสิ่งที่เพิ่มได้ (สถานี ผู้ฟัง) แต่ไม่ตัดการออกอากาศที่กำลังดำเนินอยู่
แปลงข้อความเป็นเสียง
ตั้ง TTS_COMMAND เป็นโปรแกรม TTS ภาษาไทยที่อ่านไฟล์ข้อความ {text} แล้วเขียนไฟล์ WAV {out} ใช้กับเสียงเมนูโทรศัพท์ และประกาศฉุกเฉินที่มาเป็นข้อความอย่างเดียว
ฟังทางโทรศัพท์ (Asterisk)
ถ้าใช้ Docker Compose ดู "เพิ่มเติม: ฟังทางโทรศัพท์" ด้านบน worker เขียนเมนูโทรศัพท์และคลาส Music-On-Hold ลง DATA_DIR/asterisk ทุกนาที container ของ Asterisk ดึงไฟล์เหล่านี้เข้าไปและโหลดใหม่ทุกนาที
ถ้าไม่ใช้ Docker
- ติดตั้ง Asterisk และตั้ง SIP trunk (ดู
deploy/asterisk/pjsip_trunk.example.conf) สายที่โทรเข้าต้องเข้า contextradio-ivr - แบบให้ worker ทำให้: ตั้ง
IVR_AGI_PORT=4573,IVR_AGI_PUBLIC_HOST=127.0.0.1แล้วเพิ่ม#include <IVR_ASTERISK_CONF_DIR>/extensions_radio.confในextensions.confและ#include <IVR_ASTERISK_CONF_DIR>/musiconhold_radio.confในmusiconhold.confจากนั้นให้ Asterisk โหลดใหม่เป็นระยะ (asterisk -rx "dialplan reload"และasterisk -rx "moh reload") สถานีใหม่จึงจะปรากฏ - แบบทำเอง:
radio asterisk-config --out /etc/asterisk --agi-host 127.0.0.1เพิ่มบรรทัด#includeทั้งสอง สั่งcore reloadแล้วรันradio ivr-agiเป็น service - สร้างเสียงประกาศของเมนู:
radio ivr-promptsใช้TTS_COMMANDหรืออัดเสียงเป็นไฟล์ WAV 8 kHz ใส่ในIVR_SOUNDS_DIRเอง
ถ้าทำได้ ให้เปิด 5060/udp และ 10000–10100/udp เฉพาะกับผู้ให้บริการ SIP
ประกาศฉุกเฉินแบบ CAP
- ศูนย์ควบคุมแพลตฟอร์ม → ประกาศฉุกเฉิน สร้างลิงก์ลับ
- ส่งลิงก์ (
https://<โดเมนของคุณ>/emergency/cap/<token>) ให้หน่วยงาน หน่วยงานส่ง CAP 1.2 XML มาที่ลิงก์นี้ด้วย POST - ตั้ง
TTS_COMMANDถ้าประกาศอาจมาเป็นข้อความโดยไม่มีเสียง
รายการตรวจความปลอดภัย
- เปลี่ยนค่าเริ่มต้นและค่า
change-meทุกตัวSESSION_SECRETและIP_HASH_SALTเป็นข้อความสุ่มยาว ๆ -
.envอ่านได้เฉพาะผู้ใช้ของ service (chmod 600) -
BASE_URLเป็นhttps://(แบบโดเมนหรือip-https) ใช้ip-httpเฉพาะในเครือข่ายภายในที่ไว้ใจได้ -
DATABASE_URLใช้ role ที่ไม่ใช่ superuser และต่างจากDATABASE_ADMIN_URL(migrateจะไม่ยอมทำงานถ้าเป็นผู้ใช้เดียวกัน หรือถ้า role ของ app เป็น superuser มีสิทธิ์BYPASSRLSหรือเป็นเจ้าของตาราง) - พาสคีย์ของทีมงาน: เพิ่มพาสคีย์ของคุณเองก่อน แล้วเลือก "บังคับทีมงานทุกคน … ใช้พาสคีย์เป็นการยืนยันขั้นที่ 2" ในส่วนนโยบายของแพลตฟอร์ม ที่หน้าความปลอดภัยของบัญชี (
/account/securityแสดงเฉพาะผู้ดูแลระบบ) - เงื่อนไขการใช้งาน: เผยแพร่ที่ ศูนย์ควบคุมแพลตฟอร์ม → เงื่อนไขและนโยบาย เลือก "เปลี่ยนในสาระสำคัญ" เพื่อให้ทีมงานยอมรับฉบับใหม่
-
LOGIN_RATE_LIMITเหมาะกับเครือข่ายของคุณ - ไฟร์วอลล์: เปิดเฉพาะ 80, 443, พอร์ตดีเจ (8100 ขึ้นไป) และถ้าใช้โทรศัพท์ 5060/udp กับ 10000–10100/udp ปิด 3000, 5432, 8000, 9100 ขึ้นไป และ 4573
- proxy กัน
/internal/แล้ว - เว็บฮุกส่งได้เฉพาะปลายทาง
https://สาธารณะ เว้นแต่จำเป็นต้องใช้ในเครือข่ายภายใน - สำรองข้อมูลทุกวัน และเคยทดสอบกู้คืนแล้ว
- อัปเดต: ติดตั้งรุ่นใหม่ของแพลตฟอร์มและอัปเดตความปลอดภัยของ Ubuntu สม่ำเสมอ
การแยกข้อมูลสถานีในฐานข้อมูล
row-level security แยกข้อมูลของแต่ละสถานีออกจากกัน app บอกฐานข้อมูลว่าธุรกรรมนี้เป็นของสถานีไหน และลงลายมือชื่อด้วยรหัสลับ (DB_SCOPE_SECRET หรือรหัสที่สร้างจาก SESSION_SECRET) รหัสลับนี้เก็บในตารางที่ role ของ app อ่านไม่ได้ แม้มี SQL injection ที่รันด้วย role ของ app ก็ใช้ set_config() ขยายสิทธิ์ตัวเองไม่ได้ ถ้าไม่มีลายมือชื่อที่ถูกต้องของธุรกรรมนั้นจะไม่เห็นข้อมูลของสถานีใดเลย ใช้กับตารางของสถานีทุกตาราง รวมถึงการแจ้งเตือน การเตือนโควตา บันทึกการตรวจสอบ (audit log) และรายงานปัญหาการเข้าถึง
migrateเก็บรหัสลับไว้ และเก็บใหม่เมื่อรหัสเปลี่ยน หลังเปลี่ยนDB_SCOPE_SECRETหรือSESSION_SECRETให้รันmigrateก่อนหรือพร้อมกับการเริ่ม app และ worker ใหม่ ถ้ารหัสในฐานข้อมูลไม่ตรง app จะหยุดตั้งแต่เริ่มทำงานพร้อมข้อความบอกสาเหตุ ถ้าไม่มีการตรวจนี้ ทุกหน้าจะว่างเปล่าโดยไม่มีข้อความเตือน- Kubernetes: migration job สร้างรหัสลับจาก Secret เดียวกับ pod ของ app ถ้าตั้ง
DB_SCOPE_SECRETต้องให้ทั้ง migration job, app และ worker
Content-Security-Policy และ reverse proxy
ทุกหน้าส่ง Content-Security-Policy แบบเข้มงวด สคริปต์รันได้เฉพาะไฟล์ของแพลตฟอร์มเอง ไม่มีโค้ดในหน้า สไตล์มาจากไฟล์ของแพลตฟอร์มและบล็อกสไตล์เล็ก ๆ หนึ่งบล็อกที่มี nonce ซึ่งเปลี่ยนทุกครั้งที่ส่งหน้า รูปภาพมาจากที่อยู่ https:// ใดก็ได้ เสียงและการเชื่อมต่อมาจาก URL สาธารณะของ Icecast (ICECAST_PUBLIC_URL) ที่อยู่ HLS CDN ของสถานี และ endpoint ของ S3 ได้ ฟอร์มส่งได้เฉพาะไปที่แพลตฟอร์มเอง (BASE_URL) ทุกหน้าแสดงในกรอบ (frame) ได้เฉพาะในแพลตฟอร์มเอง ส่วนเครื่องเล่นแบบฝัง ทุกเว็บฝังได้ หรือเฉพาะเว็บที่สถานีระบุไว้ในหน้าเชื่อมต่อบริการอื่น
- อย่าให้ proxy หรือ CDN เพิ่มหรือแทนที่ header
Content-Security-Policyให้ส่ง header ของ app ผ่านไปตามเดิม proxy ที่แก้ HTML (แทรกสคริปต์วิเคราะห์หรือแชต หรือย่อโค้ดในหน้า) จะทำให้หน้าเสียภายใต้นโยบายนี้ - อย่าแคชหน้า HTML ที่ CDN เพราะ nonce เปลี่ยนทุกครั้ง ไฟล์ใน
/assets/แคชได้ - ถ้าเบราว์เซอร์บล็อกอะไร รายงานจะส่งไปที่
/csp-reportและบันทึกใน log ของ app เป็นCSP violation(ไม่เกิน 60 รายการต่อนาที) อย่าปิด/csp-reportที่ proxy
สำรองและกู้คืนข้อมูล
สำรอง 3 อย่าง
- ฐานข้อมูล (ทุกวัน)
- ไฟล์:
DATA_DIRถ้าใช้STORAGE_DRIVER=local(ไฟล์เสียงอยู่ในDATA_DIR/media) หรือเปิด versioning ของ bucket ถ้าใช้ S3 ในDATA_DIRยังมีไฟล์ส่งออกและเสียงเมนูโทรศัพท์ด้วย - ไฟล์
.env(หรือ Helm values/Secret) ถ้าSESSION_SECRETไม่ตรงเดิม ทุกคนจะหลุดจากระบบ และต้องตั้งค่า Icecast ใหม่
ติดตั้งโดยตรง (ไม่รองรับ)
สำรอง
pg_dump -Fc -d "postgres://postgres:ADMIN-PASSWORD@127.0.0.1:5432/radio" -f radio-$(date +%F).dump
sudo tar czf radio-data-$(date +%F).tgz -C /var/lib/radio .
กู้คืน
sudo systemctl stop radio-app radio-worker
sudo -u radio node --env-file=.env dist/src/cli.js migrate
pg_restore --clean --if-exists -d "postgres://postgres:ADMIN-PASSWORD@127.0.0.1:5432/radio" radio-2026-09-26.dump
sudo -u radio node --env-file=.env dist/src/cli.js migrate
sudo tar xzf radio-data-2026-09-26.tgz -C /var/lib/radio
sudo systemctl start radio-app radio-worker
บนเครื่องใหม่ migrate ครั้งแรกสร้าง role ของ app ไว้ก่อนกู้คืน ครั้งที่สองให้สิทธิ์ (grant) ซ้ำอีกรอบ
Docker Compose
บริการ backup สำรองฐานข้อมูล ไฟล์ทั้งหมด และ .env ทุกคืนแบบเข้ารหัส ไปที่ /var/backups/radio และ S3 (ถ้าตั้งไว้) เก็บ 7 ชุดรายวัน 4 ชุดรายสัปดาห์ และ 6 ชุดรายเดือน สำรองทันทีด้วย sudo radioctl backup กู้คืนด้วย sudo radioctl restore ค่าตั้ง การกู้คืนบนเครื่องใหม่ และการซ้อมกู้คืนอยู่ในคู่มือดูแลระบบ
ย้ายสถานีเดียว
ส่งออก: สถานี → เชื่อมต่อบริการอื่น → ส่งออกสถานี → เริ่มส่งออก เมื่อพร้อมแล้วดาวน์โหลดไฟล์
.tar.gzนำเข้าบนแพลตฟอร์มใดก็ได้ เข้าองค์กรที่มีอยู่แล้ว
radio station-import station-export.tar.gz --org <org-slug> --slug <slug-ใหม่> [--name "ชื่อใหม่"]ถ้าใช้ Docker คำสั่ง
sudo radio station-import station-export.tar.gz --org …คัดลอกไฟล์เข้า container ให้เอง
ทดสอบการกู้คืน
ทำอย่างน้อยทุก 2–3 เดือน บนเครื่องหรือฐานข้อมูลแยก
- กู้คืนฐานข้อมูลและไฟล์ชุดล่าสุด
- เปิด app และ worker ตรวจ
/health - เข้าสู่ระบบ เปิดสถานี ฟังสตรีม และเล่นไฟล์จากคลังเสียง
- จดเวลาที่ใช้ทั้งหมด
อัปเกรด
สำรองข้อมูลก่อนเสมอ แล้วทำดังนี้
Docker Compose
sudo radioctl update --version <รุ่นใหม่>
คำสั่งนี้สำรองข้อมูล ดึงอิมเมจใหม่ เริ่มเว็บแอปใหม่ก่อน (migration) แล้วจึงเริ่มระบบออกอากาศใหม่ทีละ shard ระหว่างนั้นผู้ฟังได้ยินเสียงรอสัญญาณ ย้อนกลับด้วย radioctl rollback รายละเอียดในคู่มือดูแลระบบ ถ้ารุ่นใหม่เปลี่ยนไฟล์ในโฟลเดอร์ติดตั้งด้วย (เช่น docker-compose.yml) ให้แตก radio-<version>.tar.gz ทับ /opt/radio ก่อน (หรือ git pull ถ้าเป็นโค้ดที่ clone มา) หรือรัน scripts/install.sh อีกครั้ง ถ้าไม่มีอินเทอร์เน็ต: sudo radioctl update --offline radio-images-<version>-<arch>.tar.gz
ติดตั้งโดยตรง (ไม่รองรับ)
cd /opt/radio
# ดึงรุ่นใหม่ (เช่น git pull)
sudo -u radio npm ci
sudo -u radio npm run build
sudo -u radio node --env-file=.env dist/src/cli.js migrate
sudo systemctl restart radio-app radio-worker
Helm: push image ใหม่ แล้ว helm upgrade radio deploy/helm/radio -f my-values.yaml --set image.tag=<tag ใหม่> migration job ทำงานก่อน (ชื่อออบเจกต์คงเดิม ดู Kubernetes)
app อัปเดตฐานข้อมูลตอนเริ่มทำงาน ยกเว้นตั้ง MIGRATE_ON_START=false การรัน migrate เองก่อน ช่วยให้เห็นข้อผิดพลาดก่อน restart
การ restart worker ทำให้ Liquidsoap ของทุกสถานีเริ่มใหม่ สถานีจะหยุดออกอากาศครู่หนึ่ง (ถ้าใช้ Docker Compose ผู้ฟังได้ยินเสียงรอสัญญาณและไม่หลุดการเชื่อมต่อ) ควรอัปเกรดช่วงที่มีผู้ฟังน้อย และอ่านบันทึกประจำรุ่นเพื่อดูค่าตั้งใหม่
ติดตามสถานะระบบ
| Endpoint | ใครใช้ได้ | ข้อมูล |
|---|---|---|
/health |
ทุกคน | ตรวจว่ายังทำงาน ตอบ {"ok":true} เมื่อ app ต่อฐานข้อมูลได้ Docker และ Kubernetes ใช้เป็น probe |
/status |
สาธารณะ | หน้าสถานะ: เว็บไซต์ ฐานข้อมูล การออกอากาศ แต่ละสถานี และความพร้อมใช้งาน 30 วันของแต่ละสถานี |
/status.json |
สาธารณะ | ข้อมูลเดียวกันเป็น JSON (เปิด CORS) สำหรับระบบเฝ้าระวังภายนอก |
/platform/health |
ผู้ดูแลระบบ | หน้าสุขภาพระบบ: ระบบออกอากาศ เสียงเงียบ ผู้ฟัง งานเบื้องหลัง พื้นที่ดิสก์ |
/platform/health.json |
ผู้ดูแลระบบ (ที่เข้าสู่ระบบแล้ว) | ข้อมูลเดียวกันเป็น JSON |
/platform/alerts |
ผู้ดูแลระบบ | การแจ้งเตือนระบบที่เปิดอยู่และย้อนหลัง (ดูด้านล่าง) |
/metrics |
Prometheus (Bearer METRICS_TOKEN) |
ค่าวัดของ app สำหรับ Prometheus ส่วน worker แต่ละตัวให้บริการที่ WORKER_METRICS_PORT ปิดอยู่ถ้า METRICS_TOKEN ว่าง |
ที่หน้าสุขภาพระบบ (ศูนย์ควบคุมแพลตฟอร์ม → สุขภาพระบบ) ตั้งได้ว่า
- เป้าหมายความพร้อมใช้งานต่อเดือน
- หยุดออกอากาศนานกี่นาทีจึงส่งอีเมลแจ้งผู้ดูแลสถานี
- จะแสดงความพร้อมใช้งานและเหตุขัดข้องในหน้า
/statusสาธารณะหรือไม่
ดู log
| รูปแบบการติดตั้ง | คำสั่ง |
|---|---|
| Docker Compose | sudo radioctl logs app --follow (หรือ worker, icecast, caddy, …) |
| ติดตั้งโดยตรง (ไม่รองรับ) | journalctl -u radio-app -f, journalctl -u radio-worker -f, journalctl -u icecast2 และ /var/log/icecast2/ |
| Kubernetes | kubectl logs deploy/<release>-saint-radio-app, kubectl logs <release>-saint-radio-worker-0 |
ค่าวัดสำหรับ Prometheus
- ตั้ง
METRICS_TOKEN(อักขระสุ่ม 32 ตัวขึ้นไป) ให้ทั้ง app และ worker และตั้งWORKER_METRICS_PORTให้ worker (Docker Compose ตั้ง9464ให้แล้ว shard ที่ N ใช้9464 + N) แล้วเริ่มทั้งสองใหม่ - ตรวจด้วย
curl -H "Authorization: Bearer $METRICS_TOKEN" http://127.0.0.1:3000/metricsบน app และhttp://127.0.0.1:9464/metricsบน worker - ตั้ง Prometheus ให้ดึงค่าพร้อมโทเคนตัวอย่างที่ใช้ได้จริงอยู่ที่
deploy/monitoring/prometheus.yml(สอง job คือradio-appและradio-workerอ่านโทเคนจากไฟล์) ให้ดึงจาก app โดยตรง ไม่ผ่าน reverse proxy สาธารณะ หรือปิด/metricsที่ proxy แล้วอาศัยโทเคนกับMETRICS_ALLOW_IPS - โหลดกฎแจ้งเตือน
deploy/monitoring/alerts.yml(rule_filesในไฟล์ตัวอย่าง) ตรวจไวยากรณ์ด้วยpromtool check rules deploy/monitoring/alerts.yml
ค่าที่วัด
| โปรเซส | ค่าวัด |
|---|---|
App (/metrics) |
radio_http_request_duration_seconds (แยกตามรูปแบบเส้นทาง method และกลุ่มรหัสสถานะ) radio_http_requests_in_flight radio_websocket_connections radio_db_pool_connections (ทั้งหมด ว่าง รอคิว) radio_db_query_duration_seconds radio_mail_failures_total radio_webhook_deliveries_last_hour (แยกตามสถานะ) และค่ามาตรฐาน process_* / nodejs_* |
Worker (WORKER_METRICS_PORT) |
รายสถานี: radio_station_listeners radio_station_engine_state radio_station_silence_seconds radio_station_on_air_source radio_station_live_dj_connected radio_station_engine_restarts_10m งานของ worker: radio_worker_tick_duration_seconds radio_worker_tick_failures_total radio_worker_tick_consecutive_failures ผลการตรวจ: radio_icecast_up radio_data_dir_disk_used_ratio radio_db_scope_ok radio_etax_certificate_days_left radio_alerts_open และค่าของฐานข้อมูลกับโปรเซส |
ถ้าแบ่ง worker หลาย shard แต่ละตัวรายงานเฉพาะสถานีของตัวเอง ส่วนการตรวจระดับแพลตฟอร์ม (Icecast ดิสก์ ใบรับรอง การแจ้งเตือน) ทำที่ worker shard 0
Docker Compose: เพิ่ม METRICS_TOKEN ใน .env แล้วรัน sudo radioctl up รัน Prometheus ในเครือข่าย Compose ชื่อ radio_default ด้วยไฟล์ตัวอย่าง เป้าหมายคือ app:3000 และ engine-net:9464 (worker ใช้เครือข่ายของบริการ engine-net ร่วมกัน worker จึงไม่มีที่อยู่ของตัวเอง ถ้ามีหลาย shard เพิ่ม engine-net:9465, …)
Kubernetes (Helm): ตั้ง secrets.metricsToken และ monitoring.enabled=true แล้ว pod จะมี annotation prometheus.io/scrape|port|path (monitoring.podAnnotations) worker เปิดพอร์ตชื่อ metrics และถ้าใช้ Prometheus Operator ตั้ง monitoring.serviceMonitor.enabled=true จะได้ ServiceMonitor ที่ส่งโทเคนจาก Secret ของ chart (เพิ่ม monitoring.serviceMonitor.labels ให้ Prometheus ของคุณเลือกมัน) ส่วน alerts.yml ให้ใส่ใน PrometheusRule (ย้ายรายการ groups: ไปไว้ใต้ spec:)
แดชบอร์ด Grafana
Grafana → Dashboards → New → Import → อัปโหลด deploy/monitoring/grafana/saint-radio-dashboard.json → เลือกแหล่งข้อมูล Prometheus แดชบอร์ดแสดงการแจ้งเตือนที่เปิดอยู่ ผู้ฟัง ช่วงเงียบ สถานะและการเริ่มใหม่ของระบบออกอากาศรายสถานี (เลือกสถานีจากเมนู Station) อัตราคำขอ เวลาตอบ และข้อผิดพลาด pool และเวลาคิวรีของฐานข้อมูล เวลาและความล้มเหลวของงาน worker, Icecast, ดิสก์, ใบรับรอง e-Tax และหน่วยความจำกับ CPU ของโปรเซส
การแจ้งเตือนจากแพลตฟอร์มเอง
ไม่มี Prometheus ก็ได้ worker ตรวจเงื่อนไขเดียวกันทุกนาทีและแจ้งผู้เกี่ยวข้องเอง
| การแจ้งเตือน | แจ้งใคร |
|---|---|
| สถานีเงียบนานเกินเกณฑ์ (ค่าเริ่มต้น 2 นาที แต่ละสถานีเปลี่ยนเองได้) | ผู้ดูแลสถานี |
| ระบบออกอากาศของสถานีเริ่มใหม่เกิน 3 ครั้งใน 10 นาที | ผู้ดูแลสถานี |
| ระบบออกอากาศของสถานีไม่ทำงานนาน 2 นาที | ผู้ดูแลสถานี |
| Icecast หรือ relay ไม่ตอบ | ผู้ดูแลระบบ |
| งานของ worker ล้มเหลว 3 ครั้งติดกัน | ผู้ดูแลระบบ |
| ฐานข้อมูลไม่ยอมรับขอบเขตข้อมูลที่ลงนามของ worker | ผู้ดูแลระบบ |
ดิสก์ของ DATA_DIR ใช้เกิน 85% |
ผู้ดูแลระบบ |
| ใบรับรองลงนาม e-Tax จะหมดอายุภายใน 30 วัน | ผู้ดูแลระบบ |
แต่ละเหตุแจ้งครั้งเดียว (การแจ้งเตือนในระบบ อีเมลตามภาษาของแต่ละคน และการแจ้งเตือนบนเบราว์เซอร์สำหรับคนที่อนุญาตไว้) แจ้งซ้ำทุก ALERT_RENOTIFY_HOURS ชั่วโมงถ้ายังไม่คลี่คลาย และแจ้งอีกครั้งเมื่อคลี่คลาย ถ้าตั้ง ALERT_WEBHOOK_URL ไว้ เหตุการณ์เดียวกันจะถูกส่งเป็น JSON (event คือ alert.opened alert.reminder หรือ alert.resolved และ alert มีชนิด ความรุนแรง สถานี เวลา และสรุปภาษาอังกฤษกับภาษาไทย) ดูรายการที่เปิดอยู่และย้อนหลังได้ที่ ศูนย์ควบคุมแพลตฟอร์ม → การแจ้งเตือนระบบ (/platform/alerts) และของแต่ละสถานีที่ จัดการสถานี → การแจ้งเตือน
Log และรหัสอ้างอิงคำขอ (request id)
- app และ worker เขียน log เป็น JSON บรรทัดละหนึ่งรายการ ทุกบรรทัดมี
component(webworkersupervisoralerts…) บรรทัดของ app มีrequest_idด้วย - ทุกคำตอบมี header
X-Request-Idถ้า proxy หรือ load balancer ส่งX-Request-Idที่สั้นและปลอดภัยมา ระบบใช้ค่านั้น ไม่เช่นนั้นสร้างใหม่ หน้าข้อผิดพลาดแสดงค่านี้เป็น "รหัสอ้างอิงสำหรับแจ้งผู้ดูแล" ค้นหาค่านี้ใน log ได้ทันที - header Authorization, cookie, รหัสผ่าน, โทเคนและค่าลับใน URL ถูกบันทึกเป็น
[redacted] - อ่าน log ให้ง่ายขึ้นตอนดีบัก:
journalctl -u radio-app -o cat | npx pino-pretty
คำสั่งผู้ดูแล
เมื่อใช้ Docker radio เป็นคำสั่งที่ตัวติดตั้งวางไว้ใน /usr/local/bin: sudo radio <คำสั่ง> รัน docker compose exec app node dist/src/cli.js <คำสั่ง> ใน container ของเว็บแอปของการติดตั้งนี้ (ไฟล์ที่ส่งให้ station-import ถูกคัดลอกเข้า container ก่อน) บน NAS ใช้ sudo bash scripts/radio … ในโฟลเดอร์ติดตั้ง Kubernetes: kubectl exec deploy/<release>-saint-radio-app -- node dist/src/cli.js … ติดตั้งโดยตรง: node --env-file=.env dist/src/cli.js … ในโฟลเดอร์โค้ด
| คำสั่ง | ใช้ทำอะไร |
|---|---|
radio migrate |
อัปเดตฐานข้อมูล สร้าง role ของ app และเพิ่มแพ็กเกจเริ่มต้น |
radio create-admin <email> [name] |
สร้างผู้ใช้ หรือตั้งผู้ใช้ที่มีอยู่ให้เป็นผู้ดูแลระบบ |
radio icecast-config [--out file] [--user icecast2 --group icecast] [--log-dir dir] |
เขียน icecast.xml ของแพลตฟอร์มนี้ ถ้าไม่ใส่ --out จะแสดงออกหน้าจอ |
radio icecast-relay-config --hostname <host> [--master-host h] [--master-port 8000] [--port 8000] [--out file] [--user u --group g] [--log-dir dir] [--watch --pid <pid> | --pid-file <file>] |
เขียน icecast.xml ของ edge relay --watch อัปเดตไฟล์ให้ตลอด |
radio station-import <export.tar.gz> --org <org-slug> --slug <new-slug> [--name "Name"] |
นำเข้าสถานีที่ส่งออกไว้เข้าองค์กร |
radio station-script <slug> |
แสดงสคริปต์ Liquidsoap ที่ระบบสร้างให้สถานี |
radio asterisk-config --out <dir> --agi-host <host> [--agi-port 4573] [--icecast <url>] |
เขียนไฟล์เมนูโทรศัพท์และ Music-On-Hold ของ Asterisk |
radio ivr-prompts |
สร้างเสียงประกาศของเมนูโทรศัพท์ (ต้องมี TTS_COMMAND) หรือบอกว่าขาดไฟล์ไหน |
radio ivr-agi |
รันเซิร์ฟเวอร์ FastAGI สำหรับ Asterisk (ปกติ worker ทำให้) |
เครื่องมือใบอนุญาต (เฉพาะ SAINT Technology): node tools/license-issuer/issue.mjs keygen | issue | show
แก้ปัญหา
| ปัญหา | ตรวจอะไร |
|---|---|
| ไม่ได้รับอีเมลรหัสเข้าสู่ระบบ | ตั้ง SMTP_URL แล้วหรือยัง ถ้ายัง อีเมลเป็นไฟล์อยู่ใน DATA_DIR/mail-outbox ดู log ของ app ว่ามีข้อผิดพลาดการส่งอีเมลหรือไม่ หน้าติดตั้งมีช่องตรวจ SMTP บอกว่าตั้งแล้วหรือยัง |
| สถานีเงียบ | สถานี → ภาพรวม ดู "ระบบออกอากาศ" ถ้าขึ้นว่าหยุด ให้ดู log ของ worker มี liquidsoap ใน PATH หรือตั้ง LIQUIDSOAP_BIN แล้วหรือยัง ใช้ radio station-script <slug> ดูสคริปต์ พอร์ต harbor และพอร์ตควบคุมของสถานีว่างหรือไม่ worker ติดต่อ Icecast ได้หรือไม่ (ICECAST_HOST, ICECAST_PORT, รหัสผ่าน source) |
| ผู้ฟังเข้าไม่ได้ | สถานีอาจเต็ม: ดูจำนวนผู้ฟังพร้อมกันในแพ็กเกจ Icecast ต้องเรียกลิงก์ตรวจสิทธิ์ของ app ที่ LIQUIDSOAP_CALLBACK_URL ได้ ถ้าเปลี่ยน SESSION_SECRET หรือ URL นี้ ต้องสร้าง icecast.xml ใหม่ ตั้ง DEBUG_ICECAST_AUTH=true เพื่อดู log การตรวจแต่ละครั้ง |
| Icecast ไม่ยอมเริ่ม | Docker: sudo radioctl logs icecast worker (worker เป็นผู้เขียนค่าตั้งของ Icecast) ติดตั้งโดยตรง: ดูภาคผนวก และดู /var/log/icecast2/error.log |
| ค่าตั้งผิดหรือถูกตัด | ค่าใน .env ที่มีช่องว่าง < > หรือ JSON ไม่ได้ใส่เครื่องหมายคำพูด ใส่แบบ MAIL_FROM="Online Radio <no-reply@radio.example.th>" |
| app ต่อฐานข้อมูลไม่ได้ | DB_APP_PASSWORD ต้องตรงกับรหัสผ่านใน DATABASE_URL รัน migrate (โดยตั้ง DATABASE_ADMIN_URL) ระบบจะตั้งรหัสผ่านของ role ของ app ให้ใหม่เมื่อรหัสเดิมใช้ไม่ได้แล้ว |
| ดีเจต่อด้วย BUTT/OBS ไม่ได้ | เปิดพอร์ตของสถานี (8100 ขึ้นไป ดูได้ที่หน้าการออกอากาศสดของสถานี) บนไฟร์วอลล์และส่งต่อจากเราเตอร์แล้วหรือยัง HARBOR_PUBLIC_HOST ชี้ไปที่ IP สาธารณะของเซิร์ฟเวอร์หรือไม่ ถ้าใช้ Docker พอร์ตอยู่ในช่วง HARBOR_PORT_START–HARBOR_PORT_END หรือไม่ (radioctl doctor ตรวจให้) |
| ห้องจัดรายการบนเว็บหรือแขกรับเชิญต่อไม่ได้ | แบบ ip-http เบราว์เซอร์ไม่ให้ใช้ไมโครโฟนและ WebRTC ต้องใช้ HTTPS นอกนั้น proxy กั้น WebSocket ให้ส่งต่อ header Upgrade และ Connection และปิด buffering ถ้าแขกอยู่หลัง NAT ที่เข้มงวด เพิ่ม TURN ใน RTC_ICE_SERVERS |
| ผู้ฟังทุกคนมี IP เดียวกัน | ตั้ง ICECAST_TRUSTED_PROXIES เป็น IP ของ proxy แล้วสร้าง icecast.xml ใหม่ |
| ข้อความ LINE หรือ Telegram ไม่เข้าห้องจัดรายการ | BASE_URL ต้องเป็น https ที่เข้าถึงได้จากอินเทอร์เน็ต proxy ต้องส่ง /hooks/ ถึงแอป กด ตั้งเว็บฮุก … ให้อัตโนมัติ ใหม่หลังเปลี่ยนโดเมน ถ้า log มีสถานะ 401 จาก /hooks/line/ แปลว่า Channel secret ไม่ตรงกับ channel |
| เว็บฮุกถูกปฏิเสธ | ปลายทางเป็น http:// หรืออยู่ในเครือข่ายภายใน ดูหัวข้อเว็บฮุก: WEBHOOK_ALLOW_HTTP, WEBHOOK_ALLOW_PRIVATE |
| ไม่ได้ใบรับรองของโดเมนสถานี | โดเมนต้องชี้มาที่เครื่องนี้ และแพลตฟอร์มต้องรู้จัก (โดเมนย่อยของสถานี โดเมนของสถานีเอง หรือชื่อในหน้าโดเมนและที่อยู่) sudo radioctl logs caddy แสดงคำตอบของ Let's Encrypt |
| ทุกที่อยู่แสดงหน้าติดตั้งว่างหลังย้ายหรือเปลี่ยนชื่อโฟลเดอร์ | ข้อมูลอยู่ในวอลุ่มที่ตั้งชื่อตามโฟลเดอร์เดิม: รัน sudo bash scripts/radioctl migrate-name (ดู "การติดตั้งที่ทำก่อนใช้ชื่อ radio") |
ภาคผนวก: ติดตั้งโดยตรงโดยไม่ใช้ Docker (ไม่แนะนำ ไม่รองรับ)
ไม่แนะนำ ไม่รองรับ (unsupported) การติดตั้งที่รองรับมีแบบเดียวคือ Docker (ด้านบน) ซึ่งผ่านการทดสอบ อัปเดตด้วย
radioctlและสำรองข้อมูลอัตโนมัติ ใช้ภาคผนวกนี้เฉพาะเมื่อรัน Docker ไม่ได้ และต้องดูแลแพ็กเกจของระบบ การอัปเดต และการสำรองข้อมูลเอง ขั้น build, migration, หน้าติดตั้ง, อัปโหลดไฟล์ และเล่นเพลงอัตโนมัติเคยทดสอบครบบน Ubuntu 24.04 ส่วน systemd การโหลดค่าตั้ง Icecast ใหม่ และ Caddy ด้านล่างเป็นตัวอย่างมาตรฐานที่ไม่ได้รันในการทดสอบ
ตัวอย่างใช้ค่าต่อไปนี้
- โค้ดอยู่ที่
/opt/radio - ข้อมูลอยู่ที่
/var/lib/radio - ผู้ใช้ระบบชื่อ
radioซึ่งรัน Icecast ด้วย (Icecast จึงอ่านไฟล์ค่าตั้งที่ worker เขียนได้) - โดเมน
radio.example.thสตรีมที่https://radio.example.th/stream/<สถานี>
ตั้ง DNS เหมือนแบบโดเมน
1. ติดตั้งแพ็กเกจของระบบ
sudo apt update
sudo apt install -y postgresql ffmpeg liquidsoap icecast2 curl
แพ็กเกจ postgresql ของ Ubuntu 24.04 คือ PostgreSQL 16 ตอนติดตั้ง icecast2 อาจมีคำถามให้ตอบ ตอบอะไรก็ได้ เพราะขั้นที่ 7 จะเขียนไฟล์ตั้งค่าใหม่ทั้งหมด
2. ติดตั้ง Node.js 22
แพ็กเกจ nodejs ของ Ubuntu 24.04 เก่าเกินไป ให้ติดตั้ง Node.js 22 จาก NodeSource (หรือใช้ไฟล์ tarball ทางการจาก nodejs.org)
curl -fsSL https://deb.nodesource.com/setup_22.x -o nodesource_setup.sh
sudo bash nodesource_setup.sh
sudo apt install -y nodejs
node --version
node --version ต้องแสดง v22 ขึ้นไป ดูตำแหน่งของ node ด้วย command -v node ตัวอย่าง systemd ด้านล่างใช้ /usr/bin/node
3. สร้างผู้ใช้และ build โปรแกรม
sudo useradd --system --create-home --home-dir /var/lib/radio radio
# Icecast อ่านเสียงสำรองของสถานีจากโฟลเดอร์ข้อมูล
sudo chmod 755 /var/lib/radio
sudo mkdir -p /opt/radio
# คัดลอกหรือ clone ซอร์สโค้ดมาไว้ที่ /opt/radio
sudo chown -R radio:radio /opt/radio
cd /opt/radio
sudo -u radio npm ci
sudo -u radio npm run build
ผลการ build อยู่ในโฟลเดอร์ dist/
4. สร้างฐานข้อมูล
ตั้งรหัสผ่านให้บัญชีผู้ดูแลของ PostgreSQL แล้วสร้างฐานข้อมูล
sudo -u postgres psql -c "ALTER USER postgres PASSWORD 'ADMIN-PASSWORD'"
sudo -u postgres createdb radio
ไม่ต้องสร้าง role ของ app เอง migration สร้างให้ (ขั้นที่ 6)
5. เขียนไฟล์ตั้งค่า
sudo -u radio cp .env.example .env
sudo chmod 600 .env
sudo -u radio nano .env
.env.example เขียนไว้สำหรับ Docker Compose ถ้าติดตั้งโดยตรง ให้แก้ค่าเหล่านี้
DATABASE_URL=postgres://radio_app:APP-PASSWORD@127.0.0.1:5432/radio
DATABASE_ADMIN_URL=postgres://postgres:ADMIN-PASSWORD@127.0.0.1:5432/radio
DB_APP_PASSWORD=APP-PASSWORD
DATA_DIR=/var/lib/radio
HOST=127.0.0.1
BASE_URL=https://radio.example.th
ICECAST_PUBLIC_URL=https://radio.example.th/stream
HARBOR_PUBLIC_HOST=radio.example.th
ICECAST_HOST=127.0.0.1
LIQUIDSOAP_CALLBACK_URL=http://127.0.0.1:3000
ICECAST_TRUSTED_PROXIES=127.0.0.1
ICECAST_CONFIG_FILE=/var/lib/radio/icecast/icecast.xml
ICECAST_LOG_DIR=/var/log/icecast2
ICECAST_STANDBY=false
ข้อควรรู้
DATABASE_ADMIN_URLใช้บัญชีpostgresจากขั้นที่ 4 ใช้เฉพาะตอน migrationDATABASE_URLระบุ role ของ app (radio_app) ถ้าตั้งDB_APP_PASSWORDต้องตรงกับรหัสผ่านในDATABASE_URLถ้าเว้นว่าง ระบบใช้รหัสผ่านจากDATABASE_URL- role ของ app ต้องไม่เป็น superuser เพราะ superuser ข้าม row-level security ได้ ข้อมูลแต่ละสถานีจะไม่ถูกแยกกันอีก
migrateจะไม่ยอมทำงานถ้า role ของ app เป็น superuser มีสิทธิ์BYPASSRLSเป็นเจ้าของตาราง หรือเป็นผู้ใช้เดียวกับในDATABASE_ADMIN_URL - ตั้ง
BASE_DOMAIN(เฉพาะเมื่อใช้โดเมนย่อยของสถานี),SESSION_SECRET,IP_HASH_SALT, รหัสผ่าน Icecast และค่าอีเมล เหมือน "ติดตั้งเอง" ICECAST_STANDBY=false: สตรีมรอสัญญาณตามเวลาจริงต้องใช้อิมเมจ Icecast ของ Docker สถานีจะใช้ไฟล์สำรองของตัวเองแทน- ค่าที่มีช่องว่าง
<>หรือ JSON ต้องใส่เครื่องหมายคำพูดไว้ เหมือนใน.env.example
6. อัปเดตฐานข้อมูล (migration)
sudo -u radio node --env-file=.env dist/src/cli.js migrate
คำสั่งนี้สร้างตาราง สร้าง role ของ app ตาม DATABASE_URL (หรือตั้งรหัสผ่านให้ใหม่ถ้าไม่ตรงแล้ว) และเพิ่มแพ็กเกจเริ่มต้น ถ้ารัน migrate พร้อมกันหลายตัว ตัวที่ตามมาจะรอตัวแรกเสร็จก่อน (npm run migrate เป็นทางลัดตอนพัฒนา ทำเฉพาะ migration ไม่เพิ่มแพ็กเกจเริ่มต้น แต่หน้าติดตั้งจะเพิ่มให้ถ้ายังไม่มี)
7. ตั้งค่า Icecast
worker (shard 0) เขียนค่าตั้งของ Icecast ลง ICECAST_CONFIG_FILE และเขียนใหม่ทุกครั้งที่สถานีหรือค่าตั้งเปลี่ยน (สถานีใหม่ เปลี่ยนรหัสผ่าน SESSION_SECRET หรือ ICECAST_TRUSTED_PROXIES) Icecast รันเป็นผู้ใช้ radio จึงอ่านไฟล์นั้นได้ (ไฟล์มีรหัสผ่าน อ่านได้เฉพาะเจ้าของ) และ systemd path unit สั่งโหลด Icecast ใหม่ทุกครั้งที่ไฟล์เปลี่ยน ผู้ฟังไม่หลุด
sudo install -d -o radio -g radio -m 750 /var/lib/radio/icecast
sudo chown -R radio:radio /var/log/icecast2
# เสียงสำรองของสถานี (worker เป็นผู้เขียน) คือโฟลเดอร์ /fallback ของ Icecast
sudo install -d -o radio -g radio /var/lib/radio/ops/icecast-fallback
sudo ln -sfn /var/lib/radio/ops/icecast-fallback /usr/share/icecast2/web/fallback
# ไฟล์ค่าตั้งแรก ก่อน worker เริ่มทำงาน
sudo -u radio node --env-file=.env dist/src/cli.js icecast-config --out /var/lib/radio/icecast/icecast.xml
แก้ /etc/default/icecast2:
CONFIGFILE="/var/lib/radio/icecast/icecast.xml"
USERID=radio
GROUPID=radio
ให้โหลด Icecast ใหม่เมื่อ worker เขียนไฟล์ใหม่ /etc/systemd/system/radio-icecast-reload.path:
[Path]
PathChanged=/var/lib/radio/icecast/icecast.xml
[Install]
WantedBy=multi-user.target
และ /etc/systemd/system/radio-icecast-reload.service:
[Service]
Type=oneshot
ExecStart=/bin/systemctl reload icecast2
sudo systemctl daemon-reload
sudo systemctl enable --now radio-icecast-reload.path
sudo systemctl restart icecast2
sudo systemctl status icecast2
8. สร้าง systemd service
/etc/systemd/system/radio-app.service:
[Unit]
Description=Online Radio web app
After=network-online.target postgresql.service
Wants=network-online.target
[Service]
Type=simple
User=radio
Group=radio
WorkingDirectory=/opt/radio
EnvironmentFile=/opt/radio/.env
ExecStart=/usr/bin/node dist/src/index.js
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
/etc/systemd/system/radio-worker.service:
[Unit]
Description=Online Radio streaming worker
After=network-online.target postgresql.service icecast2.service radio-app.service
Wants=network-online.target
[Service]
Type=simple
User=radio
Group=radio
WorkingDirectory=/opt/radio
EnvironmentFile=/opt/radio/.env
ExecStart=/usr/bin/node dist/src/worker.js
Restart=always
RestartSec=5
TimeoutStopSec=30
[Install]
WantedBy=multi-user.target
ใช้ ExecStart=/usr/bin/node --env-file=/opt/radio/.env dist/src/index.js แทน EnvironmentFile= ก็ได้ เลือกอย่างใดอย่างหนึ่ง
WorkingDirectory ต้องเป็นโฟลเดอร์โค้ด เพราะ app อ่าน public/, docs/ และ src/db/migrations/ จากที่นั่น
เปิด service
sudo systemctl daemon-reload
sudo systemctl enable --now radio-app radio-worker
curl http://127.0.0.1:3000/health
/health ตอบ {"ok":true} เมื่อ app ต่อฐานข้อมูลได้
ระบบออกอากาศไม่ทำงานด้วยสิทธิ์ root
worker เปิดโปรแกรม Liquidsoap หนึ่งโปรแกรมต่อหนึ่งสถานี และ Liquidsoap ทำงานด้วยผู้ใช้ธรรมดาเสมอ
- ถ้า worker ทำงานด้วยผู้ใช้ธรรมดา (ผู้ใช้
radioข้างบน หรือผู้ใช้ 10001 ใน Docker และ Helm) Liquidsoap ใช้ผู้ใช้เดียวกัน ไม่ต้องตั้งค่าอะไร - ถ้า worker ทำงานด้วย root (คอนเทนเนอร์สำหรับพัฒนาบางแบบ) worker จะเปิด Liquidsoap ด้วย
LIQUIDSOAP_UID/LIQUIDSOAP_GIDถ้าไม่ได้ตั้ง จะใช้ผู้ใช้ระบบliquidsoapที่แพ็กเกจ Debian/Ubuntu สร้างไว้ หรือnobodyถ้าไม่มี worker มอบไฟล์ตั้งค่าของสถานี โฟลเดอร์บันทึกรายการ และโฟลเดอร์ HLS ให้ผู้ใช้นั้น ส่วนโฟลเดอร์ไฟล์เสียงขอเพียงอ่านได้ (สิทธิ์ตั้งต้นใช้ได้แล้ว)
โฟลเดอร์ของแต่ละสถานี (DATA_DIR/stations/<สถานี>/) มี station.liq ซึ่งเป็นสคริปต์ระบบออกอากาศชุดเดียวกันทุกสถานี และ station.json ซึ่งเป็นค่าตั้งของสถานีนั้น (ชื่อ ที่อยู่ พอร์ต รหัสผ่าน) สคริปต์อ่านค่าตั้งเป็นข้อมูล ข้อความที่สถานีพิมพ์จึงไม่มีทางกลายเป็นคำสั่งของระบบออกอากาศ station.json มีรหัสผ่านสตรีมอยู่ จึงอ่านได้เฉพาะผู้ใช้ของระบบออกอากาศ คำสั่ง node --env-file=.env dist/src/cli.js station-script <สถานี> แสดงไฟล์นี้
9. ตั้ง reverse proxy (Caddy)
ติดตั้ง Caddy 2.10 ขึ้นไป (จาก repository ของ Caddy แพ็กเกจของ Ubuntu เก่ากว่านั้น) แล้วเขียน /etc/caddy/Caddyfile โดยอิงจาก deploy/caddy/domain.Caddyfile และ deploy/caddy/routes.caddy แต่เปลี่ยนเป็นที่อยู่ในเครื่อง
{
email admin@radio.example.th
on_demand_tls {
ask http://127.0.0.1:3000/internal/tls-check
}
}
https:// {
tls {
on_demand
}
encode zstd gzip
handle_path /stream/* {
reverse_proxy 127.0.0.1:8000 {
flush_interval -1
}
}
@internal path /internal/*
handle @internal {
respond 404
}
handle {
reverse_proxy 127.0.0.1:3000 {
flush_interval -1
}
}
}
http:// {
redir https://{host}{uri} permanent
}
sudo systemctl reload caddy
ไฟล์นี้ทำอะไรบ้าง
- ออกใบรับรองเมื่อมีคนเรียกใช้ และออกเฉพาะโดเมนที่แพลตฟอร์มรู้จัก (ที่อยู่หลัก โดเมนย่อยของสถานี โดเมนของสถานีเอง ชื่อในหน้าโดเมนและที่อยู่)
/stream/<สถานี>ส่งไป Icecast โดยตัด/streamออก (ICECAST_PUBLIC_URL=https://radio.example.th/stream)- กัน
/internal/*จากภายนอก Icecast และ Liquidsoap เรียกเส้นทางนี้ตรงที่ 127.0.0.1 flush_interval -1ส่งเสียงและข้อมูลสดทันทีโดยไม่พักไว้- proxy ต้องให้ WebSocket ผ่านได้ห้องจัดรายการบนเว็บส่งเสียงสดผ่าน WebSocket และแขกรับเชิญระยะไกลก็เชื่อมต่อผ่าน WebSocket Caddy รองรับให้อัตโนมัติ
ถ้าใช้ proxy อื่น (nginx, HAProxy) ต้อง: ส่ง /stream/ ไป Icecast โดยตัดคำนำหน้าออก, ส่งต่อ header Upgrade และ Connection, ปิดการพักข้อมูล (buffering), ตั้ง read timeout ให้ยาว (1 ชั่วโมงขึ้นไป), ยอมให้อัปโหลดไฟล์ใหญ่, ส่ง X-Forwarded-For และกัน /internal/
10. เปิดไฟร์วอลล์
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 8100:8199/tcp
เปิดตั้งแต่ 8100 ขึ้นไปให้พอกับจำนวนสถานี ปิด 3000, 5432, 8000 และ 9100 ขึ้นไปไว้
ถ้าสถานีใช้ ถ่ายทอดไปช่องทางอื่น เซิร์ฟเวอร์ต้องเชื่อมต่อ ออก ไปยังปลายทางได้ด้วย ปกติคือ TCP 80, 443, 8000–8999 (SHOUTcast ใช้พอร์ต + 1 ด้วย), 1935 (RTMP) และ 443 (RTMPS: YouTube, Facebook) ufw อนุญาตการเชื่อมต่อขาออกอยู่แล้ว ให้ตรวจกฎขาออกของผู้ให้บริการหรือไฟร์วอลล์ขององค์กร
11. เปิดหน้าติดตั้ง
เปิด https://radio.example.th/install แล้วทำตามหัวข้อ "เริ่มใช้งานครั้งแรก"
การอัปเดต การสำรองข้อมูล และ log ของการติดตั้งโดยตรงอยู่ในส่วนที่มีคำว่า "(ไม่รองรับ)" ของหัวข้อ "สำรองและกู้คืนข้อมูล" "อัปเกรด" และ "ติดตามสถานะระบบ" ด้านบน
นำเข้าจากคลาวด์
สถานีนำเข้าไฟล์เสียงจากลิงก์แชร์ ตัวเลือกไฟล์ Dropbox / OneDrive / Google Drive และโฟลเดอร์ที่ซิงก์ได้ (คู่มือสถานี: "นำเข้าจากคลาวด์") คีย์ของแอปสำหรับตัวเลือกไฟล์และการซิงก์โฟลเดอร์ Dropbox / OneDrive ใส่หลังติดตั้งที่ ผู้ดูแลระบบ → บริการคลาวด์ (คู่มือผู้ดูแลระบบ: "บริการคลาวด์") Redirect URI ที่ต้องลงทะเบียนคือที่อยู่แพลตฟอร์มต่อด้วย /cloud/oauth/callback ไม่ต้องตั้งค่าอะไรก่อนเริ่มใช้ ค่าที่ตั้งเพิ่มได้ใน .env (app และ worker ต้องเห็นค่าเดียวกัน)
| ค่า | ค่าเริ่มต้น | ความหมาย |
|---|---|---|
CLOUD_SECRET_KEY |
(สร้างจาก SESSION_SECRET) |
คีย์เข้ารหัสข้อมูลลับของการนำเข้าจากคลาวด์ (refresh โทเคนรหัสผ่านแอป WebDAV คีย์ S3 และ secret ของแอป) ควรตั้งเป็นข้อความสุ่มยาว ๆ เพื่อให้การเปลี่ยน SESSION_SECRET ภายหลังไม่ทำให้การเชื่อมต่อที่เก็บไว้อ่านไม่ได้ ถ้าเปลี่ยนค่านี้ สถานีต้องเชื่อมต่อใหม่และต้องใส่คีย์ของแอปอีกครั้ง |
CLOUD_IMPORT_ALLOW_PRIVATE |
false |
true ให้การนำเข้าและการซิงก์เข้าถึงที่อยู่ในเครือข่ายภายในได้ (เช่น Nextcloud หรือ MinIO ในเครือข่ายของสถานี) แพลตฟอร์มที่ใช้ร่วมกันหลายองค์กรควรปิดไว้ |
CLOUD_IMPORT_MAX_PENDING |
200 |
จำนวนไฟล์ที่สถานีหนึ่งรอนำเข้าหรือกำลังดาวน์โหลดพร้อมกันได้ |
ALERT_CLOUD_SYNC_FAILURES |
3 |
จำนวนครั้งที่ซิงก์โฟลเดอร์ล้มเหลวติดกันก่อนแจ้งเตือนสถานี |
worker เป็นผู้ดาวน์โหลดไฟล์ จึงต้องออกอินเทอร์เน็ตทาง HTTPS ไปยังผู้ให้บริการ (Google, Dropbox, Microsoft, Box และลิงก์ของสถานี) ได้ และต้องมีพื้นที่ว่างใน DATA_DIR/tmp พอสำหรับไฟล์ใหญ่ที่สุดที่นำเข้า (สูงสุด 2 GB)