How to Deploy with GitHub Actions to a VPS
Use GitHub Actions and SSH to ship code to a VPS automatically after pushes, while keeping the workflow understandable enough to debug.
How to connect a GitHub repository to a VPS using an SSH deploy key, GitHub secrets, and a simple workflow file.
Small apps, static sites, and Docker Compose projects that need repeatable deployments without a large platform layer.
CI can make a bad deployment happen faster. Keep rollback steps simple and explicit.
Before you begin
- A GitHub repository with code ready to deploy.
- A VPS with SSH access and a deploy path such as
/opt/myappor/var/www/myapp. - A non-root deploy user on the VPS.
- A build or restart command you already trust when run manually.
GitHub Actions is useful when you want pushes to main to trigger a consistent deployment path. The goal is not maximum platform complexity. The goal is to move from “someone SSHes in and remembers the steps” to “the steps are written down, reviewed, and repeatable.”
Step 1: Choose a simple deployment pattern
For beginner-friendly VPS deployments, a good default pattern is:
- GitHub Actions checks out the repo.
- The workflow connects to the VPS over SSH.
- The VPS pulls the latest code or receives built files.
- The app restarts with a small command such as
docker compose up -d --buildorsystemctl restart myapp.
This guide uses an SSH-based deploy because it stays close to standard Linux tools. You can add containers, artifacts, or release directories later, but first get one clean path working end to end.
Step 2: Prepare the VPS for CI deployments
Generate a dedicated SSH key pair on your local machine or a safe admin workstation:
ssh-keygen -t ed25519 -f ~/.ssh/github-actions-myapp -C "github-actions-myapp"Add the public key to the deploy user on the VPS:
ssh deploy@your-vps 'mkdir -p ~/.ssh && chmod 700 ~/.ssh'
cat ~/.ssh/github-actions-myapp.pub | ssh deploy@your-vps 'cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys'Create or confirm the app directory on the VPS:
ssh deploy@your-vps 'mkdir -p /opt/myapp && cd /opt/myapp && git init'If you plan to deploy a Docker Compose app, test the manual command now:
ssh deploy@your-vps 'cd /opt/myapp && docker compose config'Do not move to CI until the deployment steps already make sense by hand.
Step 3: Add GitHub Actions secrets
In your GitHub repository, go to Settings → Secrets and variables → Actions and add:
VPS_HOSTwith your server IP or hostnameVPS_USERwith the deploy usernameVPS_SSH_KEYwith the full private key contents from~/.ssh/github-actions-myappVPS_PORTif you use a custom SSH port
If the app also needs environment values during deploy, keep them on the VPS in a protected .env file whenever possible. Avoid using GitHub Actions to rewrite secrets on every run unless that is part of your actual design.
Step 4: Create the workflow file
Create .github/workflows/deploy.yml in your repository:
name: Deploy to VPS
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Start ssh-agent and add key
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.VPS_SSH_KEY }}
- name: Add VPS host to known_hosts
run: |
mkdir -p ~/.ssh
ssh-keyscan -p "${{ secrets.VPS_PORT || '22' }}" -H "${{ secrets.VPS_HOST }}" >> ~/.ssh/known_hosts
- name: Deploy on VPS
run: |
ssh -p "${{ secrets.VPS_PORT || '22' }}" "${{ secrets.VPS_USER }}@${{ secrets.VPS_HOST }}" <<'EOF'
set -euo pipefail
cd /opt/myapp
if [ ! -d .git ]; then
git clone https://github.com/your-user/your-repo.git .
fi
git fetch --all
git checkout main
git reset --hard origin/main
docker compose pull
docker compose up -d --build
docker image prune -f
EOFThis workflow assumes the VPS itself can access the repository without extra authentication if it needs to clone. If your repo is private, you can instead use rsync or upload artifacts from the workflow rather than having the VPS pull directly.
Commit and push the workflow:
git add .github/workflows/deploy.yml
git commit -m "Add VPS deployment workflow"
git push origin mainThen open the Actions tab in GitHub and watch the first run carefully.
Expected outcomes and verification
When the workflow succeeds, verify both the CI side and the server side:
ssh deploy@your-vps 'cd /opt/myapp && git rev-parse --short HEAD'
ssh deploy@your-vps 'cd /opt/myapp && docker compose ps'
curl -I https://example.comYou should see the current commit deployed, the containers or services healthy, and the public site responding the way you expect.
Rollback and recovery notes
If a deployment breaks production, the fastest rollback is usually on the VPS itself:
ssh deploy@your-vps
cd /opt/myapp
git log --oneline -n 5
git reset --hard <previous-good-commit>
docker compose up -d --buildIf you use image tags or release directories, your rollback may be even cleaner. The key idea is that rollback should not depend on remembering which files changed. Keep it anchored to a known good commit or release.
If GitHub Actions itself is broken, you can temporarily return to the manual SSH deployment path while you fix the workflow. Automation should support operations, not trap them.
Troubleshooting common GitHub Actions deploy issues
SSH authentication fails.
Check that the private key in GitHub secrets matches the public key in authorized_keys, and confirm the right username and port.
The workflow connects, but git pull fails on the VPS.
For private repos, the VPS may not have repository access. Switch to rsync, artifacts, or configure a deploy key for the repo itself.
Containers restart, but the app still shows the old version.
Check caching, mounted volumes, reverse proxies, or whether the real live directory differs from the one you updated.
The first deployment modified too much.
Run the same commands manually on the VPS to understand their effect before editing the workflow again.
What to do next
Once CI deployments work, the next safety improvement is controlling image drift so a rebuild does not unexpectedly change your stack. Continue with How to Pin Docker Images and Avoid Bad Updates.
