Tutorials / Shipping & infrastructure / Redeploy a self-hosted VPS
📝 Written ● Intermediate Updated 2026-05-13

Redeploy a self-hosted VPS

Push code updates to your own server. SSH in, pull, restart the service. Or wrap it in a one-line deploy script. Time: 10–30 seconds for a typical app.

The manual flow (one-time understanding)

1

SSH to the server:

ssh deploy@your-server-ip
2

Go to your project directory and pull:

cd /var/www/myapp
git pull origin main
3

Install new dependencies (only if package.json / requirements.txt changed):

npm install --production
# or: pip install -r requirements.txt
# or: bundle install --deployment
4

Build (if needed):

npm run build
5

Restart the service:

# If using systemd:
sudo systemctl restart myapp

# If using pm2:
pm2 reload myapp

# If using Docker:
docker compose pull && docker compose up -d

That's it — the new code is live.

Wrap it in a deploy script

6

You'll do this often enough that a script saves real time. deploy.sh on the server:

#!/usr/bin/env bash
set -euo pipefail

cd /var/www/myapp
git pull origin main
npm install --production
npm run build
sudo systemctl restart myapp
echo "✓ Deployed at $(date)"

Make it executable: chmod +x deploy.sh. Now redeploy is one command (after SSHing in):

./deploy.sh

From your laptop, in one command

7

Don't even SSH manually — script it from your Mac:

# deploy.sh on your laptop
#!/usr/bin/env bash
ssh deploy@your-server-ip '/var/www/myapp/deploy.sh'

Or directly inline:

ssh deploy@your-server-ip 'cd /var/www/myapp && git pull && npm install --production && npm run build && sudo systemctl restart myapp'
Use a passwordless sudo entry just for the restart. If sudo systemctl restart myapp prompts for a password, your script breaks. Add an /etc/sudoers.d/myapp-restart entry allowing your deploy user to run that one command without a password.

Zero-downtime patterns

8

The naive restart drops in-flight requests. For real production:

  • pm2 reload — restarts Node.js workers one at a time; new ones come up before old ones go down. Built-in to pm2 ↗.
  • systemd Type=notify — your service signals "ready" before systemd kills the old one. Smaller downtime window than a default restart.
  • Two instances behind Nginx — run two copies on different ports, switch Nginx upstream, restart the other. Manual but cheap; no extra tooling.
  • Docker Compose up -d — for Compose-managed services, Docker handles rolling replacement of one container at a time if you set deploy.update_config.

Auto-deploy on git push

9

For an automatic deploy on every push to main, the cheapest patterns:

  • GitHub Action that SSHes in. workflow_dispatch or on: push: main → action runs ssh ... 'deploy.sh'. Needs an SSH deploy key stored as a GitHub Secret.
  • Webhook + a tiny listener. GitHub fires a webhook on push; a small Node/Python service on your server runs deploy.sh. Beware: public webhook endpoints need signature verification (similar to Stripe webhooks).
  • Pull mode (cron). Server polls git every 60 seconds; runs deploy if there's a new commit. Simplest, slightly delayed.

Rollback

10

If the new deploy is broken:

cd /var/www/myapp
git reset --hard HEAD~1   # back to the previous commit
npm install --production
npm run build
sudo systemctl restart myapp

This rolls forward to the previous commit. Better long-term: keep last N releases in numbered directories and symlink current at one — that gives instant rollback without rebuilding.

Database migrations need extra care. If your deploy includes schema changes, run them before restarting the service (so the new code matches the new schema) and design migrations to be backward-compatible (new code can read old schema, old code can read new schema) so an emergency rollback doesn't blow up.

Useful references

What's next