Deploy to a VPS over SSH from GitHub Actions
A VPS SSH deploy from CI means: load a deploy key from a secret, seed known_hosts, then run the deploy command over ssh and let its exit code gate the job.
This ties the whole family together. Generate a passphrase-free deploy key, authorize its public half on the VPS, store the private half as a secret, and the pipeline does the rest.
What it does
The deploy reads a private key from a CI secret into an agent (or a 600-mode file), seeds known_hosts for the VPS with ssh-keyscan, then runs the deploy script over ssh. The remote exit code passes or fails the workflow.
One-time setup
# generate a deploy key locally, no passphrase
ssh-keygen -t ed25519 -C "deploy@ci" -f ./deploy_key -N ""
# authorize it on the VPS
ssh-copy-id -i ./deploy_key.pub deploy@vps
# store the contents of ./deploy_key as the secret DEPLOY_KEYGitHub Actions workflow
- uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.DEPLOY_KEY }}
- name: Deploy
run: |
mkdir -p ~/.ssh && chmod 700 ~/.ssh
ssh-keyscan ${{ secrets.VPS_HOST }} >> ~/.ssh/known_hosts
scp ./dist/app.tar.gz deploy@${{ secrets.VPS_HOST }}:/srv/app/
ssh deploy@${{ secrets.VPS_HOST }} \
"cd /srv/app && tar xzf app.tar.gz && systemctl --user restart app"In CI
Use a dedicated passphrase-free deploy key per project, not a personal key, and restrict it on the VPS with a command= or from= prefix in authorized_keys if possible. Seed known_hosts with ssh-keyscan so StrictHostKeyChecking stays on. Start remote scripts with set -e so a failing step fails the job.
Common errors in CI
Permission denied (publickey). means the public key is not in the VPS authorized_keys, or the loaded private key is wrong, or the key file is not mode 600. "Host key verification failed." means known_hosts was not seeded; add ssh-keyscan. "Pseudo-terminal will not be allocated because stdin is not a terminal." is a harmless warning from -t; drop it. A non-zero remote exit fails the deploy by design.