← Home

Triggering VPS Deploys with GitHub Actions

Manual deploys work, but they are easy to forget.

If your deploy flow is already stable on the VPS, GitHub Actions can trigger it automatically on every push to main.

1. Preconditions on the VPS

Before wiring GitHub Actions, make sure this works manually:

cd /opt/website
git pull --ff-only origin main
podman compose -f compose.yml up -d --build
curl -I http://127.0.0.1:4321

If this is not stable yet, automate later.

2. Create a deploy SSH key for GitHub Actions

Generate a dedicated key pair locally (or in a secure ops environment):

ssh-keygen -t ed25519 -C "gha-vps-deploy" -f ./gha_vps_deploy_key -N ""

Then:

  • Add gha_vps_deploy_key.pub to your VPS user: ~/.ssh/authorized_keys
  • Add gha_vps_deploy_key (private key content) to GitHub repo secrets as VPS_SSH_KEY

Use a dedicated deploy user with access to /opt/website and Podman.

3. Add required GitHub Actions secrets

In your repository settings (Settings -> Secrets and variables -> Actions), add:

  • VPS_HOST (server IP or DNS)
  • VPS_PORT (usually 22)
  • VPS_USER (deploy user)
  • VPS_SSH_KEY (private key content, multi-line)

4. Add the workflow file

Create .github/workflows/deploy.yml:

name: Deploy VPS

on:
  push:
    branches: [main]
  workflow_dispatch:

concurrency:
  group: deploy-production
  cancel-in-progress: false

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Deploy via SSH
        uses: appleboy/ssh-action@v1.2.0
        with:
          host: ${{ secrets.VPS_HOST }}
          port: ${{ secrets.VPS_PORT }}
          username: ${{ secrets.VPS_USER }}
          key: ${{ secrets.VPS_SSH_KEY }}
          script_stop: true
          script: |
            set -euo pipefail
            cd /opt/website

            git fetch --all --prune
            git checkout main
            git pull --ff-only origin main

            if ! podman compose -f compose.yml up -d --build; then
              podman rm -f website || true
              podman compose -f compose.yml up -d --build
            fi

            podman ps --filter name=website
            curl -fsS http://127.0.0.1:4321 >/dev/null

This keeps the server pull-based and avoids shipping registry credentials into CI.

5. First run and validation

Run the workflow once via workflow_dispatch, then verify:

curl -I http://127.0.0.1:4321
curl -I https://adrian-altner.de
sudo systemctl status caddy --no-pager

If both local and public checks pass, automatic deploy is live.

6. Common failure modes

  • Permission denied (publickey): wrong private key in VPS_SSH_KEY or public key missing on VPS.
  • fatal: Not possible to fast-forward: server has diverged changes; clean or reset workflow on VPS first.
  • container name "website" is already in use: stale container state; fallback podman rm -f website handles this.
  • dial tcp 127.0.0.1:4321: connect: connection refused: app container is down; inspect podman logs website.

The key is to keep deploy boring: one branch, one server path, one repeatable command path.

← Home