Skip to main content

rig

A zero-dependency, zero-daemon developer environment supervisor and process runner for multi-service repositories. Featuring machine-wide supervision, dynamic port allocation, zero-race socket inheritance, multi-stack modes (native, container), and verified lifecycle management.


Why rig?

Modern multi-service local development often suffers from:

  • Port collisions: Multiple developers or multiple checkouts of the same repo colliding on static ports like 3000 or 8000.
  • Zombie processes: Dev servers left orphaned after an interrupted test run, keeping ports bound and blocking subsequent runs.
  • Race conditions on port binding: Allocating an ephemeral port, closing the probe socket, and having another process grab it before the service can bind.
  • Hidden global state: No way to see what services or test instances are running across all checkouts on your machine.
  • Heavy or brittle supervisors: Requiring background daemons (systemd/dockerd/supervisord) or complex Node/Ruby process supervisors just to launch a Python API and a frontend dev server.

rig solves this with:

  1. Zero External Runtime Dependencies: Standard library Python 3.10+ only (socket, subprocess, os, signal, json, fcntl, shlex, dataclasses, pathlib).
  2. Zero Persistent Daemons: Fully file-backed atomic registry (~/.local/state/rig/instances/) and non-blocking file locks (flock). Fast, crash-resilient, and stateless.
  3. Machine-Wide Supervision: Inspect all active projects across your machine (rig ps), stop any named project (rig down <project>), or tear down all active instances at once (rig down --all).
  4. Multi-Stack Modes (native vs container): Define base services and mode overlays in a single rig.json. Switch modes cleanly with mutex collision protection (rig up --mode container --switch).
  5. Zero-Race Socket Inheritance (type: "fd"): Binds listening sockets on kernel port 0, holds them open, and passes the descriptors directly into child processes (--fd {fd}). The port is never released between allocation and service start.
  6. Human-Friendly & Sticky Ports: Allocates clean, typing-friendly ports (3000 for frontend, 8000 for backend) by default, supports explicit preferred_port in rig.json, and maintains sticky port leases in instance state across restarts.
  7. One-Command Setup: rig init [--up] automatically scans your repository for Docker Compose, FastAPI, Flask, Django, Vite, or Next.js and generates a validated rig.json.
  8. AI-Friendly Protocol: Universal --json output envelope (ok, schema, data/error) and deterministic exit codes (0 to 6, 130) designed for autonomous agents and CLI automation.

Installation

Using pipx:

pipx install rig-cli

Using uv:

uv tool install rig-cli

Using standard pip:

pip install --user rig-cli

Both rig and rig-cli commands are automatically available on your $PATH.

Install from Git

Using uv:

uv tool install --force "git+https://github.com/evgesha9400/rig.git"

Using pipx / pip:

pip install --user "git+https://github.com/evgesha9400/rig.git"

Add to a Specific Project

uv add "git+https://github.com/evgesha9400/rig.git"

Direct Drop-in (Zero Installation)

Since rig is a single self-contained module with zero third-party dependencies, you can copy src/rig/cli.py directly into any repository (e.g. scripts/rig.py):

curl -fsSL https://raw.githubusercontent.com/evgesha9400/rig/main/src/rig/cli.py -o scripts/rig.py

Quick Start

1. Initialize a Project

Run rig init in your repository root. rig inspects your files, detects existing backends, frontends, and Docker Compose configurations, and writes a tailored rig.json:

rig init
# Or initialize and start services immediately:
rig init --up

You can preview the detected configuration without writing files:

rig init --dry-run

2. Verify Your Environment

Run pre-flight static verification to ensure working directories exist, binaries are executable, Docker Compose files are present, and dependency graphs contain no cycles:

rig check

3. Start & Supervise Services

# Start all services in the active or default mode
rig up

# Check status of the local checkout
rig status

# Inspect service logs
rig logs backend -n 50

# View all running projects and instances across your machine
rig ps

# Stop all services in the local checkout
rig down

Global Machine-Wide Supervision

rig maintains a machine-wide state registry under $XDG_STATE_HOME/rig/instances/ (default: ~/.local/state/rig/instances/). Every project instance records its directory, PID, PGID, active mode, and allocated ports.

