Your first service
What you get: every line of deploy.yml read, a change validated, and the deploy that follows.
The file, field by field 📝
Section titled “The file, field by field 📝”This is the complete file from the quick start, with a role, an environment variable and an explicit health check added. The app directory still holds that one file:
Directory~/bundant-tutorial/
Directoryapp/
- deploy.yml
version: 2service: hello # identifier: letters, digits, - or _role: web # defaults to webimage: traefik/whoami:v1.10 # the host pulls this, or --local-image streams ithosts: [root@box.example.com] # user@host:port forms are allowedenv:vars:BUNDANT_DEMO: "1" # merged into the container environment, never printedhealthcheck:path: / # probed by a throwaway sidecar before traffic movestimeout: 30proxy:enabled: truehost: hello.bundant.test # Host header that selects this serviceport: 80 # port the proxy targetsThe mind map groups the fields by what they change. Container side decides what process runs; routing side decides which HTTP requests reach it; identity and placement decide where it runs and what it is called.
flowchart TD root["deploy.yml<br/>the whole contract"] subgraph fields["what the fields decide"] direction TB id["identity<br/>what it is called<br/>service · role · rev"] ctr["container side<br/>what runs<br/>image · env · health · replicas"] rt["routing side<br/>who reaches it<br/>host · port"] id ~~~ ctr ~~~ rt end root -- "three groups" --> fields subgraph notes["what a change does"] direction LR cnote["image/env change<br/>starts a new container"] inote["round-trip ok<br/>name composed"] cnote ~~~ inote end fields -. "read together" .-> notes classDef local fill:#252925,color:#40c4ff,stroke:#40c4ff; classDef ok fill:#252925,color:#00c853,stroke:#00c853; classDef neutral fill:#2e332e,color:#d9d9d4,stroke:#494848; class root,ctr local; class inote ok; class id,rt,cnote neutral;Colour legend: blue = an action on your machine or a normal step · green = healthy or live · orange = the one irreversible step · purple = storage and state.
field what it decides service/rolethe name: <group>.<service>.<role>.<revision>;roleis a segment for variants of one service, defaultwebimagewhat to run; no registry is required when you use --local-imagehostsone or more ssh targets; BUNDANT_HOSTSoverrides the listenvenvironment variables (name/value pairs passed into the container); values never appear in output healthcheckthe gate that must pass before the proxy points at the new container proxyenabled: falsefor workers; when enabled,hostis requiredThe value of
proxy.hostis the route host: theHost:header a request must carry for the proxy to route it to this service. Two services on one host are told apart by that header, not by a port.Commit the edit. The new git short sha is the new revision:
Terminal window git add deploy.ymlgit commit -m 'add health, env and role'Validate before you deploy ✅
Section titled “Validate before you deploy ✅”Terminal window bundant validate -f deploy.ymlbundant validate: OKversion: 2group: defaultservice: hellorole: webrevision: 0f2c8cc (git short sha of ~/bundant-tutorial/app)container: default.hello.web.0f2c8ccproxy id: default.hello.webenv path: ~/.bundant/env/default/hello-web.envlock key: default/helloround-trip: okThe default output prints the identity a deploy would use.
--jsonadds one JSON object on stdout with the same identity:Terminal window bundant validate -f deploy.yml --json{"ok": true,"version": 2,"versionExplicit": true,"notes": [],"errors": [],"identity": { "kind": "service", "group": "default", "service": "hello" }}Validation parses the file, checks every field and the composed name, and prints the identity a deploy would use. It does not open an ssh connection and does not take the lock.
Deploy 🚀
Section titled “Deploy 🚀”Terminal window bundant deploy -f deploy.ymldeploy hello/web rev=0f2c8cc -> root@box.example.com[root@box.example.com] deploying default.hello.web.0f2c8cc[root@box.example.com] pull skipped (present, policy=missing)[root@box.example.com] network ok[root@box.example.com] run ok: default.hello.web.0f2c8cc[root@box.example.com] health gate passed (1 target(s))[root@box.example.com] proxy-ensure ok[root@box.example.com] proxy-swap ok[root@box.example.com] stop-old okdeployed default.hello.web.0f2c8cc on 1 host(s)On a host with only sshd,
bundant bootstrapinstalls podman and enables its socket first; the same phases then run over the podman endpoint instead of the docker one.What the phases mean:
- pull — the host already had the image, so this was a no-op. Without a registry,
--local-imagestreams it over the same ssh connection. - network — the shared
bundantnetwork exists. - run — the new container starts with
--restart unless-stopped; it publishes no host port. - health gate — a throwaway sidecar requests
healthcheck.path; a failure aborts before traffic moves. - proxy-ensure / proxy-swap — the proxy container is running, then its target set is swapped in one call and old connections drain.
- stop-old — the previous revision is stopped, not removed; it is the rollback material.
Terminal window bundant history -f deploy.yml== root@box.example.com (hello-web) ==* default.hello.web.0f2c8cc 1 replica Up 2026-10-20 21:31:44 +0000 UTCdefault.hello.web.11a45b2 1 replica Exited (2) Less than a second ago 2026-10-20 21:31:42 +0000 UTC* = running revisionbundant history -f deploy.ymlshows every retained revision and marks the running one with*.You should see
health gate passedfollowed byproxy-swap okanddeployed ... on 1 host(s);historymarks the new revision with*and keeps the old one stopped.- pull — the host already had the image, so this was a no-op. Without a registry,
What actually happens ⚙️
Section titled “What actually happens ⚙️”Validate runs the same zod preflight that every mutating command runs — before it creates an ssh
executor or takes the lock — so an invalid file causes zero host contact. Deploy then builds a
typed ContainerSpec (image, merged env, labels bundant.kind/bundant.revision/
bundant.replica) and posts it to /containers/create; healthcheck.port defaults to
proxy.port. The proxy route id is stable across revisions: <group>.<service>.<role>. See
Validation, the
deploy.yml reference and
Architecture.