Self-Hosted Gitea Actions: 99% ไม่ง้อ Cloud, ครบวงจรตั้งแต่ Push ถึง Production
สารบัญ
- TL;DR
- เรื่องมันเริ่มจาก
- Project ที่ใช้เป็นตัวอย่าง: FlowForge
- Architecture 3 environments
- Git Flow + Deploy Trigger
- Git Flow 1
- Git Flow 2
- Git Flow 3
- Multi-stage Docker Build
- Cache Strategy
- End-to-End Deploy Flow
- Setup Runner บนแต่ละ Host
- Secrets + Variables Management
- Variables (Repository Settings → Actions → Variables)
- Secrets (Repository Settings → Actions → Secrets)
- ทำไม 99% Self-Hosted ถึง Work
- Trade-offs ที่ต้องยอมรับ
- เหมาะกับใคร
- สรุป
- References
ตอนนี้ infrastructure ของผมแทบจะ 99% self-hosted เองทั้งหมด — Gitea ก็ self-host, runners ก็อยู่บนเครื่องตัวเอง, database ก็วิ่งใน local network, แม้แต่ AI inference (vLLM) ก็รันอยู่บน DGX Spark ที่บ้าน
บทความนี้จะเล่าว่าทำไมถึงเลือกแบบนี้ แลกอะไรมาบ้าง และเล่าประสบการณ์จริงจากการใช้งานว่าเป็นยังไง
TL;DR
Self-host Gitea Actions ด้วย act_runner บนเครื่องเองเป็นทางเลือกที่ดีสำหรับ project ขนาดเล็ก-กลางที่ต้องการ data sovereignty และควบคุม infrastructure ได้เอง — ตั้งแต่ git push, multi-stage Docker build, จนถึง deploy บน 3 environments (dev/staging/prod) โดยใช้ Docker socket sharing กับ runner ราคาที่ต้องจ่ายคือเวลาดูแลเองและไม่มี redundancy ถ้ามี homelab หรือ spare hardware อยู่แล้ว ค่าใช้จ่ายต่อเดือนแทบเป็นศูนย์
เรื่องมันเริ่มจาก
ตอนแรกใช้ GitHub Actions เหมือนคนทั่วไป ทุกอย่างสะดวก — push ปุ๊บ deploy ปั๊บ free tier ก็เยอะพอสำหรับ project ส่วนตัว
แต่พอเริ่มมีหลาย environment (dev/staging/prod) และต้องการ data sovereignty (ข้อมูลไม่ออกจาก local network) เริ่มรู้สึกว่า:
- Free tier ของ GitHub Actions ไม่พอ — minute-based pricing เริ่มสะสมค่าใช้จ่าย
- Database credentials + JWT keys ต้องไปอยู่บน cloud ของคนอื่น
- Audit trail ของ deploy ไม่ได้อยู่ในระบบตัวเอง
- Latency ระหว่าง runner กับ production database สูง (cross-region)
เลยตัดสินใจ self-host ทั้งหมด — Gitea + act_runner บนเครื่องตัวเองทั้งหมด
Note: "self-host" ไม่ได้หมายความว่าฟรีเสมอ — ต้องคิดค่าไฟ ค่าเครื่อง ค่าเวลาดูแล แต่ถ้า traffic ไม่เยอะและมีเครื่องว่างอยู่แล้ว จะถูกกว่ามากในระยะยาว
Project ที่ใช้เป็นตัวอย่าง: FlowForge
ขอยกตัวอย่างเป็น project สมมุติชื่อ FlowForge — workflow automation backend เขียนด้วย Node.js + TypeScript คล้ายกับ project ที่ผมทำงานอยู่จริง แต่เปลี่ยน domain แล้ว
Tech stack:
- Node.js 24 + TypeScript
- AdonisJS framework
- PostgreSQL 16
- Redis 7 (queue + cache)
- PM2 cluster mode
- Docker multi-stage build
- vLLM integration (สำหรับ smart workflow suggestions)
Note: ทำไม AdonisJS — มันเป็น full-stack framework แบบเดียวกับ Laravel แต่เป็น Node.js ecosystem เรา build แล้วได้ output เป็น Node.js app เดียว จบภายใน container เดียว ไม่ต้องแยก frontend/backend
Architecture 3 environments
FlowForge มี 3 environments แยกกันชัดเจน แต่ละ environment มี host เป็นของตัวเอง มี runner เป็นของตัวเอง ไม่ปนกัน (isolated):
| Branch | Environment | Host | Port | Container | Runner |
|---|---|---|---|---|---|
develop | dev | flowforge-dev.lan | 8080 | flowforge-dev | flowforge-dev |
staging | staging | flowforge-staging.lan | 8081 | flowforge-staging | flowforge-staging |
main | production | flowforge-prod.lan | 8082 | flowforge-prod | flowforge-prod |
Note: แยก host ตาม environment เป็น over-engineering ไหม — สำหรับ project ส่วนตัวอาจจะใช่ แต่ FlowForge มี feature experimental เยอะ deploy dev แล้วเกิดปัญหาบ่อย ถ้ารวม host เดียวกัน staging จะล่มตาม เลยแยก host ตาม environment
Git Flow + Deploy Trigger
Git Flow 1
Git Flow 2
Git Flow 3
Trigger rules ของ FlowForge:
| Event | Branch/Ref | Target | How |
|---|---|---|---|
push | develop | dev | Auto deploy |
push (merge from dev) | staging | staging | Auto deploy |
push tag vN.M.K | tag | prod | Auto deploy |
workflow_dispatch | main | prod | Manual deploy (approval) |
push to main branch | main | - | Ignored (use tags or manual) |
Note: ทำไม push ที่ main ไม่ deploy — main คือ release branch ไม่ใช่ deploy branch การ push ตรงเข้า main หมายถึง "รวม staging แล้วพร้อม release" แต่ยังไม่ได้ release จริง ต้อง cut tag หรือ manual dispatch ถึงจะ deploy
Multi-stage Docker Build
ไฟล์ Dockerfile.deploy แบ่ง 3 stage หลัก:
# Stage 1: deps (cached)
FROM node:24-alpine AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
# pnpm 10 ต้องการ allowBuilds declaration
RUN echo "packages: ['.']\nonlyBuiltDependencies: ['@swc/core', 'esbuild']" \
> pnpm-workspace.yaml
RUN pnpm install --prod=false
# Stage 2: build
FROM deps AS build
COPY . .
RUN node ace build
# Stage 3: production (~270MB)
FROM node:24-alpine
WORKDIR /app
# Copy build output + production deps
COPY /app/build ./
COPY /app/package.json ./
RUN npm install --omit=dev
# Security: non-root user
USER node
# Health check
HEALTHCHECK \
CMD wget -q http://127.0.0.1:8080/health || exit 1
CMD ["pm2-runtime", "ecosystem.config.cjs"]
Note: ใช้
pm2-runtimeแทนpm2—pm2-runtimeเป็น subcommand ของ pm2 package เดียวกัน แต่ออกแบบมาให้ทำงานใน container โดยเฉพาะ จับ SIGTERM ได้ถูกต้อง ไม่ต้อง fork daemon process
Cache Strategy
- Stage 1 (deps) cache ที่ layer ของ
pnpm install— invalidate เฉพาะตอนpackage.jsonหรือpnpm-lock.yamlเปลี่ยน - Stage 2 (build) cache ตอน source code ไม่เปลี่ยน
- Stage 3 (production) cache ตอน dependencies ไม่เปลี่ยน
Note: เคล็ดลับที่ช่วยให้ build เร็วขึ้นมาก — ใช้ BuildKit cache mounts (
--mount=type=cache) กับ pnpm store และ npm cache แทนที่จะ COPY เข้า image แบบเดิม — ได้ image size เท่าเดิม แต่ build time ลดลง 50%+
End-to-End Deploy Flow
จาก git push จนถึง container รัน — ภาพรวมทั้ง flow ของ 1 deploy (ใช้ develop → dev เป็นตัวอย่าง):
Note: เวลาจริง — cold build (no cache) ใช้ ~3-4 นาที, warm build (full cache) ใช้ ~15-20 วินาที ส่วนใหญ่ของเวลาไปที่ stage 1 (pnpm install) แม้จะ cache แล้วก็ยังมี COPY + npm install เล็กน้อย
Race condition: ถ้ามี push ซ้อนกัน (เช่น 2 commits ติดกัน) runner เดียวกันจะ queue งานไว้ตามลำดับ — แต่ถ้าอยาก parallel deploy ต้อง register runner หลายตัวบน host เดียวกัน แล้วใช้
concurrencyใน workflow จำกัดการ deploy พร้อมกันให้เหลือ 1 instance ต่อ environment เพื่อป้องกัน database migration ชนกัน
Setup Runner บนแต่ละ Host
ทุก deployment host ต้อง register runner กับ Gitea:
# 1. Download act_runner
curl -fsSL -o /home/apps/act_runner \
https://dl.gitea.com/act_runner/0.6.1/act_runner-0.6.1-linux-amd64
chmod +x /home/apps/act_runner
# 2. Register กับ Gitea
mkdir -p /home/apps/act-runner-data
cd /home/apps/act-runner-data
/home/apps/act_runner register \
--instance https://git.2my.xyz \
--token <RUNNER_TOKEN> \
--name flowforge-runner-<env> \
--labels "flowforge-<env>"
# 3. Run as container (แนะนำ — แชร์ Docker daemon)
docker run -d \
--name gitea-runner \
--restart unless-stopped \
-v /home/apps/.ssh:/root/.ssh:ro \
-v /home/apps/act-runner-data:/data \
-v /var/run/docker.sock:/var/run/docker.sock \
gitea/act_runner:0.6.1 \
daemon
Note: ทำไมต้อง mount
/var/run/docker.sock— เพราะ runner อยู่ใน container แต่ต้อง build image + run deployment container — เลยต้องแชร์ Docker daemon กับ host ถ้าไม่ mount ก็ต้องใช้ Docker-in-Docker (dind) ซึ่งหนักกว่าถ้าต้องการ custom image (เช่น ติดตั้ง Node.js หรือ pnpm เวอร์ชันเฉพาะไว้ล่วงหน้า) ให้สร้าง
Dockerfileจากgitea/act_runner:0.6.1แล้ว build เอง แต่ในบทความนี้ใช้ official image แล้วติดตั้ง dependency ใน workflow แทน
Secrets + Variables Management
ใน Gitea แบ่งเป็น Variables (ไม่ secret) กับ Secrets (เข้ารหัส) — ใช้ต่างกัน:
Variables (Repository Settings → Actions → Variables)
| Variable | dev | staging | prod |
|---|---|---|---|
NODE_VERSION | 24-alpine | 24-alpine | 24-alpine |
APP_NAME | FlowForge Dev | FlowForge Staging | FlowForge Prod |
PORT | 8080 | 8081 | 8082 |
LOG_LEVEL | info | info | warn |
DB_HOST | 10.0.2.30 | 10.0.2.30 | 10.0.2.30 |
DB_DATABASE | flowforge_dev | flowforge_staging | flowforge |
Security Note: IP
10.0.2.30เป็น private IP ภายใน local network — ไม่สามารถเข้าถึงได้จากภายนอก แต่ถ้า runner หรือ host ถูก compromise ก็จะเข้าถึง database ได้ ควรใช้ network segmentation + firewall rule จำกัดการเข้าถึง database ให้เฉพาะ host ที่จำเป็น
Secrets (Repository Settings → Actions → Secrets)
| Secret | Description |
|---|---|
APP_KEY | AdonisJS app key |
DB_PASSWORD | Database password |
JWT_PRIVATE_KEY | JWT private key |
JWT_PUBLIC_KEY | JWT public key |
VLLM_API_KEY | vLLM API key |
TELEGRAM_BOT_TOKEN | Telegram bot token |
Note: Secrets ไม่เคยถูก bake เข้า Docker image — workflow generates
.envfile ใน runner แล้ว mount เข้า container ตอน runtime เท่านั้น ถ้าใคร pull image ไปดูก็ไม่เจอ credentials
ทำไม 99% Self-Hosted ถึง Work
เอาจริง — ตอนแรกก็กังวลว่าจะเจอปัญหาเยอะ แต่พอทำจริงแล้ว:
สิ่งที่ง่ายกว่าที่คิด:
- Gitea setup — ใช้เวลา 30 นาที รัน Gitea ใน Docker, configure admin, สร้าง repo
- act_runner — register + run ใช้เวลา 10 นาทีต่อ host
- Deploy pipeline — เขียน workflow เหมือน GitHub Actions แทบทุกอย่าง (Gitea Actions ใช้ syntax เดียวกัน)
- Backups —
pg_dump+rsyncไป NAS ก็พอ
สิ่งที่ยากกว่าที่คิด:
- Monitoring — ไม่มี managed monitoring service แถมมาเหมือนใช้ GitHub + Grafana Cloud เลยต้องเซ็ตเอง (ใช้ Prometheus + Grafana self-hosted)
- Log aggregation — ไม่มี Datadog มาช่วย ต้องเขียน log shipping เอง
- Secrets rotation — ต้องหมุนเวียน (rotate) JWT key เองเมื่อหมดอายุ
- Disaster recovery — ถ้า host พังต้องมี backup plan เตรียมไว้ (ยังไม่เคยเกิดขึ้นจริง)
Note: สิ่งที่ยัง self-host ไม่ได้ — DNS ยังใช้ Cloudflare (เพราะ dynamic IP ที่บ้าน) และ email delivery ใช้ Resend (เพราะ self-host mail server เป็นเรื่องใหญ่) — นี่คือ 1% ที่เหลือ
Trade-offs ที่ต้องยอมรับ
เอาจริง — self-host ไม่ได้ฟรีเสมอไป:
ข้อดี:
- ข้อมูลไม่ออกจาก infrastructure ตัวเอง
- ค่าใช้จ่ายต่อเดือนคงที่ (ค่าไฟ + ค่าเครื่อง) ไม่ scaling ตาม traffic
- Latency ต่ำ — runner อยู่ใกล้ database
- ไม่ต้องเชื่อใจ third-party กับ credentials
ข้อเสีย:
- ต้องดูแลเองหมด — update Gitea, runner, OS patches
- ไม่มี redundancy — host เดียวพังคือพัง
- เวลา setup ครั้งแรกเยอะ
- ไม่มี SLA — ถ้าไฟดับที่บ้าน service ก็ดับด้วย
เหมาะกับใคร
เหมาะถ้า:
- มี homelab หรือ spare hardware อยู่แล้ว
- Privacy/data sovereignty สำคัญ (medical, finance, etc.)
- Traffic ไม่สูงมาก — burst ไม่ได้บ่อย
- ทีมเล็ก (1-5 คน) และทุกคนคุ้นเคยกับ Linux
ไม่เหมาะถ้า:
- ไม่มีเวลาดูแล infrastructure
- Traffic ใหญ่มาก — self-host ไม่ scale เท่า cloud
- ทีมใหญ่ที่ต้องการ enterprise features (SSO, audit log, compliance)
- ไม่มี backup power + UPS (ถ้าไฟดับบ่อย)
สรุป
Self-host Gitea Actions เป็นทางเลือกที่ดีสำหรับ project ที่ต้องการ control + privacy แต่ไม่ต้องการ scale ใหญ่ — ของผมใช้งานมา 2 ปีแล้ว มี downtime รวมกันไม่ถึง 1 ชั่วโมง และค่าใช้จ่ายต่อเดือนแทบเป็นศูนย์ (เฉพาะค่าไฟบ้าน)
ถ้าสนใจ ลองเริ่มจาก Gitea ก่อนตัวเดียว — แค่ migrate repo มาใช้ Gitea Actions แทน GitHub Actions ส่วนที่เหลือค่อยๆ ย้ายตาม
References
- Gitea Documentation — Act Runner
- Gitea Official Docker Image
- PM2 Runtime — Docker Integration
- AdonisJS
- pnpm 10.x — onlyBuiltDependencies
เนื้อหานี้มีประโยชน์ไหม? ช่วยสนับสนุนค่ากาแฟให้ผู้เขียนสักแก้ว
Buy Me a Coffee