Case study: running a VPS without platform overhead
How a growing VPS moved from heavyweight app orchestration to CI artifacts, PM2, Doppler and Caddy without risking live services in a big-bang cutover.
A single VPS can work well for a long time and still become heavier than it needs to be. First it hosts one website, then an analytics service, a small API, a password vault, an admin panel and a few databases. Each decision makes sense on its own. Together they can create too many containers, too much memory usage and too much routing logic hidden in platform metadata.
This case study describes a controlled simplification: moving away from platform-managed app runtimes toward CI-built artifacts, PM2 processes, Doppler secrets and Caddy as the public edge.
Starting point
The server was live, so the goal was not to rebuild everything. The goal was to untangle the setup step by step.
The symptoms were typical:
- Some builds still ran directly on the server.
- Several small apps used separate runtime containers.
- Routing was spread across multiple places.
- Old containers could come back after deployment events.
- Memory usage and operational surface area were higher than necessary.
The risk was not one broken app. The risk was the sum of small dependencies that were hard to reason about.
Target shape
The new model was intentionally boring and repeatable:
| Layer | Decision |
|---|---|
| Build | GitHub Actions builds the production artifact |
| Secrets | Doppler provides runtime configuration |
| Runtime | PM2 runs Node and app processes |
| Data | Shared database and cache services are reused |
| Edge | Caddy handles HTTPS and reverse proxying |
| Rollback | Previous releases and volumes are kept initially |
The key change: the VPS no longer builds. It unpacks an artifact, updates a release pointer and reloads PM2.
Migration approach
The first step was inventory: running containers, hostnames, ports, volumes, secrets and active PM2 processes. This prevents accidental downtime when an old-looking container is still serving production traffic.
Each app then followed the same path:
- Mirror production secrets into Doppler.
- Build a standalone artifact locally or in CI.
- Start the app as an internal PM2 canary.
- Run a final data sync if the app has local state.
- Switch the public route.
- Check status codes, headers and logs.
- Stop old containers and disable restart policies.
For one app with an embedded data directory, the old volume was copied only after a short write stop. That avoided parallel writes between the old and new runtimes.
Edge replacement
The existing proxy stayed in place as a bridge at first. That was deliberate: prove the PM2 apps first, simplify the public edge second.
The final edge moved to Caddy. Caddy works well for small VPS setups because the configuration stays readable and certificates are managed automatically. For PM2 apps, the rule is simple: public hostname in, internal upstream out. For a few legacy containers, Caddy can join the relevant Docker network and proxy by service name.
Result
After the cleanup, memory usage dropped significantly. More importantly, the operating model became simpler:
- Deployments are more reproducible.
- The server is not busy building apps.
- PM2 shows the running apps directly.
- Public routing is explicit and readable.
- Old runtime containers stay stopped.
- Rollback remains possible through previous releases and retained volumes.
The result is not a maximal platform. It is a small production setup that is easier to inspect, explain and recover.
What teams can reuse
A platform is often faster at the beginning. Later, for small setups, it can add more surface area than it removes. At that point, simple building blocks can be a better fit:
- CI for builds
- secret management outside the repo
- PM2 for simple Node runtimes
- shared infrastructure instead of duplicated database containers
- Caddy as a clear HTTPS edge
The order matters: do not shut the old system down first and hope. Start a canary, measure it, route traffic, smoke it and only then remove the old runtime.
Checklist
- Inventory live routes and active containers.
- Identify secrets and data volumes.
- Build the CI artifact.
- Start an internal PM2 canary.
- Sync data.
- Switch the public route.
- Run public smoke checks.
- Stop old containers.
- Disable restart policies.
- Document rollback.
That is how a growing VPS becomes a system you can explain in minutes again.