Before I moved all my projects to Cloudflare Pages, I was running a self-hosted VPS and deploying everything via rsync over SSH through GitHub Actions. It was a solid setup — boring in the best way — and I ended up using the same pattern across most of my personal projects.
I’m documenting it here partly for posterity, partly because I still think it’s a good approach if you’re running your own server.
The setup
You need four things:
- An SSH key pair — generate one specifically for deploys, don’t reuse your personal key
- The public key added to
~/.ssh/authorized_keyson your server - The private key added as a GitHub secret (
SSH_KEY) - Host, user, and deploy path as additional secrets
The workflow file
This is the pattern I settled on after a few iterations. Nothing fancy — build, then rsync the dist/ folder to the server.
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci && npm run build
- name: Deploy
uses: burnett01/rsync-deployments@7.0
with:
switches: -avzr --delete
path: dist/
remote_path: ${{ secrets.DEPLOY_PATH }}
remote_host: ${{ secrets.SSH_HOST }}
remote_user: ${{ secrets.SSH_USER }}
remote_key: ${{ secrets.SSH_KEY }}
Rollback
No automatic rollback — if the build fails, the old files stay on the server because rsync only runs after a successful build step. If you push bad code that deploys successfully, revert the commit and push again.
For static sites, this is almost always fine.
Why I moved to Cloudflare Pages
Honestly, the VPS setup worked great. The reason I switched wasn’t that it broke — it was that Cloudflare Pages removed a whole layer of maintenance. No server to patch, no SSH keys to rotate, no worrying about disk space. Push to main and it just deploys.
If you’re already paying for a VPS and comfortable managing it, the rsync approach is totally viable. But if you’re starting fresh, Cloudflare Pages is hard to argue with for static sites.