The DevContainer CLI, minus the parts you don't use.
A fast, focused Rust CLI for developers who use dev containers and CI pipelines — not for feature authors.
curl -fsSL https://get2knowio.github.io/deacon/install.sh | bashOther installation methods
brew install get2knowio/tap/deaconDownload from releases:
| Platform | Architecture | Download |
|---|---|---|
| Linux | x86_64 | deacon-linux-x86_64.tar.gz |
| Linux | ARM64 | deacon-linux-arm64.tar.gz |
| Linux (musl) | x86_64 | deacon-linux-musl-x86_64.tar.gz |
| macOS | x86_64 | deacon-darwin-x86_64.tar.gz |
| macOS | ARM64 (Apple Silicon) | deacon-darwin-arm64.tar.gz |
| Windows | x86_64 | deacon-windows-x86_64.zip |
| Windows | ARM64 | deacon-windows-arm64.zip |
git clone https://github.com/get2knowio/deacon.git
cd deacon
cargo build --release
./target/release/deacon --helpThe install script supports these environment variables:
DEACON_VERSION— Specific version (default: latest)DEACON_INSTALL_DIR— Install directory (default:~/.local/bin)DEACON_FORCE=true— Overwrite existing binary without prompt
# Install specific version
curl -fsSL https://get2knowio.github.io/deacon/install.sh | DEACON_VERSION=0.2.0 bash# Start a dev container
deacon up
# Run a command in the container
deacon exec -- npm install
# Stop and remove the container
deacon downVerify installation:
deacon --version| Command | Description |
|---|---|
up |
Create and start a dev container (features installed at build time via BuildKit) |
down |
Stop and remove a dev container or compose project |
exec |
Execute a command in a running container |
build |
Build a dev container image (with feature layering for Dockerfile configs) |
read-configuration |
Resolve and output devcontainer.json (with extends + variable substitution) |
run-user-commands |
Run lifecycle commands in an existing container |
set-up |
Convert an already-running container into a DevContainer (lifecycle + dotfiles + /etc patches) |
upgrade |
Regenerate the lockfile from the currently resolved feature set |
outdated |
Report current / wanted / latest feature versions |
templates apply |
Scaffold a project from a template |
config |
Configuration management subcommands |
doctor |
Environment diagnostics and support bundle creation |
| Limitation | Notes |
|---|---|
| Podman runtime | Ships as experimental in 1.0. Trait-level integration is complete and the happy path works, but rootless-Podman parity items (label=disable, --userns=keep-id, --uidmap/--gidmap) and dedicated test coverage are targeted for 1.1 — see #30. Using --runtime podman emits a one-time WARN. |
build features |
Feature installation during build is supported for Dockerfile-based configs only. Compose-build and image-reference configs still error out with features (different integration patterns; tracked as a post-1.0 follow-up). |
For the full 1.0 roadmap, see docs/ROADMAP_TO_1.0.md. Post-1.0 hardening landed in May 2026 — redaction wiring into the tracing pipeline, a workspace-trust gate for host-side lifecycle hooks, async I/O conversion across crates/core, typed errors throughout crates/core, and the json5→jsonc-parser migration (see closed #52).
Self-contained categorized examples live under examples/:
- Configuration: variable substitution, lifecycle commands basics (
examples/configuration/) - Container Lifecycle: lifecycle command execution, ordering, and variables (
examples/container-lifecycle/) - Feature System: dependencies, parallelism, caching, and lockfile support (
examples/features/) - Template Management: template application with options (
examples/template-management/)
See the full details and additional commands in examples/README.md.
Deacon uses Docker as its container runtime. Podman support ships in 1.0 as
experimental: the trait-level integration is complete, but rootless-Podman
parity items (label=disable, --userns=keep-id, --uidmap/--gidmap) and
dedicated test coverage are still required for full support, targeted for 1.1
(tracked in #30). Using
--runtime podman emits a one-time WARN.
# Explicitly select Docker (optional, it's the default)
deacon --runtime docker up
# Experimental: select Podman
deacon --runtime podman up
# Or via environment variable
DEACON_RUNTIME=docker deacon upDeacon supports both human-readable text and structured JSON logging formats.
Text Logging (Default):
deacon --help # Standard text outputJSON Logging:
export DEACON_LOG_FORMAT=json
deacon doctor # Structured JSON logs for machine parsingThe JSON format is useful for CI/CD systems and log aggregation tools that need structured data.
When running in a real terminal (stderr is a TTY) and using the default text output, deacon shows a small spinner during long-running operations like up and down. In these spinner sessions, if you haven't set DEACON_LOG, RUST_LOG, or --log-level, the default log level is temporarily set to warn so routine progress noise stays out of your way. JSON mode or non‑TTY environments (CI, redirections) do not render a spinner and keep the previous logging behavior.
Tips:
- Want details with the spinner? Set
RUST_LOG=infoor use--log-level info. - Prefer structured logs? Use
--log-format json(no spinner) and parse stderr.
Color and accessibility:
- Help/usage output uses automatic color when writing to a terminal. Spinner/status messages also use subtle colors (yellow for in‑progress, green for success, red for failures).
- Respecting your environment, color is disabled when not writing to a TTY and when
NO_COLORis set (see https://no-color.org/). To force-disable colors, exportNO_COLOR=1.
When using JSON logging format (--log-format json), lifecycle commands (onCreate, postCreate, etc.) run without PTY (pseudo-terminal) allocation by default. This is ideal for non-interactive scripts and automated environments.
However, if your lifecycle commands need interactive terminal behavior while maintaining structured JSON logs, you can force PTY allocation:
Via CLI Flag:
deacon up --log-format json --force-tty-if-jsonVia Environment Variable:
# Enable PTY allocation
export DEACON_FORCE_TTY_IF_JSON=true
deacon up --log-format json
# Disable PTY allocation (default)
export DEACON_FORCE_TTY_IF_JSON=false
deacon up --log-format jsonTruthy values (case-insensitive): true, 1, yes
Falsey values: false, 0, no, or unset
Precedence:
- CLI flag (
--force-tty-if-json) - Environment variable (
DEACON_FORCE_TTY_IF_JSON) - Default (no PTY allocation)
Important Notes:
- This setting only applies when
--log-format jsonis active - With PTY allocation enabled, interactive commands work correctly while JSON logs remain structured on stderr and machine-readable output stays on stdout
- Without PTY allocation (default), lifecycle commands run in non-interactive mode
Deacon follows a strict stdout/stderr separation contract to ensure reliable machine-readable output:
-
JSON Output Modes (
--output json,--jsonflags):- stdout: Single JSON document (newline terminated), nothing else
- stderr: All logs, diagnostics, and progress messages via
tracing - Guarantee: Scripts parsing stdout will receive only valid JSON
-
Text Output Modes (default):
- stdout: User-facing result summaries and human-readable reports only
- stderr: All logs, diagnostics, and progress messages via
tracing - Note: Text format content may evolve; use JSON modes for stable parsing
-
Error Conditions:
- Non-zero exit: stdout may be empty unless partial results are explicitly supported
- All errors: Logged to stderr, never stdout
# JSON mode - stdout contains only JSON, logs go to stderr
deacon read-configuration --output json > config.json 2> logs.txt
# Text mode - stdout contains human-readable results, logs to stderr
deacon doctor > diagnosis.txt 2> logs.txt
# Parsing JSON output safely
OUTPUT=$(deacon read-configuration 2>/dev/null)
echo "$OUTPUT" | jq '.configFilePath'- Automation/CI: Always use JSON output modes (
--json,--output json) for reliable parsing - Human Use: Default text modes provide better readability and context
- Logging: Use
--log-leveland--log-formatto control stderr verbosity and format
All Docker functionality is available when Docker is installed and running. If the Docker daemon isn’t available, deacon emits a clear runtime error with guidance to install or start Docker.
deacon up runs initializeCommand and any custom dotfiles install command on the host — outside the container sandbox. To prevent arbitrary host-side execution when cloning hostile repos, deacon gates these hooks behind a workspace-trust check that fails closed by default.
# Untrusted workspaces error out:
$ deacon up
Error: workspace is not trusted: /path/to/workspace
Re-run with --trust-workspace (or --trust-workspace-persist to remember).
# Opt in for this run only:
deacon --trust-workspace up
# Opt in and remember (persisted to ~/.local/share/deacon/trusted_workspaces.json):
deacon --trust-workspace-persist up
# CI fail-closed mode — never auto-trust, error immediately on first untrusted run:
DEACON_NO_PROMPT=1 deacon upThe check only fires when host-side hooks are actually configured. Containers without initializeCommand or a host-side dotfiles install are unaffected. See SECURITY.md for the full threat model.
The read-configuration command loads, processes, and outputs your devcontainer.json:
# In a directory with .devcontainer/devcontainer.json or .devcontainer.json
deacon read-configuration
# With explicit config path
deacon read-configuration --config /path/to/devcontainer.json
# Include merged configuration (with extends resolution)
deacon read-configuration --include-merged-configuration
# With debug logging
deacon read-configuration --log-level debugExample output:
{
"name": "my-dev-container",
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
"workspaceFolder": "/workspaces/my-project",
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
},
"customizations": {
"vscode": {
"extensions": ["ms-python.python"]
}
}
}Variable Substitution:
Variables like ${localWorkspaceFolder} are automatically replaced:
{
"workspaceFolder": "${localWorkspaceFolder}/src",
"containerEnv": {
"PROJECT_ROOT": "${localWorkspaceFolder}"
}
}becomes:
{
"workspaceFolder": "/home/user/project/src",
"containerEnv": {
"PROJECT_ROOT": "/home/user/project"
}
}Deacon uses the tracing ecosystem for structured logging.
- Default log level:
info - Default log format: text (human-readable)
- CLI flag overrides:
--log-level(error|warn|info|debug|trace),--log-format(text|json) - Environment overrides (take precedence before CLI flag processing sets
RUST_LOG):DEACON_LOG: Full filter specification (e.g.DEACON_LOG=deacon=debug,deacon_core=debug)RUST_LOG: Standard Rust filter fallback ifDEACON_LOGunset
When you specify --log-level, the CLI sets RUST_LOG internally to deacon=<level>,deacon_core=<level> prior to initializing the subscriber. Use DEACON_LOG for advanced per-module filtering; it will be honored as-is (and will emit a warning and fall back to info if invalid).
Guidance on levels:
info: High-level milestones and user‑visible state changes (container start/stop, template application summary, configuration load boundaries)debug: Detailed decision points (config discovery paths, feature resolution steps, variable substitution reports)trace: Very fine‑grained internals (iteration over collections, per-file copy decisions) – typically for deep troubleshooting onlywarn: Recoverable issues or unexpected states deviating from normal expectationserror: Failures causing command termination or skipped critical workflow phases
Examples:
# Increase verbosity for troubleshooting configuration issues
deacon read-configuration --log-level debug
# Use JSON logs (machine parsing / CI ingestion)
deacon up --log-format json
# Advanced module filtering (show trace for config, keep others at info)
DEACON_LOG=deacon_core::config=trace,deacon=info deacon read-configurationIf you disable secret redaction (--no-redact), secret values may appear in logs—avoid in shared terminals or CI unless strictly necessary.
cargo run -- --help
cargo testThe project supports both traditional cargo test and parallel execution via cargo-nextest.
# Run all tests serially (default)
make test
# Fast feedback loop: unit + bins + examples + doctests (no integration)
make test-fast
# Development fast loop: fmt-check + clippy + fast tests
make dev-fastFor faster feedback, install cargo-nextest:
cargo install cargo-nextest --locked
# Or follow: https://nexte.st/book/pre-built-binaries.htmlThen use the nextest targets:
# Fast parallel tests (excludes smoke/parity tests)
make test-nextest-fast
# Full test suite with parallel execution and test grouping
make test-nextest
# CI-aligned conservative profile
make test-nextest-ciTest Groups: The project organizes tests into groups based on resource requirements:
- docker-exclusive: Tests requiring exclusive Docker daemon access (serial)
- docker-shared: Tests that can share Docker daemon (limited parallelism)
- fs-heavy: Filesystem-intensive tests (limited parallelism)
- unit-default: Fast unit tests with high parallelism
- smoke: High-level integration tests (serial)
- parity: Upstream CLI comparison tests (serial)
Timing data is automatically captured in artifacts/nextest/ for performance analysis.
Fallback Behavior: If cargo-nextest is not installed, the Make targets will fail with clear installation instructions. Always keep the standard make test working as a fallback.
When adding new tests, classify them into the appropriate group:
-
Does the test use Docker?
- No →
unit-defaultorfs-heavy - Yes → Continue to step 2
- No →
-
Does it require exclusive Docker daemon access?
- Yes (manipulates daemon state) →
docker-exclusive - No (just runs containers) →
docker-shared
- Yes (manipulates daemon state) →
-
Does it perform heavy filesystem operations?
- Yes (large files, many I/O ops) →
fs-heavy
- Yes (large files, many I/O ops) →
-
Is it an end-to-end integration test?
- Yes (validates complete workflow) →
smoke - Yes (compares with upstream CLI) →
parity
- Yes (validates complete workflow) →
Audit test assignments:
# List all tests with their group classifications
make test-nextest-auditExample test naming for automatic classification:
// Docker-exclusive tests
#[test]
fn integration_lifecycle_full_up_down() { ... }
// Docker-shared tests
#[test]
fn integration_build_with_cache() { ... }
// Smoke tests
#[test]
fn smoke_basic_workflow() { ... }Flaky tests in parallel execution:
- Test passes with
make testbut fails withmake test-nextest - Solution: Reclassify to more conservative group (e.g.,
docker-shared→docker-exclusive) - Update test name or
.config/nextest.tomlfilter - Verify with
make test-nextest-audit - Validate with multiple runs of
make test-nextest
Tests requiring specific order:
- Preferred: Refactor test to be independent
- If unavoidable: Move to
smokegroup (serial execution) - Document the dependency in test comments
Slow tests:
- Profile to identify bottleneck
- Consider splitting into smaller tests
- Move inherently slow end-to-end tests to
smokegroup - Use
#[ignore]for very slow tests:cargo nextest run -- --ignored
For comprehensive troubleshooting, classification workflows, and remediation steps, see docs/testing/nextest.md.
Deacon focuses on consuming dev containers — building, running, and managing them — rather than authoring reusable features or publishing to registries. Coverage continues to expand across these specification domains:
- Configuration resolution and parsing (
devcontainer.json) - Feature consumption: installing and resolving community features during container builds
- Template system for scaffolding new projects
- Container lifecycle management
- Docker/OCI integration
- Cross-platform support
See the CLI specification for detailed architecture and planned features.
Current release artifacts (tar.gz / zip) are not yet code signed. Integrity is provided via SHA256SUMS published with every release. You should always:
# Download archive and checksum listing
curl -LO https://github.com/get2knowio/deacon/releases/download/<version>/SHA256SUMS
grep '<archive-filename>' SHA256SUMS | sha256sum -c -Planned enhancements (no tracking issue yet — file one if you need any of these prioritized):
- GPG detached signature for
SHA256SUMS(SHA256SUMS.asc) - macOS codesign + notarization
- Windows Authenticode signature
- Supply chain provenance (SLSA build attestation)
Until signatures are in place, rely on checksum verification and the GitHub release provenance. If you need reproducible build parity, the workflow captures the exact rustc -Vv used in each release (RUSTC_VERSION.txt asset). Deterministic builds for comparison can be performed via:
rustup toolchain install $(grep '^release:' RUSTC_VERSION.txt | cut -d':' -f2 | xargs)
cargo build --release --locked --all-featuresIf you have requirements around signed binaries and would like to help accelerate this, comment on the tracking issue once it is opened.
See CONTRIBUTING.md for development workflow, testing guidelines, and contribution requirements.
CI runs via GitHub Actions and uses the Makefile + cargo-nextest:
- Lint: rustfmt check, cargo check, clippy, and doctests
- Test (Ubuntu):
make test-nextest-fastwith Docker available - Smoke (Ubuntu):
make test-smoke(serial, Docker required) - Nextest CI (Ubuntu/macOS):
make test-nextest-ciwith timing artifactartifacts/nextest/ci-timing.json - Other OS (macOS/Windows): runs unit + non‑smoke integration tests and separate smoke tests; macOS uses Colima for Docker
Notes:
- Networked integration tests run only in selected jobs (smoke and nextest-ci) via
DEACON_NETWORK_TESTS=1to keep the fast test job hermetic. - Test grouping and concurrency are configured in
.config/nextest.toml. See docs/testing/nextest.md for details.
We use cargo-llvm-cov (LLVM source-based coverage) locally and in CI.
-
Install toolchain addon and helper:
- rustup component add llvm-tools-preview
- cargo install cargo-llvm-cov
-
Run coverage locally and open HTML report:
- cargo llvm-cov --workspace --open
-
Generate LCOV for external services:
- cargo llvm-cov --workspace --lcov --output-path lcov.info
CI enforces a minimum line coverage threshold (see MIN_COVERAGE in .github/workflows/ci.yml). To try the same locally:
- cargo llvm-cov --workspace --fail-under-lines 80
Coverage reporting is published to Coveralls for the main branch and PRs: https://coveralls.io/github/get2knowio/deacon