Sthora
Getting started

Introduction

What Sthora is, the two ways to run it, the constraints that shaped it, and what it deliberately does not do.

Sthora is a self-hostable application deployment platform. One install script turns a fresh VPS into a portal that builds and runs Dockerised web applications behind an automatic TLS reverse proxy, with server and container monitoring.

What a deployment actually does

Every deployment, whatever triggered it, runs the same pipeline:

Lock. One in-flight deployment per application, enforced by a Redis lock on the application id. A second request queues behind it or is refused with 409 — two deployments of the same application never run at once.

Materialise the source into /var/lib/sthora/sources/<appId>/<deploymentId>/, either by extracting an uploaded ZIP or by cloning the repository shallow and single-branch.

Detect the build type — a compose file, a Dockerfile, or neither, in that order. See build types.

Sanitise the compose file. Parse it, reject anything on the reject list, overwrite the platform's own settings, inject routing labels and variables, and strip published ports. See compose files.

Build. docker compose build with BuildKit. Every line of output streams to the portal and lands in a size-capped log file.

Deploy and health-gate. docker compose up -d --remove-orphans, then the container's own HEALTHCHECK if it declares one, otherwise a TCP connect to the exposed port with backoff until the health timeout.

Settle. On success, record the image tag and prune images beyond the retention count. On failure, mark the deployment failed with the captured error and leave the previous version running.

Two distribution modes, one build

Self-hostedCloud
Who runs the control planeYou, on your own VPSSomeone else, one hosted instance
Who runs the workloadsThe same VPSServers you already own
How the control plane reaches themDirectly, over the local Docker socketAn agent that dials out from your machine
LimitsNone — every ceiling is nullWhatever the plan says
SignupClosed after the first accountOpen, if the operator enabled it

The difference is configuration, not code. Both modes go through one interface — every Docker and filesystem operation is a ServerDriver call, and which machine it lands on is decided in one place.

The constraints that shaped it

The target machine is 2 vCPU, 4 GB RAM, 40 GB disk — a $12–24/month instance. That is not a footnote; it drove the architecture.

  • The platform budgets about 1 GB, leaving roughly 3 GB for your applications and builds.
  • Build queue concurrency is 1. A pnpm install inside a Node image is the memory spike, and two at once is an out-of-memory kill.
  • Redis runs maxmemory-policy noeviction. BullMQ stores job state in Redis; an LRU policy silently evicts it and deployments disappear with no error.
  • Build logs never reach PostgreSQL. They stream over Redis pub/sub and land in a size-capped file; the database keeps a pointer, a byte count, and the last 4 KB.
  • Metrics are pre-aggregated on write. 15-second samples live in Redis only; one row per minute reaches PostgreSQL, rolled up hourly after 7 days and deleted after 90.
  • Builds refuse to start below the free-disk floor. A build that fills the root filesystem takes the platform down with it.

What it deliberately does not do

Kubernetes. Docker Swarm. Multi-node scheduling. Autoscaling. A preview environment per pull request. A bundled Prometheus, Grafana, or Loki. Buildpacks beyond Nixpacks. Serverless functions. A CDN.

Each is a rabbit hole a 2 vCPU box cannot afford, and pretending otherwise would make everything above worse.

Where to go next

On this page