CI/CD Pipeline in Locally
| name: Deploy to VPS | |
| # Triggers a deploy whenever main is pushed to. | |
| # Manual trigger also available via the Actions tab (workflow_dispatch). | |
| on: | |
| push: | |
| branches: [main] | |
| workflow_dispatch: | |
| jobs: | |
| deploy: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: SSH into VPS and run deploy script | |
| uses: appleboy/ssh-action@v1.0.3 | |
| with: | |
| host: ${{ secrets.VPS_HOST }} | |
| username: ${{ secrets.VPS_USER }} | |
| key: ${{ secrets.VPS_SSH_KEY }} | |
| port: ${{ secrets.VPS_SSH_PORT || 22 }} | |
| script: | | |
| cd /var/www/your-nestjs-app | |
| ./deploy.sh main | |
| - name: Notify on failure | |
| if: failure() | |
| run: | | |
| echo "Deployment failed — check the Actions log for details." | |
| # Optional: add a Slack/Discord webhook call here too |
deploy.yml — GitHub Actions CI/CD for VPS Deployment via SSH
Triggers a deploy to your VPS every time you push to main. Works by SSHing into the server and running the deploy.sh script that's already sitting there — this workflow doesn't build or deploy anything itself, it just remotely triggers the build.
How it works
- On push to
main(or manual trigger from the Actions tab), GitHub Actions spins up a runner. - The
appleboy/ssh-actionstep connects to your VPS over SSH using stored secrets. - It runs
cd /var/www/your-nestjs-app && ./deploy.sh main— the actual git pull, build, and PM2 reload all happen on the VPS, not on the GitHub runner. - If the SSH step fails (bad build, script error, connection issue), the job fails and shows up red in the Actions tab.
Why deploy this way instead of building on the runner and pushing artifacts?
Simpler and matches a single-VPS setup: no artifact registry, no SCP step, no build cache to manage between two machines. The trade-off is the VPS itself does the build work, which is fine for small-to-medium apps but means a broken npm ci eats CPU on your production box for a few seconds. For heavier apps, building in CI and rsyncing a dist/ folder over is the next step up — not covered here.
Prerequisites
deploy.shalready present and executable on the VPS (see the companiondeploy.shgist)- A dedicated SSH key pair generated just for this, not your personal key:
ssh-keygen -t ed25519 -C "github-actions-deploy" -f deploy_key
- The public key (
deploy_key.pub) added to~/.ssh/authorized_keyson the VPS for the deploy user - The private key (
deploy_key) added as a GitHub repo secret
Setup
In your repo: Settings → Secrets and variables → Actions → New repository secret
| Secret name | Value |
|---|---|
VPS_HOST | Your server IP or domain |
VPS_USER | The deploy user (e.g. deployer, never root) |
VPS_SSH_KEY | The full private key content (deploy_key) |
VPS_SSH_PORT | Optional — only needed if you changed SSH from port 22 |
Drop this file at .github/workflows/deploy.yml in your repo.
Security notes
- Never use your root SSH key here — create a deploy-only user with a scoped key, ideally one that can only access the app directory.
- Rotate the key if it's ever exposed (leaked log, compromised laptop, etc.) — revoke it from
authorized_keyson the VPS and generate a new pair. workflow_dispatchis included so you can trigger a manual redeploy from the GitHub UI without needing a new commit — useful for retrying a failed deploy.