decompose 0.1.1

A simple and flexible scheduler and orchestrator to manage non-containerized applications
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
# decompose

![decompose logo](assets/logo.svg)

**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

```bash
cargo install decompose
```

Requires Rust 1.85 or later. If you don't have Rust installed, grab it from [rustup.rs](https://rustup.rs/).

### Prebuilt binaries

Download a tarball for your platform from the [latest release](https://github.com/sciyoshi/decompose/releases/latest), 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):

```bash
curl -sL https://github.com/sciyoshi/decompose/releases/latest/download/decompose-aarch64-apple-darwin.tar.gz \
  | tar xz -C /usr/local/bin
```

### With Nix

Run without installing:

```bash
nix run github:sciyoshi/decompose -- up
```

Or install into your profile:

```bash
nix profile install github:sciyoshi/decompose
```

You can also add it as a flake input in your own `flake.nix`:

```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

```bash
git clone https://github.com/sciyoshi/decompose
cd decompose
cargo build --release
```

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 and zsh scripts
also do dynamic service-name completion: on `start`, `stop`, `restart`,
`kill`, `logs`, `exec`, `run`, and `up`, tab-completion pulls service names
from `decompose config --json` (uses `jq` if available, falls back to a
`sed` parser otherwise).

**bash** — add to your `~/.bashrc`:

```bash
source <(decompose completion bash)
```

Or install system-wide once (e.g. under `bash-completion`):

```bash
decompose completion bash | sudo tee /etc/bash_completion.d/decompose > /dev/null
```

**zsh** — drop the script somewhere in your `$fpath`:

```bash
mkdir -p ~/.zfunc
decompose completion zsh > ~/.zfunc/_decompose
# then, in ~/.zshrc, before `compinit`:
#   fpath=(~/.zfunc $fpath)
#   autoload -U compinit && compinit
```

Or source directly in `~/.zshrc`:

```bash
source <(decompose completion zsh)
```

**fish**:

```bash
decompose completion fish > ~/.config/fish/completions/decompose.fish
```

**PowerShell** — add to your `$PROFILE`:

```powershell
decompose completion powershell | Out-String | Invoke-Expression
```

**elvish** — write to a module file and `use` it from `rc.elv`:

```bash
decompose completion elvish > ~/.config/elvish/lib/decompose.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 up` starts and attaches.
- `Ctrl-C` detaches your terminal session while keeping the daemon alive.
- `decompose up -d` starts and returns immediately.
- `decompose ps` reports empty state instead of error when nothing is running.
- Use `decompose down` from 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.

```bash
nix develop
cargo test
```

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:

```bash
decompose --file compose.yml --session myproject ps --json
```

## CLI usage examples

### Basic lifecycle

```bash
# Start everything in the background
decompose up -d

# Check what is running
decompose ps

# Follow all logs
decompose logs -f

# Tear down the environment
decompose down
```

### Starting specific services

```bash
# Start only the web and api services (dependencies are started automatically)
decompose up -d web api

# Start services without pulling in dependencies
decompose up -d --no-deps web
```

### Managing individual services

```bash
# Stop a single service
decompose stop worker

# Start it back up
decompose start worker

# Restart one or more services
decompose restart web api
```

### Viewing logs

```bash
# Stream logs from all services
decompose logs -f

# Show the last 100 lines from a specific service
decompose logs -n 100 web

# Follow logs for two services
decompose logs -f api worker
```

### Multi-file configuration

```bash
# Merge a base config with development overrides
decompose --file base.yml --file dev.yml up -d

# Check status using the same file set
decompose --file base.yml --file dev.yml ps
```

### Output modes

```bash
# Machine-readable JSON (useful in scripts and CI)
decompose ps --json

# Human-friendly table
decompose ps --table

# Pipe JSON into jq
decompose ps --json | jq '.[] | select(.status == "running")'
```

### Session isolation

```bash
# Run two independent environments from the same directory
decompose --session staging up -d
decompose --session canary  up -d

# Inspect each independently
decompose --session staging ps
decompose --session canary  ps

# Tear down one without affecting the other
decompose --session staging down
```

### Attaching to a running environment

```bash
# Start detached, then reattach from another terminal
decompose up -d
decompose attach

# Ctrl-C detaches without stopping the daemon
```

### Environment files

```bash
# Load an extra env file
decompose -e secrets.env up -d

# Skip automatic .env loading
decompose --disable-dotenv up -d
```

## Output modes

- `--json`: machine-readable
- `--table`: human-friendly
- default:
  - `table` when stdout is a TTY
  - `table` when `LLM=true` or `CI=true`
  - otherwise `json`

## Runtime model

- Per-environment daemon, isolated by working directory + config path hash.
- Local socket IPC via [`interprocess`]https://docs.rs/interprocess/latest/interprocess/local_socket/index.html.
- XDG-aware paths:
  - socket: `$XDG_RUNTIME_DIR/decompose/<instance>.sock` (fallbacks applied)
  - state: `$XDG_STATE_HOME/decompose/<instance>.pid` and `.log`

## Configuration reference

### Config file discovery

If `-f/--file` is omitted, decompose looks for the first matching file in the
current directory:

1. `decompose.yml`
2. `decompose.yaml`
3. `compose.yml`
4. `compose.yaml`

Multiple `-f` flags merge with overlay semantics -- later files override
earlier ones.

### Quick example

```yaml
processes:
  hello:
    command: "echo hello"
  date:
    command: "date"
    depends_on:
      hello:
        condition: process_completed_successfully
```

```bash
decompose up
decompose ps
decompose down
```

### Global settings

These are top-level keys in the YAML file, alongside `processes`.

```yaml
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.

```yaml
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.

```yaml
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).

