Free CI/CD with GitHub Actions: Automatically Build, Test, and Deploy to a VPS

Free CI/CD with GitHub Actions: Automatically Build, Test, and Deploy to a VPS

 Modern teams ship faster because they don’t deploy manually. With CI/CD (Continuous Integration / Continuous Deployment), every change pushed to GitHub can automatically trigger a workflow to build, test, and (optionally) deploy your web application to a VPS—reliably and repeatably.

The best part: for many projects, GitHub Actions can be used for free (especially on public repos and within generous limits for private repos). In this guide, you’ll learn how to set up a professional GitHub Actions pipeline that deploys to a VPS via SSH.


1) What CI/CD Means (In Practical Terms)

CI (Continuous Integration)

Whenever you push code or open a pull request, GitHub Actions can automatically:

  • install dependencies
  • run lint checks
  • run unit/integration tests
  • build your app

CI ensures your code stays deployable.

CD (Continuous Deployment/Delivery)

After CI passes, the pipeline can automatically deploy:

  • to a staging server (common)
  • to production (on merge to main)

CD reduces human error and makes releases consistent.


2) What You Need Before Setting Up GitHub Actions

To deploy to a VPS, you’ll need:

  1. A VPS with SSH access (Ubuntu/Debian is common)
  2. A GitHub repository containing your app
  3. A production-ready app startup method, such as:
    • systemd service
    • PM2 (Node.js)
    • Docker Compose
    • a scripted restart process
  4. Basic server security:
    • SSH key authentication preferred
    • firewall allowing SSH (port 22 or custom), plus 80/443 for web traffic

3) Deployment Strategies to a VPS (Choose One)

Before writing YAML, decide how you’ll deploy.

Strategy A: “Pull on server” (Simple and popular)

Workflow:

  1. GitHub Actions SSH into the VPS
  2. git pull latest code
  3. install/build on server
  4. restart the service

Pros: easiest, minimal tooling

Cons: build happens on server; requires build tools installed

Strategy B: “Build in Actions, upload artifact” (More controlled)

Workflow:

  1. CI builds the project in GitHub Actions
  2. uploads build output (artifact)
  3. deploy step copies it to VPS (scp/rsync)
  4. restart

Pros: consistent builds, server is simpler

Cons: more steps and config

Strategy C: “Docker image deploy” (Most professional for scaling)

Workflow:

  1. build Docker image in Actions
  2. push to registry (GHCR/Docker Hub)
  3. VPS pulls image + docker compose up -d
  4. rollback by image tag if needed

Pros: reproducible deployments, easy rollback

Cons: requires Docker workflow knowledge

This article focuses on Strategy A (fastest for beginners), while also showing how to adapt to Docker at the end.


4) Step 1 — Prepare Your VPS for Automated Deploys

A) Create a dedicated deploy user (recommended)

On your VPS:

sudo adduser deploy
sudo usermod -aG sudo deploy

Then you’ll SSH as deploy instead of root.

B) Install essentials (example for Ubuntu/Debian)

You’ll typically need:

  • Git
  • your runtime (Node/Python/PHP)
  • build tools (optional, depends on your stack)

Example:

sudo apt update
sudo apt install -y git

For Node apps, also install Node.js (via a stable method like NodeSource or nvm). For Docker-based deployments, install Docker + Docker Compose.

C) Set up your app directory

A common standard:

  • /var/www/myapp
  • owned by the deploy user
sudo mkdir -p /var/www/myapp
sudo chown -R deploy:deploy /var/www/myapp

5) Step 2 — Create an SSH Key for GitHub Actions

GitHub Actions needs a private key stored as a GitHub Secret.

A) Generate a deploy key locally (or on your VPS)

On your local machine:

ssh-keygen -t ed25519 -C "github-actions-deploy" -f github_actions_deploy_key

You’ll get:

  • github_actions_deploy_key (private key) → goes into GitHub Secrets
  • github_actions_deploy_key.pub (public key) → goes into VPS authorized_keys

B) Add the public key to the VPS

On your VPS (as deploy user):

mkdir -p ~/.ssh
chmod 700 ~/.ssh
nano ~/.ssh/authorized_keys

Paste the contents of github_actions_deploy_key.pub.

Then:

chmod 600 ~/.ssh/authorized_keys

C) Add secrets in GitHub

In your GitHub repo:

Settings → Secrets and variables → Actions → New repository secret

Add:

  • VPS_HOST = your server IP or domain
  • VPS_USER = deploy
  • VPS_SSH_KEY = contents of github_actions_deploy_key (private key)
  • VPS_PORT = 22 (or your custom SSH port)

Optional but useful:

  • APP_DIR = /var/www/myapp

6) Step 3 — Add a Basic CI Workflow (Build + Test)

Create this file in your repo:

.github/workflows/ci.yml

Example for a Node.js app

