Auto-deploy to a Hostinger VPS with GitHub Actions (push to deploy)
Every push to main deploys your app to the VPS. A dedicated SSH key, pinned host keys, a locked-down forced command and a workflow under 30 lines, with no third-party actions.
How it fits together
The simplest reliable setup has GitHub do nothing clever. On every push to `main`, a workflow opens an SSH connection to your VPS and runs the deploy script that already lives there. The server pulls the code, builds and restarts. If the script works when you run it by hand, it works from CI.
This guide assumes you have a working `deploy.sh`, like the one at the end of our Next.js guide. The same pattern works for Laravel, Django or anything else; only the script changes.
Step 1 — Make a key just for CI
Never reuse your personal SSH key. Make a separate one with no passphrase, so you can revoke CI access without touching your own login.
ssh-keygen -t ed25519 -N "" -C "github-actions" -f ./gha_deploy
# gha_deploy -> private key, goes into a GitHub secret
# gha_deploy.pub -> public key, goes onto the serverStep 2 — Allow the key to run one command only
Add the public key to the deploy user's `authorized_keys` with a forced command. Whatever the client asks for, the server runs only `deploy.sh`. If the private key ever leaks, the worst anyone can do is trigger a deploy of your own code.
nano ~/.ssh/authorized_keys
command="/var/www/myapp/deploy.sh",no-port-forwarding,no-agent-forwarding,no-X11-forwarding,no-pty ssh-ed25519 AAAA...your-public-key... github-actionsStep 3 — Store the secrets in GitHub
In the repository, open Settings → Secrets and variables → Actions and add three secrets. Pinning the host key matters: without it, the workflow would trust whatever machine answers on that IP.
ssh-keyscan -t ed25519 YOUR_SERVER_IP| Secret | Value |
|---|---|
| SSH_HOST | Your VPS IP or domain |
| SSH_KEY | The full contents of gha_deploy (private key) |
| SSH_KNOWN_HOSTS | The line printed by ssh-keyscan |
Step 4 — The workflow
Commit this file. `concurrency` queues deploys so two quick pushes can't build on top of each other.
name: Deploy
on:
push:
branches: [main]
workflow_dispatch:
concurrency:
group: production
cancel-in-progress: false
jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Configure SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
echo "${{ secrets.SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
- name: Deploy
run: ssh deploy@${{ secrets.SSH_HOST }}Step 5 — Make failures visible
Because `deploy.sh` starts with `set -euo pipefail`, a failed build exits non-zero, the SSH command fails, and the workflow goes red. GitHub emails you, and the live app keeps running the previous build because PM2 was never restarted.
- Run tests in an earlier job and add `needs: test` to the deploy job, so broken code never reaches the server.
- Keep the build on the server only while builds are quick. If they start eating the VPS's CPU while it serves traffic, build in Actions and rsync the output instead.
- Add a protected environment in GitHub if you want a manual approval before production deploys.
Troubleshooting
The errors you're most likely to see, and what they mean:
| Error | Cause and fix |
|---|---|
| Host key verification failed | SSH_KNOWN_HOSTS is empty or for a different key type. Re-run ssh-keyscan and paste the whole line. |
| Permission denied (publickey) | The public key isn't in the deploy user's authorized_keys, or the file is group-writable. Run chmod 600 ~/.ssh/authorized_keys. |
| pm2: command not found | Non-interactive SSH doesn't load your shell profile. Use the full path from `which pm2` inside deploy.sh. |
| Build killed / exit 137 | Out of memory during the build. Add swap, or build in Actions instead. |
Frequently asked questions
Can I use GitHub Actions with Hostinger shared hosting?
Yes, but over FTP or SFTP rather than SSH commands: build in Actions, then upload the output folder. Shared hosting can't run a build or a long-lived process for you.
Is it safe to store an SSH key in GitHub secrets?
Secrets are encrypted and masked in logs. Pair that with a CI-only key restricted by a forced command, and a leaked key can only trigger a deploy of your own code.
Does this cost anything?
GitHub Actions includes free minutes for private repos and is free for public ones. This workflow uses well under a minute per deploy, since the build runs on your VPS.
Ready to save an extra 20% with coupon WEBHOST2026?
Get my discount link →Disclosure: links to Hostinger on this page are referral links. If you buy through them we may earn a commission, at no extra cost to you. It doesn't change what we recommend. About us
Keep reading
Deploy a Next.js app on Hostinger VPS (step by step)
A complete walkthrough: provision the server, install Node 24, build with standalone output, keep it alive with PM2, put Nginx and free HTTPS in front, and script your redeploys.
TutorialsSecure a new Hostinger VPS: the 10-minute checklist
SSH keys, no root or password logins, a firewall, Fail2ban, automatic security updates and snapshots. The minimum to do before a fresh VPS goes on the internet, with copy-paste commands.
TutorialsMove a Next.js app from Vercel to a Hostinger VPS
What actually changes when you leave Vercel: env vars, images, cron, storage, analytics and DNS. A migration checklist with no downtime, plus what you'll miss.