devstack · Guide › Concepts & the mental model
devstack is a single static Go binary that manages Docker-based dev environments and shares infrastructure across projects — one warm Postgres/Redis/MinIO for many repos, instead of a duplicate stack per repo. This page is the mental model everything else in the guide builds on.
| Term | What it is |
|---|---|
| Workspace | A directory tree with a workspace.yaml at its root. It declares the shared engines and lists the projects. This is the unit devstack operates on. |
| Project | A repo (a directory listed under projects: in workspace.yaml) with its own devstack.yaml. It declares the app services that repo runs. |
| Shared service | A warm engine (Postgres, Redis, MinIO, Kafka, …) declared once under shared: and consumed by many projects. Reached over the shared network by DNS. |
| Ledger | A machine-global SQLite database (under XDG, not committed) that ref-counts shared services, tracks allocated host ports, and records provisioning ownership. It is keyed by Docker context so WSL2's two daemons never share counts. |
# workspace.yaml (root) — the shared engines + the project list
apiVersion: devstack/v1
kind: Workspace
name: acme
shared:
postgres: { template: postgres, params: { version: "16" } }
redis: { template: redis }
projects:
- { name: api, path: services/api }
- { name: web, path: services/web }# services/api/devstack.yaml (per repo) — the app services
apiVersion: devstack/v1
kind: Project
name: api
services:
api:
template: php.laravel.nginx
uses: [workspace.shared.postgres, workspace.shared.redis]State that you own lives in exactly two committed files:
workspace.yaml— one per workspace, at the root. Shared engines + project list + workspace-wide settings.devstack.yaml— one per project repo. The services that repo runs.
Everything else devstack needs (ref counts, port allocations, provisioning ownership, the alias registry, the template cache) is machine-global, under XDG, and not committed — it is derived state that devstack can rebuild. See Workspaces and Projects for the field-by-field walkthrough, and config-reference.md for the full schema.
There is exactly one tool-owned bridge network, pinned by name:
devstack_shared
Both the shared stack and every project stack join it. Because Compose refuses to
create or remove external: true networks, devstack owns creating and cleaning
it up. Shared services are reached by stable DNS aliases, never the bare
service name:
shared-postgres shared-redis shared-minio …
So a project's app connects to shared-postgres:5432 over the shared network —
the same alias in every workspace, no matter which container is backing it.
By default devstack publishes no host ports. Services talk to shared engines
over devstack_shared via the shared-* DNS aliases — that is all container
traffic needs. Host ports are opt-in, allocated from the ledger (inside the lock)
only when you explicitly ask for host reachability:
devstack shared expose postgres # publish shared Postgres on a stable 127.0.0.1 port
devstack shared ports # show the published ports + connection stringsSee Shared services & host access for the details. (These
verbs work as top-level expose / ports and as shared expose / shared ports
aliases.)
There is no background process (in v1). Every invocation does the same thing:
load config + ledger → take a coarse cross-process lock before mutating shared
state → drive Docker (docker compose for lifecycle; the Engine SDK read-only) → exit
Consequences worth internalising:
- No autostop. Shared engines stay warm until you reclaim them
(
shared gc --stop). - Ref-count drift self-heals lazily on the next command,
shared gc, ordoctor --fix/doctor --rebuild-state. - Concurrency is safe by design. Two terminals, an IDE + a terminal, a watch script — anything that mutates the ledger or the shared stack goes through one coarse advisory lock. Reads are lock-free snapshots.
devstack up is an idempotent saga: each phase records its state, so a re-run
skips satisfied work and a crashed run resumes where it stopped. Failure
compensates the mutating phases (refs, containers) but never destroys data —
volumes and databases survive.
devstack up
│
├─ 1. Preflight ............ probe Docker/compose/git (skip: --no-preflight)
├─ 2. Network .............. ensure the devstack_shared external network exists
├─ 3. Clone / sync ......... bring project repos onto disk (skip: --skip-clone)
├─ 4. Generate ............. render compose + build artifacts from config+templates
├─ 5. Shared infra ......... up the shared engines, then health-gate them
│ (deadline: --health-timeout)
├─ 6. Provision ............ per-project Postgres role + database
│ (skip: --no-provision)
├─ 7. Project stacks ....... compose up -d the selected services
│ (--build / --rebuild, slice with --profile/-p)
└─ 8. Hooks ................ run preUp / firstRun / postUp (skip: --no-hooks)
Because the saga is idempotent, devstack up is also the everyday "make sure
everything is running" command — see Lifecycle.
- Installation, doctor & updates — get the binary and verify your host.
- Workspaces — author
workspace.yamland runinit. - Projects — author
devstack.yamland its services. - Shared services & host access — the shared engines,
expose,ports. - config-reference.md — the full two-file schema.