mothership 0.0.13

Process supervisor with HTTP exposure - wrap, monitor, and expose your fleet
Documentation
# Mothership

Process supervisor with HTTP exposure. Launches your fleet, routes traffic, runs WASM plugins.

## Features

- **Fleet Management**: Launch and monitor processes with dependency ordering
- **Named Binds**: Multiple listeners (http, https, ws) routed to ships via TCP or Unix sockets
- **Static File Serving**: Serve static assets with configurable path prefix
- **Response Compression**: Automatic gzip/deflate/br compression
- **Health Checks**: HTTP-based monitoring with circuit breakers
- **Crash Loop Protection**: Exponential backoff for failing ships
- **TUI Dashboard**: Real-time fleet status with logs (`--tui`)
- **WASM Plugins**: Request/response processing with sandboxing
- **Prometheus Metrics**: Ship status, restarts, request counts at `/metrics`
- **Tag Filtering**: `--only web` or `--except workers` for selective launching
- **Base Templates**: Reduce config duplication
- **Graceful Shutdown**: Dependency-aware termination

## Installation

```bash
cargo install mothership
```

Or from source:
```bash
git clone https://github.com/seuros/mothership.git
cd mothership
cargo build --release
```

## Quick Start

```bash
# Initialize a new manifest
mothership init

# Validate configuration
mothership clearance

# Run the fleet
mothership

# Run with TUI dashboard
mothership --tui
```

## Configuration

Create `ship-manifest.toml`:

```toml
# Global configuration
[mothership]
metrics_port = 9090
compression = true  # Enable gzip/deflate/br compression

# Static files (served before routing to ships)
[mothership.static_dir]
path = "./public"
prefix = "/static"  # Requests to /static/* served from ./public

# Named binds - external listeners
[mothership.bind]
http = "0.0.0.0:80"
https = "0.0.0.0:443"
ws = "0.0.0.0:8080"

# Web ships
[[fleet.web]]
name = "app"
command = "ruby"
args = ["app.rb"]
bind = "tcp://127.0.0.1:3000"
healthcheck = "/health"
routes = [
  { bind = "http", pattern = "/.*" },
  { bind = "https", pattern = "/.*" },
]

[[fleet.web]]
name = "cable"
command = "anycable-go"
bind = "tcp://127.0.0.1:8081"
routes = [
  { bind = "ws", pattern = "/cable" },
]

# Background workers
[[fleet.workers]]
name = "sidekiq"
command = "bundle"
args = ["exec", "sidekiq"]
critical = false  # Won't kill fleet on crash

# One-shot jobs
[[fleet.jobs]]
name = "migrate"
command = "rails"
args = ["db:migrate"]
oneshot = true

[[fleet.web]]
name = "main-app"
depends_on = ["migrate"]  # Wait for migration

# WASM modules (optional)
[[modules]]
name = "rate_limiter"
wasm = "./modules/rate_limiter.wasm"
routes = ["/api/.*"]
phase = "request"
```

### Ship Configuration

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `name` | string | required | Unique identifier (ASCII only, no spaces) |
| `command` | string | required | Command to execute |
| `args` | string[] | `[]` | Command arguments |
| `bind` | string | - | Internal bind address (`tcp://host:port` or `unix:///path`) |
| `healthcheck` | string | - | Health check endpoint path |
| `routes` | array | `[]` | HTTP routes (see below) |
| `depends_on` | string[] | `[]` | Ships to wait for before starting |
| `env` | table | `{}` | Environment variables |
| `critical` | bool | `true` | Crash kills entire fleet |
| `oneshot` | bool | `false` | Run once and exit |
| `tags` | string[] | `[]` | Tags for filtering (`--only`, `--except`) |
| `comment` | string | - | Description (for documentation) |

### Route Configuration

Routes map mothership binds to ships. Three formats supported:

```toml
# Object format (explicit)
routes = [
  { bind = "http", pattern = "/api/.*" },
  { bind = "ws", pattern = "/cable" },
]

# Shorthand format (bind:pattern)
routes = ["http:/api/.*", "ws:/cable"]
```

