Skip to content

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.

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.

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.

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.

envctl up loads the compose files with compose-go, the library Docker’s own compose uses, then rewrites the in-memory project:

RewriteEffect
Project nameNamespaces containers, networks, and volumes.
Host portsStripped in domains mode; replaced with registry ports on 127.0.0.1 in registry mode.
container_namePrefixed with the project when a service pins one.
Labelsdev.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.

Two environments cannot both publish container port 5432 on host port 5433. envctl resolves this one of two ways.

ModeBehaviourWhen
domainsNo host ports are published. Services are reached at service.project.orb.local.OrbStack, which gives every container a DNS name reachable from the host.
registryEvery 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.

Each render writes .envctl/<name>/env with KEY=VALUE lines:

ENVCTL_ENV=feat-x
ENVCTL_BRANCH=feat/x
ENVCTL_PROJECT=mg-feat-x
ENVCTL_BACKEND=local
ENVCTL_PORT_MODE=registry
ENVCTL_HOST_POSTGRES=127.0.0.1
ENVCTL_PORT_POSTGRES_5432=41000

Makefiles, 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.