Inspect All Projects (rig ps)

rig ps

Example output:

PROJECT         INSTANCE      MODE       STATUS    ACTIVE  PORTS                    ROOT
deltalytic      68d374ab9c34  native     running   2/2     backend:54123, ui:54124  /Users/alice/projects/deltalytic
my-api          a1b2c3d4e5f6  container  running   1/1     db:5432                  /Users/alice/work/my-api

STATUS reports the instance as a whole: running when every recorded service is up, partial when only some are, stopped when none are, and orphaned when the checkout directory no longer exists.

Add --health to probe HTTP endpoints for live health checks:

rig ps --health

Targeted Teardown

Stop a project from anywhere on your machine, even if you are not inside its directory:

# Stop by project name slug
rig down deltalytic

# Stop by specific instance ID
rig down 68d374ab9c34

# Stop ALL running instances across the entire machine
rig down --all

rig stores process group IDs (PGID) and Docker Compose project references in its state registry, allowing it to cleanly terminate orphaned services even if the original working tree was deleted (rm -rf).

Cleanup Stale Instances

# Clean up dead instances whose processes are no longer running
rig prune

# Force-kill any lingering processes in unmanaged instances and prune
rig prune --force

prune reclaims an instance's recorded state but keeps its checkout.lock file, so a concurrent rig command can never take a lock on a file nobody else can see. prune --force stops services dependents-first and exits 1 while preserving any dependency whose dependent refused to stop.


Multi-Stack Modes (native vs container)

rig supports multi-stack modes within a single rig.json. For example, you can run database dependencies in containers while developing application code natively, or run the entire stack in containers.

Example rig.json with Modes:

{
  "$schema": "https://raw.githubusercontent.com/evgesha9400/rig/main/rig.schema.json",
  "project": "my-app",
  "default_mode": "native",
  "services": {
    "db": {
      "type": "compose",
      "compose_file": "docker-compose.yml",
      "compose_service": "postgres",
      "health_tcp": 5432
    }
  },
  "modes": {
    "native": {
      "services": {
        "backend": {
          "type": "fd",
          "cwd": "backend",
          "command": ".venv/bin/python -m app.main --fd {fd}",
          "health": "/healthz",
          "depends_on": ["db"]
        },
        "frontend": {
          "type": "port",
          "cwd": "frontend",
          "command": "npm run dev -- --port {port}",
          "health": "/",
          "depends_on": ["backend"],
          "env": {
            "VITE_API_PORT": "{backend_port}"
          }
        }
      }
    },
    "container": {
      "services": {
        "backend": {
          "type": "compose",
          "compose_file": "docker-compose.yml",
          "compose_service": "backend",
          "health": "http://127.0.0.1:8000/healthz",
          "depends_on": ["db"]
        },
        "frontend": {
          "type": "compose",
          "compose_file": "docker-compose.yml",
          "compose_service": "frontend",
          "health": "http://127.0.0.1:3000/",
          "depends_on": ["backend"]
        }
      }
    }
  }
}

Switching Modes Safely

rig prevents accidental multi-mode conflicts. If services are currently running in native mode, attempting to start container mode without stopping the old services will be safely rejected:

# Fails with exit code 3 (E_MODE_CONFLICT) to prevent colliding processes:
rig up --mode container

# Cleanly tears down native services first and boots container mode:
rig up --mode container --switch

AI Agent & Automation Protocol

rig is designed from the ground up for reliable operation by AI coding assistants, orchestrators, and CI pipelines:

Universal --json Envelope

Every command accepts --json and emits a predictable schema:

{
  "schema": "rig.ps/1",
  "ok": true,
  "data": [
    {
      "project": "my-app",
      "instance_id": "68d374ab9c34",
      "mode": "native",
      "state": "running",
      "services_count": 2,
      "services_active": 2,
      "root": "/path/to/my-app",
      "ports": {"backend": 54123, "frontend": 54124}
    }
  ]
}

Errors emit structured details with recovery hints:

