proc-compose is a local process orchestrator for development and small deployments. It starts every service in your stack from one YAML file, streams their logs with stable names and colors, waits for readiness, restarts failed processes when configured, and shuts the whole tree down cleanly with one Ctrl+C.
Use it when a project needs more than one command to run:
- a frontend plus API server
- several microservices plus workers
- a local stack that needs ordered startup
- a single-container deployment that still runs multiple child processes
- a public demo stack using
merge-portortunnel
It is intentionally smaller than Docker Compose: no containers, no image builds, no networks, no volumes. Commands run on your machine exactly as you would run them in a terminal, but proc-compose gives them one lifecycle, one log stream, one config, and one control surface.
proc-compose up
proc-compose starting 3 processes
backend configured
frontend configured
merge-port configured
Press Ctrl+C to stop all
frontend │ ready
backend │ ready
merge-port │ ready
frontend │ VITE v5.4.2 ready in 237ms
backend │ listening on :3001
merge-port │ merge-port is running on port 8080
With brokit (recommended — handles install, update, and uninstall across the whole tool family in one command):
brokit install proc-compose # install latest release
brokit update proc-compose # upgrade to latest
brokit list # see installed tools and versions
brokit remove proc-compose # uninstallbrokit reads the GitHub releases for this repo, verifies the binary, and drops it into /usr/local/bin. It also installs the optional companions (merge-port, tunnel, env-vault, proxy-relay) the same way, which is the easiest way to unlock proc-compose's merge: and tunnel features.
From release binary (Linux/macOS, single-tool install):
curl -sSL https://raw.githubusercontent.com/anivaryam/proc-compose/main/install.sh | bashThe script downloads a release binary for your platform into ~/.local/bin/
(override with PROC_COMPOSE_INSTALL_DIR=/path/to/dir). Pin a specific
version with PROC_COMPOSE_VERSION=v1.2.3. If no release matches your
platform but go is on $PATH, the script falls back to go install.
With Go:
go install github.com/anivaryam/proc-compose/cmd/proc-compose@latestFrom source:
git clone https://github.com/anivaryam/proc-compose.git
cd proc-compose
make install # copies to ~/.local/bin/proc-compose unlocks additional capabilities when these binaries are available on your PATH. The simplest way to install them is brokit install <name>; alternatives are listed for reference.
| Binary | Enables | Install |
|---|---|---|
merge-port |
merge: config section — combine frontend + backend into one port |
brokit install merge-port |
tunnel |
Public URL for a local port (see "Full-stack with public tunnel" example) | brokit install tunnel |
If merge: is configured but merge-port is not on PATH, up, validate, and up --dry-run fail with an actionable error. doctor reports a finding. Without merge:, merge-port is not required.
The fastest path is bootstrap: it scans your project, proposes a config, and can verify that the generated config parses through proc-compose before writing anything.
proc-compose bootstrap # inspect generated config
proc-compose bootstrap --verify # verify generated config without writing
proc-compose bootstrap --write # create proc-compose.yml when none exists
proc-compose up # start the stackIf you prefer a template, generate a starter config:
proc-compose init
# edit proc-compose.yml, then:
proc-compose upOr write proc-compose.yml by hand:
merge:
client: 5173 # frontend port
server: 3001 # backend port
port: 8080 # combined proxy port (merge-port required)
processes:
frontend:
cmd: npm run dev
dir: ./client
env:
PORT: "5173"
backend:
cmd: go run ./cmd/server
dir: ./server
env:
PORT: "3001"
restart: on-failureproc-compose upFrontend, backend, and the merge-port proxy all start together. Logs are interleaved with color-coded prefixes. Ctrl+C stops everything cleanly.
proc-compose.yml declares named processes. Each process is a shell command with a mode plus optional working directory, environment, readiness probe, dependencies, and restart policy. Most processes are long-running services. Tasks run once for setup work such as migrations, seeding, codegen, or prestart build steps.
When you run proc-compose up, the runner:
- parses and validates the config
- starts processes whose dependencies are ready
- prefixes and streams stdout/stderr into one log
- tracks readiness through
http,tcp, orlogprobes - marks successful tasks completed so dependents can start
- applies restart policies to services
- forwards shutdown to the full process tree
Daemon commands (status, logs, monitor, restart, reload, stop) talk to the background runner for the current config file.
proc-compose bootstrap [--write] [--verify] [--json] Generate and optionally verify setup
proc-compose doctor [--write] [--json] Scan project and diagnose setup
proc-compose init [--template T] Generate starter config
proc-compose validate Parse config without starting
proc-compose up [processes...] Start all or named processes
proc-compose up --silent Run daemon in background
proc-compose monitor Open live TUI for daemon
proc-compose status [--json] Show daemon process states
proc-compose logs [-n N] Show daemon log file (default 50 lines)
proc-compose restart <process> Restart one running process
proc-compose reload Reload config and restart changed processes
proc-compose stop [--timeout N] [--force] Stop daemon gracefully (or SIGKILL)
proc-compose list List config processes
proc-compose man [--dir DIR] Generate man pages
proc-compose uninstall --name <n> Remove systemd unit
proc-compose completion <shell> Generate shell completion (bash/zsh/fish/powershell)
proc-compose --version Print version
Aliases:
up=u monitor=m restart=r reload=rl status=st|ps list=l
init=i doctor=doc validate=check uninstall=un
Global flags:
-f, --file string Config file path (default "proc-compose.yml"; falls back to proc-compose.yaml and walks parent dirs)
-v, --version Print version (note: on `up`, -v is --verbose; use --version to be explicit)
Important flags (up):
-s, --silent Daemonize — run in the background and exit
-v, --verbose Verbose debug output (overrides root -v on this subcommand)
--wait-ready With --silent, block until every started process passes its readiness probe
--wait-timeout int Seconds to wait when --wait-ready is set (default 60)
--log-file string Write all output to a file (use with --silent)
--max-log-size int Rotate log file when it exceeds this size in bytes (0 = no rotation)
--survive Generate a systemd user unit for auto-restart on reboot
--name string Name used for the systemd unit file (used with --survive)
--install Install and enable the systemd unit (used with --survive)
--force Overwrite an existing unit file (used with --install)
--dry-run Validate config and list processes without starting
--no-color Disable ANSI color output (also: NO_COLOR env var)
--log-format string Log output format: text or json (default "text")
--health-port int HTTP port for /health endpoint (0 = disabled)
Important flags (bootstrap):
--write Create proc-compose.yml when no config exists
--force Overwrite an existing config when used with --write
--verify Verify generated config through proc-compose
--json Emit machine-readable JSON
Flags (stop):
--timeout int Seconds to wait for graceful shutdown before SIGKILL (default 10)
--force SIGKILL immediately without graceful shutdown
Flags (init):
--template string Starter template: minimal (default), node, go, python
Flags (logs):
-n, --tail int Show last N lines (default 50; 0 = entire file)
| Variable | Read by | Effect |
|---|---|---|
NO_COLOR |
runner, monitor | Any non-empty value disables ANSI colour output |
PORT |
merge-port config | Used as merge.port when not set explicitly (cloud platforms) |
XDG_RUNTIME_DIR |
daemon | Base directory for socket / PID / log files; falls back to $TMPDIR |
PROC_COMPOSE_INSTALL_DIR |
install.sh |
Override install target (default ~/.local/bin) |
PROC_COMPOSE_VERSION |
install.sh |
Pin a specific release tag (e.g. v1.2.3) |
Each daemon is scoped per absolute config path (hashed into a stable id):
$XDG_RUNTIME_DIR/pc-<hash>.sock # IPC socket (status/monitor/restart/reload/stop talk to this)
$XDG_RUNTIME_DIR/pc-<hash>.pid # daemon PID file
$XDG_RUNTIME_DIR/pc-<hash>.log # default log file when --log-file is not set
logs reads the default .log path. If you ran up --silent --log-file PATH, the daemon writes to PATH instead and logs will report "no log file found" — read PATH directly.
proc-compose upproc-compose up frontend backendproc-compose up --silent --log-file app.logThe process starts daemonized. Logs are written to app.log. Use monitor to watch it live or stop to shut it down. A task-only stack runs in the foreground because no service remains for the daemon to manage, so --silent requires at least one service process.
proc-compose monitorOpens an interactive TUI showing process status, readiness, resource usage, and a scrollable log area:
proc-compose monitor q=quit ?=help ↑↓/jk=nav ⏎=filter a=all PgUp/Dn=scroll
ready 3/3 tasks 1 done failed 0 restarting 0
PROCESS STATUS RESTARTS CPU% MEM READY UPTIME
▶ backend ● running 0 12.5% 45.2MB ready 1m32s
frontend ● running 0 8.1% 32.1MB ready 1m32s
merge-port ● running 0 0.2% 8.4MB ready 1m32s
migrate ✓ completed 0 0.0% 0.0MB done 1m32s
──────────────────────────────────────────────────────────────────────────────────────────────
Logs: all
backend │ listening on :3001
frontend │ VITE v5.4.2 ready in 237ms
merge-port │ merge-port is running on port 8080
Keys:
| Key | Action |
|---|---|
q / Ctrl+C |
Quit |
? / h |
Toggle help overlay (Esc, Space, or any key dismisses; q quits) |
↑ / ↓ or k / j |
Navigate processes |
Enter |
Toggle log filtering for the selected process |
a |
Show all logs (clear filter) |
PgUp / PgDn |
Scroll log area |
G |
Jump to live tail |
g |
Jump to top of log buffer |
NO_COLOR=1 (or --no-color on up) disables colour output in both the runner and the monitor.
Completed task rows stay visible as ✓ completed with readiness done.
To make proc-compose survive machine reboots, generate a systemd user unit:
proc-compose up --survive --name myapp --installThis writes a unit to ~/.config/systemd/user/proc-compose-<name>.service and enables it.
The service starts proc-compose up --silent in daemon mode, which manages all processes.
To manually install without enabling immediately:
proc-compose up --survive --name myapp > ~/.config/systemd/user/proc-compose-myapp.service
systemctl --user daemon-reload
systemctl --user enable --now proc-compose-myappproc-compose restart backendSends a restart signal to the named process without affecting other running processes. The process is stopped and restarted immediately.
proc-compose reloadRe-reads the config file (proc-compose.yml, or proc-compose.yaml if you
prefer that extension) and restarts any processes whose definition has
changed (command, env, dir, etc.). Unchanged processes keep running.
If you added or removed processes, reload reports partial and lists
which names were skipped — hot-add and hot-remove aren't supported, so
restart the daemon to pick those up.
proc-compose stop # graceful, 10s timeout, then SIGKILL
proc-compose stop --timeout 30 # graceful, 30s timeout
proc-compose stop --force # SIGKILL immediatelystop sends SIGTERM and waits up to --timeout seconds (default 10) for
the daemon to exit cleanly. If the deadline passes the process is force-killed.
proc-compose status # human-readable table
proc-compose status --json # machine-readable, scriptablestatus reads a one-shot snapshot from the running daemon and exits. The
text table includes a MODE column, and successful tasks appear as
completed. The --json output includes a ready flag per process that
distinguishes "started" from "started and passed its readiness probe".
proc-compose validate # parse-only, no processes startedReturns non-zero if the config has any errors (unknown fields, bad
restart policies, circular depends_on, invalid regex, etc.).
proc-compose doctor # scan project and report suggestions
proc-compose doctor --write # create proc-compose.yml when none exists
proc-compose doctor --json # emit machine-readable reportdoctor helps first-time setup and existing config debugging. Without a config file, it scans common Node, Go, and Python project layouts, then prints suggested processes, ports, readiness probes, and YAML. It understands npm/yarn/pnpm/bun package runners, common frontend dev servers (Next.js, Vite, SvelteKit, Astro), Go cmd/**/main.go binaries, and Python Django/Flask/Uvicorn/Hypercorn entrypoints. With an existing config, it checks paths, env files, port conflicts, readiness gaps, restart loops, merge-port setup, and likely missing binaries.
--write is safe by default: it only creates proc-compose.yml when neither proc-compose.yml nor proc-compose.yaml exists.
When you pass -f path/to/proc-compose.yml, doctor scans and resolves relative paths from that config file's directory instead of the current directory.
proc-compose bootstrap # dry run: show generated config
proc-compose bootstrap --write # create proc-compose.yml when none exists
proc-compose bootstrap --write --force # overwrite an existing generated or stale config
proc-compose bootstrap --verify # test generated config without writing it
proc-compose bootstrap --write --verify # write, then verifybootstrap builds on doctor: it scans the repo, proposes a config, and can verify the config through proc-compose. The default mode is safe and read-only. Use --write to create proc-compose.yml; it refuses to overwrite existing configs unless you also pass --force. Use --verify without --write to validate a temporary generated config without creating the final file; use --write --verify to verify the file it wrote.
Like doctor, bootstrap -f path/to/proc-compose.yml scans and writes relative to that config file's directory.
When you do not pass -f, proc-compose searches upward from the current directory for proc-compose.yml or proc-compose.yaml. This lets up, monitor, status, doctor, and bootstrap work from nested package directories in a monorepo while still using the repository-level config.
Use doctor when you want diagnostics only. Use bootstrap when you want a first-run setup path.
proc-compose up --silent --log-file app.log --wait-ready --wait-timeout 60Without --wait-ready, up --silent returns once the daemon has bound
its IPC socket — processes may still be starting. With --wait-ready the
command blocks until every started process passes its ready_when probe
and every task completes successfully (or --wait-timeout elapses). Useful
in container entrypoints and CI fixtures that need a hard guarantee before
running the next step. A task-only stack cannot daemonize because no service
remains to manage.
When running with --log-file, use --max-log-size to automatically rotate logs when the file exceeds a given size. Up to 3 rotated files are kept.
proc-compose up --silent --log-file app.log --max-log-size 52428800 # 50 MBproc-compose up -f services.ymlproc-compose listproc-compose processes
backend go run ./cmd/server (on-failure)
frontend npm run dev (never)
merge-port merge-port --client 5173 --server ... (on-failure)
The config file is proc-compose.yml by default; if that's missing,
proc-compose falls back to proc-compose.yaml automatically. Pass
-f path/to/file to use any other path. Each process has the following fields:
processes:
name:
cmd: "command to run" # required, passed to the platform shell
mode: service # optional, service or task
dir: "./working/directory" # optional — working directory
env_file: ".env.backend" # optional — load env vars from file
env: # optional — extra environment variables
KEY: "value"
restart: "never" # optional — restart policy
max_restarts: 5 # optional — give up after N restarts
shutdown_timeout: 30 # optional — seconds before SIGKILL
ready_when: # optional — readiness probe
http: http://localhost:3000/health
ready_timeout: 60 # optional — seconds to wait for readiness
depends_on: # optional — wait for these processes
- db| Field | Required | Default | Description |
|---|---|---|---|
cmd |
Yes | — | Shell command to run. Linux/macOS use sh -c; Windows uses cmd /c. |
mode |
No | service |
Process mode: service for long-running processes, task for one-shot work |
dir |
No | . |
Working directory for the process; relative paths are resolved from the config file's directory, not the invocation CWD |
env_file |
No | — | Path to .env file; values are merged before env block |
env |
No | — | Extra environment variables (merged with system env; overrides env_file) |
restart |
No | never |
Restart policy: never, on-failure, or always |
max_restarts |
No | 0 (unlimited) |
Stop restarting after N restarts; process enters "failed" state |
shutdown_timeout |
No | 5 |
Seconds to wait for graceful shutdown before SIGKILL |
ready_when |
No | — | Readiness probe (see below) |
ready_timeout |
No | 60 |
Seconds to wait for readiness before failing (-1 = no limit) |
depends_on |
No | — | List of process names to wait for before starting |
Every process is either a service (long-running) or a task (one-shot).
The default is service. Set mode: task for migrations, seeding, codegen,
prestart build steps, or any command meant to run once and exit cleanly.
processes:
migrate:
mode: task
cmd: npm run migrate
api:
cmd: npm run dev
depends_on: [migrate] # api waits for migrate to exit 0Differences:
| Aspect | Service (mode: service) |
Task (mode: task) |
|---|---|---|
| Lifecycle | Long-running; expected to stay up | Runs once, exits |
| Ready when | Started (or ready_when passes) |
Exits with code 0 |
ready_when |
Allowed (http / tcp / log) |
Not allowed |
ready_timeout |
Allowed (default 60s, -1 = none) |
Not allowed |
restart |
never, on-failure, always |
Must be never |
max_restarts |
Allowed when restart != never |
Not allowed |
| As a dependency | Dependents start once it's ready | Dependents start once it exits 0 |
| Failure | Exit triggers restart per policy | Non-zero exit fails the stack |
| Status | running, restarting, failed |
completed (✓) or failed |
up --silent |
Counts as a managed process | Daemon refuses a task-only stack |
up --silent requires at least one service in the started closure —
the daemon has nothing to manage once every task has exited, so
task-only stacks always run in the foreground.
The monitor's status line shows tasks N done; completed task rows
stay visible as ✓ completed with readiness done. status and
status --json include a MODE column / mode field so scripts can
treat tasks differently from services.
Restart policies apply to service processes. Task processes always run once
and must use restart: never.
| Policy | Behavior |
|---|---|
never |
Process runs once. If it exits, it stays dead. |
on-failure |
Restarts if the process exits with a non-zero code. |
always |
Restarts on any exit, including clean exits. |
On restart, proc-compose uses exponential backoff starting at 1 second, doubling up to a max of 30 seconds. The backoff resets to 1 second after a process runs successfully for longer than the current backoff interval. Use max_restarts to cap the number of retries:
processes:
backend:
cmd: npm run start
restart: on-failure
max_restarts: 5 # give up after 5 restarts, exit non-zeroUse ready_when to define how proc-compose detects when a process is ready to accept traffic. Three probe types are supported:
processes:
backend:
cmd: npm run start
ready_when:
http: http://localhost:3000/health # GET returns 2xx
db:
cmd: postgres -D /data
ready_when:
tcp: localhost:5432 # TCP connect succeeds
worker:
cmd: python worker.py
ready_when:
log: "Worker ready" # regex matched in stdoutProbes poll every 3 seconds until success or process exit. The default readiness deadline is 60 seconds — set ready_timeout to override (-1 disables the limit). Without ready_when, a service is considered ready as soon as it starts. Tasks cannot use ready_when or ready_timeout; they become ready only after they exit successfully.
Use depends_on to control process startup order. A process waits until all its dependencies are ready (as determined by their ready_when probe) before starting:
processes:
db:
cmd: postgres -D /data
ready_when:
tcp: localhost:5432
backend:
cmd: npm run start
depends_on:
- db
ready_when:
http: http://localhost:3000/healthWhen a merge: section is present, proc-compose auto-injects depends_on on merge-port for every upstream process so you don't need to wire it manually — see merge-port Integration below.
If a dependency fails before becoming ready, dependent processes also fail.
Task dependencies are useful for one-time setup before services start:
processes:
migrate:
mode: task
cmd: npm run migrate
api:
cmd: npm run dev
depends_on: [migrate]
ready_when:
http: http://localhost:3000/healthWith mode: task, the command runs once. Exit code 0 marks the process
completed, sets ready: true, and lets dependents start. A non-zero exit
fails the stack and stops running processes. Tasks can't use ready_when,
ready_timeout, restart policies other than never, or max_restarts.
Use env_file to load environment variables from a .env file. The env block overrides any conflicting keys from env_file:
processes:
backend:
cmd: npm run start
env_file: ./server/.env
env:
PORT: "3000" # overrides PORT from .env fileThe optional merge section auto-injects a merge-port process to combine your client and server into a single port.
Simple mode — api_prefixes are auto-detected by scanning your server source files for top-level route registrations (app.use, app.get, etc.). You usually don't need to list them manually:
merge:
client: 5173
server: 3001Explicit API prefixes (override auto-detection, or when your backend serves non-standard prefixes):
merge:
client: 5173
server: 5000
api_prefixes:
- /api
- /health
- /uploadsRoute mode (multiple backends on different ports):
merge:
routes:
- /api=3001
- /auth=3002
- /=3000| Field | Default | Description |
|---|---|---|
client |
— | Client/frontend port (simple mode) |
server |
— | Server/backend port (simple mode) |
port |
$PORT → 8080 |
Proxy listen port — reads $PORT env var if not set, then falls back to 8080 |
api_prefixes |
[/api] |
Path prefixes routed to server (simple mode) |
routes |
— | Explicit prefix=target routes (route mode) |
client_process |
auto | Override the process name treated as the client (for env auto-injection) |
routes cannot be combined with client, server, or api_prefixes.
Auto-detection of api_prefixes only matches top-level Express/Koa/Fastify/Hono handlers (app.use, server.get, etc.) and Go chi/gorilla/net/http handlers (Route, Mount, Handle, HandleFunc). Sub-router files and other frameworks (Django, Rails, Flask, etc.) need explicit api_prefixes.
When a merge: section is present, proc-compose wires the upstream ports automatically so the proxy never starts before its backends are accepting connections:
- Each upstream process (
client/serverin simple mode; every backing process in route mode) gets aready_when: tcp localhost:<port>probe — but only when you haven't supplied your ownready_when. User probes (http,log) always win. - The injected
merge-portprocess getsdepends_onset to every upstream, so the runner blocksmerge-portstartup until the probes pass.
The upshot: a fresh proc-compose up on the simple example below boots client and server, waits for both to bind their ports, then starts merge-port. Requests that hit :8080 after the daemon prints "ready" can never land on an unbound upstream.
Override the auto-detected client process with merge.client_process (also used by the env-injection step). Upstream ports that don't match any managed process are left alone — proc-compose won't fabricate dependencies on external services.
Before starting, proc-compose always checks that the merge-port proxy output port (merge.port, or $PORT/8080) is free. It also checks upstream ports owned by managed processes — ownership is determined by a process env.PORT matching merge.client, merge.server, or a merge.routes target port. Managed upstream ports must be free because proc-compose is about to start those processes. Upstream ports with no matching managed process are treated as external services and may already be listening; proc-compose does not reject those occupied external targets.
When a merge: section is present, proc-compose looks at the client process's .env.example (or .env) and, for any of these well-known frontend variables present in the file, injects the merge-port URL into the client's environment:
VITE_API_BASE_URL, VITE_API_URL, VITE_SERVER_URL, REACT_APP_API_URL, REACT_APP_BASE_URL, REACT_APP_API_BASE_URL, NEXT_PUBLIC_API_URL, NEXT_PUBLIC_API_BASE_URL, NUXT_PUBLIC_API_BASE, PUBLIC_API_URL.
The path suffix from the example value is preserved (e.g. http://1.2.3.4:3000/api → http://localhost:8080/api). Anything you set explicitly under the process's env: block always wins. The client process is detected by matching merge.client against each process's PORT env var, or by name (client, frontend, web, ui, app); set merge.client_process to override.
merge:
client: 5173
server: 3001
processes:
frontend:
cmd: npm run dev
dir: ./frontend
env:
PORT: "5173"
backend:
cmd: go run .
dir: ./backend
env:
PORT: "3001"
DATABASE_URL: "postgres://localhost:5432/myapp"merge:
client: 5173
server: 5000
api_prefixes:
- /api
- /health
- /uploads
- /welcome
processes:
client:
cmd: npm run dev
dir: ./client
server:
cmd: npm run dev
dir: ./server
restart: on-failureprocesses:
gateway:
cmd: go run ./cmd/gateway
env:
PORT: "8080"
users:
cmd: go run ./cmd/users
env:
PORT: "8081"
orders:
cmd: go run ./cmd/orders
env:
PORT: "8082"
worker:
cmd: go run ./cmd/worker
restart: alwaysTunnel runs as a regular process. The tunnel binary spawns a background daemon (managed via a unix socket in $TMPDIR); the foreground command is a client attached to it. If proc-compose kills the client, the daemon keeps running and the public URL stays open. On Linux/macOS, wrap the command with bash and trap so SIGTERM stops the daemon too:
merge:
client: 5173
server: 3001
port: 8080
processes:
frontend:
cmd: npm run dev
dir: ./frontend
env:
PORT: "5173"
backend:
cmd: go run .
dir: ./backend
env:
PORT: "3001"
tunnel:
cmd: bash -c 'trap "tunnel stop --name myapp" EXIT TERM INT; tunnel http 8080 --name myapp; wait'
depends_on:
- merge-port
restart: on-failure
ready_when:
http: http://localhost:8080/healthThe trap runs tunnel stop --name myapp on shutdown so the daemon and its socket are cleaned up. Without it, proc-compose stop (or Ctrl+C) leaves the tunnel exposing your port until you kill it manually. This bash/trap wrapper is a Linux/macOS example. On Windows, use an explicit Windows process wrapper that stops the tunnel on exit, or stop the tunnel manually.
Do not pass
--silenttotunnel. That flag daemonizes and exits immediately — proc-compose treats the immediate exit as a crash and restarts in a loop. Default (foreground) is correct here.
Same Linux/macOS pattern repeated per service. Each tunnel waits on its upstream via depends_on + ready_when, so the public URL only opens once the local service serves requests:
processes:
api-a:
cmd: npm run dev
dir: ./service-a
env:
PORT: "7001"
ready_when:
http: http://localhost:7001/health
restart: on-failure
api-b:
cmd: npm run dev
dir: ./service-b
env:
PORT: "7002"
ready_when:
http: http://localhost:7002/health
restart: on-failure
api-a-tunnel:
cmd: bash -c 'trap "tunnel stop --name api-a" EXIT TERM INT; tunnel http 7001 --name api-a; wait'
depends_on:
- api-a
restart: on-failure
ready_when:
http: http://localhost:7001/health
api-b-tunnel:
cmd: bash -c 'trap "tunnel stop --name api-b" EXIT TERM INT; tunnel http 7002 --name api-b; wait'
depends_on:
- api-b
restart: on-failure
ready_when:
http: http://localhost:7002/healthVerify a clean shutdown:
proc-compose stop
ls /tmp/tunnel-*.sock 2>/dev/null # empty
ps aux | grep "tunnel http" | grep -v grep # emptyproc-compose works as a single container entrypoint on Railway, Render, Fly.io, and similar platforms.
Cloud platforms assign a port via the PORT environment variable. When merge.port is not set in your config, proc-compose reads $PORT automatically and passes it to the merge proxy — no config change needed.
merge:
client: 5173
server: 3001
# no port: line — proc-compose reads $PORT at runtime
processes:
frontend:
cmd: npm run dev
env:
PORT: "5173"
backend:
cmd: node server.js
env:
PORT: "3001"
restart: on-failureDeploy this as-is. The merge proxy binds to whatever port the platform assigns.
If a process with restart: never exits with a non-zero code, proc-compose exits with a non-zero code too. The platform sees the failure and restarts the container. Processes with restart: on-failure or restart: always never trigger this — they keep retrying until context cancellation.
Cloud log aggregators don't render ANSI color codes. Disable them with the --no-color flag, set the NO_COLOR environment variable (recognized automatically), or use structured JSON logging:
proc-compose up --no-color# or use structured JSON logging for easier parsing by log aggregators:
proc-compose up --log-format jsonCloud platforms run a single entrypoint and gate traffic on its readiness
probe. Combine --silent, --wait-ready, and --health-port to make
proc-compose a complete entrypoint with a real liveness signal:
proc-compose up --silent \
--log-format json \
--wait-ready --wait-timeout 90 \
--health-port 8081The container's HTTP healthcheck targets :8081/health (returns 200 only
when every managed process is running). --wait-ready blocks the
entrypoint until probes pass so platforms don't route traffic to a
half-started stack.
# or set in the platform's environment variables dashboard:
NO_COLOR=1Set these in Railway's environment variables dashboard before deploying:
| Variable | Value |
|---|---|
NO_COLOR |
1 |
NODE_ENV |
production |
Everything else (PORT, database URLs, secrets) is set as normal Railway env vars and inherited by all child processes.
proc-compose.yml
│
▼
proc-compose up
│
├── platform shell "npm run dev" → frontend │ http://localhost:5173
├── platform shell "go run ." → backend │ http://localhost:3001
└── platform shell "merge-port ..." → merge-port │ http://localhost:8080 ← merge: section
│
Ctrl+C → context cancellation → SIGTERM → SIGKILL (5s) → all processes stop
The two tools compose a layered stack:
- proc-compose — orchestrates all processes, handles logs, restarts, and shutdown
- merge-port — auto-injected from the
merge:config section; routes/api→ backend,/*→ frontend on a single port
Each process runs through the platform shell in its own process group so child trees (npm → node, nodemon → ts-node, etc.) terminate cleanly. Linux/macOS use sh -c; Windows uses cmd /c. Names are sorted alphabetically for deterministic color assignment.
# Build
make build
# Run tests
make test
# Install to ~/.local/bin/
make install| Feature | Linux | macOS | Windows |
|---|---|---|---|
| Process orchestration | Full | Full | Full |
merge: sections |
Full | Full | Full (requires merge-port) |
--survive / systemd auto-restart |
Full | N/A | N/A |
uninstall |
Full | N/A | N/A |
| Named pipes IPC | Unix sockets | Unix sockets | Full |
Graceful stop (--timeout) |
Full (SIGTERM) | Full (SIGTERM) | N/A (always taskkill /F) |
init template |
POSIX shell | POSIX shell | PowerShell |
Full support including --survive for systemd user unit auto-restart on reboot.
Full process orchestration support. No systemd user services, so --survive, --install, and uninstall are unavailable. Use launchd or a similar mechanism for auto-restart on macOS.
Full process orchestration support. Named pipes IPC via go-winio. Windows Job Objects for process tree termination via go-winjob. No --survive or uninstall (no systemd user units on Windows). stop --timeout is ignored; stop always uses taskkill /T /F.
Shell commands in configs use cmd /c. Use cmd environment syntax such as echo %PORT%. If you want PowerShell syntax, make it explicit in the command text: powershell -NoProfile -Command "Write-Output $env:PORT". POSIX syntax such as $VAR, $(...), trap, export, and shell loops is not promised to be portable. Existing $VAR translation is best-effort compatibility only. Paths with spaces need quoting, and Windows-native commands may need backslashes. POSIX examples elsewhere in this README are Linux/macOS examples unless labelled otherwise. The init --template minimal on Windows generates a PowerShell-based template.
# Via Go
go install github.com/anivaryam/proc-compose/cmd/proc-compose@latest
# Or download a release binary from:
# https://github.com/anivaryam/proc-compose/releasesNote: merge-port must be installed separately if you use the merge: feature.
MIT
