compose-envkit

module
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 18, 2026 License: MIT

README

compose-envkit

A small Go CLI (cenvkit) built on Docker's own compose-go loader. It does two things:

  • Debugs the one gap native Compose leaves open. A value defined only in a service env_file: is invisible to compile-time ${VAR} interpolation, so ports: "${APP_PORT:-3000}:80" silently falls back to 3000. cenvkit env-debug detects + explains it with provenance (daemon-free), and cenvkit gap-report is a CI/pre-build lint. No other tool does this. (See The gap.)
  • Populates the env-chain for any consumer. cenvkit compose (→ docker compose), cenvkit run -- <cmd> (any process, no docker), cenvkit env (emit dotenv/json/shell) — one chain, one source of truth for compose and local dev. It composes with make/just; it doesn't replace them.

cenvkit is a Go CLI built on Docker's own compose loader (compose-spec/compose-go). It is the only implementation — the original POSIX-sh engine has been removed. The Go CLI needs only a Go toolchain (no Python, no Node).


Install — cenvkit (the Go CLI, v1 · current)

cenvkit is built on Docker's own loader (compose-spec/compose-go, pinned v2.11.0). env-debug/gap-report are daemon-free; cenvkit compose/validate shell out to your Docker Compose v2.24+ (for include: + env_file: required:) and are tested in CI against the latest release. Two distribution modes:

# Installed (recommended)
go install github.com/InfernalRabbit/compose-envkit/cmd/cenvkit@latest
# or ephemeral: go run github.com/InfernalRabbit/compose-envkit/cmd/cenvkit@latest <args>

In your project:

cenvkit init               # seed .X from example.X (no-clobber), fan out one level
cenvkit gap-report         # CI lint: exit 1 if a ${VAR} is satisfied only by env_file:
cenvkit env-debug          # inspect the chain / provenance (daemon-free)
cenvkit compose config     # render config (interpolation = your Layer-1 chain)
cenvkit run -- npm test    # run any command with the merged chain env (no docker)
cenvkit env --format shell # emit the merged env (eval-able)

Or vendor it: commit the Go module + the POSIX cenvkit shim and run ./cenvkit <args> (needs a Go toolchain; for speed, go build -o .cenvkit.bin ./cmd/cenvkit — gitignored — and run that). Full command + behavior reference: docs/cenvkit.md.


What it is, and why

Modern Docker Compose already handles most of what a hand-rolled wrapper used to do — COMPOSE_ENV_FILES (last-wins project chain), COMPOSE_FILE overlays, DOCKER_DEFAULT_PLATFORM. compose-envkit wraps those for ergonomics, but it exists for the one job that has no native equivalent.

The gap native compose doesn't close

Docker Compose keeps two things deliberately separate:

Layer Populates Used for compose-time ${VAR} in the YAML?
Project-level env (--env-file / COMPOSE_ENV_FILES) interpolation context yes
A service's env_file: the container's runtime env no

So if APP_PORT lives only in a service's env_file: and you write ports: "${APP_PORT:-3000}:80", compose interpolates ${APP_PORT} before that env_file: is ever read — you silently get the :-3000 fallback. This is an intentional, long-standing design split upstream (docker/compose#3435, open since 2016).

cenvkit does not paper over this by folding service env_file:s into interpolation — doing so flattens every service's env into one global namespace (a ${PORT} collision footgun, since Compose interpolates the whole YAML against one env map). Instead it keeps env_file: runtime-only and gives you a daemon-free gap-detector: env-debug tells you when a ${VAR} is satisfied only by a service env_file: (so the run falls back), shows the runtime value, and recommends the fix — put values you reference as ${VAR} into the Layer-1 chain. Full reference: docs/cenvkit.md.

cenvkit env-files                          # the resolved COMPOSE_ENV_FILES (Layer-1 chain)
cenvkit env-debug --trace --var APP_PORT   # in the chain, or an env_file gap?
cenvkit gap-report                         # CI/pre-build lint: non-zero if any ${VAR} is env_file-only

Deliver the chain anywhere — run / env

The same Layer-1 chain that feeds compose can drive any consumer, so you keep one source of truth instead of two tools:

