pinball.sh

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.

v0.1 · the agent is done · the control plane is being built now

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

Replacesssh into the boxdeploy.shdeploy-prod.shwhich host was it again?

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

POST /v1/deploy · TLS + scoped token

Control plane

one binary + SQLite · resolves and dispatches

the only inbound surface in the system

agent dials out · mTLS · persistent
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

agentjob 9f2c1a · stack lgkentang-pinball-staging
agentlock acquired
dockerlogin ··· Succeeded
dockerpull lgkentang/pinball:5efd1b2 ··· done
gitremote set-url origin (existing repo)
gitfetch origin staging
git7ab9bb3..5efd1b2 staging -> origin/staging
gitreset --hard 5efd1b2
composepull ··· done
composeup -d --wait
composeContainer pinball-web-1 Recreated
composeContainer pinball-web-1 Waiting
composeContainer pinball-web-1 Healthy
agentlock released
statussuccess

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

dev
docker-compose.dev.yml
Development VPS
staging
docker-compose.staging.yml
Staging VPS
main
docker-compose.prod.yml
Production VPS

(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.

SSH scriptspinball.shKubernetes

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

M0Agent — one deploy, end to endVerified against real Docker and git.Done
M1Control plane skeletonSQLite, registries, /v1/deploy.Planned
M2Security & auditEmbedded CA, mTLS, enrollment, scoped tokens.Planned
M3Multi-team & multi-hostTenant scoping, routing across hosts.Planned
M4Rollbacks & deployment UIHealth verification, drift detection.Planned

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.

Deploy by pushing git

Keep Compose. Skip Kubernetes. Delete the SSH scripts.