{
  "schema": "rig.error/1",
  "ok": false,
  "error": {
    "code": "E_MODE_CONFLICT",
    "message": "Instance is running in mode 'native'; cannot start mode 'container'",
    "hint": "Pass --switch to stop the active mode first, or run 'rig down' before starting a new mode.",
    "details": {"active_mode": "native", "requested_mode": "container"}
  }
}

Deterministic Exit Codes

Exit Code Constant Meaning
0 EXIT_OK Command completed successfully.
1 EXIT_OP_FAILED Service failed to start, healthcheck timed out, or teardown failed.
2 EXIT_USAGE Invalid command line arguments or invalid manifest syntax.
3 EXIT_MUTEX_CONFLICT Instance lock busy (checkout.lock) or mode conflict without --switch.
4 EXIT_NOT_FOUND Project, service, or instance target not found.
5 EXIT_REFUSED Operation refused (e.g. destructive action without confirmation).
6 EXIT_EXTERNAL_TOOL Missing external requirement (docker, compose, lsof).
130 EXIT_INTERRUPTED Interrupted by signal (SIGINT, SIGTERM).

Manifest Reference (rig.json)

To inspect or validate manifest configurations against the formal JSON Schema:

rig schema

Root Fields

Field Type Required Description
project string Yes Project identifier slug used for isolation and Docker Compose naming.
default_mode string No Mode to use when --mode is omitted (defaults to first mode in modes or native).
services object No Base services active across all modes.
modes object No Dictionary of mode configurations ({"native": {"services": {...}}, "container": ...}).

Service Fields

Field Type Required Description
type "fd" | "port" | "compose" Yes Port allocation strategy.
command string For fd / port Command line to execute. Supports {fd}, {port}, and {<service>_port} placeholders.
cwd string No Working directory relative to repository root (defaults to .).
health string No HTTP path to poll for 200 OK (e.g. /healthz, /).
health_tcp integer No TCP port to poll for socket connection (ideal for databases like Postgres/Redis).
depends_on string[] No Services that must be healthy before this service starts.
aliases string[] No Alternative names for scope targeting (e.g. ["ui"] for frontend).
env map No Environment variables. Supports {<service>_port} placeholders.
env_files string[] No Dotenv-style files, relative to the repository root, loaded before env.
inherit string[] No Ambient environment variables to pass through beyond the base safe allowlist.
compose_file string For compose Path to Docker Compose file.
compose_service string For compose Name of service inside Docker Compose file.
compose_port integer No Container port whose published host port is recorded as the service URL.
docker_context string No Docker context every command for this service is pinned to.

Environment and Docker Endpoint for compose Services

env, env_files and inherit apply to compose services as well as to fd and port services. The resulting environment is handed to docker compose itself, so it drives ${VAR} interpolation inside the compose file and reaches the containers.

That environment is an allowlist, so no ambient DOCKER_*, COMPOSE_* or application variable can leak in and point a service at another project's resources. The Docker client settings (DOCKER_CONFIG, DOCKER_CERT_PATH, DOCKER_TLS_VERIFY) are the exception: they are passed through so a TLS or rootless setup can still reach its own daemon.

Those client settings are recorded with the service, and every later plain docker command — the label query, the inspection, stop and rm — is given the recorded ones instead of whatever the terminal holds. A service started against its own DOCKER_CONFIG therefore stays reachable for rig status and rig down, and a DOCKER_CONFIG exported afterwards cannot redirect them.

The Docker endpoint in force at startup — DOCKER_HOST and the Docker context — is recorded with the service. Every later status query and teardown is pinned to that endpoint, so a DOCKER_HOST that changes between rig up and rig down can never send the query to a daemon that does not hold the container.

The endpoint is chosen in Docker's own order of precedence:

  1. the docker_context the manifest declares;
  2. the ambient DOCKER_CONTEXT, which is read even though the service environment is an allowlist, so DOCKER_CONTEXT=colima rig up is honoured;
  3. the ambient DOCKER_HOST, when neither of the above names a context;
  4. otherwise the active context, resolved with docker context show.