cenvkit run -- npm run dev             # exec a process with the merged chain env (no docker)
cenvkit run --no-expand -- printenv    # leave ${VAR} literal
cenvkit env                            # emit the merged env (dotenv); --format json|shell
eval "$(cenvkit env --format shell)"   # load it into the current shell

run / env expand ${VAR} / ${VAR:-def} via compose-go's own dotenv engine, so cenvkit env --expand is byte-identical to what docker compose interpolates (and to env-debug --effective). -e <env> overrides CENVKIT_ENV for one call; shell-set vars win over the chain (compose parity).

Named chains — give .cenvkit.envchain optional [name] sections and pick one with --chain <name> (default = the header-less / [default] list); sections are standalone (no inheritance) and orthogonal to CENVKIT_ENV.

Secrets are out of scope — cenvkit never masks/encrypts/manages them; .secrets.env just loads last (last-wins), nothing written to disk. For real secret management, wrap it: sops exec-env -- cenvkit run -- <cmd>.


The env-chain

The project chain is COMPOSE_ENV_FILES — the interpolation context. Listed in .cenvkit.envchain (or built-in defaults when that file is absent). The default chain is:

.env             # non-secret defaults (committed via example.env)
.${ENV}.env      # per-environment overlay: .dev.env / .prod.env / …
.secrets.env     # secrets, gitignored — loaded LAST so it wins

${CENVKIT_ENV} (alias ${ENV}) is resolved as **shell CENVKIT_ENV

.env's CENVKIT_ENV > "dev"**. Non-existent files are silently skipped.

Service env_file: is runtime-only — it configures the container, not interpolation, and is not added to COMPOSE_ENV_FILES. cenvkit still loads the real, include-aware model and enumerates those paths, but only to power env-debug (the gap-detector and the --files runtime-only view).

  shell CENVKIT_ENV / .env / "dev"  ─┐
                                     ├─►  ${ENV} substitution
  .cenvkit.envchain (Layer 1)  ──────┴─►  COMPOSE_ENV_FILES  ──►  docker compose
                                                                  (interpolation)
  service env_file:  ──►  container runtime env only  +  env-debug gap-detector

See cenvkit env-files for the chain, and cenvkit env-debug --files for the runtime-only env_file paths.


Run from root AND from a subproject

cenvkit resolves everything relative to its project directory: the current directory by default, or --project-dir <dir> to point elsewhere. All env-chain resolution and env_file: enumeration happen relative to that directory — so running from the repo root and running from a subproject each resolve their own files correctly.

cenvkit env-files                    # resolve from the current directory
cd web && cenvkit compose config     # resolve web/'s own chain + env_file:
cenvkit --project-dir web env-files  # same, without changing directory
Monorepo — root orchestrates subprojects

The flip side of subproject isolation is the unified stack: a root compose that include:s each subproject, run as one stack from the root. cenvkit loads the real, include-aware model, so env-debug sees the whole project. Note the rule: a ${WEB_PORT} declared only in web/.web.env (a service env_file:) falls back at the root — env_file: is runtime-only. cenvkit env-debug --trace --var WEB_PORT flags the gap; promote WEB_PORT to the Layer-1 chain if you need it interpolated.

Runnable blueprint: examples/monorepo/.


The debug flow at a glance

cenvkit env-debug is provenance-backed and daemon-free: it loads the compose model in-process (compose-go), with no docker compose shell-out, and never hardcodes your variable or service names. Add --json to any mode for the structured report (tooling/CI).

Mode What it shows
--list (default) the Layer-1 chain files, in load order (secrets last)
--files two groups: interpolation (COMPOSE_ENV_FILES, Layer 1) + runtime-only (service env_file: paths)
--overview per-file layering walk (+/~/· markers, raw values) + per-service env_file: layers + inline environment:, with ⚠ gap lines
--effective [--service S] each service's effective env, with the source of every value (env_file: vs inline environment:)
--trace --var NAME NAME's chain winner + where ${NAME} took effect — or the gap (NAME is only in a service env_file:, so the run falls back)
--value --var NAME NAME's winning value, one line (for scripts)

