A CI/CD pipeline on a Hong Kong VPS automates the journey from code commit to production deployment — enabling development teams to ship updates to their Asia-Pacific applications in minutes rather than hours, with automated testing and zero-downtime deployments built into every release.
This guide builds a complete CI/CD pipeline using GitHub Actions as the CI orchestrator and your Hong Kong VPS as both the self-hosted runner and deployment target — eliminating the need for external CI/CD services and keeping your build and deployment infrastructure close to your Asia-Pacific production environment.
Architecture Overview
Developer pushes to main branch
│
▼
GitHub Actions Workflow triggers
│
├─ Run tests (GitHub-hosted runner)
│
├─ Build Docker image
│
├─ Push to container registry
│
└─ Deploy to Hong Kong VPS (self-hosted runner OR SSH deploy)
│
▼
Zero-downtime container swap
Application updated ✓Option A: SSH-Based Deployment (Simpler)
GitHub Actions runs on GitHub’s servers and deploys to your Hong Kong VPS via SSH. No self-hosted runner needed.
Step 1: Generate deployment SSH key
# On your Hong Kong VPS
ssh-keygen -t ed25519 -C "github-actions-deploy" -f ~/.ssh/github_deploy -N ""
cat ~/.ssh/github_deploy.pub >> ~/.ssh/authorized_keys
cat ~/.ssh/github_deploy # Copy this private key to GitHub SecretsStep 2: Add secrets to GitHub repository
In your GitHub repository → Settings → Secrets and variables → Actions:
VPS_HOST— your VPS IP addressVPS_PORT— your SSH port (e.g. 2277)VPS_USER— deploy usernameVPS_SSH_KEY— the private key from above
Step 3: Create GitHub Actions workflow
mkdir -p .github/workflows
nano .github/workflows/deploy.ymlname: Test and Deploy to Hong Kong VPS
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Run linting
run: npm run lint
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- uses: actions/checkout@v4
- name: Build Docker image
run: |
docker build -t myapp:${{ github.sha }} .
docker save myapp:${{ github.sha }} | gzip > myapp.tar.gz
- name: Transfer image to VPS
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.VPS_HOST }}
port: ${{ secrets.VPS_PORT }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
source: myapp.tar.gz
target: /tmp/
- name: Deploy on VPS
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.VPS_HOST }}
port: ${{ secrets.VPS_PORT }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
script: |
# Load the new image
docker load < /tmp/myapp.tar.gz rm /tmp/myapp.tar.gz # Zero-downtime swap: start new container, stop old one docker stop myapp_old 2>/dev/null || true
docker rename myapp myapp_old 2>/dev/null || true
docker run -d \
--name myapp \
--restart unless-stopped \
-p 127.0.0.1:3000:3000 \
--env-file /home/deploy/apps/myapp/.env \
myapp:${{ github.sha }}
# Health check — wait for new container to be ready
sleep 5
if curl -sf http://127.0.0.1:3000/health; then
echo "Deployment successful"
docker rm -f myapp_old 2>/dev/null || true
docker image prune -f
else
echo "Health check failed — rolling back"
docker stop myapp
docker rename myapp_old myapp
docker start myapp
exit 1
fiOption B: Self-Hosted Runner on Hong Kong VPS (Faster Builds)
Running the GitHub Actions runner directly on your Hong Kong VPS means builds run in the same environment as production — faster feedback, no image transfer overhead, and closer proximity to your production data.
Install the self-hosted runner
# On your Hong Kong VPS
mkdir -p /home/deploy/actions-runner
cd /home/deploy/actions-runner
# Download the runner (get the current URL from GitHub repo Settings → Actions → Runners)
curl -o actions-runner-linux-x64-2.319.1.tar.gz -L \
https://github.com/actions/runner/releases/download/v2.319.1/actions-runner-linux-x64-2.319.1.tar.gz
tar xzf ./actions-runner-linux-x64-2.319.1.tar.gz
# Configure with your repo token (from GitHub Settings → Actions → Runners → New runner)
./config.sh --url https://github.com/yourusername/your-repo --token YOUR_TOKEN
# Install as a service
sudo ./svc.sh install deploy
sudo ./svc.sh startUpdate the workflow to use self-hosted runner
deploy:
needs: test
runs-on: self-hosted # Changed from ubuntu-latest
# ... rest of deploy jobWith a self-hosted runner, your deployment step can be simpler — no SSH needed, just run docker-compose directly:
- name: Deploy with docker-compose
run: |
cd /home/deploy/apps/myapp
docker compose pull
docker compose up -d --remove-orphans
docker system prune -fStep 4: Docker Registry for Image Management
For larger teams, push images to a registry rather than transferring archives via SSH:
# Using GitHub Container Registry (free with GitHub)
# In your workflow:
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
- name: Deploy on VPS
uses: appleboy/ssh-action@v1.0.3
with:
script: |
docker pull ghcr.io/${{ github.repository }}:${{ github.sha }}
docker stop myapp && docker rm myapp
docker run -d --name myapp \
ghcr.io/${{ github.repository }}:${{ github.sha }}Step 5: Deployment Notifications
# Add to your workflow to notify on deployment success/failure
- name: Notify Telegram on success
if: success()
run: |
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMessage" \
-d chat_id="${{ secrets.TELEGRAM_CHAT_ID }}" \
-d text="✅ Deployment successful: ${{ github.repository }} @ ${{ github.sha }}"
- name: Notify Telegram on failure
if: failure()
run: |
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMessage" \
-d chat_id="${{ secrets.TELEGRAM_CHAT_ID }}" \
-d text="❌ Deployment failed: ${{ github.repository }} @ ${{ github.sha }}"Conclusion
A CI/CD pipeline targeting a Hong Kong VPS gives Asia-Pacific development teams automated testing and deployment with production infrastructure close to their Chinese and regional users. GitHub Actions handles the CI orchestration; your Hong Kong VPS receives automated deployments with health-checked zero-downtime container swaps on every merge to main.
Build your CI/CD pipeline on Server.HK’s Hong Kong VPS plans — KVM virtualisation with Docker support and CN2 GIA routing as standard.
Frequently Asked Questions
Should I use a self-hosted runner or GitHub-hosted runner for a Hong Kong VPS deployment?
GitHub-hosted runners are simpler to set up and require no VPS resources. Self-hosted runners on your Hong Kong VPS are faster for builds involving large Docker images (no image transfer overhead) and give your pipeline direct access to production resources. Start with GitHub-hosted runners + SSH deployment; switch to self-hosted when build times or image transfer become bottlenecks.
How do I handle database migrations in a CI/CD pipeline?
Run migrations as a separate step before the container swap, with a rollback mechanism if the migration fails. A common pattern: (1) run migrations with the new code against the production database, (2) if migrations succeed, swap the container, (3) if migrations fail, abort deployment without touching the running container. Never roll back applied migrations automatically — design migrations to be backward-compatible with the previous application version.
Can I use GitLab CI instead of GitHub Actions with a Hong Kong VPS?
Yes. GitLab CI/CD uses a similar runner model — install the GitLab Runner agent on your Hong Kong VPS and register it with your GitLab instance. The deployment patterns (SSH-based or local runner) are identical. Replace the GitHub-specific YAML syntax with GitLab CI YAML equivalents.