mothership 0.0.45

Process supervisor with HTTP exposure - wrap, monitor, and expose your fleet
Documentation
mothership-0.0.45 has been yanked.

Mothership

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

Features

  • Fleet Management: Launch and monitor ships and bays with dependency ordering
  • Prelaunch Jobs: Run migrations and setup tasks before any ship starts
  • Flagship Coordination: Multi-server leader election for prelaunch (static or PostgreSQL)
  • Uplinks: Pre-flight connectivity checks for external dependencies (databases, caches)
  • Bays (Docking Protocol): WebSocket multiplexing over Unix sockets for real-time apps
  • Named Binds: Multiple listeners (http, https, ws) routed via TCP or Unix sockets
  • Static File Serving: Serve static assets with configurable path prefix
  • Response Compression: Automatic gzip/deflate/br compression
  • CORS Preflight Cache: Cache backend OPTIONS responses per-origin
  • 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 Payloads: 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

cargo install mothership

Or from source:

git clone https://github.com/seuros/mothership.git
cd mothership
cargo build --release

Quick Start

# Initialize a new manifest
mothership init

# Validate configuration
mothership clearance

# View routing chart
mothership chart

# Run the fleet
mothership

# Run with TUI dashboard
mothership --tui

Configuration

Create ship-manifest.toml:

# Global configuration
[mothership]
metrics_port = 9090
compression = true

# Static files (served before routing to ships, with fallthrough)
# Longest prefix wins; if file not found, falls through to routes
[[mothership.static_dirs]]
path = "./public"
prefix = "/static"

# 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 = "/.*" },
]

# Background workers
[[fleet.workers]]
name = "sidekiq"
command = "bundle"
args = ["exec", "sidekiq"]
critical = false

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

# Docked bays (WebSocket multiplexing)
[[bays.websocket]]
name = "orbitcast"
command = "./orbitcast"
routes = [{ bind = "ws", pattern = "/ws" }]
config = { redis_url = "redis://localhost:6379" }

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

Ships vs Bays

Ships are traditional processes that bind to a port (TCP or Unix socket). Mothership proxies HTTP/WebSocket traffic to them.

Bays use the docking protocol for WebSocket multiplexing. Multiple client connections are multiplexed over a single Unix socket. Ideal for:

  • Real-time apps (chat, notifications)
  • Reducing connection overhead
  • Processes that don't need their own HTTP server

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/bays 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)

Bay Configuration

Field Type Default Description
name string required Unique identifier (ASCII only, no spaces)
command string required Command to execute
args string[] [] Command arguments
routes array [] WebSocket routes
depends_on string[] [] Ships/bays to wait for
env table {} Environment variables
config table {} Config passed via docking protocol
critical bool true Crash kills entire fleet
tags string[] [] Tags for filtering

Route Configuration

Routes map mothership binds to ships/bays:

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

# Shorthand format
routes = ["http:/api/.*", "ws:/cable"]

# With path stripping
routes = [
  { bind = "http", pattern = "/api/.*", strip_prefix = "/api" },
]

Bind Formats

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

Docking Protocol

Bays communicate with mothership via Unix sockets using a binary protocol:

Message Direction Purpose
Dock Bay → Mothership Bay ready, sends version
Moored Mothership → Bay Docking confirmed, sends config
Boarding Mothership → Bay New WebSocket client connected
Disembark Mothership → Bay Client disconnected
Cargo Bidirectional WebSocket data payload

Environment variables provided to bays:

  • MS_PID - Mothership process ID
  • MS_SHIP - Bay name
  • MS_SOCKET_DIR - Socket directory
  • MS_SOCKET_PATH - Full socket path
  • MS_BAY_TYPE - Bay type (e.g., "websocket")

Commands

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

# Run with TUI
mothership --tui

# Filter by tags
mothership --only web
mothership --only web,api
mothership --except workers

# Pre-flight check (validate + verify uplinks)
mothership preflight

# View routing chart
mothership chart

# Validate manifest (no network access)
mothership clearance

# Initialize new manifest
mothership init

Base Templates

Reduce duplication with base templates:

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

[base.bay]
critical = true
tags = ["realtime"]

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

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

# Bays inherit from base.bay
[[bays.websocket]]
name = "orbitcast"
command = "./orbitcast"

TUI Dashboard

The TUI shows real-time fleet status:

  • Overview Tab: Ship/bay status, group, PID, health, routes
  • Logs Tab: Per-process stdout/stderr with scroll
  • Modules Tab: WASM payload status

Controls:

  • Tab - Switch tabs
  • ↑/↓ - Navigate ships
  • PgUp/PgDn - Scroll logs
  • q - Quit

WASM Payloads

Payloads process requests/responses at the proxy layer:

[[modules]]
name = "auth"
wasm = "./modules/auth.wasm"
routes = ["/admin/.*"]
phase = "request"

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

Payloads can:

  • Block requests (return custom status/body)
  • Modify request path/headers
  • Modify response headers

Static Files & Compression

Static File Serving

Multiple static directories can be configured. Longest prefix wins, with implicit fallthrough to routes if file not found.

# Specific assets directory
[[mothership.static_dirs]]
path = "./public/assets"
prefix = "/assets"

# Catch-all for other static files (with fallthrough to routes)
[[mothership.static_dirs]]
path = "./public"
prefix = "/"
bind = "http"  # Optional: limit to specific bind

Response Compression

[mothership]
compression = true

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

CORS Preflight Cache

Cache CORS preflight (OPTIONS) responses from backends to reduce load:

[mothership]
cors_cache = true  # Enable with defaults

