ติดตั้ง Jenkins บน VPS ระบบ CI/CD อัตโนมัติ
Jenkins คือ automation server ที่ใช้สร้างระบบ CI/CD — pull code จาก GitHub/GitLab อัตโนมัติ run test compile build และ deploy ไปยัง production ทั้งหมดโดยไม่ต้องรันด้วยมือ บทความนี้สอนติดตั้ง Jenkins บน VPS ด้วย Docker เชื่อมต่อกับ GitHub ตั้ง Declarative Pipeline ด้วย Jenkinsfile และใช้ Blue Ocean UI เพื่อ monitor build แบบ visual
เหตุใดจึงต้องใช้ Jenkins บน VPS
- Automation: ทุกครั้งที่ push code ไป GitHub หรือ merge PR อัตโนมัติ trigger build/test/deploy เลย ไม่ต้องรันคำสั่ง ssh ด้วยมือ
- Decoupling from GitHub Actions: GitHub Actions มี limitation free tier (3000 minutes/month) Jenkins on VPS ดีกว่าถ้า repo private หรือ build หนักที่ต้อง many hours/month
- Full control: Jenkins รันบน VPS ของเรา control ได้ 100% ได้กำหนด resource limit, cache policy, artifact storage เอง ไม่ต้องยึดติดกับ GitHub pricing
- Webhook integration: GitHub/GitLab ส่ง webhook ไปยัง Jenkins URL — Jenkins update git local repo และ trigger pipeline ทันที
- Multiple repositories: ใช้ Jenkins 1 ตัว manage pipeline สำหรับ 10+ repositories ได้พร้อมกัน
ติดตั้ง Jenkins ด้วย Docker
mkdir -p /opt/jenkins/{data,workspace} && cd /opt/jenkins
# สร้าง docker-compose.yml
cat > docker-compose.yml << 'EOF'
version: "3.9"
services:
jenkins:
image: jenkins/jenkins:lts
restart: unless-stopped
ports:
- "127.0.0.1:8080:8080"
- "127.0.0.1:50000:50000"
environment:
JENKINS_OPTS: "-Djenkins.install.runSetupWizard=false"
JENKINS_JAVA_OPTS: "-Xmx1024m -Xms512m"
volumes:
- ./data:/var/jenkins_home
- ./workspace:/var/jenkins_home/workspace
- /var/run/docker.sock:/var/run/docker.sock
user: "0:0"
volumes:
jenkins_data:
EOF
docker-compose up -d
# รอ 30 วินาที แล้วดู log
sleep 30
docker-compose logs -f jenkins | grep -i "jenkins is running"
ตั้ง Nginx Reverse Proxy สำหรับ Jenkins
nano /etc/nginx/sites-available/jenkins
server {
listen 80;
server_name ci.yourdomain.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name ci.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/ci.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ci.yourdomain.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_connect_timeout 600s;
proxy_send_timeout 600s;
proxy_read_timeout 600s;
}
}
ln -s /etc/nginx/sites-available/jenkins /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
certbot --nginx -d ci.yourdomain.com
ตั้งค่า Jenkins และเปิด Jenkins UI
# ดูรหัส initial admin password
docker-compose exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword
# เข้า https://ci.yourdomain.com
# Paste password ใน Setup Wizard
# Select "Install suggested plugins" ปล่อยให้ install plugins ตัวแนะนำ
# Create Admin User
# Save and Continue
# ตั้งค่า Jenkins
# Manage Jenkins → Manage Plugins
# Available → ค้นหา "Blue Ocean" → Install
# ค้นหา "GitHub Integration" → Install
# Restart Jenkins: curl -s http://127.0.0.1:8080/restart
สร้าง Personal Access Token ใน GitHub
# GitHub → Settings → Developer settings → Personal access tokens → Tokens (classic)
# Generate new token
# Scopes ต้องเลือก:
# ✓ repo (full control of private repositories)
# ✓ admin:repo_hook (write access to hooks)
# ✓ admin:org_hook (for org-level webhooks)
# Generate token → copy token ไว้
# ใน Jenkins:
# Manage Jenkins → Credentials → System → Global credentials
# Add Credentials → Kind: GitHub App or Username with password
# Username: your-github-username
# Password: [paste token ที่ copy ไว้]
# ID: github-credentials
# Save
เชื่อมต่อ GitHub และสร้าง Webhook
# Manage Jenkins → Configure System
# GitHub → GitHub Servers
# Add: https://api.github.com
# Credentials: [select github-credentials ที่สร้างไว้]
# Test connection → "Credentials verified"
# ใน GitHub repo ที่อยากอัตโนมัติ:
# Repository → Settings → Webhooks → Add webhook
# Payload URL: https://ci.yourdomain.com/github-webhook/
# Content type: application/json
# Events: Just the push event (หรือ select "Let me select individual events")
# - ✓ Pushes
# - ✓ Pull requests
# Active: ✓
# Add webhook
# ทดสอบ webhook
# ใน GitHub webhook: Recent Deliveries → ดูว่า status เป็น 200 OK
สร้าง Pipeline Job และ Jenkinsfile
# ใน GitHub repo สร้างไฟล์ Jenkinsfile ที่ root
cat > Jenkinsfile << 'EOF'
pipeline {
agent any
options {
buildDiscarder(logRotator(numToKeepStr: '10'))
timeout(time: 30, unit: 'MINUTES')
}
environment {
NODE_ENV = 'production'
REGISTRY = 'docker.io'
IMAGE_NAME = 'myapp:${BUILD_NUMBER}'
}
stages {
stage('Checkout') {
steps {
checkout scm
sh 'git log -1 --oneline'
}
}
stage('Install Dependencies') {
steps {
sh 'npm install'
}
}
stage('Run Tests') {
steps {
sh 'npm test'
}
}
stage('Build') {
steps {
sh 'npm run build'
}
}
stage('Deploy') {
when {
branch 'main'
}
steps {
sh '''
echo "Deploying to production..."
scp -r dist/ [email protected]:/var/www/app/
ssh [email protected] "cd /var/www/app && systemctl restart app"
'''
}
}
}
post {
always {
cleanWs()
}
success {
echo "Build succeeded!"
}
failure {
echo "Build failed!"
}
}
}
EOF
# Commit ไปยัง GitHub
git add Jenkinsfile
git commit -m "Add Jenkins pipeline"
git push origin main
# ใน Jenkins: New Item
# Item name: my-app-pipeline
# Type: Pipeline
# Pipeline → Definition: Pipeline script from SCM
# SCM: Git
# Repository URL: https://github.com/yourname/yourrepo.git
# Credentials: github-credentials
# Branch: */main
# Script Path: Jenkinsfile
# Save
# Test trigger: ใน Jenkins job page → Build Now
# หรือ push code ไป GitHub webhook จะ auto trigger
ตั้ง Blue Ocean Dashboard
# ใน Jenkins หลัก → Open Blue Ocean
# Create a new pipeline
# Select GitHub
# Credentials: github-credentials
# Search your organization
# Select repository
# Create pipeline
# Blue Ocean จะ auto-scan Jenkinsfile และ visualize pipeline
# ทุก stage แสดงเป็น card พร้อมสถานะ (Running/Success/Failed)
# คลิก stage เพื่อดูละเอียด log
# Build history → ดูทุก build ที่ผ่านมา
# Activity → real-time ของ current build
Manage Jenkins → Manage Nodes → New Node → Permanent Agent → Configure SSH key และ labelsตรวจสอบว่า Jenkins ทำงานถูกต้อง
# ตรวจ container status
docker-compose ps
# ตรวจ Jenkins ready
curl -s http://127.0.0.1:8080/api/json | python3 -m json.tool | head -20
# ดู version
curl -s -I http://127.0.0.1:8080 | grep X-Jenkins
# ดู recent jobs
curl -s http://127.0.0.1:8080/api/json?tree=jobs[name,lastBuild[number,result]] \
| python3 -m json.tool
# ตรวจ executor (agents available)
curl -s http://127.0.0.1:8080/api/json?tree=numExecutors | python3 -m json.tool
# ตรวจ webhook delivery ใน GitHub
# Repository → Settings → Webhooks → [webhook URL] → Recent Deliveries → ดูสถานะ
docker-compose logs jenkins | grep -i webhookแก้ปัญหาที่พบบ่อย
Error: "Failed to connect to GitHub"
Jenkins ไม่สามารถเชื่อมต่อ GitHub API หรือ credentials ไม่ถูกต้อง
# ตรวจ credentials ใน Jenkins
# Manage Jenkins → Credentials → System → Global credentials
# ดูว่า github-credentials มีอยู่ไหม
# ทดสอบ GitHub token ด้วยตรง
curl -H "Authorization: token YOUR_GITHUB_TOKEN" \
https://api.github.com/user
# ถ้า token หมดอายุ ให้ regenerate ใน GitHub → Settings → Developer settings
# อัพเดท credentials ใน Jenkins
# ตรวจ DNS ภายใน container
docker-compose exec jenkins nslookup api.github.com
# ถ้า DNS fail ใส่ extra hosts ใน docker-compose
# services:
# jenkins:
# extra_hosts:
# - "api.github.com:140.82.113.6"
Error: "Webhook delivery failed with status 403"
GitHub webhook ส่ง payload ไปที่ Jenkins แต่ Jenkins ปฏิเสธ (Forbidden) — มักเกิดจากสิทธิ์ของ endpoint หรือ IP ถูกบล็อก
# ตรวจ Jenkins firewall
sudo ufw status
# ต้องเปิด port 8080 หรือ 443 (ถ้าผ่าน nginx proxy)
# ตรวจ nginx config สำหรับ /github-webhook
# ต้องมี location rule สำหรับ /github-webhook/:
# nginx config example:
# location /github-webhook/ {
# proxy_pass http://127.0.0.1:8080/github-webhook/;
# }
# ปิด CSRF protection ชั่วคราวเพื่อ test (ไม่ recommendations):
# Manage Jenkins → Configure Global Security
# CSRF Protection: unchecked [only for testing!]
# ดู Jenkins log เพื่อหา error
docker-compose logs jenkins | tail -50
Error: "Build failed: No executor available"
Jenkins master node ไม่มี executor available (ทั้งหมดบ่อยแล้ว) หรือ build ถูก queue ไว้
# ตรวจ executor count
curl -s http://127.0.0.1:8080/api/json?tree=numExecutors
# ตรวจ queue
curl -s http://127.0.0.1:8080/api/json?tree=queue[items[id,task[name],why]] \
| python3 -m json.tool
# เพิ่ม executor count:
# Manage Jenkins → Configure System → # of executors → เพิ่มเป็น 4 หรือมากกว่า
# Save
# ตรวจ Java memory ไม่ได้ OOM:
docker-compose exec jenkins free -h
docker-compose logs jenkins | grep -i "OutOfMemory"
# ถ้า OOM เพิ่ม memory ใน docker-compose:
# environment:
# JENKINS_JAVA_OPTS: "-Xmx2048m -Xms1024m"
Error: "Pipeline syntax error in Jenkinsfile"
Jenkinsfile มี syntax error — stages ไม่ถูกต้อง หรือ indentation ผิด
# ตรวจ syntax ด้วย Jenkins validator
# Jenkins → Pipeline Syntax → Declarative Directive Generator
# Paste Jenkinsfile และ validate
# Common errors:
# - stages ต้องมี stage อย่างน้อย 1 อัน
# - step ต้อง sh / bat / echo เป็นต้น
# - when block ต้องมี condition เช่น branch() / expression() เป็นต้น
# - post block ต้องมี {always/success/failure} เป็นต้น
# ตรวจ Jenkinsfile ใน GitHub:
# Settings → Code security & analysis → Dependabot alerts
# ดูว่ามี warning ไหม
# ตรวจด้วย shebang
cat Jenkinsfile | head -1
# ต้อง NOT start ด้วย #! — Jenkins handle เอง
Error: "ssh deploy@server: Permission denied (publickey)"
Jenkins container ไม่มี SSH key สำหรับ deploy ไปยัง production server
# สร้าง SSH key ใน Jenkins container
docker-compose exec jenkins ssh-keygen -t ed25519 -f ~/.ssh/id_jenkins -N ""
# ดู public key
docker-compose exec jenkins cat ~/.ssh/id_jenkins.pub
# Copy ไปเพิ่มใน server destination:
# ~/.ssh/authorized_keys ของ deploy user
# ทดสอบ ssh connection
docker-compose exec jenkins ssh -i ~/.ssh/id_jenkins [email protected] "whoami"
# ถ้ายังไม่ได้ ตรวจ known_hosts:
docker-compose exec jenkins ssh-keyscan -t ed25519 yourdomain.com >> ~/.ssh/known_hosts
คำถามที่พบบ่อย
Jenkins vs GitHub Actions ตัวไหนดีกว่า?
GitHub Actions ฟรี 3000 minutes/month ง่ายสำหรับ public repo Jenkins on VPS ต้องจ่ายค่า hosting แต่ได้ unlimited builds และ full control ถ้า private repo หนักหรือ build ต้อง 100+ hours/month Jenkins จ่ายได้ประหยัดกว่า GitHub Actions free tier สำหรับองค์กร Jenkins ยังมีความยืดหยุ่นมากกว่า (custom plugins, agent distribution, artifact management)
ปลอดภัยไหมที่ใช้ Personal Access Token ของ GitHub ใน Jenkins?
ปลอดภัยพอหากเก็บ token ใน Jenkins Credentials เข้ารหัส (Jenkins ทำ auto) และ token มีสิทธิ์จำกัด (scope repo + webhook เท่านั้น ไม่ต้องให้ delete repo access) ดีกว่าอีกขั้นคือ สร้าง GitHub App แทนใช้ PAT — GitHub App expire ได้ และมี rate limit แยกจาก user API ดู Jenkins GitHub App plugin สำหรับวิธีตั้ง
Jenkins agent กับ master มีความแตกต่างยังไง?
Master (controller) จัดการ UI, credentials, scheduling — ถ้า master ว่าง executor สามารถรัน build ได้เลย Agent เป็น node แยกที่ master ส่ง job ไปรัน — เหมาะสำหรับ build หนัก เช่น Docker build, unit test ที่ต้อง GPU หรือเมื่อต้อง scale: เพิ่ม agent node แล้ว master ส่ง job ไปเสมอ master ว่างเตรียมรับ job ตัวถัดไป production Jenkins มักใช้ 1 master + 5-10 agent nodes