Docker & DevOps

CI/CD basics: deploy to a VPS with GitHub Actions

Set up a simple CI/CD pipeline that tests your code on every push and deploys it to your VPS over SSH with GitHub Actions, secrets and a restricted deploy key.

IntermediateLinux VPS3 min read

CI/CD (continuous integration and continuous deployment) means every push is tested automatically and, if the tests pass, deployed without manual steps. GitHub Actions is built into GitHub and free for public repositories with a monthly allowance for private ones, which makes it the easiest way to add CI/CD for a VPS.

Before you start

  • A GitHub repository with your app.
  • A Linux VPS with a deploy user (not root) that can write to the app folder.
  • Your app already deployable by hand, for example with git pull or by copying a build folder.

How the pipeline works

  1. You push to main.
  2. A GitHub-hosted runner checks out the code, installs dependencies and runs tests (the CI part).
  3. If tests pass, the runner connects to your VPS over SSH and deploys (the CD part).

Create a dedicated deploy key

  1. On your computer, generate a key pair used only by GitHub Actions:
    ssh-keygen -t ed25519 -f gha_deploy -N "" -C "github-actions"
  2. Append gha_deploy.pub to /home/deploy/.ssh/authorized_keys on the VPS.
  3. Get the server's host key so the runner can verify it:
    ssh-keyscan -t ed25519 203.0.113.10

Add repository secrets

In GitHub open Settings → Secrets and variables → Actions → New repository secret and add:

NameValue
SSH_PRIVATE_KEYContents of gha_deploy (private key)
SSH_KNOWN_HOSTSOutput of the ssh-keyscan command
SSH_HOSTServer IP or hostname
SSH_USERdeploy

Then delete the local private key file or store it in a password manager.

Write the workflow

Create .github/workflows/deploy.yml. This example builds a Node.js app and copies the result with rsync; adapt the build steps to your stack:

name: CI/CD
on:
  push:
    branches: [main]

jobs:
  test-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - run: npm ci
      - run: npm test
      - run: npm run build

      - name: Set up SSH
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
          chmod 600 ~/.ssh/id_ed25519
          echo "${{ secrets.SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts

      - name: Deploy
        run: |
          rsync -az --delete dist/ ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:/var/www/app/
          ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} 'sudo systemctl restart myapp'

For a PHP site, swap the Node steps for composer install --no-dev and tests with PHPUnit. For a server-side git pull deployment, replace the rsync line with ssh ... 'cd /var/www/app && git pull --ff-only'.

Limit what the deploy user can do

  • Allow only the restart command via sudo, in /etc/sudoers.d/deploy:
    deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart myapp
  • Give the deploy user write access only to the app folder.
  • Protect the main branch so only reviewed code triggers deploys.

Check the pipeline

Push a commit and open the Actions tab. Each step shows its log; a green tick means tested and deployed. Break a test on purpose once to confirm a failure stops the deploy.

Troubleshooting

  • Host key verification failed: SSH_KNOWN_HOSTS is missing or for a different key type. Re-run ssh-keyscan.
  • Permission denied (publickey): the public key is not in the deploy user's authorized_keys, or the private key secret lost its line breaks; paste it again including the BEGIN/END lines.
  • sudo: a password is required: the sudoers rule does not match the exact command path.
  • Firewall blocks the runner: GitHub runners use many changing IPs. Allow SSH from them, use a self-hosted runner, or connect through a VPN.

CI/CD FAQ

Is it safe to store an SSH key in GitHub secrets?

Secrets are encrypted and hidden from logs. Reduce the risk further with a dedicated key, a limited user and narrow sudo rules.

GitHub Actions or GitLab CI?

Both work the same way. Use the one built into the platform that hosts your code.

Can I deploy to cPanel hosting with GitHub Actions?

Yes, over SSH or SFTP if your account has SSH access, using the same rsync approach with your cPanel username and home path.

Next steps

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.