Whenever a context decides, it is recorded alone and no host is recorded with it, because --context outranks DOCKER_HOST. A later docker context use colima therefore does not strand the container: rig status, rig down and rig prune still reach the context that holds it.


Stable & Human-Friendly Port Allocation

rig eliminates random ephemeral ports (e.g. 58472) and maintains stable, human-friendly ports across restarts:

1. Precedence Hierarchy

  1. Explicit preferred_port (or port): Defined per service in rig.json (e.g. "preferred_port": 3000).
  2. Sticky Leased Port: Rig persists assigned ports in instance state (~/.local/state/rig/instances/<instance>/state.json), reusing the same port across rig down and rig up.
  3. Role-Based Friendly Defaults:
    • frontend / web / ui / client / vite / next: starts at 3000
    • backend / api / server / app / worker: starts at 8000
    • docs / storybook / admin: starts at 4000
    • other / unmatched: starts at 5000
  4. Collision-Safe Probing: If the target port is occupied (e.g. by another checkout running simultaneously), rig probes port + 1, port + 2, etc., avoiding collisions automatically without jumping to high ephemeral numbers.

2. Configuration Example

{
  "project": "my-app",
  "services": {
    "frontend": {
      "type": "port",
      "preferred_port": 3000,
      "command": ["npm", "run", "dev", "--", "--port", "{port}"]
    },
    "backend": {
      "type": "fd",
      "preferred_port": 8000,
      "app": "main:app"
    }
  }
}

How Socket Inheritance Works (type: "fd")

When a service specifies type: "fd", rig:

  1. Creates a TCP socket bound to 127.0.0.1:0. The OS kernel allocates a free ephemeral port immediately.
  2. Marks the socket listening (listen(128)).
  3. Keeps the descriptor open and passes it via subprocess.Popen(pass_fds=[fd]).
  4. Passes the integer descriptor to the command line via --fd {fd}.

Python / Uvicorn Example:

import argparse
import socket
import uvicorn

parser = argparse.ArgumentParser()
parser.add_argument("--fd", type=int, default=None)
args = parser.parse_args()

if args.fd is not None:
    sock = socket.fromfd(args.fd, socket.AF_INET, socket.SOCK_STREAM)
    uvicorn.run("myapp.main:app", fd=sock.fileno())
else:
    uvicorn.run("myapp.main:app", host="127.0.0.1", port=8000)

Symmetrical Makefile Integration

RIG ?= rig

up:
	@$(RIG) up

down:
	@$(RIG) down

status:
	@$(RIG) status

ps:
	@$(RIG) ps

check:
	@$(RIG) check

logs:
	@tail -n 200 -F .local-run/logs/*.log

Development & Testing

# Clone the repository
git clone https://github.com/evgesha9400/rig.git
cd rig

# Run full test suite with uv
uv run --with pytest pytest tests/

License

MIT

Release files for rig-cli 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for rig-cli 1.0.0
File Size Uploaded
rig_cli-1.0.0.tar.gz 154.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for rig-cli 1.0.0
File Interpreter ABI Platform
rig_cli-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 233.3 kB

Release files / rig_cli-1.0.0.tar.gz

Download URL rig_cli-1.0.0.tar.gz
Size 154.5 kB
Tags Source
SHA-256 checksum
How to use checksums
02314b3ee202db32b483c12b7f1944ac28cfcf1f3569230ce2dff2cebefe0951
BLAKE2b-256 checksum
How to use checksums
5b3ccb3348f7293615d78c3a2b86d5f25012f04c93a646569d4cb3b227182ed7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / rig_cli-1.0.0-py3-none-any.whl

Download URL rig_cli-1.0.0-py3-none-any.whl
Size 78.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
1ac72c87681b3dd02d8edcc01eadc72fb884a59ba74fe66a5a655ebb9a4c4e81
BLAKE2b-256 checksum
How to use checksums
bf36c5bdf17ebae2535b463ab04c320a3eec9a68df0487f88686895eb7140dfd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release history Release notifications | RSS feed

1.1.0

2 release files

1.0.4

2 release files

1.0.3

2 release files

1.0.2

2 release files

1.0.1

2 release files

This release

1.0.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page