Snapshot your database before every deploy. Gate on migration success.
Roll back automatically if health checks fail. One TOML file, one binary, no cluster required.
demo.mp4
Baton manages your entire stack on one machine: containers, native processes, workers, cron jobs. It handles dependency ordering, health checks, crash recovery with backoff, service discovery via environment variables, and graceful shutdown.
What makes it different: baton deploy snapshots your stateful services before every deploy, runs migrations in dependency order, gates on health checks, and rolls back automatically if anything fails. No other single-node tool does this.
[app]
name = "myapp"
domain = "myapp.com"
[[service]]
name = "db"
image = "postgres:16"
volume = "pg_data"
backup = "pg_dump"
[[service]]
name = "redis"
image = "redis:7"
[[service]]
name = "api"
run = "./api serve"
port = 4000
health = "/health"
after = ["db", "redis"]
migrate = "./api migrate"
[[service]]
name = "worker"
run = "./api process-jobs"
after = ["db", "redis"]
[[service]]
name = "reports"
run = "./api generate-reports"
schedule = "0 2 * * *"
after = ["db"]$ baton up --ui
loaded 3 vars from .env
starting myapp...
[ok] db postgres:16 on :5432
[ok] redis redis:7 on :6379
[ok] api ./api serve on :4000
[ok] worker ./api process-jobs running
[ok] reports ./api generate-reports scheduled (0 2 * * *)
[ui] dashboard at http://localhost:9500
all services running. ctrl+c to stop.
$ baton deploy
loaded 3 vars from .env
deploying myapp...
snapshotting stateful services...
[ok] db (pg_dump)
[ok] redis (redis)
running migrations...
api ... ok
restarting services...
[ok] api (container)
[ok] worker (signalled)
checking health...
api :4000/health ... ok
deploy complete.
If the health check had failed, baton would have restored the database snapshot and reported the rollback. No manual intervention required.
cargo install baton
Or build from source:
git clone https://github.com/michaelmillar/baton.git
cd baton
cargo build --release
cd your-project
baton init # detects your stack, generates baton.toml
baton up # starts everything (development)
baton up --ui # starts everything + web dashboard on :9500
baton deploy # safe production deploy with snapshots + rollback
baton init detects Rust, Go, Node.js, Elixir, and Dockerfile projects automatically.
baton deploy runs the following steps in order:
- Validate config and check all services are reachable
- Snapshot stateful services (Postgres via pg_dump, Redis via BGSAVE, or custom command)
- Migrate in dependency order (each service's
migratecommand, if set) - Restart application services
- Health gate (wait for each service's
/healthendpoint to pass) - Roll back if health fails (restore snapshot, report failure)
- Record the full event timeline to
.baton/history.json
If any step fails, everything after it is skipped and the appropriate rollback runs.
baton deploy # safe deploy: snapshot, migrate, restart, health gate
baton rollback # restore the latest snapshot
baton rollback <id> # restore a specific snapshot
baton history # show deploy timeline
baton snapshot # take a manual snapshot without deploying
baton restore <id> # restore a specific snapshot manually
For known database images, baton snapshots automatically:
| Image | Method | Automatic |
|---|---|---|
postgres:* |
pg_dump | Yes |
redis:* |
BGSAVE + copy | Yes |
For anything else, set the backup field to a shell command. Baton sets BATON_SNAPSHOT_PATH and BATON_SERVICE_NAME in the environment. Your command must write to $BATON_SNAPSHOT_PATH.
[[service]]
name = "search"
image = "elasticsearch:8"
backup = "./scripts/backup-elastic.sh"Snapshots are stored locally in .baton/snapshots/ with a meta.json manifest.
baton add postgres
baton add redis
baton add worker --run "./app process-jobs"
baton add cron --name reports --run "./app report" --schedule "0 2 * * *"
baton add static
baton add spa
baton add process --name api --run "./api serve" --port 4000
Known types: postgres, redis, mysql, mariadb, mongo, rabbitmq, nats, worker, cron, static, spa, process.
[app]
name = "myapp"
domain = "myapp.com" # enables reverse proxy with subdomain routing
proxy_port = 8443 # reverse proxy port (default 8443)Each [[service]] must have one of run, build, image, or static.
| Field | Type | Purpose |
|---|---|---|
name |
string | Unique service name |
run |
string | Shell command to execute |
image |
string | Container image to pull and run |
build |
string | Path to build context (Dockerfile) |
static |
string | Path to static files to serve |
port |
int | Port the service listens on |
health |
string | HTTP health check path |
after |
list | Services that must start first |
volume |
string | Named volume for persistent data |
schedule |
string | Cron expression for scheduled tasks |
backup |
string | Backup method or command (auto-detected for postgres/redis) |
migrate |
string | Migration command to run during deploys |
runtime |
string | Runtime hint (e.g. "beam" for Elixir) |
spa |
bool | Enable SPA routing for static sites |
Override the app domain per environment:
[environments.staging]
domain = "staging.myapp.com"
[environments.prod]
domain = "myapp.com"baton up --env staging # uses staging.myapp.com as domain
Baton injects environment variables so services can find each other:
| Service type | Variables |
|---|---|
| Any service with a port | {NAME}_HOST, {NAME}_PORT |
| Postgres | DATABASE_URL |
| Redis | REDIS_URL |
| MySQL/MariaDB | DATABASE_URL |
| MongoDB | MONGO_URL |
Database passwords are generated per app (stable across restarts) or overridden via .env:
# .env
POSTGRES_PASSWORD=my-secure-password
baton up --ui # dashboard on :9500
baton up --ui --ui-port 8080 # custom port
The dashboard shows live service state, updating every 2 seconds. It reflects actual process status: running, restarting, crashed, stopped. Restart counts are tracked per service.
When you press ctrl+c, baton:
- Sends SIGTERM to all managed processes
- Waits up to 10 seconds for each to exit
- Sends SIGKILL to any that did not stop
- Stops containers in reverse dependency order
See the examples directory:
- simple-api ... API with Postgres, Redis, worker, and scheduled reports
- static-site ... SPA with static file serving
- multi-service ... Multiple services with environment overrides
Every existing tool either requires Docker and cannot manage native processes, or manages native processes and cannot manage containers. Nothing does both from a single config, as a single binary, with deploy-aware state management.
| Tool | Native processes | Containers | Cron | Deploy snapshots | Migration gates | Auto rollback | Single binary |
|---|---|---|---|---|---|---|---|
| Docker Compose | No | Yes | No | No | No | No | No (needs Docker) |
| Nomad | Yes | Yes | Yes | No | No | No | Yes |
| Dokku | No | Yes | Plugin | No | No | No | No (shell scripts) |
| Coolify | No | Yes | Partial | No | No | No | No (Laravel) |
| Kamal | No | Yes | No | No | No | Yes (deploys) | No (Ruby gem) |
| systemd | Yes | Yes (Podman) | Yes (timers) | No | No | No | Built-in |
| PM2 | Yes | No | Partial | No | No | No | No (Node.js) |
| Baton | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
Where baton is stronger. Mixed runtime (a Go binary, a Postgres container, and a Python cron job in the same TOML). Pre-deploy database snapshots with automatic rollback on health failure. Migration orchestration in dependency order. Single binary with zero runtime dependencies.
Where baton is weaker. Compose has a much larger ecosystem and community. Dokku and Coolify handle TLS and git-push workflows. Kamal has zero-downtime rolling deploys across multiple hosts. Nomad scales to clusters. systemd has decades of battle-testing and cgroup resource limits. Baton has none of these yet.
The closest alternative is Nomad. Nomad already covers mixed workloads, periodic jobs, and has a web UI. But Nomad was designed for clusters, carries that weight in configuration complexity, and has no concept of pre-deploy snapshots, migration gates, or automatic data rollback. Baton is what you would build if you wanted Nomad's capability model scoped to a single node with deploy safety built in.
82 tests. Single binary. Zero external dependencies at runtime.
Not yet implemented:
- TLS via Let's Encrypt
- Rolling deploys / zero-downtime updates
- Log aggregation
- Resource limits (CPU/memory)
- MySQL/MariaDB snapshot support (Postgres and Redis are supported)
- Remote snapshot storage
MIT