# GitHub Actions CI/CD to DomainIndia VPS — Build, Test, Deploy Automation
TL;DR
Turn git push into production deploy. GitHub Actions runs your tests, builds artifacts, and deploys to your DomainIndia VPS — all from .github/workflows/*.yml. This guide shows working pipelines for Node.js, PHP, Python, and Docker apps with secrets, zero-downtime deploys, and PR previews.
## Why GitHub Actions
- Free 2,000 minutes/month for private repos, unlimited for public
- No separate CI service to run + secure
- Native integration with PRs, issues, releases
- Massive marketplace of pre-built actions
## Anatomy of a workflow
`.github/workflows/deploy.yml`:
```yaml
name: Deploy
on:
push:
branches: [main]
workflow_dispatch: # allow manual trigger from GitHub UI
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- run: npm test
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Deploy
env:
VPS_SSH_KEY: ${{ secrets.VPS_SSH_KEY }}
VPS_HOST: ${{ secrets.VPS_HOST }}
run: |
...
```
## Pattern 1 — SSH deploy (simplest)
Rsync code to VPS, restart the service.
### Step 1 — Generate deploy SSH key
On your laptop:
```bash
ssh-keygen -t ed25519 -C "github-deploy" -f ~/.ssh/github_deploy -N ""
# Creates: ~/.ssh/github_deploy (private) + ~/.ssh/github_deploy.pub (public)
```
Add public key to your VPS:
```bash
ssh root@your-vps
# Create restricted deploy user:
useradd -m -s /bin/bash deploy
mkdir -p /home/deploy/.ssh
# Paste github_deploy.pub into /home/deploy/.ssh/authorized_keys
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keys
chown -R deploy:deploy /home/deploy/.ssh
```
Grant specific sudo for service restart (no password):
```bash
# /etc/sudoers.d/deploy
deploy ALL=(ALL) NOPASSWD: /bin/systemctl restart myapp, /bin/systemctl status myapp
```
### Step 2 — Add secrets to GitHub
Repo → Settings → Secrets and variables → Actions → New repository secret:
- `VPS_HOST` = your vps IP or hostname
- `VPS_USER` = `deploy`
- `VPS_SSH_KEY` = contents of `~/.ssh/github_deploy` (private key)
### Step 3 — Workflow
```yaml
name: Deploy to VPS
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.VPS_SSH_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -H ${{ secrets.VPS_HOST }} >> ~/.ssh/known_hosts
- name: Install deps (locally, build artifact)
run: |
npm ci
npm run build
- name: Deploy
run: |
rsync -avz --delete
--exclude='.git'
--exclude='node_modules'
-e "ssh -i ~/.ssh/id_ed25519"
./ ${{ secrets.VPS_USER }}@${{ secrets.VPS_HOST }}:/home/deploy/myapp/
- name: Install deps on server + restart
run: |
ssh -i ~/.ssh/id_ed25519 ${{ secrets.VPS_USER }}@${{ secrets.VPS_HOST }} '
cd /home/deploy/myapp &&
npm ci --production &&
sudo systemctl restart myapp
'
```
## Pattern 2 — Docker deploy
Build image in CI, push to registry, pull on VPS.
```yaml
name: Build & Deploy Docker
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Login to registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build & push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
ghcr.io/yourorg/myapp:latest
ghcr.io/yourorg/myapp:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Deploy via SSH
env:
SSH_KEY: ${{ secrets.VPS_SSH_KEY }}
run: |
echo "$SSH_KEY" > key && chmod 600 key
ssh -o StrictHostKeyChecking=no -i key deploy@${{ secrets.VPS_HOST }} '
docker pull ghcr.io/yourorg/myapp:latest &&
docker stop myapp || true &&
docker rm myapp || true &&
docker run -d --name myapp --restart=always -p 3000:3000
--env-file /home/deploy/.env
ghcr.io/yourorg/myapp:latest
'
```
## Pattern 3 — PHP + Composer deploy
```yaml
name: Deploy PHP
on:
push: { branches: [main] }
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with: { php-version: '8.3' }
- run: composer install --no-dev --optimize-autoloader
- name: Rsync to VPS
uses: burnett01/
[email protected]
with:
switches: -avzr --delete --exclude=.env --exclude=storage/logs
path: ./
remote_path: /home/deploy/myapp/
remote_host: ${{ secrets.VPS_HOST }}
remote_user: deploy
remote_key: ${{ secrets.VPS_SSH_KEY }}
- name: Laravel post-deploy
uses: appleboy/
[email protected]
with:
host: ${{ secrets.VPS_HOST }}
username: deploy
key: ${{ secrets.VPS_SSH_KEY }}
script: |
cd /home/deploy/myapp
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan opcache:clear
```
## Pattern 4 — Test matrix
Run tests on multiple versions / environments:
```yaml
jobs:
test:
strategy:
matrix:
php: [8.2, 8.3, 8.4]
db: [mysql, postgres]
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
options: --health-cmd pg_isready
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with: { php-version: ${{ matrix.php }} }
- run: vendor/bin/phpunit
env:
DB_CONNECTION: ${{ matrix.db }}
```
## Pattern 5 — PR preview deployments
For static sites or lightweight apps — deploy each PR to `pr-123.yourcompany.com`:
```yaml
on:
pull_request:
types: [opened, synchronize]
jobs:
preview:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
- name: Deploy to subdomain
run: |
# upload to VPS under /var/www/previews/pr-${{ github.event.pull_request.number }}
# nginx wildcard: server_name ~^pr-(?d+).yourcompany.com$;
# root /var/www/previews/pr-$pr;
scp -r dist/* deploy@vps:/var/www/previews/pr-${{ github.event.pull_request.number }}/
- name: Comment on PR
uses: marocchino/sticky-pull-request-comment@v2
with:
message: "Preview: https://pr-${{ github.event.pull_request.number }}.yourcompany.com"
```
## Secrets management
Never commit secrets. Use:
1. Repository secrets (`${{ secrets.NAME }}`)
2. Environment secrets (scoped per environment like `production`)
3. Organization secrets (shared across repos)
For rotation: change secret in GitHub → next workflow run picks it up. No config file to update.
## Required reviews before deploy
Repo → Settings → Environments → Create "production":
- Required reviewers: 1+
- Wait timer: 0 (or add delay)
- Allowed branches: main
Workflow:
```yaml
jobs:
deploy:
environment: production # requires approval before running
runs-on: ubuntu-latest
```
GitHub pauses the deploy job until a reviewer clicks approve.
## Rollback strategy
Tag every release:
```yaml
- name: Create release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
generate_release_notes: true
```
To rollback: `git checkout `, push branch, deploy that. Or have a manual `rollback.yml` workflow that re-deploys a specific tag.
## Common pitfalls
## FAQ
Q
GitHub Actions vs GitLab CI vs Jenkins?
Actions — easiest for GitHub-hosted repos. GitLab CI — better for GitLab. Jenkins — self-hosted, powerful, heavy. For most DomainIndia customers using GitHub, Actions wins.
Q
How much does it cost?
Public repos: free. Private: 2,000 minutes/month free (Linux), then $0.008/min. Most small teams fit in free tier.
Q
Self-hosted runner?
Useful if you need to deploy into a private network or use specific hardware. Install runner on a DomainIndia VPS, register to your repo.
Q
Deployment of multiple services?
Use path filters: paths: ['services/frontend/**'] triggers only when frontend changes. Keep monorepo workflows efficient.
Q
Deploy to k3s/Kubernetes?
kubectl apply -f k8s/ after building image. Use actions/setup-kubectl. Store kubeconfig as secret.
Automate deploys to your DomainIndia VPS with GitHub Actions.
View VPS plans