### Bind Formats

```toml
# TCP with explicit prefix
bind = "tcp://127.0.0.1:3000"

# TCP without prefix
bind = "0.0.0.0:8080"

# Port only (defaults to 127.0.0.1)
bind = "3000"

# Unix socket
bind = "unix:///tmp/app.sock"
```

### Unix Socket Example (Puma)

Configure Puma in `config/puma.rb`:
```ruby
bind "unix:///tmp/puma.sock"
```

Then in `ship-manifest.toml`:
```toml
[[fleet.web]]
name = "puma"
command = "bundle"
args = ["exec", "puma"]
bind = "unix:///tmp/puma.sock"
healthcheck = "/up"
routes = ["/.*"]
```

Health checks and HTTP proxying both work over Unix sockets.

## Commands

```bash
# Run fleet (default)
mothership
mothership run
mothership run -c /path/to/manifest.toml

# Run with TUI
mothership --tui

# Filter by tags
mothership --only web           # Only ships tagged "web"
mothership --only web,api       # Ships tagged "web" OR "api"
mothership --except workers     # All except "workers"

# Validate manifest
mothership clearance

# Initialize new manifest
mothership init
```

## Base Templates

Reduce duplication with base templates:

```toml
[base.ship]
env = { RAILS_ENV = "production" }
critical = true
tags = ["ruby"]

[base.module]
phase = "request"
tags = ["security"]

# Ships inherit from base.ship
[[fleet.web]]
name = "app"
command = "ruby"
tags = ["web"]  # Combined: ["ruby", "web"]
# Inherits: env, critical from base
```

## TUI Dashboard

The TUI shows real-time fleet status:

- **Overview Tab**: Ship status, group, PID, health, routes
- **Logs Tab**: Per-ship stdout/stderr with scroll
- **Modules Tab**: WASM module status

Controls:
- `Tab` - Switch tabs
- `↑/↓` - Navigate ships
- `PgUp/PgDn` - Scroll logs
- `q` - Quit

## WASM Modules

Modules process requests/responses at the proxy layer:

```toml
[[modules]]
name = "auth"
wasm = "./modules/auth.wasm"
routes = ["/admin/.*"]
phase = "request"  # or "response"

[[modules]]
name = "cache"
wasm = "./modules/cache.wasm"
routes = ["/api/.*"]
phase = "response"
config = { ttl = "3600" }
```

Modules can:
- Block requests (return custom status/body)
- Modify request path/headers
- Modify response headers

## Static Files & Compression

### Static File Serving

Serve static assets directly from a directory:

```toml
[mothership.static_dir]
path = "./public"           # Directory containing static files
prefix = "/static"          # URL prefix (default: "/static")
bind = "http"               # Optional: limit to specific bind
```

Requests to `/static/css/app.css` serve `./public/css/app.css`.

### Response Compression

Enable automatic compression for all responses:

```toml
[mothership]
compression = true
```

Supports gzip, deflate, and brotli based on client `Accept-Encoding`.

## Environment Variables

Ships inherit all environment variables from the mothership process. Pass secrets at runtime:

```bash
MASTER_KEY=secret DATABASE_URL=postgres://... mothership run
```

Ship-specific env vars override inherited values:

```toml
[[fleet.web]]
name = "app"
command = "ruby"
env = { RAILS_ENV = "production" }  # adds to inherited env
```

Shell expansion is supported:

```toml
env = {
  DATABASE_URL = "${DATABASE_URL}",
  REVISION = "${git rev-parse HEAD}",
}
```

## Metrics

Enable Prometheus metrics:

```toml
[mothership]
metrics_port = 9090
```

Scrape `http://127.0.0.1:9090/metrics`:

```
mothership_ship_status{ship="app",group="web"} 1
mothership_ship_healthy{ship="app",group="web"} 1
mothership_ship_restarts_total{ship="app",group="web"} 0
mothership_requests_total{route="/api"} 1234
mothership_fleet_ships_total 3
```

Also serves `/health` for liveness probes.

## License

MIT