Bosun Manifest System
The Bosun manifest system generates Docker Compose, Traefik, and Gatus configurations from declarative service manifests. It uses a provision-based approach where reusable templates are composed together and customized through variable interpolation.
Overview
The manifest system solves the problem of maintaining consistent infrastructure configurations across multiple services. Instead of duplicating compose files, Traefik routes, and monitoring endpoints, you define a service manifest that references provisions (templates) and provides configuration values.
┌─────────────────┐ ┌──────────────┐ ┌─────────────────┐
│ Service Manifest│────▶│ Renderer │────▶│ Output Files │
│ (myapp.yml) │ │ │ │ │
└─────────────────┘ │ ┌────────┐ │ │ - compose/ │
│ │Interp. │ │ │ - traefik/ │
┌─────────────────┐ │ └────────┘ │ │ - gatus/ │
│ Provisions │────▶│ ┌────────┐ │ └─────────────────┘
│ (templates) │ │ │ Merge │ │
└─────────────────┘ │ └────────┘ │
└──────────────┘
Manifest Structure
A service manifest defines what to deploy and how to configure it.
Complete Schema
# Service name - used for interpolation and output naming
name: string # REQUIRED
# Type: "raw" for passthrough mode, omit for normal provisioning
type: string # OPTIONAL
# Provisions to apply (in order)
provisions: # OPTIONAL
- provision-name
- another-provision
# Variables for interpolation into provisions
config: # OPTIONAL
key: value
port: "8080"
domain: example.com
# Shorthand for common dependencies with defaults
needs: # OPTIONAL
- postgres
- redis
# Explicit sidecar configuration (overrides defaults)
services: # OPTIONAL
postgres:
version: "17"
db: mydb
redis:
version: "7"
# Raw compose passthrough (only used with type: raw)
compose: # OPTIONAL
service-name:
image: ...
Field Reference
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Service name, used in ${name} interpolation |
type |
string | No | Set to "raw" for compose passthrough mode |
provisions |
list | No | Provision templates to apply in order |
config |
map | No | Variables for interpolation |
needs |
list | No | Shorthand for sidecars with defaults |
services |
map | No | Explicit sidecar configuration |
compose |
map | No | Raw compose config (only with type: raw) |
Variable Interpolation
Variables use the ${varname} syntax and are replaced before YAML parsing.
Syntax
# In provisions:
compose:
services:
${name}:
image: ${image}
container_name: ${name}
environment:
PORT: "${port}"
Available Variables
Variables come from multiple sources, applied in order:
-
Built-in variables:
${name}- Service name from manifest
-
Config variables:
- Any key-value pair from the
configsection
- Any key-value pair from the
-
Sidecar variables (for sidecar provisions):
${sidecar}- Sidecar type (postgres, redis, etc.)- Sidecar-specific defaults (see Sidecars)
Type Conversion
All variable values are converted to strings:
| Type | Example | Result |
|---|---|---|
| string | "hello" |
hello |
| int | 8080 |
8080 |
| bool | true |
true |
| float | 3.14 |
3.14 |
Error Handling
Missing variables cause an error:
error: interpolate provision webapp: missing variables: ${port}, ${domain}
Merge Semantics
When multiple provisions are applied, their outputs are merged using specific strategies.
Merge Strategies by Key
| Strategy | Keys | Behavior |
|---|---|---|
| Union | networks, depends_on |
Set union (no duplicates) |
| Extend | endpoints |
Append to list |
| Replace | All other lists | Later value replaces earlier |
| Recursive | All maps | Deep merge |
Union Keys
Lists are merged as sets - duplicates are removed:
# Provision A:
networks:
- internal
# Provision B:
networks:
- internal
- proxynet
# Result:
networks:
- internal
- proxynet
Extend Keys
Lists are appended:
# Provision A:
endpoints:
- name: health
# Provision B:
endpoints:
- name: metrics
# Result:
endpoints:
- name: health
- name: metrics
Environment/Labels Normalization
The environment and labels keys are normalized from list to map before merging:
# Input (list format):
environment:
- FOO=bar
- BAZ=qux
# Normalized (map format):
environment:
FOO: bar
BAZ: qux
This allows provisions to safely merge environment variables:
# Provision A:
environment:
TZ: America/Chicago
# Provision B:
environment:
DEBUG: "true"
# Result:
environment:
TZ: America/Chicago
DEBUG: "true"
Merge Order
- Provisions are applied in the order listed
needssidecars are applied after provisionsservicessidecars are applied last- Later values override earlier values (except for union/extend keys)
Provisions
Provisions are reusable templates that generate output for one or more targets.
Provision File Structure
# Optional: inherit from other provisions
includes:
- base-provision
- another-provision
# Output for docker-compose.yml
compose:
services:
${name}:
image: ${image}
# ...
volumes:
${name}_data:
networks:
internal:
# Output for Traefik dynamic.yml
traefik:
http:
routers:
${name}:
rule: Host(`${subdomain}.${domain}`)
# ...
services:
${name}:
loadBalancer:
# ...
# Output for Gatus endpoints.yml
gatus:
endpoints:
- name: ${name}
url: https://${subdomain}.${domain}
# ...
Inheritance with Includes
Provisions can inherit from other provisions:
# webapp.yml
includes:
- container
- healthcheck
- reverse-proxy
- homepage
- monitoring
Inheritance chain:
- Load all included provisions first
- Merge them together in order
- Merge this provision on top
Cycle detection:
The system tracks loaded provisions and skips already-loaded ones to prevent infinite loops.
Built-in Provisions
| Provision | Description | Required Variables |
|---|---|---|
container |
Base container configuration | image |
healthcheck |
HTTP health check | port |
reverse-proxy |
Traefik integration | subdomain, domain, port |
homepage |
Homepage dashboard labels | group, icon, description |
monitoring |
Gatus endpoint | subdomain, domain, group |
webapp |
All of the above combined | All of the above |
postgres |
PostgreSQL sidecar | version, db, db_password |
redis |
Redis sidecar | version |
Creating Custom Provisions
Place .yml files in your provisions directory:
# provisions/my-custom.yml
compose:
services:
${name}:
labels:
my.custom.label: "${custom_value}"
Sidecars
Sidecars are auxiliary services (databases, caches) that run alongside your main service.
Using needs Shorthand
The simplest way to add a sidecar:
name: myapp
provisions:
- container
needs:
- postgres
- redis
config:
image: myapp:latest
db_password: secret123
Sidecar Defaults
When using needs, these defaults are applied:
| Sidecar | Defaults |
|---|---|
postgres |
version: "17", db: ${name}, db_password: ${db_password} |
redis |
version: "7" |
mysql |
version: "8", db: ${name}, db_password: ${db_password} |
mongodb |
version: "7", db: ${name} |
Explicit Sidecar Configuration
Override defaults using services:
name: myapp
provisions:
- container
services:
postgres:
version: "16" # Override version
db: custom_db_name # Override database name
config:
image: myapp:latest
db_password: secret123
What Sidecars Inject
A sidecar provision typically adds:
- Sidecar service (
${name}-db,${name}-redis) - Environment variables on the main service
- depends_on relationship
- Shared network (internal)
- Persistent volume
Example postgres sidecar output:
services:
myapp-db:
image: postgres:17-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_USER: postgres
POSTGRES_PASSWORD: secret123
volumes:
- myapp_db_data:/var/lib/postgresql/data
networks:
- internal
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
myapp:
environment:
POSTGRES_HOST: myapp-db
POSTGRES_PORT: "5432"
POSTGRES_DB: myapp
POSTGRES_USER: postgres
POSTGRES_PASSWORD: secret123
depends_on:
- myapp-db
networks:
- internal
volumes:
myapp_db_data:
networks:
internal:
Output Targets
The manifest system generates three output types.
Compose Output
Docker Compose configuration in compose/<stack>.yml:
services:
myapp:
image: ghcr.io/example/myapp:latest
container_name: myapp
restart: unless-stopped
environment:
TZ: America/Chicago
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/health"]
interval: 30s
timeout: 5s
retries: 3
labels:
traefik.enable: "true"
traefik.http.routers.myapp.rule: Host(`myapp.example.com`)
networks:
- proxynet
Traefik Output
Traefik dynamic configuration in traefik/dynamic.yml:
http:
routers:
myapp:
rule: Host(`myapp.example.com`)
service: myapp
entrypoints:
- websecure
tls:
certResolver: letsencrypt
services:
myapp:
loadBalancer:
servers:
- url: http://myapp:8080
Gatus Output
Gatus monitoring endpoints in gatus/endpoints.yml:
endpoints:
- name: myapp
group: Applications
url: https://myapp.example.com
interval: 60s
conditions:
- "[STATUS] == 200"
Stacks
Stacks combine multiple service manifests into a single deployment.
Stack File Structure
# Include service manifest files
include:
- service-a.yml
- service-b.yml
- service-c.yml
# Network definitions for the stack
networks:
default:
driver: bridge
proxynet:
external: true
Values Overlay
Apply configuration overrides to all services in a stack:
# values.yml
domain: production.example.com
db_password: production_secret
Examples
Simple Web App
A minimal web application with health checks and reverse proxy:
# services/stirling-pdf.yml
name: stirling-pdf
provisions:
- container
- healthcheck
- homepage
- reverse-proxy
- monitoring
config:
image: frooodle/s-pdf:latest
port: "8080"
subdomain: pdf
domain: example.com
group: Tools
icon: mdi-file-pdf-box
description: PDF toolkit
Or using the webapp bundle:
name: stirling-pdf
provisions:
- webapp
config:
image: frooodle/s-pdf:latest
port: "8080"
subdomain: pdf
domain: example.com
group: Tools
icon: mdi-file-pdf-box
description: PDF toolkit
App with Database Sidecar
Using needs shorthand:
name: myapp
provisions:
- webapp
needs:
- postgres
config:
image: ghcr.io/example/myapp:latest
port: "3000"
subdomain: myapp
domain: example.com
group: Apps
icon: mdi-application
description: My Application
db_password: secret123
With explicit sidecar configuration:
name: norish
provisions:
- container
- healthcheck
- homepage
- reverse-proxy
- monitoring
services:
postgres:
version: "17"
db: norish
db_password: production_secret
config:
image: ghcr.io/norishapp/norish:latest
port: "3000"
subdomain: recipes
domain: example.com
group: Apps
icon: mdi-food
description: Recipe manager
App with Custom Traefik Config
Override or extend Traefik configuration:
name: api-gateway
provisions:
- container
- reverse-proxy
config:
image: ghcr.io/example/api:latest
port: "8080"
subdomain: api
domain: example.com
Then create a custom provision for additional middleware:
# provisions/api-middleware.yml
traefik:
http:
middlewares:
${name}-ratelimit:
rateLimit:
average: 100
burst: 50
${name}-cors:
headers:
accessControlAllowMethods:
- GET
- POST
accessControlAllowOriginList:
- https://${domain}
routers:
${name}:
middlewares:
- ${name}-ratelimit
- ${name}-cors
Multi-Service Stack
# stacks/production.yml
include:
- frontend.yml
- api.yml
- worker.yml
networks:
proxynet:
external: true
internal:
driver: bridge
# services/frontend.yml
name: frontend
provisions:
- webapp
config:
image: ghcr.io/example/frontend:latest
port: "3000"
subdomain: app
domain: example.com
group: Production
icon: mdi-web
description: Frontend application
# services/api.yml
name: api
provisions:
- webapp
needs:
- postgres
- redis
config:
image: ghcr.io/example/api:latest
port: "8080"
subdomain: api
domain: example.com
group: Production
icon: mdi-api
description: API server
db_password: ${API_DB_PASSWORD}
# services/worker.yml
name: worker
provisions:
- container
needs:
- redis
config:
image: ghcr.io/example/worker:latest
Raw Passthrough Mode
For services that need full compose control:
name: legacy-app
type: raw
compose:
legacy-app:
image: legacy/app:v1
container_name: legacy-app
restart: unless-stopped
volumes:
- /custom/path:/data
ports:
- "9000:9000"
environment:
CUSTOM_VAR: value
Directory Structure
Recommended project layout:
manifest/
├── provisions/ # Provision templates
│ ├── container.yml
│ ├── healthcheck.yml
│ ├── reverse-proxy.yml
│ ├── homepage.yml
│ ├── monitoring.yml
│ ├── webapp.yml
│ ├── postgres.yml
│ └── redis.yml
├── services/ # Service manifests
│ ├── myapp.yml
│ └── another-app.yml
├── stacks/ # Stack definitions
│ └── production.yml
└── output/ # Generated files
├── compose/
│ └── production.yml
├── traefik/
│ └── dynamic.yml
└── gatus/
└── endpoints.yml
CLI Usage
# Render a single service
bosun manifest render services/myapp.yml
# Render a stack
bosun manifest render-stack stacks/production.yml
# Render with values overlay
bosun manifest render-stack stacks/production.yml --values values.yml
# Dry run (show output without writing files)
bosun manifest render services/myapp.yml --dry-run
# List available provisions
bosun manifest list-provisions