โพสต์ก่อนหน้า ใช้ runs-on: ubuntu-latest ทุก job คือให้ GitHub หาเครื่องมาให้ โพสต์นี้เล่าอีกทางหนึ่ง คือเอาเครื่องของเราเองมารับงาน
runner คือเครื่องที่รับ job ไปรัน
GitHub มี runner ให้สองแบบ
- GitHub-hosted runner GitHub เปิด VM ใหม่ให้ทุก job มี Ubuntu, Windows และ macOS พอ job จบ GitHub ก็ทิ้ง VM นั้นไป รอบต่อไปจึงได้เครื่องใหม่ที่สะอาดทุกครั้ง
- Self-hosted runner เราติดตั้งโปรแกรม runner ของ GitHub ลงเครื่องที่เรามี จะเป็น VPS, เครื่องในออฟฟิศ หรือ server ตัวเดียวกับที่ deploy ก็ได้ โปรแกรมนี้เป็นฝ่ายต่อออกไปหา GitHub ทาง HTTPS port 443 แล้วรอรับ job เราจึงไม่ต้องเปิด port ให้ใครต่อเข้ามา
ใน workflow ต่างกันแค่ runs-on
jobs:
test:
runs-on: [self-hosted, linux, x64]
self-hosted runner คุ้มในสามกรณี
นาทีของแพ็กเกจไม่พอ repo ส่วนตัวบนแพ็กเกจ Free ได้ 2,000 นาทีต่อเดือน ถ้าใช้เกินและไม่ได้ตั้งงบเผื่อไว้ job บนเครื่องของ GitHub จะหยุดรันจนถึงรอบบิลใหม่ ส่วนที่เกินคิดเป็นนาที Linux 2 core นาทีละ $0.006 และ macOS นาทีละ $0.062 (ราคาของ GitHub Actions) repo ของเว็บนี้เองเดือนตุลาคมใช้ไป 1,906 นาที เพราะ CI รันรอบละหลายนาทีและ push วันละหลายครั้ง
self-hosted runner ตอนนี้ไม่คิดค่านาที GitHub เคยประกาศเมื่อธันวาคม 2568 ว่าจะเก็บนาทีละ $0.002 ตั้งแต่มีนาคม 2569 แล้วเลื่อนออกไปโดยยังไม่บอกวันใหม่ ถ้าวางแผนระยะยาวก็ควรเปิดหน้าราคาดูเป็นระยะ
อยากให้ CI เร็วขึ้น เครื่องของ GitHub เริ่มจากศูนย์ทุกรอบ ต้องติดตั้ง PHP, เปิด container ฐานข้อมูล แล้วโหลด package ใหม่ เครื่องของเรามีทุกอย่างอยู่แล้ว และ cache ของ composer กับ npm ก็ยังอยู่จากรอบก่อน
ต้องใช้เครื่องแบบเฉพาะ หรือต้องเข้าถึงระบบข้างใน เช่น ต่อฐานข้อมูลที่อยู่ในวงแลนของออฟฟิศ หรือ deploy ด้วย runner ที่ติดตั้งบน server เอง แบบนี้ไม่ต้องเก็บ SSH key ของ server ไว้ใน GitHub
ถ้าไม่เข้ากรณีไหนเลย เครื่องของ GitHub ง่ายกว่า เพราะไม่มีเครื่องให้ดูแล
ข้อแลกที่ต้องรู้ก่อนใช้
- เครื่องไม่ได้ล้างระหว่าง job ไฟล์ ฐานข้อมูล และ process ที่ job ก่อนทิ้งไว้ยังอยู่ test อาจผ่านเพราะของที่ค้างจากรอบก่อน เราจึงให้ CI โหลดฐานข้อมูล test จาก snapshot ทุกรอบ และสั่ง
php artisan optimize:clearก่อนคำสั่ง artisan อื่น เผื่อมี config cache ค้างอยู่ - runner หนึ่งตัวรับทีละ job ถ้าหลาย repo ใช้เครื่องเดียวกัน job จะต่อคิวกัน job ที่รอคิวเกิน 24 ชั่วโมงจะ fail เอง
- ระบบปฏิบัติการอาจไม่ตรงกับ production ถ้า runner เป็น macOS แต่ production เป็น Linux ระบบไฟล์ของ macOS ไม่สนตัวพิมพ์เล็กใหญ่ test ที่โหลด
pages/dashboard.jsxด้วยชื่อDashboardจะผ่านบน runner แล้วไปพังบน production เราต้องมี test ที่เช็กชื่อไฟล์แบบตรงตัวอักษรเพิ่ม - เครื่องที่ใช้ Docker ไม่ได้ เช่น macOS ใช้
services:แบบในโพสต์ก่อนไม่ได้ ต้องติดตั้งฐานข้อมูลลงเครื่องเอง
ติดตั้ง runner
เปิด repo แล้วไปที่ Settings → Actions → Runners → New self-hosted runner เลือกระบบปฏิบัติการกับ architecture หน้านั้นจะแสดงคำสั่งให้รันบนเครื่องเราตามลำดับ คือดาวน์โหลดโปรแกรม แตกไฟล์ แล้วลงทะเบียน
./config.sh --url https://github.com/my-org/my-app --token <TOKEN> --labels deploy
token ในคำสั่งหมดอายุใน 1 ชั่วโมง ลงทะเบียนเสร็จแล้วสั่งให้ runner รันเป็น service ไว้ เครื่อง reboot แล้วจะได้กลับมารับงานเอง บน Linux กับ macOS ใช้ ./svc.sh install แล้ว ./svc.sh start (เอกสารการติดตั้ง)
สามเรื่องที่ควรตั้งตั้งแต่แรก
- รันด้วย user แยกที่ไม่ใช่ root ให้สิทธิ์เท่าที่ job ต้องใช้ ถ้าต้อง reload PHP-FPM ก็ให้ sudo ได้แค่คำสั่งนั้นคำสั่งเดียว
- ลงทะเบียนระดับ organization ถ้ามีหลาย repo แล้วใช้ runner group เลือกว่า repo ไหนใช้ runner ตัวไหนได้บ้าง repo ใหม่ที่ยังไม่ได้เพิ่มเข้า group จะค้างคิวเป็น queued ไปเรื่อย ๆ
- ปล่อยให้ runner อัปเดตตัวเอง ซึ่งเป็นค่าเริ่มต้นอยู่แล้ว ถ้าปิดไว้ ต้องอัปเดตเองภายใน 30 วันหลังเวอร์ชันใหม่ออก ไม่อย่างนั้น GitHub จะหยุดส่งงานให้ (เอกสารของ runner)
label ใช้เลือกเครื่องให้แต่ละ job
runner ทุกตัวได้ label self-hosted กับชื่อระบบปฏิบัติการและ architecture ของเครื่อง เช่น linux และ x64 เราเพิ่ม label ของเราเองได้ด้วย --labels ตอนลงทะเบียน หรือเพิ่มทีหลังในหน้า Settings
job จะไปรันบน runner ที่มีครบทุก label ที่เขียนใน runs-on
jobs:
test:
runs-on: [self-hosted, linux] # runner Linux ตัวไหนก็ได้
deploy:
runs-on: [self-hosted, deploy] # เฉพาะเครื่องที่ติด label deploy
ตั้งชื่อ label ตามหน้าที่ เช่น deploy หรือ gpu จะดีกว่าตั้งตามชื่อเครื่อง วันที่เปลี่ยนเครื่องจะได้ย้าย label ไปเครื่องใหม่ โดยไม่ต้องแก้ workflow ทุก repo
ห้ามให้ PR จากคนนอกรันบน self-hosted runner
ตอนเปิด pull request ไฟล์ workflow ที่ GitHub ใช้คือไฟล์ใน PR นั้น ไม่ใช่ไฟล์บน main ใครก็ตามที่ fork repo แล้วเปิด PR ได้ จึงแก้ ci.yml ให้รันคำสั่งอะไรก็ได้บนเครื่องเรา อ่าน .env ทิ้งโปรแกรมค้างไว้ในเครื่อง หรือแก้ไฟล์ที่ job ถัดไปจะใช้
เอกสารด้านความปลอดภัยของ GitHub จึงแนะนำว่าแทบไม่ควรใช้ self-hosted runner กับ repo สาธารณะเลย ถ้าจำเป็นต้องใช้จริง
- repo สาธารณะใช้ runner เฉพาะ job ที่มาจาก push ให้ PR รันบนเครื่องของ GitHub ซึ่งไม่คิดเงินสำหรับ repo สาธารณะอยู่แล้ว
- เปิดให้ต้องกดอนุมัติก่อน workflow ของ PR จาก fork จะรัน ตั้งได้ใน Settings → Actions → General
- ใช้ runner group แยก runner ใน group ไม่รับงานจาก repo สาธารณะเป็นค่าเริ่มต้น อย่าเปิดตรงนี้ถ้าไม่จำเป็น
- ระวัง
pull_request_targettrigger นี้รันด้วยสิทธิ์ของ repo และเข้าถึง secret ได้ ถ้า checkout โค้ดจาก PR มารันด้วย ก็เท่ากับยื่นสิทธิ์นั้นให้คนเปิด PR
repo ส่วนตัวก็มีเรื่องเดียวกันในขนาดเล็กกว่า คนที่เปิด PR ได้คือคนในทีม แต่ PR ก็ยังรัน workflow ของตัวเองได้อยู่ดี กฎที่เราใช้คือ job ที่ PR สั่งได้ ต้องไม่รันบน runner ที่ deploy runner ที่ deploy รับเฉพาะ workflow จาก push เข้า main
ข้อนี้รวม AI agent ด้วย ถ้าให้ agent เปิด PR เอง ไฟล์ workflow ที่ agent แก้ใน PR นั้นก็รันบน runner ของเราเหมือน PR ของคน อ่าน diff ของ .github/workflows ก่อนปล่อยให้รันทุกครั้ง
ทำให้แต่ละ job เริ่มจากเครื่องที่สะอาดขึ้น
- ephemeral runner ลงทะเบียนด้วย
--ephemeralrunner จะรับ job เดียวแล้วถอนตัวเอง ต้องมีระบบสร้างเครื่องใหม่มาแทน เช่น เปิด VM หรือ container ใหม่ทุกครั้ง เหมาะกับทีมที่มีงาน CI เยอะ - ล้าง workspace ก่อน job ที่สำคัญ อย่าง job ที่นำไปสู่การ deploy ลบไฟล์ในโฟลเดอร์งานให้หมด รวม
.gitด้วย เพราะ config ของ git ซ่อน hook ไว้ได้ - ส่ง secret เป็น environment variable อย่าใส่ไว้ใน argument ของคำสั่ง process อื่นบนเครื่องเดียวกันมองเห็น argument ได้
- อย่าเก็บของสำคัญไว้บนเครื่อง runner เครื่องที่รัน CI ควรเห็นแค่สิ่งที่ CI ต้องใช้ ถ้าเครื่องนั้นมีข้อมูลอื่นหรือเข้าถึงระบบอื่นได้ ให้คิดว่าทุกคนที่สั่ง workflow ได้ก็เข้าถึงสิ่งเหล่านั้นได้เหมือนกัน






