GitOps for ordinary servers
Deploy by pushing git
Your repo's Compose file becomes the running state of your VPS.
No SSH. No scripts. No Kubernetes.
You
git push
a branch, like always
CI
builds image
pushes it to your registry
Control plane
pinball
resolves the registry, dispatches the job
Your VPS
✓ healthy
compose up, healthchecks pass
The only part you write
POST /v1/deploy
Authorization: Bearer <deploy-token>
{
"project": "pinball",
"environment": "staging",
"image_tag": "abc1234"
}What you get
— What actually changes
One way to deploy everything
Same model in every repo. No deploy.sh, no deploy-prod.sh.
Developers never touch the server
Push the branch. That's the whole interface.
Healthy, not just started
up -d --wait gates success on your healthchecks.
No lock-in
Plain Compose in your repo. Delete Pinball, it still works by hand.
How it works
— Two planes, one job
Control plane
Decides. Never runs Docker.
- ›Project & environment registry
- ›Resolves and dispatches jobs
- ›Deployment history
- ›Audit logs
Agent
Executes. Only its own machine.
- ›Outbound connection only
- ›Git operations & image pulls
- ›Per-stack deployment locking
- ›Streaming logs & health checks
CI
your pipeline, anywhere
Control plane
one binary + SQLite · resolves and dispatches
the only inbound surface in the system
jobs are pushed back down this same open link
Agent · your VPS
git + docker compose, on its own machine
no listener · no open port · zero inbound
Proof
— A real redeploy, streamed back from the agent
M0, verified against real Docker and git: a new commit on staging, fetched and reset, container recreated, and success only once it was healthy — not merely once it started. Reconnect-with-backoff was proven by killing the control plane mid-session; the deploy kept running, because docker compose lives on the host, not on the wire.
Environments
— Your repo already has the answer
(project, environment) → branch + compose file + agent. That's the whole routing decision.
Security
— Nothing on your VPS listens
Agent dials out
No listener, no open port, zero inbound surface per host.
Mutual TLS
Rejected at the handshake without a valid cert. The CP is its own CA.
Jobs carry no secrets
The agent holds its own git and registry creds.
Why not just…
— Where this sits, and what it isn't
…SSH and a script?
Fine until five repos and three hosts. Then every repo deploys differently.
…Kubernetes?
A real answer at scale. Overkill for a few VPSs.
What it is not
- ✕Kubernetes
- ✕A PaaS
- ✕A container scheduler
- ✕A Terraform replacement
- ✕A secret manager
- ✕A service mesh
What it is
A lightweight deployment control plane.
Infrastructure should be boring.
Git should govern operations.
Developers shouldn't SSH into production.
Status
— Where this actually is
The POST /v1/deploy shown above is the M1 contract — designed and locked, not yet shipped. Watch the repo if you want to know when it lands.