decompose
Run your stack at native speed.
decompose is a Rust process orchestrator for local development and agentic coding loops.
No image builds. No container cold starts. No bridge-network translation overhead.
Just your real processes, fast, with a familiar compose-like interface.

Installing
From crates.io
Requires Rust 1.88 or later. If you don't have Rust installed, grab it from rustup.rs.
Prebuilt binaries
Download a tarball for your platform from the latest release, extract it, and put decompose on your $PATH. Builds are published for:
| Target | OS | Arch |
|---|---|---|
x86_64-unknown-linux-gnu |
Linux | x86_64 |
aarch64-unknown-linux-gnu |
Linux | ARM64 |
x86_64-apple-darwin |
macOS | Intel |
aarch64-apple-darwin |
macOS | Apple Silicon |
Quick install example (macOS Apple Silicon):
|
With Nix
Run without installing:
Or install into your profile:
You can also add it as a flake input in your own flake.nix:
inputs.decompose.url = "github:sciyoshi/decompose";
The flake also exposes a devShell for contributors — nix develop drops you into a shell with cargo, rustc, rustfmt, and clippy pinned.
From source
The binary will be at target/release/decompose. You can also use cargo install --path . to install it directly into your Cargo bin directory.
Shell completions
decompose completion <shell> prints a ready-to-source completion script for
bash, zsh, fish, powershell, or elvish. The bash, zsh, fish, and
PowerShell scripts also do dynamic completion: on start, stop,
restart, kill, logs, exec, run, and up, tab-completion pulls
service names from decompose config --json, and --session /
--project-name values are pulled from decompose ls --json. The helpers
forward --file, -e/--env-file, --session/--project-name, and
--disable-dotenv to the backing config call so completion is correct in
multi-project and multi-session setups. jq is optional but recommended;
without it, a sed fallback parses the JSON.
The Nix flake installs bash, zsh, and fish completion files into the package
output automatically. cargo install decompose installs the same completion
generator as part of the decompose binary; Cargo does not support installing
shell completion files or editing shell startup files during cargo install,
so Cargo users should source or write the generated script as shown below.
bash — add to your ~/.bashrc:
Or install system-wide once (e.g. under bash-completion):
|
zsh — drop the script somewhere in your $fpath:
# then, in ~/.zshrc, before `compinit`:
# fpath=(~/.zfunc $fpath)
# autoload -U compinit && compinit
Or source directly in ~/.zshrc:
fish:
PowerShell — add to your $PROFILE:
decompose completion powershell | Out-String | Invoke-Expression
elvish — write to a module file and use it from rc.elv:
# then add `use decompose` to ~/.config/elvish/rc.elv
Why this is better for day-to-day coding
- Native performance: run directly on host processes and filesystems.
- Faster inner loops: no Dockerfile rebuilds just to iterate on app code.
- Lower complexity: no container networking setup for every local workflow.
- Agent-friendly: predictable JSON/table output and deterministic control from other tabs.
- Familiar UX:
up,ps,down, compose-style YAML, dependencies, replicas.
Built for humans and agents
decompose upstarts and attaches.Ctrl-Cdetaches your terminal session while keeping the daemon alive.decompose up -dstarts and returns immediately.decompose psreports empty state instead of error when nothing is running.- Use
decompose downfrom any tab/agent to stop the environment.
Reproducible with Nix
This repo ships a flake.nix so you can pair Nix + decompose and get most of Docker's local-dev benefits (isolated environments, consistent versions across machines) without container runtime overhead.
Nix pins the toolchain and dependencies; decompose orchestrates native processes on top of that reproducible environment.
Commands
decompose up [-d|--detach] [--no-deps] [SERVICE...]
decompose down
decompose ps
decompose attach
decompose logs [-f|--follow] [-n|--tail N] [SERVICE...]
decompose start [SERVICE...]
decompose stop [SERVICE...]
decompose restart [SERVICE...]
Global flags (--file, --session, -e, --disable-dotenv, --json,
--table) go before the subcommand:
CLI usage examples
Basic lifecycle
# Start everything in the background
# Check what is running
# Follow all logs
# Tear down the environment
Starting specific services
# Start only the web and api services (dependencies are started automatically)
# Start services without pulling in dependencies
Managing individual services
# Stop a single service
# Start it back up
# Restart one or more services
Viewing logs
# Stream logs from all services
# Show the last 100 lines from a specific service
# Follow logs for two services
Multi-file configuration
# Merge a base config with development overrides
# Check status using the same file set
Output modes
# Machine-readable JSON (useful in scripts and CI)
# Human-friendly table
# Pipe JSON into jq
|
Session isolation
# Run two independent environments from the same directory
# Inspect each independently
# Tear down one without affecting the other
Attaching to a running environment
# Start detached, then reattach from another terminal
# Ctrl-C detaches without stopping the daemon
Environment files
# Load an extra env file
# Skip automatic .env loading
Output modes
--json: machine-readable--table: human-friendly- default:
tablewhen stdout is a TTYtablewhenLLM=trueorCI=true- otherwise
json
Runtime model
- Per-environment daemon, isolated by working directory + config path hash.
- Local socket IPC via
interprocess. - XDG-aware paths:
- socket:
$XDG_RUNTIME_DIR/decompose/<instance>.sock(fallbacks applied) - state:
$XDG_STATE_HOME/decompose/<instance>.pidand.log
- socket:
Configuration reference
Config file discovery
If -f/--file is omitted, decompose looks for the first matching file in the
current directory:
decompose.ymldecompose.yamlcompose.ymlcompose.yaml
Multiple -f flags merge with overlay semantics -- later files override
earlier ones.
Quick example
processes:
hello:
command: "echo hello"
date:
command: "date"
depends_on:
hello:
condition: process_completed_successfully
Global settings
These are top-level keys in the YAML file, alongside processes.
environment: # Global env vars applied to every process
SHARED_KEY: value
exit_mode: wait_all # How the daemon behaves when processes exit
disable_env_expansion: false # Disable ${VAR} interpolation globally
| Field | Type | Default | Description |
|---|---|---|---|
environment |
map or list | {} |
Environment variables applied to all processes. Accepts a YAML map (KEY: value) or a list of KEY=VALUE strings. |
exit_mode |
string | wait_all |
Controls daemon behavior when processes exit. One of: wait_all (keep running until all processes finish or down is called), exit_on_failure (stop everything if any process exits non-zero), exit_on_end (stop everything when any process exits). |
disable_env_expansion |
bool | false |
When true, disables ${VAR} interpolation in all string fields. |
processes |
map | required | Map of process name to process configuration. At least one process must be defined. |
Process settings
Each key under processes defines a named service.
processes:
web:
command: "npm start"
description: "Frontend dev server"
working_dir: "./frontend"
environment:
PORT: "3000"
env_file:
- "frontend.env"
disabled: false
replicas: 1
ready_log_line: "Listening on port \\d+"
restart_policy: on_failure
backoff_seconds: 2
max_restarts: 5
| Field | Type | Default | Description |
|---|---|---|---|
command |
string | required | Shell command to run. Executed via the system shell. |
description |
string | null |
Optional human-readable description. |
working_dir |
string | config file directory | Working directory for the process. Relative paths resolve from the config file location. |
environment |
map or list | {} |
Per-process environment variables. Same format as the global environment (map or list of KEY=VALUE). Merged on top of global vars. |
env_file |
list of strings | [] |
Additional .env files to load for this process. Paths are relative to the config file directory. |
disabled |
bool | false |
When true, the process is visible in ps output but not auto-started by up. Can be started explicitly with start. |
replicas |
integer | 1 |
Number of instances to run. When greater than 1, instances are named service[1], service[2], etc. Must be at least 1. |
ready_log_line |
string (regex) | null |
A regex pattern matched against process stdout/stderr. When a line matches, the process is marked as "log ready". Required if any other process depends on this one with process_log_ready condition. |
restart_policy |
string | no |
Restart behavior: no (never restart), on_failure (restart on non-zero exit), always (restart on any exit). |
backoff_seconds |
integer | 1 |
Delay in seconds between restart attempts. |
max_restarts |
integer or null | null |
Maximum number of restarts. null means unlimited. |
Dependencies
Use depends_on to control startup order. Each dependency names another
process and a condition that must be met before the dependent process starts.
processes:
db:
command: "postgres -D ./data"
readiness_probe:
exec:
command: "pg_isready"
api:
command: "cargo run"
ready_log_line: "Listening on 0.0.0.0:8080"
depends_on:
db:
condition: process_healthy
web:
command: "npm start"
depends_on:
api:
condition: process_log_ready
| Condition | Description |
|---|---|
process_started |
The dependency has been started (default if omitted). |
process_completed |
The dependency has exited (any exit code). |
process_completed_successfully |
The dependency has exited with code 0. |
process_healthy |
The dependency's readiness probe is passing. Requires readiness_probe to be configured on the dependency. |
process_log_ready |
The dependency's ready_log_line regex has matched. Requires ready_log_line to be configured on the dependency. |
Circular dependencies are detected at config load time and produce an error.
Health probes
Both readiness_probe and liveness_probe share the same schema. The
readiness probe sets the process's "healthy" flag (used by
process_healthy dependency condition). The liveness probe restarts the
process if it fails.
Each probe supports one check type: exec (run a command) or http_get
(make an HTTP request).
processes:
api:
command: "cargo run"
readiness_probe:
exec:
command: "curl -sf http://localhost:8080/health"
period_seconds: 10
timeout_seconds: 1
initial_delay_seconds: 5
success_threshold: 1
failure_threshold: 3
liveness_probe:
http_get:
host: "127.0.0.1"
port: 8080
scheme: http
path: /healthz
period_seconds: 30
failure_threshold: 5
Probe timing fields:
| Field | Type | Default | Description |
|---|---|---|---|
period_seconds |
integer | 10 |
How often to run the check. |
timeout_seconds |
integer | 1 |
Timeout for each check attempt. |
initial_delay_seconds |
integer | 0 |
Delay before the first check after the process starts. |
success_threshold |
integer | 1 |
Consecutive successes required to pass. |
failure_threshold |
integer | 3 |
Consecutive failures required to fail. |
Exec check:
| Field | Type | Description |
|---|---|---|
exec.command |
string | Shell command to run. Exit code 0 means healthy. |
HTTP check:
| Field | Type | Default | Description |
|---|---|---|---|
http_get.host |
string | 127.0.0.1 |
Host to connect to. |
http_get.port |
integer | required | Port number. |
http_get.scheme |
string | http |
URL scheme (http or https). |
http_get.path |
string | / |
Request path. |
Shutdown configuration
Control how processes are stopped when decompose down, stop, or kill
is called.
processes:
worker:
command: "python worker.py"
shutdown:
command: "python cleanup.py" # Run before sending signal
signal: 15 # Signal number (15 = SIGTERM)
timeout_seconds: 30 # Wait this long before SIGKILL
| Field | Type | Default | Description |
|---|---|---|---|
shutdown.command |
string | null |
Optional command to run before sending the stop signal. |
shutdown.signal |
integer | 15 |
Signal to send to the process (15 = SIGTERM, 2 = SIGINT, etc.). |
shutdown.timeout_seconds |
integer | 10 |
Seconds to wait after sending the signal before sending SIGKILL. |
Environment variables
Precedence (lowest to highest)
Environment variables are merged in this order. Later sources override earlier ones:
.envfile in the config directory (auto-loaded unless--disable-dotenv)- Explicit env files via
-eCLI flag - Global
environmentblock in the YAML - Per-process
env_fileentries - Per-process
environmentblock
Variable interpolation
String fields support ${VAR} substitution from the merged environment.
| Syntax | Description |
|---|---|
${VAR} |
Substitute the value of VAR. Empty string if unset. |
$VAR |
Same as ${VAR}. |
${VAR:-default} |
Substitute VAR if set, otherwise use default. |
$$ |
Literal $ character (escape). |
Interpolation is applied to these fields: command, description,
working_dir, ready_log_line, shutdown.command, and all environment
variable values.
Disable interpolation globally by setting disable_env_expansion: true at
the top level.
Environment format
Both map and list formats are accepted anywhere environment variables are defined:
# Map format
environment:
PORT: "3000"
DEBUG: "true"
# List format
environment:
- PORT=3000
- DEBUG=true
Migrating from Docker Compose
decompose is designed to feel familiar to Docker Compose users. If you already have a docker-compose.yml, most of it can be adapted with minimal changes.
What maps directly
These fields work the same way (or very similarly) in both tools:
| Docker Compose field | decompose equivalent | Notes |
|---|---|---|
command |
command |
Runs as a native shell command instead of inside a container |
environment |
environment |
Map or list of KEY=VALUE entries |
env_file |
env_file |
Additional .env files to load |
working_dir |
working_dir |
Defaults to the config file directory |
depends_on |
depends_on |
Supports conditions: process_started, process_completed, process_completed_successfully, process_healthy, process_log_ready |
healthcheck |
readiness_probe / liveness_probe |
Similar concept, slightly different schema (see below) |
restart |
restart_policy |
Supports no, on_failure, always |
deploy.replicas |
replicas |
Directly on the process definition |
stop_grace_period |
shutdown.timeout_seconds |
Time to wait before SIGKILL |
stop_signal |
shutdown.signal |
Signal number (e.g., 15 for SIGTERM) |
What doesn't apply
Since decompose runs native processes instead of containers, these Docker Compose fields have no equivalent and should be removed:
image-- Usecommandto run the process directly (e.g.,node server.js,python app.py).build-- No container image builds. If you need a build step, add it as a separate process with a dependency.ports-- No port mapping needed; processes bind to host ports directly.volumes-- No mount translation; processes access the host filesystem natively.networks-- No container networking; processes communicate over localhost.expose,links,extra_hosts-- Not applicable.container_name,hostname,domainname-- Not applicable.entrypoint-- Fold intocommand.cap_add,cap_drop,privileged,security_opt-- Not applicable.
Config file naming
decompose auto-discovers config files in this order:
decompose.ymldecompose.yamlcompose.yml(same filename Docker Compose uses)compose.yaml
You can keep your file named compose.yml and decompose will find it, or rename to decompose.yml to avoid ambiguity.
CLI command parity
Works the same:
| Command | Notes |
|---|---|
up [-d] [SERVICE...] |
Starts services; -d detaches |
down |
Stops the environment |
ps |
Shows process status |
logs [-f] [-n N] [SERVICE...] |
View/follow logs |
start [SERVICE...] |
Start stopped services |
stop [SERVICE...] |
Stop running services |
restart [SERVICE...] |
Restart services |
Not implemented (container-specific or not applicable):
build, pull, push, create, run, exec, port, top, events, images, pause, unpause, kill, cp, wait
Health check conversion
Docker Compose:
services:
web:
healthcheck:
test:
interval: 10s
timeout: 1s
start_period: 5s
retries: 3
decompose:
processes:
web:
command: "node server.js"
readiness_probe:
exec:
command: "curl -f http://localhost:8080/health"
period_seconds: 10
timeout_seconds: 1
initial_delay_seconds: 5
failure_threshold: 3
decompose also supports http_get probes as an alternative to exec:
readiness_probe:
http_get:
host: "127.0.0.1"
port: 8080
path: /health
scheme: http
Quick conversion checklist
- Rename or copy your
docker-compose.ymltocompose.yml(ordecompose.yml). - Remove the top-level
services:key and replace it withprocesses:(or keepservices:-- decompose usesprocesses:). - Replace
image:withcommand:-- specify the shell command that starts each service (e.g.,python manage.py runserver,npm start). - Remove
build:,ports:,volumes:,networks:, and any other container-specific fields. - Keep
environment:,env_file:,working_dir:, anddepends_on:-- these work as-is. - Convert
healthcheck:toreadiness_probe:using the schema shown above. - Convert
restart:torestart_policy:-- valuesno,on-failure/on_failure, andalwaysare supported. - Convert
deploy.replicas:toreplicas:at the process level. - Test with
decompose configto validate your converted file, thendecompose up.
Before and after example
Docker Compose:
services:
api:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://localhost/mydb
depends_on:
db:
condition: service_healthy
db:
image: postgres:16
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test:
interval: 5s
volumes:
pgdata:
decompose:
processes:
api:
command: "npm start"
environment:
DATABASE_URL: postgres://localhost/mydb
depends_on:
db:
condition: process_healthy
db:
command: "pg_ctl start -D /usr/local/var/postgresql@16 -l db.log"
readiness_probe:
exec:
command: "pg_isready"
period_seconds: 5