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.pubto your VPS user:~/.ssh/authorized_keys - Add
gha_vps_deploy_key(private key content) to GitHub repo secrets asVPS_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(usually22)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 inVPS_SSH_KEYor 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; fallbackpodman rm -f websitehandles this.dial tcp 127.0.0.1:4321: connect: connection refused: app container is down; inspectpodman logs website.
The key is to keep deploy boring: one branch, one server path, one repeatable command path.