Manually SSHing into your Hong Kong VPS to deploy updates is a source of human error, inconsistency, and deployment anxiety. GitHub Actions automates the entire pipeline: run tests, build your application, and deploy to your VPS — triggered automatically on every push to main, with zero manual intervention. This guide sets up a complete CI/CD pipeline from GitHub to a Hong Kong VPS for both traditional server deployments and Docker-based applications.
Architecture
Developer pushes to main branch
↓
GitHub Actions Runner (GitHub's servers)
1. Checkout code
2. Run tests
3. Build application
4. SSH into HK VPS → pull code → restart services
↓
Hong Kong VPS (CN2 GIA)
- Updated application running
- Zero downtime via PM2 reload / Docker rolling update
Step 1: Create a Deploy User on Your VPS
# On your Hong Kong VPS:
useradd -m -s /bin/bash deploy
mkdir -p /home/deploy/.ssh
# Generate SSH key pair for GitHub Actions (on your local machine)
ssh-keygen -t ed25519 -C "github-actions-deploy" -f ~/.ssh/github_deploy -N ""
# Copy PUBLIC key to the VPS
cat ~/.ssh/github_deploy.pub >> /home/deploy/.ssh/authorized_keys
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keys
chown -R deploy:deploy /home/deploy/.ssh
# Allow deploy user to restart services without password
echo "deploy ALL=(ALL) NOPASSWD: /bin/systemctl restart myapp, /bin/systemctl reload nginx, /usr/bin/docker compose" \
>> /etc/sudoers.d/deployStep 2: Add Secrets to GitHub Repository
In your GitHub repository: Settings → Secrets and variables → Actions → New repository secret
VPS_HOST— your Hong Kong VPS IP addressVPS_USER—deployVPS_SSH_KEY— contents of~/.ssh/github_deploy(the PRIVATE key)VPS_PORT— your SSH port (e.g., 22222)
Step 3: GitHub Actions Workflow for Node.js App
Create .github/workflows/deploy.yml in your repository:
name: Test and Deploy to HK VPS
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
name: Run Tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
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 linter
run: npm run lint
deploy:
name: Deploy to Hong Kong VPS
needs: test # Only deploy if tests pass
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' # Only deploy from main branch
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
port: ${{ secrets.VPS_PORT }}
script: |
set -e
echo "Pulling latest code..."
cd /var/www/myapp
git pull origin main
echo "Installing dependencies..."
npm ci --production
echo "Running database migrations..."
npm run migrate
echo "Reloading application (zero downtime)..."
pm2 reload myapp --env production
echo "Verifying deployment..."
pm2 show myapp | grep "status.*online"
echo "Deployment complete!"Step 4: Docker-Based Deployment Pipeline
For Docker-based applications, build the image in CI and push to GitHub Container Registry, then pull on the VPS:
name: Build, Push, and Deploy Docker Image
on:
push:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
outputs:
image-tag: ${{ steps.meta.outputs.tags }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha,prefix=sha-
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy:
needs: build-and-push
runs-on: ubuntu-latest
steps:
- name: Deploy to Hong Kong VPS
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
port: ${{ secrets.VPS_PORT }}
envs: REGISTRY,IMAGE_NAME,GITHUB_SHA
script: |
set -e
# Log in to GitHub Container Registry
echo ${{ secrets.GITHUB_TOKEN }} | \
docker login ghcr.io -u ${{ github.actor }} --password-stdin
# Pull new image
NEW_IMAGE="ghcr.io/$IMAGE_NAME:sha-${GITHUB_SHA::7}"
docker pull $NEW_IMAGE
# Update docker-compose.yml to use new image tag
cd /opt/myapp
sed -i "s|image: ghcr.io/$IMAGE_NAME:.*|image: $NEW_IMAGE|" docker-compose.yml
# Rolling update with Docker Compose
docker compose up -d --no-deps app
# Wait for health check
sleep 10
docker compose ps app | grep "healthy\|Up"
# Remove old images
docker image prune -f
echo "Docker deployment complete: $NEW_IMAGE"Step 5: Laravel Deployment Pipeline
name: Deploy Laravel to HK VPS
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_DATABASE: testing
MYSQL_ROOT_PASSWORD: testpass
options: --health-cmd="mysqladmin ping" --health-interval=10s
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: mbstring, mysql, redis
- run: composer install --no-dev --optimize-autoloader
- run: cp .env.testing .env && php artisan key:generate
- run: php artisan migrate --force
- run: php artisan test
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
port: ${{ secrets.VPS_PORT }}
script: |
set -e
cd /var/www/mylaravel
git pull origin main
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
php artisan queue:restart
sudo systemctl reload php8.3-fpm
echo "Laravel deployment complete"Step 6: Notifications and Rollback
Slack Deployment Notifications
- name: Notify Slack on success
if: success()
uses: slackapi/slack-github-action@v1.25.0
with:
payload: |
{
"text": ":white_check_mark: Deploy to HK VPS succeeded: ${{ github.sha }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
- name: Notify Slack on failure
if: failure()
uses: slackapi/slack-github-action@v1.25.0
with:
payload: |
{
"text": ":x: Deploy to HK VPS FAILED: ${{ github.sha }} — check Actions logs"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}Automatic Rollback on Failure
script: |
set -e
cd /var/www/myapp
# Save current commit for rollback
PREVIOUS_COMMIT=$(git rev-parse HEAD)
git pull origin main
npm ci --production
# Test the application starts
pm2 reload myapp --env production
sleep 5
# Health check — rollback if fails
if ! curl -sf http://localhost:3000/health > /dev/null; then
echo "Health check failed — rolling back to $PREVIOUS_COMMIT"
git reset --hard $PREVIOUS_COMMIT
npm ci --production
pm2 reload myapp --env production
exit 1
fi
echo "Deployment verified — health check passed"Step 7: Environment-Specific Deployments
<code">name: Multi-Environment Deploy
on:
push:
branches:
- main # → Production (HK VPS)
- staging # → Staging server
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: ${{ github.ref_name == 'main' && 'production' || 'staging' }}
url: ${{ github.ref_name == 'main' && 'https://yourdomain.com' || 'https://staging.yourdomain.com' }}
steps:
- uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.VPS_HOST }} # Different secret per environment
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
script: |
cd /var/www/${{ github.ref_name == 'main' && 'production' || 'staging' }}
git pull && npm ci && pm2 reload appGitHub Environments allow separate secrets per environment (production vs staging) and require manual approval before production deployments — add reviewers in Settings → Environments → production → Required reviewers.
Conclusion
GitHub Actions CI/CD to your Hong Kong VPS eliminates manual deployment steps and ensures every push to main automatically tests, builds, and deploys your application with consistent results. The pipeline’s health check and automatic rollback guard against bad deployments reaching users, while Slack notifications keep the team informed of deployment status.
Automate your deploys: Browse Server.HK Hong Kong VPS plans — any plan supports the SSH-based GitHub Actions deployment pattern described here.