Concepts
envctl is a thin layer over docker-compose. It does not replace compose, run a daemon, or keep a database. It rewrites a compose project so that many copies of it can run side by side.
Environment
Section titled “Environment”An environment is the unit of isolation: one running copy of the stack on one backend, with a name. Today the only backend is local, the Docker host in front of you. A VM backend arrives later; see Roadmap.
By default the name is a slug of the git branch of the current worktree: feat/imported-traffic-pricing becomes feat-imported-traffic-pricing. On a detached HEAD the worktree directory name is used. Name one explicitly with --env, for example a throwaway --env scratch, or with envctl env create.
The compose project name is <prefix>-<name>, for example mg-feat-imported-traffic-pricing. That name prefixes every container, network, and volume, which is what keeps environments apart.
Linked branch
Section titled “Linked branch”The branch is an attribute of an environment, not its identity. A default-named environment is linked to its branch when first created. envctl env link points an environment at another branch, env unlink detaches it. Locally the code always comes from the worktree you run in; the link decides which pushes update the environment in CI. Environments created with env create are kept: CI never destroys them on merge.
Each environment’s record lives at .envctl/<name>/env.json in the worktree: name, backend, linked branch, dataset, parent, kept, timestamps.
Manifest
Section titled “Manifest”envctl.yaml at the repository root tells envctl which compose files make up the stack, which port mode to use, and which services are user-facing entrypoints. It is the whole contract between a repository and envctl. See Configuration.
Rendered project
Section titled “Rendered project”envctl up loads the compose files with compose-go, the library Docker’s own compose uses, then rewrites the in-memory project:
| Rewrite | Effect |
|---|---|
| Project name | Namespaces containers, networks, and volumes. |
| Host ports | Stripped in domains mode; replaced with registry ports on 127.0.0.1 in registry mode. |
container_name | Prefixed with the project when a service pins one. |
| Labels | dev.envctl.env, dev.envctl.backend, and dev.envctl.project on every service (dev.envctl.feature is still written as an alias). |
The result is written to .envctl/<feature>/compose.yaml and run with plain docker compose. Your source files are never modified, and anything compose can do still works on the rendered file.
Port modes
Section titled “Port modes”Two environments cannot both publish container port 5432 on host port 5433. envctl resolves this one of two ways.
| Mode | Behaviour | When |
|---|---|---|
domains | No host ports are published. Services are reached at service.project.orb.local. | OrbStack, which gives every container a DNS name reachable from the host. |
registry | Every published port is replaced by a stable port from a local registry, bound to 127.0.0.1. | Docker Desktop, Colima, Linux engines. |
auto, the default, picks domains on OrbStack and registry elsewhere. Force one with --port-mode or the manifest.
Env file
Section titled “Env file”Each render writes .envctl/<name>/env with KEY=VALUE lines:
ENVCTL_ENV=feat-xENVCTL_BRANCH=feat/xENVCTL_PROJECT=mg-feat-xENVCTL_BACKEND=localENVCTL_PORT_MODE=registryENVCTL_HOST_POSTGRES=127.0.0.1ENVCTL_PORT_POSTGRES_5432=41000Makefiles, native test runners, and agents source this file instead of assuming a port. See How to work with envctl.
envctl keeps no server and no database. Environments are discovered from Docker through project names and labels. The rendered files live in the worktree. The only other state is the port registry at ~/.config/envctl/ports.json, which maps project/service/port to a host port so allocations stay stable across restarts. envctl down -v releases a project’s allocations.