Single-command deployments for side projects that share a VPS

I kept doing the same boring CI/CD setup work for side projects. I don’t care how smooth (read: easy to set up) your PaaS offering is, it always ends up costing some time. Also, I wanted these all to be on a single cheap Hetzner server without getting into each others way. When in doubt, I would prefer for the machine to choke on the traffic instead of scaling up and costing me a lot of money until i finally notice.

So I “built”[1] a CLI utility called pp, a little Go CLI that acts as a glorified deployment script. Using this avoids most of the pitfalls compared to literally just sending the files over to a server using rsync, and then fumbling around with systemd to make it run and so on. Most notably, this does use Docker. This avoids polluting the side project host with a bunch of crappy libraries for running legacy projects, keeps some minimal amount of security and separation between the containers, and I dont have to make a systemd configuration every time.

Deployment process outline:

  1. I do changes and commit them.
  2. pp gets run in a repo,
  3. reads a committed deploy.yml configuration file, which is the most high-level configuration. (Defines target domain, project name, modes, etc)
  4. It builds Docker images locally,
  5. copies them to the target server over SSH,
  6. renders Docker Compose files and runs health checks.
  7. It also assigns stable local ports for Caddy routes and keeps enough release state for status and rollback.

This is for servers that already have Docker, SSH access, and a reverse proxy. pp does not provision the machine or pretend to manage a fleet. The useful range is one side project on one server through to a small multi-service application where writing and maintaining the Compose setup by hand has become annoying.

I generated three deployment examples on GitHub:

  • Simple website builds one frontend container, gives it a local port, checks it, and routes a domain to it.
  • Stateful Go website adds a secret, a persistent volume, SQLite migrations, and a custom entrypoint.
  • Self-hosted Convex website is the much more complex example. It runs the site, Convex backend and dashboard, Postgres, and MinIO on one server. Deployment hooks, route templates, and smoke checks handle the work around those services.

The simple example is where I expect most projects to begin. The stateful one covers the common case where an application owns some local data. The Convex example has more services and setup, but the result is genuinely useful. Convex is a handy technology for side projects, and this lets me run it in self-hosted mode without turning pp into a platform of its own.

Success?

GPT 5.6-Sol was able to set up the configurations entirely on its own, with no hand-holding. The result might be a bit sloppy in some of the details, but fulfilled the goal of “not wasting time on deployments”. It runs stable and deployments work consistently.

This is still a project in progress. I will update this post as I move more of my sites over and find out which parts are actually useful.

TODO

  • Verification and detailed cleanup of the Convex Setup to enable easy reusability of the code/config/template, without use of LLM
  • Examples for other software stacks, e.g. elixir phoenix, php laravel, … ?
  • Examples for classic self-hosted different projects, e.g. nextcloud / mail server / …

[1] Had built by GPT. Not going to lie here.