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.85 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.
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