A CLI/TUI tool for managing fleets of agents each inside there own isolated devcontainers. This tool enables hugly parallel development, especially if you utilize the fleet-admiral skill (auto added to your claude code on install).
I made this tool to solve my own problem, and hopefully yours as well. I wanted a tool that allowed the following.
- Running unlimited parallel agents that don't step on each other. I mean
this in a way beyond the
git worktreeI don't want ports to step on each other, I don't want one agent deleting the whole computer to step on another agent. I want full isolation. - Use my claude code (or any agent you want) subscription at the subscription price! Many similar tools exist to this, but require you to pay API rates. This tool just uses terminals so you will never need to pay API rates.
- Not need to constantly setup new environments for agents. Environments need automated setup.
- Give me a one screen dashboard to manage all my agents, including knowing when they need attention.
fleet solves all these problems. I use it every day. A few other senior developers at my company use it every day. You should use it every day!
- Install
- Usage
- TUI Keybindings
- MCP Server
- Remote MCP — expose MCP & gRPC to remote agents via a fleet gateway
- Environment Variables
- Requirements
- Development
- Windows / WSL Setup
- Devcontainer Customizations
- Fleet Launch (in-instance TUI)
- Devcontainer BuildKit
- Devcontainer UID Rewrite
Deep-dive docs (in doc/):
- The Fleet Gateway — how the reverse tunnel relays both MCP and gRPC to your daemon
sudo curl -sL https://raw.githubusercontent.com/BenjaminBenetti/fleet-man/main/install.sh | shTo install a specific version:
sudo curl -sL https://raw.githubusercontent.com/BenjaminBenetti/fleet-man/main/install.sh | sh -s -- --version v0.2.0Run fleet with no arguments to launch the interactive TUI, or use subcommands directly:
# Launch TUI
fleet
# Spawn instances from the current repo
fleet up agent-1
fleet up agent-2
# Stop and restart an existing instance without removing it
fleet stop agent-1
fleet start agent-1
# List instances
fleet ls
# Exec into an instance
fleet exec agent-1 bash
# Open VS Code on an instance
fleet code agent-1
# Launch the in-instance link/app grid (run inside an instance)
fleet launch
# View logs
fleet logs agent-1
# Rebuild an instance's container in place (e.g. after editing devcontainer.json)
fleet rebuild agent-1
# Remove an instance
fleet down agent-1
# Remove a fleet and all its instances
fleet destroy my-project
# Spawn from anywhere with explicit repo
fleet up agent-1 --repo git@github.com:org/my-project.git
# Reference an existing fleet from anywhere
fleet up my-project/agent-3
# Configure automation: agents (workers) and triggers (what fires them)
fleet agent create my-project nightly-builder --system-prompt "Build and report"
fleet trigger create my-project nightly --agent nightly-builder --cron "0 0 * * *" --prompt "Run the nightly build"
# A 'bash' trigger polls a command on its cron and fires only when it exits 0
# (stdout becomes the event payload) — for sources without a webhook
fleet trigger create my-project new-issues --type bash --agent triager --cron "*/5 * * * *" \
--script 'gh issue list --label needs-triage --json number -q ".[].number" | grep -q .' --prompt "Triage the new issues"
fleet agent list my-project
fleet trigger list my-project
fleet trigger logs my-project nightly # inspect a trigger's recorded firings| Key | Action |
|---|---|
j/k |
Navigate |
space |
Expand/collapse fleet |
enter/e |
Exec into instance |
s |
Stop/start instance |
o |
Open instance in new terminal |
a |
Add instance |
n |
New fleet |
d |
Delete instance/fleet |
c |
Open VS Code |
R |
Rebuild instance (preserves workspace) |
L |
View logs (instance logs, or a selected trigger's event logs in the automation view) |
→/l, ←/h |
Select / deselect the PR-status auto tag |
enter (on PR status) |
Open the PR in a browser |
A |
Switch armada (remote fleet) |
r |
Refresh |
q |
Quit |
The fleet daemon also runs an MCP server over Streamable HTTP, so AI agents (and any MCP client) can drive fleet programmatically. It starts automatically with the daemon — no extra command.
- It binds
127.0.0.1:6012, or the next free port if 6012 is taken. - The active port is written to
~/.fleet/mcp.port, so clients can discover the endpoint. The URL ishttp://127.0.0.1:<port>. - Requests must carry
Authorization: Bearer <token>, where the token is read from~/.fleet/mcp.token. The loopback port is reachable by any local user, so the token (file mode0600, same-user only) is the access boundary — matching the daemon's unix socket. The token is generated once and reused across restarts.
Claude Code needs no setup: launching the fleet TUI registers the server in
~/.claude.json as the user-scope fleet entry (URL and token included) and
re-syncs it on every launch, so a daemon restart that lands on a new port heals
itself. It also installs the Fleet Admiral skill (~/.claude/skills/fleet-admiral),
which teaches the agent how to drive these tools. (Local daemons only: when
FLEET_GATEWAY/FLEET_SERVER point the TUI at a remote daemon, registration
is skipped — point Claude Code at the Public MCP URL from the settings page
instead, see Remote MCP. Registration is best-effort and never
blocks startup.) The snippet below is for other MCP clients or manual setups.
For convenience the server also writes ~/.fleet/mcp.env (mode 0600) with the
endpoint as shell exports, and wires ~/.bashrc to source it, so new shells get:
FLEET_MCP_PORT=6012
FLEET_MCP_URL=http://127.0.0.1:6012
FLEET_MCP_TOKEN=<token>An MCP client config (mcp.json) can then reference them directly:
{
"mcpServers": {
"fleet": {
"type": "http",
"url": "${FLEET_MCP_URL}",
"headers": { "Authorization": "Bearer ${FLEET_MCP_TOKEN}" }
}
}
}(zsh users: add [ -f "$HOME/.fleet/mcp.env" ] && . "$HOME/.fleet/mcp.env" to
~/.zshrc.)
Tools mirror the non-interactive CLI: fleet_list, fleet_status,
fleet_version, fleet_logs, fleet_up, fleet_start, fleet_stop,
fleet_down, fleet_destroy_fleet, fleet_clone, fleet_rebuild, fleet_exec,
the tmux session tools fleet_session_spawn / fleet_session_exec /
fleet_session_read / fleet_session_list, and the automation CRUD tools
fleet_automation_list, fleet_agent_create / fleet_agent_update /
fleet_agent_delete, fleet_trigger_create / fleet_trigger_update /
fleet_trigger_delete, and fleet_trigger_logs (mirroring the fleet agent and
fleet trigger CLI commands).
Interactive, open-ended commands (fleet shell, log following) are intentionally
not exposed.
By default the MCP server is loopback-only. To let a remote agent drive your fleet, you can expose it through a fleet gateway — a small public relay you (or someone) runs. Your daemon dials out to the gateway (so it works behind NAT/firewalls) and the gateway routes inbound MCP requests back down that tunnel:
agent ──HTTPS──▶ fleet gateway ──reverse tunnel──▶ your fleetd ──▶ local MCP server
⚠️ Exposing fleet over the internet: the bearer token is the only thing gating access (and over gRPC it grants full daemon control), and the gateway operator can see your traffic — use a gateway you trust and keep the token secret. See The Fleet Gateway for the trust model.
In Settings → Fleet Remote (MCP):
- Set Gateway URL to the gateway's gRPC endpoint, e.g.
https://gateway.example.com:50051(this is where the daemon registers and dials out — the gateway's--grpc-addr, default port50051; behind a proxy it's whatever host:port routes to that listener). It is not the public address agents use. - Flip Enable Remote MCP on.
Once connected, the read-only Public MCP URL appears (e.g.
https://gateway.example.com/mcp/<id>) — that is the address external tools use.
The line shows the live connection state (connecting / connected / error).
A second, independent toggle — Enable Remote Fleet — exposes the daemon's
gRPC control surface through the same gateway, so a remote fleet binary can
drive this instance (see Remote control). With it off,
fleetd never negotiates the gRPC tunnel feature, so the gateway rejects any
incoming gRPC commands aimed at the daemon. With it on, a read-only Public GRPC
URL appears under the Public MCP URL (computed by the gateway from its
--public-grpc-url flag) — that value is what you feed another fleet as
FLEET_GATEWAY. The toggles are independent: expose MCP, remote control, or both.
A third, independent toggle — Enable Webhook — exposes this daemon's
automation webhook endpoint through the same gateway. With it on, a read-only
Public Webhook URL base appears (e.g. https://gateway.example.com/webhook/<id>),
served on the gateway's public HTTP listener — no extra gateway flag needed. A
remote system (CI, a SaaS webhook, curl) POSTs an event to
<public-webhook-url>/<name>, where <name> is a webhook trigger
you defined; the gateway relays it down the tunnel and fleetd fires that trigger's
agents if the event passes the trigger's filter (a regex over the body, or a JSON
path/value match). The trigger's create/edit dialog renders its full URL ready to
paste into the remote system. With the toggle off, fleetd never negotiates the
webhook feature, so the gateway 404s the route for this daemon.
Point the remote MCP client at the Public MCP URL, with the same bearer token
your daemon uses (from ~/.fleet/mcp.token):
{
"mcpServers": {
"fleet-remote": {
"type": "http",
"url": "https://gateway.example.com/mcp/<id>",
"headers": { "Authorization": "Bearer <token from ~/.fleet/mcp.token>" }
}
}
}The gateway is the same binary: fleet gateway. TLS is optional: give it a
publicly-trusted certificate (e.g. from Let's Encrypt) and it terminates TLS
itself, or omit the cert and run it behind a TLS-terminating reverse proxy (see
below).
fleet gateway \
--public-url https://gateway.example.com \
--tls-cert /etc/fleet/tls/fullchain.pem \
--tls-key /etc/fleet/tls/privkey.pem| Flag | Default | Purpose |
|---|---|---|
--public-url |
(required) | External base URL agents use; session URLs are <public-url>/mcp/<id>. Scheme (https/http) must match how the public endpoint is actually served |
--public-grpc-url |
(optional) | External base URL of the gRPC endpoint remote fleet clients dial, e.g. https://gateway.example.com:50051. Daemons that enable Remote Fleet are handed <public-grpc-url>/grpc/<id> as their Public GRPC URL (shown in the TUI, used as FLEET_GATEWAY). Unset = no URL is computed |
--tls-cert / --tls-key |
(optional) | TLS certificate + key (PEM). Provide both to serve HTTPS, or neither for plain HTTP behind a proxy. A lone cert or key is an error |
--public-addr |
:443 |
MCP + /healthz listener — HTTP/1.1 (HTTPS when a cert is set, else HTTP) |
--grpc-addr |
:50051 |
Native gRPC listener — HTTP/2 (h2c when cert-less, h2 under TLS). Hosts remote fleet control and fleetd registration. Empty disables both |
--max-sessions |
1024 |
Cap on concurrent tunnels |
--session-key |
(random per boot) | Secret key signing the session-resume tokens daemons present on reconnect. Set it (or FLEET_GATEWAY_SESSION_KEY) so daemons keep the same session URL across gateway restarts; left unset, a restart hands every daemon a fresh URL |
Every flag is also settable via environment variable — FLEET_GATEWAY_<FLAG> with
dashes as underscores (e.g. FLEET_GATEWAY_PUBLIC_URL,
FLEET_GATEWAY_MAX_SESSIONS) — which is handy for configuring the
Docker image in Kubernetes. A flag given on the command
line wins over its environment variable.
Two ports must be reachable: expose --public-addr to MCP agents and --grpc-addr
to remote fleet clients and your daemons (they register over it). A
GET /healthz on the public listener returns ok.
There is no separate control port. fleetd registers and carries its reverse tunnel over a long-lived gRPC bidi stream on
--grpc-addr(HTTP/2), so the whole gateway is plain HTTP/HTTP-2 — no raw TCP.
Both listeners are L7 — there's no raw-TCP port anymore, so a standard HTTP/gRPC ingress fronts the entire gateway:
--public-addr(MCP, HTTP/1.1) → a Traefik HTTPIngressRoute(TLS-terminating).--grpc-addr(gRPC, HTTP/2) → a Traefik gRPC route (h2c backend) — see Traefik's gRPC guide. This one carries both remotefleetcontrol RPCs and fleetd registration (the same long-lived bidi stream).
Run the gateway with no --tls-cert/--tls-key (plain HTTP/h2c) and let the
proxy terminate TLS. For example:
# MCP — L7 HTTP, TLS terminated at Traefik:
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata: { name: fleet-gateway-mcp }
spec:
entryPoints: [websecure]
routes:
- match: Host(`gateway.example.com`) && PathPrefix(`/mcp`)
services: [{ name: fleet-gateway, port: 80 }] # gateway --public-addr
tls: { secretName: gateway-tls }
---
# gRPC (remote control + fleetd registration) — L7 gRPC, h2c to the backend:
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata: { name: fleet-gateway-grpc }
spec:
entryPoints: [websecure]
routes:
- match: Host(`grpc.gateway.example.com`)
services: [{ name: fleet-gateway, port: 50051, scheme: h2c }] # gateway --grpc-addr
tls: { secretName: gateway-tls }Set --public-url https://gateway.example.com; the daemon's Gateway URL is the
gRPC endpoint, e.g. https://grpc.gateway.example.com (or https://gateway.example.com:50051
without a proxy). Clients (and daemons) verify TLS against the system roots at the
proxy edge; the proxy speaks plain HTTP/h2c to the cert-less gateway, which can then
bind unprivileged ports and needs no cert.
Every tagged release also publishes a gateway image to the GitHub Container
Registry, so you don't have to build the binary yourself. It's the same
fleet gateway, with the flags passed as docker run arguments (or as
FLEET_GATEWAY_* environment variables — see the table above):
docker run -d --name fleet-gateway \
-p 443:443 -p 50051:50051 \
-v /etc/fleet/tls:/tls:ro \
ghcr.io/benjaminbenetti/fleet-man/gateway:latest \
--public-url https://gateway.example.com \
--tls-cert /tls/fullchain.pem \
--tls-key /tls/privkey.pemImage tags track releases: :latest (newest stable), :X.Y.Z, and :X.Y.
Images are multi-arch (linux/amd64 + linux/arm64). To run behind a
TLS-terminating proxy, drop the -v .../tls mount and the --tls-cert/--tls-key
flags (and a --public-url http://… or proxy-fronted https://…); the container
then serves plain HTTP.
- No gateway authentication. Anyone can open a tunnel; the gateway just routes bytes. Isolation comes from the unguessable 256-bit id in the public URL.
- The bearer token is the real access boundary. The gateway forwards the
Authorizationheader untouched to your loopback MCP server, whose existing token check gates every request. Treat the Public MCP URL as a capability, but the token is the secret — share both only with agents you trust. - The id in the URL is not the reconnect credential: the daemon holds a separate secret (never placed in the URL), so a URL holder cannot hijack your tunnel.
- Session-resume tokens are bearer reclaim capabilities. Each registration
returns a JWT signed with
--session-key; the daemon stores it (mode0600, next to the secret) and presents it on reconnect, which is what keeps the URL stable across gateway restarts. Anyone holding the token can claim that session, so guard the signing key like any other server secret. - The public connection is TLS when the gateway holds a cert (the daemon verifies it against the system roots), or when a reverse proxy terminates TLS in front of it. Running the gateway itself on plain HTTP is only safe behind such a proxy (or on a trusted private network) — never expose a cert-less gateway directly.
The same gateway tunnel can also carry the daemon's gRPC API, so a remote
fleet client can drive your daemon directly — list/up/down, watch live status,
stream logs, change config, and so on. It rides the same tunnel as remote MCP
but has its own toggle: flip Enable Remote Fleet in Settings → Fleet MCP
(independent of Enable Remote MCP, so you can expose MCP, remote control, or
both). The gateway serves native gRPC on its dedicated --grpc-addr listener
(default :50051); the client dials it as an ordinary gRPC endpoint and sends the
daemon's session id (the same id as in the MCP URL) as the fleet-session
metadata header, so the gateway can route.
Point a fleet client at it with two env vars — the URL is the gRPC endpoint plus
the session id. When the gateway runs with --public-grpc-url, the settings page
shows this exact value as the Public GRPC URL (e.g.
https://gateway.example.com:50051/grpc/<id>); otherwise compose it from the
gateway's --grpc-addr (or whatever host:port your reverse proxy exposes for
gRPC) plus the session id:
export FLEET_GATEWAY="https://gateway.example.com:50051/<id>"
export FLEET_TOKEN="<token from ~/.fleet/mcp.token>" # omit on the same host as the daemon
fleet ls # …now talks to the REMOTE daemonFLEET_GATEWAY takes precedence over FLEET_SERVER and the local socket; a remote
endpoint is never auto-spawned, and a daemon/client version mismatch is a hard
error. Use http://… instead of https://… only when the gateway serves plain
h2c with no TLS in front of it (a trusted private network).
⚠️ Security — this is full remote control. The bearer token authorizes every gRPC call, so the same~/.fleet/mcp.tokennow grants full control of your daemon over the internet — not just the read-mostly MCP tools. The gateway terminates TLS, so its operator can see your traffic and token (the same trust model as remote MCP). Only enable this with a gateway you run or trust, and treat the token as a high-value secret.
Note: interactive
fleet exec(a remote shell) is not yet supported over the gateway — it currently runs the command on the client's host. Every other RPC works remotely today; remote interactive shell awaits a server-sideExechandler.
Instead of juggling FLEET_GATEWAY/FLEET_TOKEN by hand, register remote fleets
once in Settings → Fleet Armada: press + Remote Fleet, paste the gateway
URL and the remote's bearer token, and fleet runs a connection test before
saving. Each registered remote shows a live connection status (pinged while the
settings page is open) and a [ delete ] button (press twice to confirm).
The main page's list border carries an Armada selector
(╭─ Armada [ local ] ───╮). It is part of the normal j/k (and arrow-key)
navigation — cycle up past the top row to land on it (continuing up wraps to the
bottom of the list) — or jump straight to it with the A key or a mouse
click. Selecting it opens a dropdown of local + every registered remote; pick
one to switch the TUI live: the connection, fleet list, sessions, and shells
all retarget to the chosen daemon. Registering a remote never switches to it;
every boot starts on local unless FLEET_GATEWAY (or FLEET_SERVER) is set,
in which case that boot endpoint appears in the dropdown as (env).
Remotes are shown by hostname rather than the full gateway URL; if two
registered fleets live on the same host, they're disambiguated by the first 8
characters of their session id (fleet.example.com - 8e7d1f0a).
The registry is stored on your own machine at ~/.fleet/armada.json (mode
0600 — it holds bearer tokens) and always round-trips through your local
daemon, even while the TUI is connected to a remote fleet.
Variables fleet reads (set them to configure behavior):
| Variable | Values / format | What it does |
|---|---|---|
FLEET_GATEWAY |
https://gw:50051/<id> (or http://… for a cert-less/h2c gateway) |
Drive a remote daemon through a fleet gateway (full gRPC control). Takes precedence over FLEET_SERVER and the local socket. See Remote MCP. |
FLEET_TOKEN |
bearer token | Token for FLEET_GATEWAY. Defaults to ~/.fleet/mcp.token on the daemon's own host. |
FLEET_SERVER |
host:port |
Drive a remote daemon over plain TCP (no gateway). |
FLEET_DEVCONTAINER_BUILDKIT |
auto (default), never |
BuildKit mode for Fleet-managed devcontainers. See Devcontainer BuildKit. |
FLEET_DEVCONTAINER_UPDATE_REMOTE_USER_UID |
default, never, on, off |
Remote-user UID/GID rewrite mode. See Devcontainer UID Rewrite. |
FLEET_SSH_AGENT_SOCK |
absolute path, off, or none (case-insensitive) |
Override the bind source for SSH agent forwarding into instances (off/none disables it). On macOS the default is Docker Desktop's VM-side /run/host-services/ssh-auth.sock (OrbStack and colima --ssh-agent are path-compatible); set this if your Docker backend exposes the agent elsewhere (default Colima, Podman machine, Rancher Desktop). |
CODER_URL |
URL | Coder deployment URL (Coder backend). |
CODER_SESSION_TOKEN |
token | Coder API token (Coder backend). |
CODER_CONFIG_DIR |
path | Override the Coder CLI config dir. |
Variables fleet exports for MCP clients (written to ~/.fleet/mcp.env, sourced from ~/.bashrc):
| Variable | What it does |
|---|---|
FLEET_MCP_URL |
The loopback MCP endpoint, http://127.0.0.1:<port>. |
FLEET_MCP_TOKEN |
The MCP bearer token. |
FLEET_MCP_PORT |
The MCP server's port. |
Fleet also respects standard environment when present: HOME (the ~/.fleet
location), TMUX (enables split-pane mode when run inside tmux), SSH_AUTH_SOCK
(forwarded into instances for SSH/git), and WSL_DISTRO_NAME / WSL_INTEROP
/ WAYLAND_DISPLAY (platform detection for clipboard and browser integration).
- Linux (including Ubuntu on WSL2) or macOS
- Docker
- devcontainer CLI (
npm install -g @devcontainers/cli)
Building from source, tests, and the protobuf workflow: see DEVELOPMENT.md.
Fleet is a Linux CLI, but it can run on Windows through WSL2. The confirmed setup is:
- Ubuntu on WSL2
- Docker Desktop with WSL integration enabled for the Ubuntu distro
- Node installed with
nvm @devcontainers/cliinstalled under the nvm-managed Node- Fleet installed somewhere on the WSL
PATH, such as~/.local/bin/fleet
Inside WSL:
nvm install 22
nvm alias default 22
nvm use default
npm install -g @devcontainers/cliOn Docker Desktop plus WSL, if devcontainer startup fails while building the
UID-adjustment or feature image, disable BuildKit for Fleet-managed
devcontainers. If the failure is specifically in updateUID.Dockerfile,
disable the devcontainer CLI's remote user UID rewrite as well:
export FLEET_DEVCONTAINER_BUILDKIT=never
export FLEET_DEVCONTAINER_UPDATE_REMOTE_USER_UID=neverTo persist that setting:
echo 'export FLEET_DEVCONTAINER_BUILDKIT=never' >> ~/.bashrc
echo 'export FLEET_DEVCONTAINER_UPDATE_REMOTE_USER_UID=never' >> ~/.bashrcSee Windows WSL notes for a full health check and disposable smoke-test workflow.
Fleet reads project-level settings from a customizations.fleet block in a
repo's devcontainer.json. This follows the standard devcontainer pattern
where each tool owns a namespaced sub-object under customizations (VS Code
uses vscode, GitHub uses codespaces, and so on) — Fleet reads fleet and
ignores the rest.
The address the built-in browser (b in the TUI) opens to instead of
about:blank. Handy for jumping straight to a dev server or app running
inside the instance.
A directory of links to the dev services running inside the instance. Each entry has:
title— the primary label for the link.subTitle— secondary descriptive text shown under the title (optional).url— the address the link navigates to.icon— an image shown before the title, used verbatim as an<img>source so any path the browser can load works, e.g. anhttpsURL or adata:URI (optional).healthCheck— an address polled to indicate whether the service is reachable; a healthy service shows a green heart, an unreachable one a red skull, and hovering reveals the HTTP status (optional).
Embedded apps shown as extra tabs on the Fleet Launch page, alongside the Links tab. Each app gets its own tab; opening it starts the app inside the instance and embeds it in an iframe. This is how you surface a web UI that lives in the instance — a log viewer, a database console, a build dashboard — without leaving Fleet Launch. Each entry has:
title— the label shown on the app's tab.command— a bash command that starts the app. It runs the first time the tab is opened, unless the app'sportis already answering (so a second open or a browser relaunch won't double-start it). The command is started in the background, so it can be a blocking server or a self-detaching one likedocker run -d. Optional — omit it for an app that is already running and only needs to be embedded.port— the localhost port the app serves on. Once it answers, the tab iframeshttp://localhost:<port>; if it never comes up, the tab shows an error instead.
The app is shown in an iframe, so it must allow being framed. An app that
sends X-Frame-Options: deny/sameorigin or a restrictive
Content-Security-Policy: frame-ancestors will render as a blank "refused
to connect" box; configure it to permit embedding. For example, Grafana
needs GF_SECURITY_ALLOW_EMBEDDING=true.
For example, to replace Fleet's former built-in Dozzle log viewer:
"apps": [
{
"title": "Logs",
"command": "docker run -d -p 16768:8080 -v /var/run/docker.sock:/var/run/docker.sock amir20/dozzle:latest",
"port": 16768
}
]When a devcontainer.json sets both initialUrl and a Fleet Launch
block (any fleetLaunch.sites or fleetLaunch.apps), the per-fleet
Prefer Fleet Launch setting decides which the browser opens to:
- off —
initialUrlwins. - on — the Fleet Launch page wins.
The first time you open the browser on a fleet whose workspace has both
configured, the TUI prompts you to choose, and saves your answer as the
fleet's Prefer Fleet Launch setting. You can change it later by editing the
fleet (e in the TUI). When only one of the two is configured, that one is
used and the setting has no effect.
fleet launch is a small terminal UI you run inside an instance (over
fleet exec, in a fleet code terminal, or any shell in the container). It
reads the workspace's customizations.fleet.fleetLaunch block — the same
sites and apps described above — and lays them out as a navigable grid of
squares: a "Links" section for fleetLaunch.sites and an "Apps" section for
fleetLaunch.apps.
# auto-detect .devcontainer/devcontainer.json (or ./devcontainer.json) in cwd
fleet launch
# or point it at an explicit devcontainer.json
fleet launch --config ./path/to/devcontainer.json
# print the configured links and apps (and the names you can launch), then exit
fleet launch list
# open a link or app directly by name (a unique prefix is enough), as if clicked
fleet launch grafIn the grid, navigate with the arrow keys or hjkl, or click a square with the
mouse; enter or a click activates the selected square, and q/esc/ctrl+c
quits. Activating a link opens the host browser to its url; activating
an app first starts the app's command on its port inside the instance
(only if the port isn't already answering), waits for it to come up, then
opens the host browser to http://localhost:<port>.
fleet launch list prints the configured Links and Apps with their targets and
exits — handy for discovering the names. fleet launch <name> performs that
same link/app activation headlessly, without opening the grid. The name is
matched case-insensitively against the titles: an exact title wins, otherwise a
unique prefix is enough (so fleet launch graf opens "Grafana"). If a prefix
matches more than one, the candidates are listed so you can type more.
The browser lives on the host (it is proxied into the container by
privoxy), so the in-instance TUI can't open it directly. Instead it drives the
host browser over a control socket — a unix domain socket the host fleet
TUI creates per instance and bind-mounts into the instance (the same mechanism
as mounting docker.sock). The running host fleet TUI listens on that socket
and opens or navigates the browser when fleet launch sends it a request. If
the host fleet TUI isn't running, fleet launch still renders the grid so
you can browse the configured options, but shows a status line noting that
opening won't work until a host connection exists.
Only instances created or cloned after this feature was added get the control-socket mount; pre-existing instances need to be recreated for in-instance launch to drive the host browser.
Fleet uses the devcontainer CLI's default BuildKit behavior unless explicitly configured. If Docker Desktop on WSL fails while building the devcontainer UID-adjustment image, disable BuildKit for Fleet-managed devcontainers:
export FLEET_DEVCONTAINER_BUILDKIT=neverAccepted values are auto and never.
Fleet uses the devcontainer CLI's default remote-user UID/GID rewrite behavior
unless explicitly configured. On Docker Desktop plus WSL, that rewrite can fail
while building updateUID.Dockerfile. To disable it for Fleet-managed
devcontainers:
export FLEET_DEVCONTAINER_UPDATE_REMOTE_USER_UID=neverAccepted values are default, never, on, and off.


{ "image": "mcr.microsoft.com/devcontainers/base:ubuntu", "customizations": { "fleet": { "browser": { "initialUrl": "http://localhost:3000" }, "fleetLaunch": { "sites": [ { "title": "API", "subTitle": "REST backend", "url": "http://localhost:3000", "healthCheck": "http://localhost:3000/healthz" } ], "apps": [ { "title": "Logs", "command": "docker run -d -p 16768:8080 -v /var/run/docker.sock:/var/run/docker.sock amir20/dozzle:latest", "port": 16768 } ] } } } }