name: CI

on:
  pull_request:
  push:
    branches: [ "main" ]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"

      - name: Install deps
        run: npm ci

      - name: Lint
        run: npm run lint --if-present

      - name: Test
        run: npm test --if-present

      - name: Build
        run: npm run build --if-present

This gives you immediate value: every PR can be validated automatically.


7) Step 4 — Add the Deploy Workflow (SSH into VPS)

Create:

.github/workflows/deploy.yml

This deploy workflow will run only when code is pushed to main (you can change this to tags/releases if you prefer).

Deploy example (pull and restart)

name: Deploy to VPS

on:
  push:
    branches: [ "main" ]

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Deploy over 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
            cd /var/www/myapp

            # If repo not cloned yet, clone it (one-time setup)
            if [ ! -d ".git" ]; then
              git clone <https://github.com/><YOUR_USERNAME>/<YOUR_REPO>.git .
            fi

            git fetch --all
            git reset --hard origin/main

            # Install/build steps (example Node)
            npm ci
            npm run build --if-present

            # Restart your service (choose one)
            # 1) systemd:
            sudo systemctl restart myapp

            # OR 2) PM2:
            # pm2 reload ecosystem.config.js --env production

Important notes

  • Replace <YOUR_USERNAME>/<YOUR_REPO> with your real repo path.

  • Make sure your VPS has permissions to restart the service.

    If you use sudo systemctl restart myapp, your deploy user must have sudo permission for that command (see next section).


8) Step 5 — Restart the App Correctly (systemd Example)

For production, systemd is a stable way to keep apps running.

Example systemd service (Node)

Create:

/etc/systemd/system/myapp.service

[Unit]
Description=MyApp Node Service
After=network.target

[Service]
Type=simple
User=deploy
WorkingDirectory=/var/www/myapp
Environment=NODE_ENV=production
ExecStart=/usr/bin/node /var/www/myapp/server.js
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

Then:

sudo systemctl daemon-reload
sudo systemctl enable myapp
sudo systemctl start myapp

Allow deploy user to restart service without password

Edit sudoers safely:

sudo visudo

Add:

deploy ALL=NOPASSWD: /bin/systemctl restart myapp, /bin/systemctl status myapp

Now GitHub Actions can restart the service via SSH without hanging on a password prompt.


9) Production Considerations: Secrets, .env, and Configuration

Don’t store production secrets in GitHub Actions scripts

Your repo should not contain real secrets.

On the VPS, keep a production .env file:

  • /var/www/myapp/.env (not in Git)

Make sure your app loads it safely (for Node: use dotenv in your runtime, or systemd EnvironmentFile).

Example systemd improvement:

EnvironmentFile=/var/www/myapp/.env

Then restart.


10) Add Safety: Deploy Only When CI Passes

A common mistake is deploying even if tests fail. You can chain workflows by putting CI + deploy in one workflow, or make deploy depend on CI.

Simple approach: single workflow with two jobs

One file, .github/workflows/pipeline.yml:

  • test job runs first
  • deploy runs only if test succeeds

This prevents broken code from reaching production.


11) Optional: Faster, Cleaner Deploy with Docker Compose

If your VPS uses Docker, your deploy script can be as simple as:

cd /var/www/myapp
git reset --hard origin/main
docker compose up -d --build

Benefits:

  • consistent runtime
  • easier dependency management
  • straightforward rollback (by Git tag or image tags)

12) Common Problems (and Quick Fixes)

“Permission denied (publickey)”

  • Make sure the VPS has the public key in ~/.ssh/authorized_keys
  • Ensure correct file permissions:
    • ~/.ssh = 700
    • authorized_keys = 600

Workflow hangs during restart

  • Your command is waiting for a sudo password prompt

    Fix with NOPASSWD sudoers rule for the restart command.

Deploy works but website still shows old version

  • You forgot to restart the process
  • You have caching (Nginx, CDN) serving old assets
  • Your build output wasn’t updated or your app reads from the wrong folder

Tests pass locally but fail in Actions

  • Node/Python version mismatch
  • missing environment variables for tests
  • OS differences (Linux runner vs local machine)

Conclusion

With GitHub Actions, you can create a free and professional CI/CD pipeline that automatically builds, tests, and deploys your application to a VPS. The key is to keep the pipeline simple and reliable:

  • run CI on every PR/push
  • deploy only from main (or tagged releases)
  • deploy over SSH with a dedicated deploy user
  • restart using systemd/PM2/Docker Compose
  • store secrets securely (GitHub Secrets + server-side env files)

Once this foundation is in place, you can extend it with staging environments, rollback workflows, Docker registry deployments, and notifications—without changing the core idea: automation makes production safer.

Komentar

Postingan populer dari blog ini

Docker for Beginners: How to Build, Run, and Deploy Containers on a VPS/Cloud