Manifest examples
Progressive examples, from a one-service deployment to a multi-network
deployment with public ingress. Every example is a jaco.yaml plus a
docker-compose.yml pair; copy them into the same directory and run
jaco apply <jaco.yaml> — the compose file auto-resolves.
0. Routes-only: compose supplies the rest
The slimmest legal jaco.yaml (issue #99). No services: block —
every compose service inherits a single-replica spread placement,
and deploy.replicas in the compose file (if set) supplies the
default count. The overlay only declares the public route, because
that's the only thing compose itself can't express.
jaco.yaml:
deployment: slim
routes:
- domain: slim.example.com
service: web
port: 80
tls: autodocker-compose.yml:
services:
web:
image: nginx:1.27
deploy:
replicas: 3The merged ServiceSpec lands web at three replicas, placement
spread, networks [_default]. Add a services: entry to override
any of those (see Example 5 for a multi-service overlay).
1. One service, no ingress
The shortest legal pair. Three replicas of nginx, spread across the cluster, no routes.
jaco.yaml:
deployment: hello
services:
- name: web
replicas: 3docker-compose.yml:
services:
web:
image: nginx:1.27
healthcheck:
test: ["CMD", "curl", "-fsS", "http://127.0.0.1/"]
interval: 5s
timeout: 3s
retries: 5Replicas are reachable east-west by their service name (web) inside
the deployment's default network. Not reachable from outside the
cluster — that needs a routes entry.
2. One service, public route with auto TLS
The canonical sample from cmd/jaco/testdata/sample.jaco.yaml. Adds a
public route so the embedded Caddy on every node serves
web.example.com from a healthy replica of web.
jaco.yaml:
deployment: sample
services:
- name: web
replicas: 2
routes:
- domain: web.example.com
service: web
port: 80
tls: autodocker-compose.yml: same as example 1.
For tls: auto to succeed, DNS for web.example.com must resolve to
at least one cluster node's public IP. Until then, the cert state in
jaco status stays pending with retry backoff and plaintext HTTP
continues to serve.
3. Host pinning
A database-style workload that must run only on specific hosts (perhaps because they have the right disk).
deployment: data
services:
- name: db
replicas: 1
placement: hosts
hosts: [storage-1]services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: example
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
volumes:
pgdata:placement: hosts with a single host means a single home. If
storage-1 is unreachable, jaco status data reports pending: cannot satisfy host placement: storage-1 unreachable and no replica is
scheduled elsewhere.
The pgdata volume lands on storage-1 as the literal docker volume
jaco_data_pgdata — the jaco_<deployment>_<key> prefix prevents
collisions with any other deployment that also declares a pgdata
key. See Supported compose fields → volumes
and Migration → How JACO names volumes.
4. Two networks inside one deployment
Same pattern as testdata/isolation/dep-front.jaco.yaml (used by the
isolation rig). Two services attached to two disjoint networks; they
cannot reach each other.
jaco.yaml:
deployment: split
services:
- name: svc-a
replicas: 2
networks: [net-a]
- name: svc-b
replicas: 2
networks: [net-b]docker-compose.yml:
services:
svc-a:
image: busybox:1.36
command: ["sh", "-c", "nc -lk -p 9999 -e /bin/sh"]
networks: [net-a]
svc-b:
image: busybox:1.36
command: ["sh", "-c", "nc -lk -p 9998 -e /bin/sh"]
networks: [net-b]
networks:
net-a: {}
net-b: {}svc-a → svc-b resolution returns NXDOMAIN; even by-IP attempts are
dropped at FORWARD by the per-(deployment, network) nftables rules.
See Isolation.
5. Multi-tier with shared and segregated networks
A web tier on a frontend network, a db on a backend network, a gateway service that bridges both.
deployment: app
services:
- name: web
replicas: 3
networks: [frontend]
- name: gateway
replicas: 2
networks: [frontend, backend]
- name: db
replicas: 1
placement: hosts
hosts: [storage-1]
networks: [backend]
routes:
- domain: app.example.com
service: web
port: 80
tls: auto
- domain: app.example.com
service: gateway
port: 8080
tls: auto
path: /api/services:
web:
image: nginx:1.27
networks: [frontend]
gateway:
image: ghcr.io/example/gateway:1.0
networks: [frontend, backend]
environment:
DB_HOST: db
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: example
volumes:
- pgdata:/var/lib/postgresql/data
networks: [backend]
volumes:
pgdata:
networks:
frontend: {}
backend: {}webreachesgatewayoverfrontend;gatewayreachesdboverbackend.webcannot reachdb— they share no network.- Two routes for
app.example.comco-exist because their paths differ (/api/and the implicit""catch-all). The longer prefix wins.
6. Raw-TCP ingress alongside HTTP
A redis service exposed publicly on TCP 6379 from every node; plus a web tier on HTTPS.
deployment: cache
services:
- name: redis
replicas: 1
placement: hosts
hosts: [storage-1]
- name: web
replicas: 2
routes:
- domain: cache.example.com
service: web
port: 80
tls: autoservices:
redis:
image: redis:7-alpine
ports:
- "6379:6379"
web:
image: ghcr.io/example/cache-ui:1.0
networks: {}The redis ports: ["6379:6379"] declares a cluster-wide TCP listener:
every node accepts TCP on :6379 and forwards to the (single) redis
replica wherever it runs. Failover to a surviving replica is the same
mechanism as HTTP — within the route-removal window after the
unhealthy replica is observed.
Publishing 80 or 443 is rejected — those belong to the Caddy ingress.
7. Stack-scoped .env for compose-style ${VAR} interpolation
Move registry URLs, database credentials, and any other per-stack
value out of the committed compose file and into a sibling .env
the CLI loads at apply time. The jaco.yaml top-level
environment: <path> is the entry point;
every ${VAR} in the compose document interpolates from there.
Real-world bench shape under
tests/samples/jaco/env-interpolation/.
jaco.yaml:
deployment: env-demo
environment: .env # path relative to this jaco.yaml
routes:
- domain: env-demo.example.com
service: api
port: 8080
tls: offcompose.yml:
services:
api:
image: ${REGISTRY:-docker.io}/myorg/api:1
environment:
DB_URL: ${DB_URL}
REGION: ${AWS_REGION:-us-east-1}.env (sibling of jaco.yaml; keep out of git):
REGISTRY=ghcr.io
DB_URL=postgres://db.internal/env-demo
# AWS_REGION not set; the ${AWS_REGION:-us-east-1} default applies.What the daemon receives, after CLI-side interpolation:
services:
api:
image: ghcr.io/myorg/api:1
environment:
DB_URL: postgres://db.internal/env-demo
REGION: us-east-1The interpolation source is the env file only — the operator's
process environment does not participate. Service-level env_file:
keeps working alongside the new field; the precedence rules are in
Supported compose fields → env_file resolution.