You have a VPS, you have a working app, and now you need a repeatable way to ship code to it. The easiest path depends on how often you deploy, how much you value zero-downtime and rollbacks, and how much shell scripting you want to maintain. There are five common approaches — manual SSH/rsync, Capistrano, GitHub Actions, custom CI scripts, and a hosted deploy tool like [DeployHQ](https://www.deployhq.com). This post compares them on the metrics that actually matter in production, and recommends the one that wins for most teams.

If you are still deciding whether a VPS is the right host in the first place, start with the [VPS 101 guide](https://www.deployhq.com/blog/vps-101-understanding-virtual-private-servers).

## What easy means once you are past the first deploy

The first deploy is always easy. You SCP a folder, restart Nginx, and it works. The hard part is what happens next:

- A teammate pushes a hotfix on a Friday afternoon. Can they deploy without you on the call?
- A migration breaks production at 23:00. How fast can you roll back to the previous release?
- Your build step pulls 400 MB of `node_modules`. Does it run on the VPS (and starve the live app of CPU) or on a build agent?
- You add a second server behind a load balancer. Do both boxes get the same release at the same moment?
- The senior dev leaves. Is the deploy process documented, or does it live in their `~/.bash_history`?

Any deploy approach that ignores those five questions is easy only on day one. The rest of this article evaluates each option against them.

## Option 1: Manual SSH + `rsync` or `git pull`

The classic. You SSH into the box, `git pull` the new code (or `rsync` from your laptop), restart the service, and check the logs.

**What works:** zero tooling. No new account, no new YAML, no new vendor. Great for a side project you deploy once a month.

**Where it falls apart:**

- **No atomic deploys.** While `rsync` is mid-flight, the live app is serving half-old, half-new files. Static asset hashes mismatch, partial PHP files 500.
- **No rollback story.** Roll back means `git reset --hard <prev-sha>` plus reversing any migration by hand. At 2 a.m., during an incident, this is where careers wobble.
- **Build artifacts on the production box.** `npm run build` competes for CPU with the live process. We have seen 300 ms request latency spikes during deploys for this exact reason.
- **State drifts off Git.** Hand-edited config files, ad-hoc `chmod` changes, a `.env` someone updated last quarter. Six months in, nobody can recreate the box.
- **No audit trail.** Who deployed what, when? You have shell history if you are lucky.

**Verdict:** fine for a personal blog. Do not run a customer-facing product on it.

## Option 2: Capistrano (or Mina, Deployer)

Ruby's old reliable, also good for PHP via [Deployer](https://www.deployhq.com/blog/deployhq-vs-deployer-a-comparative-analysis-of-automated-deployment-tools). You define `tasks` in a `Capfile`, run `cap production deploy`, and the tool SSHes in, clones a release into a timestamped directory, symlinks `current/`, and restarts services.

**What works:**

- **Atomic releases via symlink swap.** Roll back is `cap production deploy:rollback` — one command, no Git surgery. This is the genuine appeal.
- **Predictable file layout** (`releases/`, `shared/`, `current/`).
- **Local config, version-controlled.** The deploy logic lives in your repo.

**Where it falls apart:**

- **Ruby-shaped tool in a not-Ruby world.** [Capistrano](https://www.deployhq.com/compare/deployhq-vs-capistrano) expects Bundler on the deploy machine. Teams that aren't Ruby-native end up maintaining a Ruby toolchain just for deploys.
- **Build runs on production.** Out of the box, `cap deploy` runs `bundle install` (or `npm ci`) on the target server. Same CPU contention problem as Option 1.
- **No queueing or concurrency control.** Two engineers running `cap deploy` simultaneously will fight for the lock and one will lose.
- **Hooks and logs are ephemeral.** Output streams to whoever ran the command; nothing is persisted unless you bolt it on.
- **Build pipelines are bring-your-own.** You wire up tests, asset compilation, container builds yourself, in `Capfile`.

**Verdict:** still excellent for Rails monoliths with a small team. Painful for polyglot teams or anyone who needs a real audit trail.

## Option 3: GitHub Actions (or GitLab CI) deploying via SSH

The default 2026 answer for I need automation: write a `.github/workflows/deploy.yml`, store an SSH key as an Actions secret, and push to `main` to deploy. For a deeper feature-by-feature breakdown, see our [DeployHQ vs GitHub Actions comparison](https://www.deployhq.com/compare/deployhq-vs-github-actions).

**What works:**

- **Free for public repos** , and reasonable quota for private ones.
- **Pipeline lives next to the code.** Tests, builds, and deploys are in the same YAML.
- **Build runs on a fresh runner** , not on the VPS — so production CPU stays clean.
- **Pull-request previews** are achievable with extra YAML.

**Where it falls apart:**

- **The deploy step is your responsibility.** `appleboy/ssh-action` runs whatever shell you give it. You write the `rsync`, the symlink swap, the migration runner, the rollback. By the time a Capistrano-equivalent pipeline is wired up, the YAML is 200 lines of brittle shell.
- **No first-class atomic deploys, rollbacks, or release history.** You build all of it on top of `ssh` and pray your runbook is current.
- **Secrets sprawl.** SSH keys, server lists, environment variables — every project's Actions secrets diverge over time. A 30-project agency ends up with a different snowflake per client.
- **Pipeline ownership = DevOps task.** Every framework upgrade or runner change can break deploys, and the pipeline is rarely the team's primary skill.
- **Cost compounds at scale.** Heavy build pipelines on hosted Actions runners get expensive once you have CI minutes that matter.

**Verdict:** great for teams that are already living in GitHub Actions for tests and lints. The deploy logic itself is the weakest link — you are essentially rebuilding Capistrano in YAML.

## Option 4: Custom CI scripts (Jenkins, Drone, GitLab Runners)

A self-hosted CI pipeline — typically [Jenkins](https://www.deployhq.com/compare/deployhq-vs-jenkins), Drone, or GitLab Runners — that runs a bash script which `ssh`es to your VPS. Same shape as Option 3, but you also operate the runner.

**Where it falls apart:** all the cons of Option 3, plus the runner is now a server you have to patch, scale, and back up. Almost never the right answer for a small team. Worth it only if you are at hundreds of repos and have a platform-engineering team that already runs Jenkins for other reasons.

**Verdict:** skip unless you already have it.

## Option 5: A hosted deployment tool (DeployHQ)

This is the category that exists because Options 1–4 keep failing on the same five questions. A hosted deploy tool connects to your Git host, runs the build on its own infrastructure, transfers only changed files to your VPS, swaps the release atomically, and gives you a one-click rollback and an audit log.

**What [DeployHQ](https://www.deployhq.com) gives you out of the box:**

- **Git-triggered deploys** from [GitHub](https://www.deployhq.com/deploy-from-github), [GitLab](https://www.deployhq.com/deploy-from-gitlab), Bitbucket, or self-hosted Git — push `main`, and the deploy runs.
- **Build runs in an isolated container per deploy** via [build pipelines](https://www.deployhq.com/features/build-pipelines), so your VPS never burns CPU on `npm ci`.
- **Atomic releases with [zero-downtime deployments](https://www.deployhq.com/features/zero-downtime-deployments)** — symlink-swap, just like Capistrano, without the Ruby toolchain.
- **[One-click rollback](https://www.deployhq.com/features/one-click-rollback)** to any previous release. Useful at 23:00.
- **SSH commands** before and after deploy for migrations, cache warm-ups, queue restarts.
- **Config files** managed outside the repo, so secrets don't end up in Git.
- **Multi-server deploys** for load-balanced setups — same release, same moment, on every box.
- **Audit log and team permissions** — who deployed what, when, and who is allowed to.

**Where it isn't the answer:**

- You are deploying once a quarter to a hobby project — Option 1 is genuinely fine.
- You have a PaaS-shaped app and you actually want serverless. Vercel, Netlify, Fly, and Railway are better. (DeployHQ also handles [serverless deployments](https://www.deployhq.com/blog/serverless-deployments-with-deployhq-aws-lambda-vercel-and-netlify) if you have a mixed estate.)
- You have a hard requirement that the deploy tool run inside your VPC and never touch a third party. Self-hosted GitLab CI or Jenkins is your road.

**Verdict:** for a team running customer-facing applications on one or more VPSes, this is the path with the lowest ongoing cost. The setup is roughly the same effort as wiring up a serious GitHub Actions pipeline, and you stop owning the deploy logic.

## Side-by-side comparison

| | Manual SSH | Capistrano | GitHub Actions | Custom CI | DeployHQ |
| --- | --- | --- | --- | --- | --- |
| Atomic releases | No | Yes | DIY | DIY | Yes |
| One-click rollback | No | Yes | DIY | DIY | Yes |
| Build off-server | No | No | Yes | Yes | Yes |
| Audit log | No | No | Partial | Partial | Yes |
| Multi-server | DIY | DIY | DIY | DIY | Yes |
| Setup time | 5 min | 1–2 hrs | 1–2 days | 1+ week | 15–30 min |
| Ongoing maintenance | High | Medium | Medium-High | High | Low |
| Cost (small team) | $0 | $0 | $0–low | $$ | $ |

## Our recommendation

For most teams running production apps on a VPS — agencies, SaaS startups, internal-tools teams — **DeployHQ is the easiest defensible answer**. You stop maintaining bespoke shell scripts, you get atomic releases and rollbacks for free, and the deploy itself is one `git push`.

If you are an absolute Rails purist with a one-app, one-server setup, Capistrano is still respectable — but you're trading off the audit trail and the off-box build for ideological consistency.

If you are a hobbyist with one weekend project, manual `git pull` is fine. Don't over-engineer.

Everything else — the GitHub Actions YAML, the Jenkins runner, the bespoke `bash` deploy script — is rebuilding a deployment platform in your spare time, badly. The world has enough of those.

## What setting up DeployHQ actually looks like

A first deploy on a fresh VPS, end to end, is around fifteen minutes:

1. **Connect your Git host.** Authorise GitHub/GitLab/Bitbucket once.
2. **Add your server.** Hostname, SSH user, key (DeployHQ shows you exactly what to add to `authorized_keys`), deploy path. Use a non-root user — see [our setup guide for Git-based deployment on a VPS](https://www.deployhq.com/blog/setting-up-git-based-deployment-on-a-virtual-private-server-vps) for the user/permissions setup.
3. **Pick a build pipeline** if your stack needs one. `npm ci && npm run build`, `composer install --no-dev`, `pnpm install && pnpm build`, whatever your `package.json`/`composer.json` already runs locally.
4. **Add SSH commands** for post-deploy work — migrations, `php artisan octane:reload`, `systemctl reload nginx`, etc.
5. **Push.** The first deploy uploads the full release; subsequent deploys transfer only the diff.

The first time you watch a green deploy roll across three VPSes in 40 seconds without a shell prompt being involved, you stop missing the YAML.

## When the VPS itself is the bottleneck

Sometimes make deploys easier is masking a different problem. If you are seeing CPU steal time, your IOPS are pinned, or your single VPS keeps OOMing during deploys, no deploy tool will fix that — you need to upgrade the host. The [VPS 101 guide](https://www.deployhq.com/blog/vps-101-understanding-virtual-private-servers) covers what to look for in a provider; if you are already on shared hosting and considering a move, our [shared hosting vs VPS comparison](https://www.deployhq.com/blog/shared-hosting-vs-vps-a-comprehensive-guide-for-junior-developers) is the right starting point.

## Get started

[Start a free](https://www.deployhq.com/signup)[DeployHQ](https://www.deployhq.com) trial — connect your repo, point it at your VPS, and your next `git push` is your next deploy. No credit card required.

Running multiple sites or client projects? The [agency plan](https://www.deployhq.com/for-agencies) gives unlimited team members and project templating; full pricing is on the [plans page](https://www.deployhq.com/pricing).

Questions about migrating to a VPS or wiring up automated deploys? Email us at [support@deployhq.com](mailto:support@deployhq.com) or ping [@deployhq](https://x.com/deployhq) on X.

