@ilies-bel/fleet
v2.1.0
Published
Local QA environment manager — spin up isolated Docker containers per feature branch
Maintainers
Readme
Fleet
Run multiple feature-branch versions of the app simultaneously on localhost.
How it works
The gateway runs two ports:
| Port | Purpose |
|------|---------|
| 3000 | Transparent proxy — forwards all traffic to the active feature container |
| 4000 | Admin dashboard + management API + OAuth relay |
Feature containers run on the internal Docker network (fleet-net) only — no host port exposure. Each feature runs as a single container named fleet-<name> with supervisord as PID 1, managing all services and peers. You point your browser at port 3000 to interact with the app, and port 4000 to manage which feature is active.
localhost:4000 ← dashboard & API
localhost:3000 ← transparent proxy → active feature container
↓
fleet-<name>:80 (internal)
supervisord (PID 1)
├── service: backend:8081
├── service: frontend:3000
├── peer: wiremock:8080
└── nginx:80 (internal path fan-out)Migration from qa-fleet
If you were using v0.1, see MIGRATION.md for a full rename reference.
Prerequisites
- Docker (with Docker Compose v2)
- bash
- Node 20+ (only for local dashboard development)
Install
There are two ways to install fleet, depending on whether you just want to use it or also work on it.
For users — install from npm
Install the fleet CLI globally from the public npm registry:
npm install -g @ilies-bel/fleetOr run it without a global install:
npx @ilies-bel/fleet <command>Then, in any project you want to manage:
cd <your-project>
fleet init # interactive setup wizardTo upgrade later:
npm install -g @ilies-bel/fleet@latestFor contributors — dev mode (live symlink)
If you're hacking on fleet itself and want your local changes to take effect
immediately, clone the repo and link it. npm link registers a global fleet
that points at your working copy, so edits are picked up with no reinstall:
git clone https://github.com/ilies-bel/fleet.git
cd fleet
npm link # symlinks global `fleet` -> this checkoutNow fleet anywhere on your machine runs your local source. To switch back to
the published release, unlink and reinstall:
npm rm -g @ilies-bel/fleet # remove the symlink
npm install -g @ilies-bel/fleetThe fleet CLI
All operations go through a single fleet dispatcher. fleet init symlinks it to /usr/local/bin/fleet so it is available anywhere.
fleet <command> [options]
Commands:
init Initialize fleet for the current project (no args)
add <name> [--title <t>] [--direct] Start a multi-service feature
ls [--json] List feature containers and status
rm <name>|--all|--nuke Remove feature(s) or everything
restart <name> Restart a feature container
stop <name>|--all Stop feature container(s) without destroying them
start <name>|--all Resume stopped feature container(s)
push <name> Push service branches to remote
sync <name> [--regenerate-sources] [--rebuild] Pull latest code and rebuild
help [<command>] Show help, or help for a command
Environment:
FLEET_GATEWAY Gateway base URL (default: http://localhost:4000)One-time setup
Run fleet init from the root of the project you want to manage — it takes no
arguments:
cd /path/to/my-project
fleet initIf .fleet/fleet.toml does not exist, init walks you through an interactive
wizard that auto-detects your stack (Spring Boot, Go, Next.js, Vite, Node)
and writes the config for you. If the file already exists, init reads it and
reconfigures idempotently (use fleet init --override to regenerate it).
init will:
- Create/load
.fleet/fleet.tomlin the project root - Create the
fleet-netDocker network - Build the
fleet-gatewayimage (includes the dashboard) - Generate
.fleet/Dockerfile.feature-basefor the detected stack and build it - Start the gateway on ports 3000 and 4000
Safe to run again — idempotent. Interactive prompts default to n when no tty
is attached, so it degrades gracefully in CI.
Start your first feature once init completes:
fleet add my-featureSupported stacks
Dockerfiles live in cli/stacks/ and are selected automatically during init:
Dockerfile.spring— Spring BootDockerfile.go— GoDockerfile.next— Next.jsDockerfile.vite— ViteDockerfile.node— generic Node
Adding a feature
fleet add <name> <branch> [--direct]name— lowercase alphanumerics, dots, hyphens only (e.g.login-fix,auth-v2,feat.auth-v2). Must match^[a-z0-9]([a-z0-9-]*(\.[a-z0-9-]+)*)?$branch— git branch name (checked against the frontend repository)--direct— skip the worktree and build directly fromAPP_ROOT
Example:
fleet add login-fix feature/auth-fixThis will:
- Verify the branch exists (local or remote)
- Create a git worktree under
<app-root>/.fleet-worktrees/<name>(unless--direct) - Start a single
fleet-<name>container running supervisord with all configured services and peers - Register it with the gateway (auto-activated if it is the first feature)
Follow build progress with:
docker logs -f fleet-<name>Once up, activate it from the dashboard or API, then visit http://localhost:3000.
Other feature commands
fleet feature -c <name> [<branch>] # Scaffold worktree + compose, don't start
fleet restart <name> # Restart container
fleet sync <name> # Pull latest code and rebuild
fleet sync <name> --regenerate-sources # Re-run source generation (e.g. OpenAPI)
fleet push <name> # Push the worktree branch(es) to remoteDashboard
Open http://localhost:4000 to:
- See all registered feature containers and their health status
- Activate a feature (switches port 3000 to proxy it)
- Preview a feature in the embedded iframe
- Kill a feature container
- Open an iTerm2 terminal into a running container (macOS only)
Activating a feature
Only one feature is active on port 3000 at a time. Activate via:
Dashboard — click [ACTIVATE] on the feature card.
API:
curl -X POST http://localhost:4000/_fleet/api/features/login-fix/activateThe first feature registered is activated automatically.
Removing features
fleet rm <name> # remove one feature
fleet rm --all # remove all features, keep gateway running
fleet rm --nuke # remove everything: features, gateway, network, configConfiguration (fleet.toml)
Generated by the fleet init wizard, or copy .fleet/fleet.toml.example manually. The TOML schema defines your project, services, and optional peer stubs.
Project Metadata
[project]
name = "my-app"
root = "/path/to/project"
[ports]
proxy = 3000 # gateway transparent proxy
admin = 4000 # gateway admin API
db = 5432 # host-mapped postgres port (optional; 0 = disabled)Services
Each [[services]] entry is a deployable unit (frontend, backend, etc.) running in supervisord:
[[services]]
name = "backend"
dir = "backend"
stack = "spring" # or: go, next, vite, node
port = 8081
build = "mvn package -DskipTests -q"
run = "java -jar /home/developer/backend.jar"
[[services]]
name = "frontend"
dir = "frontend"
stack = "next"
port = 3000
build = "npm run build"
run = "npm run dev"Peers (Optional Stubs)
Peers are co-located stub services (wiremock, static servers, custom processes) running on localhost inside the feature container — not exposed externally:
[[peers]]
name = "wiremock-edf"
type = "wiremock"
port = 8080
mappings = "wiremock-edf/mappings"
files = "wiremock-edf/__files"
[[peers]]
name = "mock-api"
type = "static-http"
port = 9090
[[peers]]
name = "custom-stub"
type = "shell"
port = 7070
cmd = "node /app/custom-stub/server.js"Peer types:
wiremock— Mock HTTP endpoints with stateful request/response mappingsstatic-http— Minimal static HTTP server for test fixturesshell— Arbitrary shell command (e.g. Node.js stub server)
Sidecars (Sibling Containers)
Sidecars are full sibling containers (Postgres, Redis, MinIO, …) declared in [[sidecars]] that run alongside feature containers on fleet-net. Unlike peers, they live in their own containers and can be shared across features (scope = "project") or isolated per feature (scope = "feature"):
[[sidecars]]
name = "rag-postgres"
image = "postgres:16"
scope = "project" # shared across all features
env = { POSTGRES_PASSWORD = "dev", POSTGRES_DB = "rag" }
volumes = [{ path = "pgdata", target = "/var/lib/postgresql/data", mode = "volume" }]Reached via Docker DNS — postgres://fleet-rag-postgres:5432 (project-scope) or fleet-<feature>-<name> (feature-scope). Lazily started by fleet add, removed by fleet rm --nuke. See docs/ARCHITECTURE.md for the full lifecycle.
Multi-repo projects (frontend and backend in separate git roots) are detected automatically — fleet add creates a worktree per repo.
OAuth setup
Register a single OAuth callback URL with your provider:
http://localhost:4000/auth/callbackIn your app, encode the state parameter to include the feature name:
const state = btoa(JSON.stringify({ feature: "login-fix", returnTo: "/" }));The gateway decodes state, activates the matching feature on port 3000, then redirects the browser to http://localhost:3000/auth/callback — which forwards to the now-active container.
API reference
All management endpoints are on port 4000.
| Method | Path | Description |
|--------|------|-------------|
| GET | /_fleet/api/features | List all registered features (includes status for not-yet-started containers) |
| GET | /_fleet/api/features/:name/health | Health check for a container (up/down) |
| POST | /_fleet/api/features/:name/activate | Set the active feature on port 3000 |
| GET | /_fleet/api/status | Gateway uptime, active feature, feature count |
| POST | /register-feature | Register a feature (called by fleet add) |
| DELETE | /register-feature/:name | Deregister a feature (called by fleet rm) |
| GET | /auth/callback | OAuth relay endpoint |
Claude Code commands
The .claude/commands/fleet/ directory contains slash commands for Claude Code that automate common fleet workflows. These commands are self-contained: a fresh Claude session can execute them cold without additional setup.
Available commands
| Command | Description |
|---------|-------------|
| /fleet:init <project-path> [branch] | End-to-end fleet init — auto-tunes fleet.conf, runs fleet init non-interactively, waits for the container, verifies /actuator/health. Use this instead of running fleet init manually. |
Install (make commands globally available)
# Symlink the fleet command namespace into your global Claude commands directory
ln -s "$(pwd)/.claude/commands/fleet" ~/.claude/commands/fleetAfter symlinking, /fleet:init is available in any Claude Code session, regardless of which directory you open Claude from. Pass the project path as an argument:
/fleet:init /path/to/my-project feature/my-branchUse without installing (repo-local)
Open Claude Code from the fleet repo root — commands in .claude/commands/ are automatically available as slash commands in that session:
cd /path/to/fleet
claude # opens Claude Code
# then: /fleet:init ../my-project mainLocal dashboard development
cd dashboard
npm install
npm run devVite proxies /_fleet/ to the gateway at localhost:4000, so you get hot-reload against live data.
Testing fleet init
test/project/ is a ready-to-use copy of test/reference/ (Spring Boot backend + Next.js frontend).
fleet init test/project mainRe-copy to reset: cp -rp test/reference test/project.
Troubleshooting
Gateway not starting
docker logs fleet-gateway-containerContainer unreachable (502)
Port 3000 returns 502 when the active feature container is not responding. Check its build/startup logs:
docker logs -f fleet-<name>The container builds the app internally — it may still be compiling.
No active feature (503)
Port 3000 returns 503 when no feature is active. Open the dashboard at http://localhost:4000 and click [ACTIVATE] on a feature.
Name validation error
Feature names must match ^[a-z0-9]([a-z0-9-]*(\.[a-z0-9-]+)*)?$.
Valid: my-feature, auth-fix-v2
Invalid: MyFeature, auth fix, auth_fix
Branch not found
fleet add checks the branch exists in the frontend repository before starting the container. Fetch remote branches first:
git -C <app-root>/<FRONTEND_DIR> fetch originOAuth state error
The state param must be base64-encoded JSON containing a feature key that matches a registered feature name:
btoa(JSON.stringify({ feature: "login-fix" })).fleet-config not found
Every command except init reads APP_ROOT from .fleet-config at the fleet root. Run fleet init first to create it.
Lifecycle hooks
Fleet can run inline shell commands at four points in the fleet add / fleet rm lifecycle. Add a [hooks] table to .fleet/fleet.toml:
[hooks]
pre_add = "cp -R frontend/node_modules frontend/.worktrees/{name}/node_modules"
post_add = "./bin/seed-db --feature {name}"
pre_rm = "echo 'tearing down {name} (branch: {branch})' >&2"
post_rm = "curl -s -X POST https://hooks.slack.com/... -d 'feature {name} removed'"Hook points
| Hook | When it runs |
|------|-------------|
| pre_add | After the worktree is validated, before the container starts |
| post_add | After the container is healthy |
| pre_rm | Before any container teardown |
| post_rm | After the container and feature directory are fully removed |
Inline string contract
- Each value is a single string. Fleet runs it with
sh -c "<string>"(POSIX sh, not bash). - Working directory:
project.root(the directory containing.fleet/fleet.toml). - For multi-step hooks, call an external script:
pre_add = "bash scripts/seed-worktree.sh". - Omit a key entirely (or leave it out of the table) to disable that hook — no silent default.
Variable substitution
Fleet substitutes {var} placeholders in the string before passing it to sh. The same values are also exported as environment variables so hook scripts can use either style:
| Placeholder | Env var | Value |
|-------------|---------|-------|
| {name} | FLEET_FEATURE_NAME | Feature name (e.g. bd-foo) |
| {project} | FLEET_PROJECT_NAME | project.name from fleet.toml |
| {worktree_path} | FLEET_WORKTREE_PATH | Absolute worktree path (or project root in --direct mode) |
| {branch} | FLEET_BRANCH | Git branch the worktree is on |
| {direct} | FLEET_DIRECT | "true" or "false" |
Failure semantics
pre_add,pre_rm— non-zero exit aborts the operation. Fleet printshook failed: <name> (exit <code>)to stderr and exits non-zero.post_add,post_rm— non-zero exit is a warning only. Fleet printswarning: post-hook <name> exited <code>to stderr and continues normally (the container is already up or already removed).
Example: seeding node_modules
The canonical motivating use case — seed each new worktree's node_modules from the primary checkout so npm install becomes a fast no-op inside the container:
[hooks]
pre_add = "cp -R frontend/node_modules frontend/.worktrees/{name}/node_modules"fleet add my-feature runs the copy (cwd = project root) before the container starts. The worktree already has node_modules by the time supervisord launches the frontend process.