# Or with custom settings
[mothership.cors_cache]
enabled = true
default_ttl = 3600      # Fallback TTL in seconds (default: 3600)
max_entries = 10000     # Maximum cache entries (default: 10000)

Cache key includes: origin, path, Access-Control-Request-Method, and Access-Control-Request-Headers. TTL is extracted from the backend's Access-Control-Max-Age header when present.

Browser A → OPTIONS /api/login → Backend → cache + return
Browser B → OPTIONS /api/login → cached ✅ (no backend hit)
Browser C (different origin) → OPTIONS /api/login → Backend → cache + return

Environment Variables

Ships and bays inherit environment variables from mothership:

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

Process-specific env vars override inherited values:

[[fleet.web]]
name = "app"
command = "ruby"
env = { RAILS_ENV = "production" }

Prelaunch Jobs

Run setup tasks (migrations, cache warmup, etc.) before any ship or bay starts. All prelaunch jobs must complete successfully or the launch is aborted.

[[mothership.prelaunch]]
name = "ar-migrate"
command = "rails"
args = ["db:migrate"]

[[mothership.prelaunch]]
name = "memgraph-migrate"
command = "./migrate-memgraph"
depends_on = ["ar-migrate"]  # runs after ar-migrate completes

Prelaunch Configuration

Field Type Default Description
name string required Unique identifier
command string required Command to execute
args string[] [] Command arguments
env table {} Environment variables
depends_on string[] [] Other prelaunch jobs to wait for

Execution Order

  1. Uplinks are verified (if configured)
  2. Prelaunch jobs run in dependency order
  3. Ships and bays launch

If any prelaunch job fails (non-zero exit), the entire launch is aborted.

Flagship (Multi-Server Coordination)

When deploying multiple Motherships across different servers, the Flagship feature ensures only one instance runs prelaunch jobs (migrations, etc.) while others wait.

┌─────────────────────────────────────────────────────────────┐
│                         FLEET                               │
│  ┌───────────┐    ┌───────────┐    ┌───────────┐           │
│  │ Server A  │    │ Server B  │    │ Server C  │           │
│  │ Mothership│    │ Mothership│    │ Mothership│           │
│  │ ⭐FLAGSHIP│    │  (escort) │    │  (escort) │           │
│  │           │    │           │    │           │           │
│  │ 1. uplinks│    │ 1. uplinks│    │ 1. uplinks│           │
│  │ 2. migrate│    │ 2. wait...│    │ 2. wait...│           │
│  │ 3. signal │───►│ 3. ready! │    │ 3. ready! │           │
│  │ 4. launch │    │ 4. launch │    │ 4. launch │           │
│  └───────────┘    └───────────┘    └───────────┘           │
└─────────────────────────────────────────────────────────────┘
  • Uplinks are verified on ALL instances (each checks its own connectivity)
  • Prelaunch jobs run on ONLY ONE instance (the Flagship)
  • If the Flagship fails, the entire deployment aborts

Static Election

Explicitly designate the flagship via environment variable or command:

[mothership.flagship]
enabled = true
election = "static"
static_flagship = "$MS_FLAGSHIP"  # truthy: "true", "1", "yes"

Or via command output:

[mothership.flagship]
enabled = true
election = "static"
static_flagship = { command = "hostname", equals = "server-a" }

Examples:

  • Kamal: first host runs with MS_FLAGSHIP=true
  • Heroku: { command = "printenv DYNO", equals = "web.1" }

PostgreSQL Election

Automatic leader election using advisory locks:

[mothership.flagship]
enabled = true
election = "postgres"
election_url = "$DATABASE_URL"

The first instance to acquire the lock becomes Flagship. Escorts wait for the ready signal via LISTEN/NOTIFY. Requires the tokio-postgres feature:

cargo build --features tokio-postgres

Flagship Configuration

Field Type Default Description
enabled bool false Enable flagship coordination
election string "static" Election backend: "static" or "postgres"
election_url string - PostgreSQL URL (supports $ENV expansion)
static_flagship string/table - Env var ("$VAR") or command ({ command, equals })
prelaunch_timeout u64 300 Seconds escorts wait for flagship
election_timeout u64 30 Seconds to acquire lock

Uplinks

Verify external dependencies are reachable before launching the fleet. If any uplink fails, mothership aborts startup.

[[mothership.uplinks]]
url = "postgres://localhost:5432/mydb"
name = "postgres"

[[mothership.uplinks]]
url = "$DATABASE_URL"  # env var expansion
name = "primary-db"
timeout = "10s"  # default: 5s

[[mothership.uplinks]]
url = "redis://cache.internal:6379"
name = "redis"

Pre-flight Command

mothership preflight

Validates the manifest and verifies all uplinks are reachable.

Note: preflight requires network access to the configured uplinks. Running it on your local machine may fail if uplinks are only accessible from the deployment environment (e.g., internal databases, VPC-only services). Use clearance for offline manifest validation.

Supported Schemes

Scheme Default Port Check
postgres://, postgresql:// 5432 TCP
mysql:// 3306 TCP
redis:// 6379 TCP
memgraph://, neo4j:// 7687 TCP
http://, https:// 80/443 HTTP GET
tcp:// required TCP

Deduplication

Uplinks with the same host:port are checked only once:

# These two share the same postgres server - only one TCP check
[[mothership.uplinks]]
url = "postgres://localhost:5432/app_production"
name = "primary"

[[mothership.uplinks]]
url = "postgres://localhost:5432/app_replica"
name = "replica"

Metrics

Enable Prometheus metrics:

[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