Client Area

GitHub Actions CI/CD to DomainIndia VPS — Build, Test, Deploy Automation

ByDomain India Team·DomainIndia Engineering
6 min readPublished 24 Apr 2026Updated 22 Jun 2026178 views

In this article

  • 1Why GitHub Actions
  • 2Anatomy of a workflow
  • 3Pattern 1 — SSH deploy (simplest)
  • 4Step 1 — Generate deploy SSH key
  • 5Step 2 — Add secrets to GitHub

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-(?<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 <previous-tag>, 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

Was this article helpful?

Your feedback helps us improve our documentation

Still need help? Submit a support ticket