Skip to main content

Self-Hosted Gitea Actions: 99% ไม่ง้อ Cloud, ครบวงจรตั้งแต่ Push ถึง Production

· 11 min read

ตอนนี้ 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):

BranchEnvironmentHostPortContainerRunner
developdevflowforge-dev.lan8080flowforge-devflowforge-dev
stagingstagingflowforge-staging.lan8081flowforge-stagingflowforge-staging
mainproductionflowforge-prod.lan8082flowforge-prodflowforge-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:

EventBranch/RefTargetHow
pushdevelopdevAuto deploy
push (merge from dev)stagingstagingAuto deploy
push tag vN.M.KtagprodAuto deploy
workflow_dispatchmainprodManual deploy (approval)
push to main branchmain-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
RUN npm install -g [email protected]
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 --from=build /app/build ./
COPY --from=build /app/package.json ./
RUN npm install --omit=dev
RUN npm install -g [email protected]

# Security: non-root user
USER node

# Health check
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget -q http://127.0.0.1:8080/health || exit 1

CMD ["pm2-runtime", "ecosystem.config.cjs"]

Note: ใช้ pm2-runtime แทน pm2pm2-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)

Variabledevstagingprod
NODE_VERSION24-alpine24-alpine24-alpine
APP_NAMEFlowForge DevFlowForge StagingFlowForge Prod
PORT808080818082
LOG_LEVELinfoinfowarn
DB_HOST10.0.2.3010.0.2.3010.0.2.30
DB_DATABASEflowforge_devflowforge_stagingflowforge

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)

SecretDescription
APP_KEYAdonisJS app key
DB_PASSWORDDatabase password
JWT_PRIVATE_KEYJWT private key
JWT_PUBLIC_KEYJWT public key
VLLM_API_KEYvLLM API key
TELEGRAM_BOT_TOKENTelegram bot token

Note: Secrets ไม่เคยถูก bake เข้า Docker image — workflow generates .env file ใน 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 เดียวกัน)
  • Backupspg_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


แชร์บทความ

เนื้อหานี้มีประโยชน์ไหม? ช่วยสนับสนุนค่ากาแฟให้ผู้เขียนสักแก้ว

Buy Me a Coffee
Loading...