```yaml
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.

```yaml
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:

1. `.env` file in the config directory (auto-loaded unless `--disable-dotenv`)
2. Explicit env files via `-e` CLI flag
3. Global `environment` block in the YAML
4. Per-process `env_file` entries
5. Per-process `environment` block

#### 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:

```yaml
# 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`** -- Use `command` to 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 into `command`.
- **`cap_add`**, **`cap_drop`**, **`privileged`**, **`security_opt`** -- Not applicable.

### Config file naming

decompose auto-discovers config files in this order:

1. `decompose.yml`
2. `decompose.yaml`
3. `compose.yml` (same filename Docker Compose uses)
4. `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:

```yaml
services:
  web:
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 10s
      timeout: 1s
      start_period: 5s
      retries: 3
```

decompose:

```yaml
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`:

```yaml
    readiness_probe:
      http_get:
        host: "127.0.0.1"
        port: 8080
        path: /health
        scheme: http
```

### Quick conversion checklist

1. **Rename or copy** your `docker-compose.yml` to `compose.yml` (or `decompose.yml`).
2. **Remove the top-level `services:` key** and replace it with `processes:` (or keep `services:` -- decompose uses `processes:`).
3. **Replace `image:` with `command:`** -- specify the shell command that starts each service (e.g., `python manage.py runserver`, `npm start`).
4. **Remove `build:`**, `ports:`, `volumes:`, `networks:`, and any other container-specific fields.
5. **Keep `environment:`, `env_file:`, `working_dir:`, and `depends_on:`** -- these work as-is.
6. **Convert `healthcheck:` to `readiness_probe:`** using the schema shown above.
7. **Convert `restart:` to `restart_policy:`** -- values `no`, `on-failure`/`on_failure`, and `always` are supported.
8. **Convert `deploy.replicas:` to `replicas:`** at the process level.
9. **Test** with `decompose config` to validate your converted file, then `decompose up`.

### Before and after example

Docker Compose:

```yaml
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: ["CMD", "pg_isready"]
      interval: 5s

volumes:
  pgdata:
```

decompose:

```yaml
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
```