Skip to content

Coming from docker-compose

If you think in compose, this page re-bases that thinking. It’s deliberately not a converter: doze’s model differs in ways worth adopting, not transliterating. Ten minutes here saves the “wait, where’s my depends_on” moments later.

You knowdoze’s counterpartThe difference that matters
services: entriesEngine blocks: postgres "app" { … }The block type picks real software (a signed module), not an image name from a registry of mutable tags.
image: postgres:16version = 16Resolves to a pinned exact version in doze.lock:16 moves, your lock doesn’t.
ports: ["5432:5432"]port = 5432There’s no NAT — the engine listens on localhost natively. What you declare is what lsof shows.
depends_on:References: sqs = sqs.jobs.nameThe dependency is derived from the config value, validated at lint, and boots in order automatically.
healthcheck:Built-in readiness per engineModules know their engine’s real readiness (a socket accept, a protocol ping); you don’t write retry loops.
volumes:Nothing to writeEach instance gets its own data dir under the project’s doze home. doze reset when you want a clean slate.
.env / environment:variable blocks + --var / DOZE_VAR_*Typed, referenced expressions instead of string interpolation.
docker compose up -ddoze upConverges structure, boots in dependency order, then everything idles to zero when unused — the part compose has no equivalent for.
docker compose down -vdoze down / doze resetdown sleeps everything; reset wipes data (re-clones fresh).
docker compose logs -f appdoze logs -f appEngine logs are also just files on your disk.
docker exec -it db psqldoze shell appA real psql to a native process — no exec, no TTY plumbing.

What has no equivalent — in either direction

Section titled “What has no equivalent — in either direction”

Compose things you stop doing: waiting for a VM; host.docker.internal; bind-mount permission archaeology; hand-rolled wait-for-it.sh; pulling images on hotel wifi; picking restart policies for a laptop.

doze things compose can’t say:

  • Convergence. role "app" { … }, extension "pgvector" {}, buckets, queues, grants — declared in the same file and converged into the running engine. In compose-world this lives in init-script volumes and entrypoint hacks.
  • Structure, not data. doze creates roles/databases/buckets; your migrations own schema and rows. doze sync reconciles when you change the declaration.
  • The lockfile. doze.lock pins engines, modules, and publisher keys — compose has image digests if you remember to use them; nobody does.
  • Sleep. Idle instances reap to zero and wake on the next connection in well under a second. Your laptop is quiet because nothing is running.
# docker-compose.yml (before)
services:
db:
image: postgres:16
ports: ["5432:5432"]
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: app
volumes: ["dbdata:/var/lib/postgresql/data"]
cache:
image: redis:7
ports: ["6379:6379"]
minio:
image: minio/minio
command: server /data
ports: ["9000:9000"]
volumes:
dbdata:
# doze.hcl (after)
postgres "db" {
version = 16
port = 5432
owner = "app"
role "app" {
password = "app"
login = true
}
}
valkey "cache" {
version = 9 # the open-source Redis lineage
port = 6379
}
s3 "uploads" {
port = 9000 # S3 API, no MinIO container
}

Then doze up, commit doze.hcl + doze.lock, and delete the YAML. Your app’s connection strings don’t change — same ports, same localhost.

Read next: Core concepts for the model underneath, and Why HCL if the config language choice needs justifying to your team.