- GitHub Actions: Deploy แอปลง DigitalOcean ด้วย Docker Compose ผ่าน SSH อย่างปลอดภัย
- 2. เตรียม DigitalOcean Droplet
- 3. สร้าง SSH Key สำหรับ Deployment
- 4. Harden SSH
- 5. Firewall
- 6. Docker Compose สำหรับ Production
- 7. Environment File บน Server
- 8. Application Secrets
- 9. GitHub Secrets / Environment
- 10. อย่าปิด SSH Host Verification
- 11. Workflow แบบง่าย: SSH ไป Deploy
- 12. Workflow แบบ Production: Test → Build → Push → Deploy
- 13. Authentication จาก Droplet ไป GHCR
- 14. เพิ่ม Health Check หลัง Deployment
- 15. Rollback
- 16. ตัวอย่าง Deploy Script บน Server
- 17. จำกัดสิทธิ์ GitHub Actions
- 18. Pin Third-Party Actions
- 19. GitHub Environment Protection
- 20. Docker Security
- 21. Database Deployment
- 22. ไม่ควรเปิด Database Port สู่ Internet
- 23. Reverse Proxy + HTTPS
- 24. Zero/Low-Downtime
- 25. Security Checklist
- 26. โครงสร้าง Repository
- 27. Deployment Flow ที่แนะนำ
- สรุป
#GitHub Actions: Deploy แอปลง DigitalOcean ด้วย Docker Compose ผ่าน SSH อย่างปลอดภัย
การ deploy แอปพลิเคชันที่รันด้วย Docker Compose บน DigitalOcean Droplet
สามารถทำให้เป็นอัตโนมัติด้วย GitHub Actions ได้ โดยเมื่อมีการ push หรือ merge
โค้ดเข้าสู่ branch main ระบบ CI/CD จะทดสอบและ build Docker image จากนั้น
server จะรับคำสั่งผ่าน SSH เพื่อดึง image รุ่นใหม่และ restart service ด้วย Docker
Compose
บทความนี้เน้นรูปแบบที่เหมาะกับ production มากกว่าการ SSH เข้า server แล้ว
git pull และ docker compose build โดยตรง กล่าวคือ:
Developer
|
| git push
v
GitHub Repository
|
v
GitHub Actions
|
+--> Test
+--> Build Docker Image
+--> Push Image
|
v
Container Registry
|
| SSH
v
DigitalOcean Droplet
|
+--> docker compose pull
+--> docker compose up -d
+--> Health Check
#1. ทำไมควร Build Image ใน CI
แนวทางที่ทำได้ง่ายที่สุดคือให้ GitHub Actions SSH ไปยัง Droplet แล้วสั่ง:
git pull
docker compose build
docker compose up -d
แต่ production ที่ต้องการ reproducibility และ rollback ที่ชัดเจน ควร build image ใน CI แล้ว push ไปยัง registry ก่อน เช่น GitHub Container Registry (GHCR)
ข้อดีคือ:
- Production server ไม่จำเป็นต้องมี source code ทั้งหมด
- ไม่ต้อง compile/build application บน production
- Docker image ที่ผ่าน CI คือ artifact เดียวกับที่นำไป deploy
- ระบุ version ด้วย Git commit SHA ได้
- rollback ไป image รุ่นก่อนหน้าได้ง่าย
- ลดความแตกต่างระหว่าง build environment และ production
ตัวอย่างชื่อ image:
ghcr.io/example/myapp:8f36b9a
ควรหลีกเลี่ยงการพึ่งพา latest เพียงอย่างเดียว เพราะไม่สามารถระบุได้ชัดเจนว่า
production กำลังรัน source revision ใด
#2. เตรียม DigitalOcean Droplet
ตัวอย่างนี้สมมติว่าใช้ Ubuntu Server และมี Docker Engine กับ Docker Compose plugin แล้ว
ตรวจสอบ:
docker --version
docker compose version
สร้าง user สำหรับ deployment แยกจาก root เช่น:
sudo adduser deploy
หาก deployment user ต้องสั่ง Docker:
sudo usermod -aG docker deploy
จากนั้น logout/login ใหม่ แล้วทดสอบ:
docker ps
หมายเหตุด้านความปลอดภัย: สมาชิกกลุ่ม
dockerมีสิทธิ์สูงมากและในทางปฏิบัติสามารถยกระดับสิทธิ์บน host ได้ จึงควรให้เฉพาะบัญชีที่จำเป็นเท่านั้น
สร้าง directory:
sudo mkdir -p /opt/myapp
sudo chown -R deploy:deploy /opt/myapp
#3. สร้าง SSH Key สำหรับ Deployment
ควรแยก deploy key ออกจาก SSH key ส่วนตัวของผู้ดูแลระบบ
สร้าง key บนเครื่องที่เชื่อถือได้:
ssh-keygen -t ed25519 -C "github-actions-deploy"
ตัวอย่าง:
github-actions-deploy
github-actions-deploy.pub
นำ public key ไปเพิ่มที่ Droplet:
mkdir -p ~/.ssh
chmod 700 ~/.ssh
cat github-actions-deploy.pub >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
ส่วน private key จะนำไปเก็บเป็น GitHub Secret
ห้าม commit private key ลง repository
#4. Harden SSH
ไฟล์:
/etc/ssh/sshd_config
ค่าที่ควรพิจารณา:
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
ตรวจสอบ configuration ก่อน restart:
sudo sshd -t
แล้วจึง:
sudo systemctl restart ssh
อย่าปิด SSH session เดิมจนกว่าจะทดสอบว่า session ใหม่ login ด้วย key ได้สำเร็จ
#5. Firewall
Production web server โดยทั่วไปควรเปิดเฉพาะ port ที่จำเป็น เช่น:
22 SSH
80 HTTP
443 HTTPS
Database เช่น PostgreSQL, MySQL หรือ Redis ไม่ควรเปิด public port หาก application ใช้งานอยู่ใน Docker network เดียวกัน
DigitalOcean Cloud Firewall เป็น stateful firewall และ traffic inbound ที่ไม่ได้อนุญาตจะถูก block ควรจำกัด SSH source ให้แคบที่สุดเท่าที่ architecture รองรับ
#6. Docker Compose สำหรับ Production
ตัวอย่าง compose.yaml:
services:
app:
image: ${APP_IMAGE}
restart: unless-stopped
environment:
NODE_ENV: production
ports:
- "127.0.0.1:3000:3000"
healthcheck:
test:
[
"CMD",
"wget",
"--no-verbose",
"--tries=1",
"--spider",
"http://localhost:3000/health"
]
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
networks:
- app-network
networks:
app-network:
driver: bridge
การ bind application port กับ 127.0.0.1 ทำให้ port 3000 ไม่ถูก expose
ตรงสู่อินเทอร์เน็ต โดยสามารถให้ Caddy หรือ Nginx ทำ reverse proxy จาก 80/443
เข้ามาแทน
#7. Environment File บน Server
สร้าง:
/opt/myapp/.env
ตัวอย่าง:
APP_IMAGE=ghcr.io/example/myapp:8f36b9a
กำหนด permission:
chmod 600 /opt/myapp/.env
อย่า commit production .env ลง Git
เพิ่มใน .gitignore:
.env
.env.*
!.env.example
#8. Application Secrets
ไม่ควรใส่ secret ลง Dockerfile:
ENV DB_PASSWORD=my-secret-password
และไม่ควร hard-code ลง compose.yaml
สำหรับ secret ที่ application รองรับแบบ file-based สามารถใช้ Compose secrets เช่น:
services:
app:
image: ${APP_IMAGE}
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txt
ภายใน container จะอ่านได้จาก:
/run/secrets/db_password
ควรจำกัด permission ของ secret files บน host และไม่ commit directory ดังกล่าว
#9. GitHub Secrets / Environment
ใน GitHub repository สร้าง Environment:
production
จากนั้นกำหนด secrets เช่น:
DO_HOST
DO_USER
DO_SSH_PRIVATE_KEY
DO_SSH_HOST_KEY
ตัวอย่าง:
DO_HOST = 203.0.113.10
DO_USER = deploy
DO_SSH_PRIVATE_KEY คือ private key สำหรับ deployment
DO_SSH_HOST_KEY คือ host key ของ Droplet ที่ตรวจสอบ fingerprint แล้ว เช่น:
203.0.113.10 ssh-ed25519 AAAAC3...
Environment สามารถใช้ร่วมกับ deployment protection rules เช่น required reviewers เพื่อให้ production deployment ต้องผ่าน approval ก่อน
#10. อย่าปิด SSH Host Verification
ตัวอย่างที่พบได้บ่อย:
ssh -o StrictHostKeyChecking=no deploy@$HOST
ไม่ควรใช้ใน production เพราะเป็นการลดการตรวจสอบ identity ของ server
ควรใช้:
-o StrictHostKeyChecking=yes
พร้อม known_hosts ที่เตรียมและตรวจสอบไว้ล่วงหน้า
ตัวอย่าง:
printf '%s\n' "$SSH_HOST_KEY" > ~/.ssh/known_hosts
chmod 600 ~/.ssh/known_hosts
การเรียก ssh-keyscan ระหว่าง workflow
แล้วเชื่อถือผลลัพธ์ทันทีโดยไม่มีช่องทางอื่นสำหรับตรวจ fingerprint ไม่ได้ให้ assurance
แบบเดียวกับการ pin host key ที่ตรวจสอบแล้ว
#11. Workflow แบบง่าย: SSH ไป Deploy
สร้างไฟล์:
.github/workflows/deploy.yml
name: Deploy Production
on:
push:
branches:
- main
permissions:
contents: read
concurrency:
group: production
cancel-in-progress: false
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: production
steps:
- name: Configure SSH
env:
SSH_PRIVATE_KEY: ${{ secrets.DO_SSH_PRIVATE_KEY }}
SSH_HOST_KEY: ${{ secrets.DO_SSH_HOST_KEY }}
run: |
install -m 700 -d ~/.ssh
printf '%s\n' "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
printf '%s\n' "$SSH_HOST_KEY" > ~/.ssh/known_hosts
chmod 600 ~/.ssh/known_hosts
- name: Deploy
env:
HOST: ${{ secrets.DO_HOST }}
USER: ${{ secrets.DO_USER }}
run: |
ssh \
-i ~/.ssh/deploy_key \
-o IdentitiesOnly=yes \
-o StrictHostKeyChecking=yes \
"$USER@$HOST" << 'EOF'
set -euo pipefail
cd /opt/myapp
docker compose pull
docker compose up -d --remove-orphans
docker compose ps
EOF
set -euo pipefail ช่วยให้ deployment script หยุดเมื่อ command
ล้มเหลวหรือมีการอ้างตัวแปรที่ไม่ได้กำหนด
#12. Workflow แบบ Production: Test → Build → Push → Deploy
ตัวอย่างนี้ใช้ GHCR
name: CI/CD Production
on:
push:
branches:
- main
permissions:
contents: read
packages: write
concurrency:
group: production
cancel-in-progress: false
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@<FULL_COMMIT_SHA>
- name: Run tests
run: |
echo "Run your tests here"
build:
needs: test
runs-on: ubuntu-latest
outputs:
image: ${{ steps.image.outputs.value }}
steps:
- name: Checkout
uses: actions/checkout@<FULL_COMMIT_SHA>
- name: Login to GHCR
uses: docker/login-action@<FULL_COMMIT_SHA>
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set image name
id: image
run: |
IMAGE="${REGISTRY}/${IMAGE_NAME}:${GITHUB_SHA}"
IMAGE=$(echo "$IMAGE" | tr '[:upper:]' '[:lower:]')
echo "value=$IMAGE" >> "$GITHUB_OUTPUT"
- name: Build image
env:
IMAGE: ${{ steps.image.outputs.value }}
run: |
docker build -t "$IMAGE" .
- name: Push image
env:
IMAGE: ${{ steps.image.outputs.value }}
run: |
docker push "$IMAGE"
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: production
permissions:
contents: read
packages: read
steps:
- name: Configure SSH
env:
SSH_PRIVATE_KEY: ${{ secrets.DO_SSH_PRIVATE_KEY }}
SSH_HOST_KEY: ${{ secrets.DO_SSH_HOST_KEY }}
run: |
install -m 700 -d ~/.ssh
printf '%s\n' "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
printf '%s\n' "$SSH_HOST_KEY" > ~/.ssh/known_hosts
chmod 600 ~/.ssh/known_hosts
- name: Deploy image
env:
HOST: ${{ secrets.DO_HOST }}
USER: ${{ secrets.DO_USER }}
IMAGE: ${{ needs.build.outputs.image }}
run: |
ssh \
-i ~/.ssh/deploy_key \
-o IdentitiesOnly=yes \
-o StrictHostKeyChecking=yes \
"$USER@$HOST" \
"APP_IMAGE='$IMAGE' bash -s" << 'EOF'
set -euo pipefail
cd /opt/myapp
printf 'APP_IMAGE=%s\n' "$APP_IMAGE" > .env.deploy
docker compose \
--env-file .env.deploy \
pull
docker compose \
--env-file .env.deploy \
up -d --remove-orphans
docker compose ps
EOF
ตัวอย่าง
<FULL_COMMIT_SHA>เป็น placeholder ต้องแทนด้วย SHA ที่ตรวจสอบแล้วของ action แต่ละตัวก่อนใช้งานจริง GitHub แนะนำการ pin third-party actions ด้วย full-length commit SHA เพื่อให้ action reference เป็น immutable
#13. Authentication จาก Droplet ไป GHCR
หาก package เป็น private Droplet ต้องสามารถ authenticate กับ registry ได้
ควรใช้ credential ที่มีสิทธิ์ต่ำที่สุดที่จำเป็นสำหรับการ pull และ login บน server
ตามกลไกที่ registry รองรับ
จากนั้น:
docker pull ghcr.io/example/myapp:<commit-sha>
ต้องทำงานได้ก่อนเปิด CI/CD
อย่าใส่ registry password ลง compose.yaml
#14. เพิ่ม Health Check หลัง Deployment
การที่:
docker compose up -d
สำเร็จ ไม่ได้หมายความว่า application พร้อมใช้งานจริง
ควรตรวจ endpoint เช่น:
https://example.com/health
ตัวอย่างใน workflow:
for i in {1..12}; do
if curl --fail --silent --show-error \
https://example.com/health > /dev/null; then
echo "Application is healthy"
exit 0
fi
echo "Waiting for application..."
sleep 5
done
echo "Health check failed"
exit 1
ทำให้ workflow fail เมื่อ application ไม่พร้อมหลัง deploy
#15. Rollback
ก่อน deploy ควรเก็บ image version ปัจจุบัน
ตัวอย่าง:
cp .env.deploy .env.previous
จากนั้น deploy version ใหม่:
printf 'APP_IMAGE=%s\n' "$NEW_IMAGE" > .env.deploy
docker compose \
--env-file .env.deploy \
pull
docker compose \
--env-file .env.deploy \
up -d
หาก health check ไม่ผ่าน:
mv .env.previous .env.deploy
docker compose \
--env-file .env.deploy \
up -d
แนวคิดคือ:
v1 running
|
v
deploy v2
|
+---- health OK ----> keep v2
|
+---- health FAIL --> rollback v1
#16. ตัวอย่าง Deploy Script บน Server
แทนที่จะเขียน shell script ยาวใน workflow สามารถเก็บ deployment logic ฝั่ง server:
/opt/myapp/deploy.sh
#!/usr/bin/env bash
set -euo pipefail
IMAGE="${1:?Docker image is required}"
cd /opt/myapp
if [ -f .env.deploy ]; then
cp .env.deploy .env.previous
fi
printf 'APP_IMAGE=%s\n' "$IMAGE" > .env.deploy
docker compose \
--env-file .env.deploy \
pull
docker compose \
--env-file .env.deploy \
up -d --remove-orphans
docker compose ps
permission:
chmod 750 /opt/myapp/deploy.sh
จาก GitHub Actions:
ssh deploy@$HOST \
"/opt/myapp/deploy.sh '$IMAGE'"
ข้อดีคือ deployment logic มีจุดเดียวและ workflow อ่านง่ายขึ้น
#17. จำกัดสิทธิ์ GitHub Actions
อย่าปล่อย default permissions กว้างเกินความจำเป็น
ตัวอย่าง:
permissions:
contents: read
หาก job ต้อง push GHCR:
permissions:
contents: read
packages: write
และสามารถลดสิทธิ์เฉพาะ job ได้
หลักการคือ least privilege
#18. Pin Third-Party Actions
หลีกเลี่ยงการอ้าง action ที่เปลี่ยนแปลงได้ง่ายใน workflow ที่ต้องการ security สูง เช่น:
uses: vendor/action@main
สำหรับ production GitHub แนะนำ pin ด้วย full-length commit SHA:
uses: vendor/action@0123456789abcdef0123456789abcdef01234567
และควรใช้ Dependabot หรือกระบวนการ review เพื่ออัปเดต SHA เมื่อ action มี release ใหม่
#19. GitHub Environment Protection
สร้าง Environment:
production
จากนั้นกำหนด deployment protection ตามความเหมาะสม เช่น:
main
|
v
CI Test
|
v
Build Image
|
v
Push Registry
|
v
Waiting for Approval
|
v
Production Deploy
วิธีนี้ลดโอกาสที่การ push โดยไม่ตั้งใจจะ deploy production ทันที
#20. Docker Security
ควรพิจารณา:
- ใช้ base image ที่จำเป็นเท่านั้น
- ไม่รัน application process เป็น
rootหากไม่จำเป็น - scan image vulnerability ก่อน deploy
- ไม่ใส่ secret ใน Docker image layer
- pin dependency versions ตามความเหมาะสม
- ใช้ read-only filesystem สำหรับ workload ที่รองรับ
- จำกัด Linux capabilities
- กำหนด resource limits
- update base images และ OS security patches สม่ำเสมอ
ตัวอย่าง Dockerfile:
FROM node:24-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
USER node
CMD ["node", "server.js"]
#21. Database Deployment
ไม่ควรทำ destructive migration โดยอัตโนมัติโดยไม่มีแผน rollback
เช่น:
docker compose exec -T app npm run migrate
ก่อนใช้ production ต้องพิจารณา:
- migration backward-compatible หรือไม่
- application รุ่นเก่ายังทำงานกับ schema ใหม่ได้หรือไม่
- มี backup หรือ snapshot หรือไม่
- rollback application แล้ว database จะยัง compatible หรือไม่
แนวทางที่เหมาะกับระบบที่ต้องการ downtime ต่ำคือ expand-and-contract migration
#22. ไม่ควรเปิด Database Port สู่ Internet
ตัวอย่าง Compose:
services:
app:
networks:
- backend
postgres:
image: postgres:18
networks:
- backend
ไม่จำเป็นต้อง:
ports:
- "5432:5432"
หากมีเพียง application container ที่ต้องเข้าถึง PostgreSQL
#23. Reverse Proxy + HTTPS
Production architecture สามารถเป็น:
Internet
|
| HTTPS :443
v
DigitalOcean Firewall
|
v
Caddy / Nginx
|
| Docker Network
v
Application
|
v
Database
เปิด public เฉพาะ:
80
443
ส่วน application port และ database port ให้อยู่ภายใน host/Docker network
#24. Zero/Low-Downtime
คำสั่ง:
docker compose up -d
เหมาะกับระบบขนาดเล็ก แต่ไม่ได้ให้ zero-downtime guarantee โดยตัวมันเอง
ถ้าระบบต้องการ availability สูงขึ้น สามารถใช้:
- Blue-Green Deployment
- Reverse proxy switching
- Multiple application replicas
- Load balancer
- Kubernetes
- DigitalOcean Kubernetes (DOKS)
สำหรับเว็บขนาดเล็กถึงกลางที่ใช้ Droplet เดียว Docker Compose + reverse proxy + health check + rollback เป็นจุดเริ่มต้นที่ดูแลง่าย
#25. Security Checklist
ก่อนใช้ production ตรวจสอบอย่างน้อย:
[ ] ใช้ SSH key แทน password
[ ] ไม่ deploy ด้วย root
[ ] ปิด root/password SSH login ตามความเหมาะสม
[ ] มี dedicated deploy key
[ ] Private key อยู่ใน GitHub Secret/Environment
[ ] ตรวจสอบและ pin SSH host key
[ ] StrictHostKeyChecking=yes
[ ] จำกัด GitHub Actions permissions
[ ] Pin third-party Actions ด้วย full commit SHA
[ ] ใช้ GitHub production Environment
[ ] ใช้ approval ก่อน production หากเหมาะสม
[ ] ไม่ commit .env
[ ] ไม่ใส่ secret ใน Dockerfile
[ ] จำกัด firewall ports
[ ] ไม่ expose database สู่ public
[ ] ใช้ immutable image tag เช่น Git SHA
[ ] มี application health check
[ ] มี rollback strategy
[ ] มี database backup
[ ] scan Docker images/dependencies
[ ] update OS และ Docker security patches
[ ] เปิด monitoring และ alerting
#26. โครงสร้าง Repository
ตัวอย่าง:
myapp/
├── .github/
│ └── workflows/
│ └── deploy.yml
│
├── src/
│
├── Dockerfile
├── compose.yaml
├── compose.production.yaml
├── .dockerignore
├── .gitignore
└── README.md
Server:
/opt/myapp/
├── compose.yaml
├── compose.production.yaml
├── .env.deploy
├── deploy.sh
└── secrets/
#27. Deployment Flow ที่แนะนำ
1. Developer pushes to main
|
v
2. GitHub Actions starts
|
v
3. Unit / Integration Tests
|
v
4. Security / Dependency Scan
|
v
5. Build Docker Image
|
v
6. Tag Image with Git SHA
|
v
7. Push to Container Registry
|
v
8. Production Approval
|
v
9. SSH to DigitalOcean
|
v
10. docker compose pull
|
v
11. docker compose up -d
|
v
12. Health Check
/ \
/ \
PASS FAIL
| |
v v
Complete Rollback
#สรุป
GitHub Actions + Docker Compose + DigitalOcean Droplet เป็น CI/CD architecture ที่ไม่ซับซ้อนและเหมาะกับระบบจำนวนมาก แต่จุดสำคัญไม่ใช่เพียงทำให้ workflow SSH เข้า server ได้
Production deployment ที่ดีควรออกแบบให้:
Build Once → Deploy Immutable Artifact → Verify → Rollback
แทน:
SSH → Git Pull → Build บน Production → Restart
องค์ประกอบสำคัญคือ SSH key authentication, non-root deployment user, verified SSH host key, least-privilege GitHub token, GitHub Environment, immutable Docker image tags, firewall, secrets management, health checks, backups และ rollback strategy
เมื่อระบบเติบโตขึ้น architecture นี้สามารถพัฒนาไปสู่ Blue-Green Deployment, Load Balancer, GitOps หรือ Kubernetes ได้โดยไม่ต้องเปลี่ยนหลักการพื้นฐานเรื่อง immutable artifact และ least privilege
#เอกสารอ้างอิง
- GitHub Docs --- Secure use reference: https://docs.github.com/en/actions/reference/security/secure-use
- GitHub Docs --- Deployments and environments: https://docs.github.com/en/actions/concepts/workflows-and-actions/deployment-environments
- Docker Docs --- Use Compose in production: https://docs.docker.com/compose/how-tos/production/
- Docker Docs --- Secrets in Compose: https://docs.docker.com/compose/how-tos/use-secrets/
- DigitalOcean Docs --- Production-Ready Droplet: https://docs.digitalocean.com/products/droplets/getting-started/recommended-droplet-setup/
- DigitalOcean Docs --- Cloud Firewalls: https://docs.digitalocean.com/products/networking/firewalls/