Scale and groups
What you get: several copies of one service behind the proxy, and a group whose web service waits for a stateful database before it starts.
Three terms carry this chapter. A replica is one of N copies of the same revision on a host:
replicas: 3 names them .1, .2, .3 and the proxy balances across all of them. An
accessory is a stateful group member (a database, a queue) with a stable name, named volumes
(a volume is storage that outlives the container), no revision, no proxy, and never recreated by a
deploy. A group is a directory with a group.yml that lists members and their needs edges;
the install order is the topological sort of those edges.
More than one replica 📈
Section titled “More than one replica 📈”This is the complete
deploy.ymlwithreplicas: 3added. Commit it and deploy:version: 2service: hellorole: webimage: traefik/whoami:v1.10hosts: [root@box.example.com]replicas: 3env:vars:BUNDANT_DEMO: "2"healthcheck:path: /timeout: 30proxy:enabled: truehost: hello.bundant.testport: 80Terminal window git add deploy.ymlgit commit -m 'run three replicas'bundant deploy -f deploy.ymldeploy hello/web rev=7db591d -> root@box.example.com[root@box.example.com] deploying default.hello.web.7db591d.1, default.hello.web.7db591d.2, default.hello.web.7db591d.3[root@box.example.com] pull skipped (present, policy=missing)[root@box.example.com] network ok[root@box.example.com] run ok: default.hello.web.7db591d.1, default.hello.web.7db591d.2, default.hello.web.7db591d.3[root@box.example.com] health gate passed (3 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.7db591d.1, default.hello.web.7db591d.2, default.hello.web.7db591d.3 on 1 host(s)Terminal window bundant status '*' -f deploy.ymldefault.hello.web rev=7db591d live=3/3 [default.hello.web.7db591d.3, default.hello.web.7db591d.2, default.hello.web.7db591d.1]scalechanges the running revision without a deploy. It never pulls an image and never edits your file. Scale up to 5:Terminal window bundant scale --replicas 5 -f deploy.yml== root@box.example.com ==revision 7db591d: 3 running (1, 2, 3) -> 5run ok (default.hello.web.7db591d.4, default.hello.web.7db591d.5)health ok (2 target(s))proxy-swap okscaled to 5 replica(s)bundant: scale is temporary - deploy.yml replicas: stays authoritative; the next `bundant deploy` re-applies it.deploy.ymlstill says 3, so after the scale upstatusshows the drift:Terminal window bundant status '*' -f deploy.ymldefault.hello.web rev=7db591d live=5/3 [default.hello.web.7db591d.5, default.hello.web.7db591d.4, default.hello.web.7db591d.3, default.hello.web.7db591d.2, default.hello.web.7db591d.1]live=5/3means five containers are running while the file still asks for three. Scale back down withbundant scale --replicas 3 -f deploy.yml; the proxy loses the two targets before the containers stop, so no in-flight request is cut off:Terminal window bundant scale --replicas 3 -f deploy.yml== root@box.example.com ==revision 7db591d: 5 running (1, 2, 3, 4, 5) -> 3proxy-swap okstop-old ok (default.hello.web.7db591d.4, default.hello.web.7db591d.5)scaled to 3 replica(s)bundant: scale is temporary - deploy.yml replicas: stays authoritative; the next `bundant deploy` re-applies it.The topology below shows both shapes: three replicas behind one proxy route, and a group whose web member depends on an accessory that owns a volume.
flowchart TD subgraph svc["hello · replicas 3"] direction LR P1["proxy<br/>hello.bundant.test"] -- "one proxy route" --> H1["replicas .1 · .2 · .3"] end subgraph grp["group shop"] direction TB P2["proxy<br/>shop.bundant.test"] -- "health gate" --> W["shop.web.web.e05b32b"] W -. "needs: db" .-> D["shop.db<br/>accessory"] D -- "named volume" --> V[("pgdata")] end H1 -- "same host" --> BRIDGE["two routes, one host"] -- "same host" --> P2 subgraph notes["notes"] direction LR N2["accessory keeps<br/>its name and data"] N1["replicas share<br/>one proxy route"] N3["needs: db<br/>is an install order"] N2 ~~~ N1 ~~~ N3 end V -. "deploy never recreates it" .-> N2 H1 -. "three containers" .-> N1 W -. "tcp 5432" .-> N3 classDef local fill:#252925,color:#40c4ff,stroke:#40c4ff; classDef ok fill:#252925,color:#00c853,stroke:#00c853; classDef storage fill:#252925,color:#ae81ff,stroke:#ae81ff; classDef neutral fill:#2e332e,color:#d9d9d4,stroke:#494848; class P1,P2 local; class H1,W ok; class D,V storage; class BRIDGE,N1,N2,N3 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.
A group with an accessory 🧩
Section titled “A group with an accessory 🧩”A group is a directory.
shop/group.ymllists the members; each member has its own complete file:Directory~/bundant-tutorial/
Directoryapp/
- deploy.yml
Directoryshop/
- group.yml
Directorydb/
- accessory.yml
Directoryweb/
- deploy.yml
shop/group.yml version: 2group: shopservices:- name: dbfile: ./db/accessory.yml- name: webfile: ./web/deploy.ymlneeds: [db]The database is an accessory: no revision, no proxy, a stable name and a named volume.
shop/db/accessory.yml version: 2kind: accessoryservice: dbimage: docker.io/library/postgres:16-alpinehosts: [root@box.example.com]env:vars:POSTGRES_DB: shopPOSTGRES_USER: shopPOSTGRES_PASSWORD: tutorialvolumes:- "pgdata:/var/lib/postgresql/data"healthcheck:tcp: 5432timeout: 60proxy:enabled: falseshop/web/deploy.ymlis a normal service file. It names the serviceweband routes toshop.bundant.test:shop/web/deploy.yml version: 2service: webimage: traefik/whoami:v1.10hosts: [root@box.example.com]healthcheck:path: /timeout: 30proxy:enabled: truehost: shop.bundant.testport: 80Validate the group, then install it:
Terminal window bundant validate -f shop/group.ymlbundant validate: OKversion: 2group: shoprevision: e05b32b (git short sha of ~/bundant-tutorial/app/shop)order: db -> webmember db: shop.db round-trip=truemember web: shop.web.web.e05b32b round-trip=truegroup installdeliberately refuses to create the database for you:Terminal window bundant group install -f shop/group.ymlbundant group install - shop rev=e05b32b order=db -> webshop.db: absent on root@box.example.combundant: accessory 'db' is not ready - start it first:bundant start -f ~/bundant-tutorial/app/shop/db/accessory.ymlgroup install report: applied [] failed [db] skipped [db, web]Start it explicitly, then install the group again:
Terminal window bundant start -f shop/db/accessory.ymlbundant group install -f shop/group.ymlbundant accessory boot - shop.db on root@box.example.com (image docker.io/library/postgres:16-alpine, runtime docker)== root@box.example.com ==container shop.db startedtcp readiness ok on :5432booted shop.db on 1 host(s)bundant group install - shop rev=e05b32b order=db -> webshop.db: ready (tcp:5432) on root@box.example.comdeploy web/web rev=e05b32b -> root@box.example.com[root@box.example.com] deploying shop.web.web.e05b32b[root@box.example.com] pull skipped (present, policy=missing)[root@box.example.com] network ok[root@box.example.com] run ok: shop.web.web.e05b32b[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 shop.web.web.e05b32b on 1 host(s)group install report: applied [db, web]Terminal window bundant group status -f shop/group.yml | grep -E '===|volumes:|replicas:|revision:|health'curl -s -H 'Host: shop.bundant.test' http://box.example.com/=== shop/db (accessory) ===volumes: pgdata=== shop/web (service) ===replicas: live 1 / expected 1revision: shop.web.web.e05b32bhealth(shop.web.web.e05b32b): HTTP 200Hostname: 7f047f6dcc1aIP: 127.0.0.1IP: ::1You should see
order: db -> webfrom validation andgroup install report: applied [db, web]after the accessory is running.needs: [db]is why web waits for the TCP probe on 5432.
What actually happens ⚙️
Section titled “What actually happens ⚙️”Each replica is a separate container named <group>.<service>.<role>.<rev>.<i> and labelled
bundant.replica=<i>, all carrying the same bundant.revision. Deploy gates each one, then makes
one proxy call with the full target set. Scaling down reverses the safety order: proxy first
(drain), then stop and remove. A group install takes one group lock, then each service’s own lock;
an accessory dependency is checked by a bare TCP connect from a throwaway busybox sidecar on the
bundant network. The rules are in the
deploy.yml reference, including
group.yml and
accessory.yml.