Output is colored on a terminal (markers, headers, gaps) and plain when piped / --json / NO_COLOR / CI — control with --color=auto|always|never.

Full reference: docs/cenvkit.md.

cenvkit env-debug                              # the chain, in load order
cenvkit env-debug --trace --var DATABASE_HOST  # who set/shadowed DATABASE_HOST
cenvkit env-debug --effective --service web     # final values compose will use for web
cenvkit env-debug --trace --var APP_PORT --json # the whole resolution, as JSON

Requirements

  • A Go toolchain to install or build cenvkit (go install / go run …@latest, or go build in vendored mode). No Python, no Node.
  • Docker Compose for the compose-touching commands (cenvkit compose …, cenvkit validate). The env-debug provenance modes load the model in-process via compose-go and need no running Docker daemon.
  • Cross-platform: cenvkit is a pure-Go binary — it runs natively on Linux, macOS, and Windows.

Layout

compose-envkit/
├── cmd/cenvkit/         # the Go CLI entry (cobra)
├── internal/
│   ├── chain/           # Layer 1 — the .cenvkit.envchain project chain (pure Go)
│   ├── engine/          # service env_file: enumeration for env-debug (the only compose-go importer)
│   ├── envfiles/        # merge / order / dedup into COMPOSE_ENV_FILES
│   ├── provenance/      # env-debug provenance model + human/JSON render
│   └── bootstrap/       # cenvkit init
├── cenvkit              # vendored-mode POSIX shim (runs `go run ./cmd/cenvkit`)
├── go.mod / go.sum      # module + compose-go v2.11.0 pin
├── examples/monorepo/   # runnable root-include:s-subprojects blueprint (cenvkit-driven)
├── test/                # acceptance suite driving the cenvkit binary
└── docs/cenvkit.md      # the canonical command + behavior reference

Documentation

  • docs/guide.mdthe full user guide — start here: install, the env-chain (Layer 1/2), every command with worked examples, monorepos, env-debug provenance, CI, the behavior contracts, and troubleshooting.
  • docs/cenvkit.md — the one-page reference (commands + behavior contracts at a glance).
  • docs/superpowers/ — the design spec and implementation plans (historical record).
  • AGENTS.md — integration guide for AI agents.
  • examples/monorepo/ — runnable, cenvkit-driven root-include:s-subprojects blueprint.

License

MIT — see LICENSE.

Directories

Path Synopsis
cmd
cenvkit command
Command cenvkit assembles COMPOSE_ENV_FILES from a layered env chain and execs `docker compose`.
Command cenvkit assembles COMPOSE_ENV_FILES from a layered env chain and execs `docker compose`.
internal
bootstrap
Package bootstrap implements `cenvkit init`: seed .<X> from example.<X> no-clobber, fanning out one directory level.
Package bootstrap implements `cenvkit init`: seed .<X> from example.<X> no-clobber, fanning out one directory level.
chain
Package chain resolves Layer-1: the .cenvkit.envchain file list plus the "K=V" seed environment for the engine.
Package chain resolves Layer-1: the .cenvkit.envchain file list plus the "K=V" seed environment for the engine.
engine
Package engine wraps compose-go (v2.11.0).
Package engine wraps compose-go (v2.11.0).
envfiles
Package envfiles merges the Layer-1 chain and the Layer-2 enumerated set into the final ordered COMPOSE_ENV_FILES list.
Package envfiles merges the Layer-1 chain and the Layer-2 enumerated set into the final ordered COMPOSE_ENV_FILES list.
envmap
Package envmap flattens a resolved Layer-1 chain into a merged KEY=VALUE environment for the populator (cenvkit run / cenvkit env).
Package envmap flattens a resolved Layer-1 chain into a merged KEY=VALUE environment for the populator (cenvkit run / cenvkit env).
provenance
Package provenance is the pure-Go leaf that owns the env-debug data model and its human/JSON rendering.
Package provenance is the pure-Go leaf that owns the env-debug data model and its human/JSON rendering.
style
Package style is the ONLY package that imports lipgloss.
Package style is the ONLY package that imports lipgloss.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL