Files
openclaw/docs/cli/fleet.md

329 lines
23 KiB
Markdown

---
summary: "CLI reference for provisioning and managing isolated per-tenant OpenClaw cells"
read_when:
- You host multiple tenant trust domains on one machine
- You need to create, inspect, upgrade, or remove fleet cells
title: "Fleet"
---
# `openclaw fleet`
`openclaw fleet` manages complete OpenClaw instances called **cells**. Each cell has its own Gateway, state, credentials, channel accounts, container, and loopback-only host port. Use one cell for each tenant trust boundary; do not use one shared Gateway as a hostile multi-tenant boundary.
Fleet is **experimental**. Command names, flags, output shapes, and the container profile can change between releases without a deprecation window while the surface settles.
Fleet supports Docker and Podman. The default image is `ghcr.io/openclaw/openclaw:latest`.
Fleet is tested on Linux and macOS hosts. Windows hosts are currently untested.
## Quick start
```bash
openclaw fleet create acme
openclaw fleet status acme
openclaw fleet list
```
`fleet create` prints the generated Gateway token once along with the cell URL. Store the token immediately, then configure each tenant's channel accounts inside that tenant's cell.
## Tenant IDs
Tenant IDs must match:
```text
^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$
```
This allows 1 to 40 lowercase letters, digits, and internal hyphens. An ID must start and end with a letter or digit. Uppercase letters, underscores, slashes, dots, whitespace, and traversal strings such as `../acme` are rejected.
The ID becomes part of the container name: `openclaw-cell-<tenant>`.
## `fleet create`
Create a cell and start it:
```bash
openclaw fleet create acme
```
Create a Podman cell on a fixed port without starting it:
```bash
openclaw fleet create acme \
--runtime podman \
--port 19125 \
--no-start
```
Pass tenant-specific environment variables by repeating `--env`:
```bash
openclaw fleet create acme \
--env TZ=America/Los_Angeles \
--env OPENCLAW_DISABLE_BONJOUR=1
```
Environment keys use letters, digits, and underscores and cannot start with a digit. Values must be single-line because Fleet passes them through a protected runtime environment file. Fleet rejects attempts to override the managed container-path and Gateway-token variables listed under [Storage and container layout](#storage-and-container-layout).
### Create options
| Option | Default | Description |
| ------------------------- | ------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `--image <ref>` | `ghcr.io/openclaw/openclaw:latest` | Container image for the cell. |
| `--runtime <runtime>` | `docker` | Container CLI: `docker` or `podman`. |
| `--port <number>` | Automatically allocated from `19100` | Loopback host port. An explicitly selected port must not belong to another registered cell. |
| `--memory <value>` | `2g` | Container memory limit in Docker/Podman syntax. |
| `--cpus <value>` | `2` | Container CPU limit. |
| `--disk <size>` | None | Cap the container writable layer when the storage backend supports quotas. |
| `--network <mode>` | `bridge` | Outbound network mode: `bridge` or `internal`. |
| `--pids-limit <number>` | `512` | Maximum number of processes in the container. |
| `--env <KEY=VALUE>` | None | Pass an environment variable to the cell. Repeat for multiple values. |
| `--gateway-token <value>` | Random 32-character hexadecimal token | Use a supplied Gateway token instead of generating one. See [Token handling](#token-handling). |
| `--no-start` | Cell starts | Create the container without starting it. |
| `--json` | Human-readable output | Print machine-readable output. |
Automatic allocation selects the first unused registry port at or above `19100`. Fleet rejects duplicate tenant IDs and explicit ports already assigned to another cell.
Image references are passed as one container-runtime argument. Empty references and values beginning with `-` are rejected so an image cannot be interpreted as a Docker or Podman option.
The selected Docker or Podman endpoint must be local. Fleet rejects remote Docker contexts, `DOCKER_HOST` endpoints, and remote Podman services before reserving a port or creating local state; remote cell hosts need a separate storage and endpoint contract and are deferred from this MVP.
When Fleet starts a new cell, create waits up to about a minute for its Gateway to answer `/healthz`. If the cell does not become healthy, Fleet leaves its container and registry row intact for `fleet status`, `fleet logs`, or explicit removal. `--no-start` skips this health gate. The generated Gateway token of an unhealthy new cell is not lost - it remains in the container environment (`docker|podman inspect`), and because the cell has served no traffic yet, `fleet rm --force` followed by a fresh create is always a safe alternative.
### Pinning by digest
Create and upgrade accept digest-pinned image references such as `--image ghcr.io/openclaw/openclaw@sha256:<digest>`. Fleet passes the image reference through verbatim to Docker or Podman, which lets an operator keep a cell on immutable image bytes instead of a moving tag.
The create result includes the tenant ID, container name, host port, Gateway token, and local URL. Even in JSON output, treat the result as secret-bearing because it contains the token.
### Disk limits
`--disk` limits only the container writable layer. The bind-mounted per-tenant state and auth directories remain host storage; use host filesystem project quotas when those directories also need a hard limit.
| Runtime/storage backend | `--disk` support |
| ----------------------- | ---------------------------------------------------------------------------- |
| Docker overlay2 on XFS | Requires the XFS `pquota` mount option. |
| Docker btrfs or zfs | Supported by the storage driver. |
| Podman overlay | Requires XFS backing storage. |
| Other backends | Container creation fails with the daemon error and Fleet's backend guidance. |
### Egress policy
| Mode | Docker | Podman |
| ---------- | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `bridge` | Supported; outbound egress is unrestricted by default. | Supported; outbound egress is unrestricted by default. |
| `internal` | Rejected because Docker does not preserve the published loopback Gateway port on an internal network. | Supported; the loopback Gateway remains published while outbound egress is blocked. |
For Docker, keep the bridge mode and enforce outbound policy with host firewall rules such as the `DOCKER-USER` chain.
## `fleet list`
List cells in tenant-ID order:
```bash
openclaw fleet list
openclaw fleet ls
openclaw fleet list --json
```
The table contains:
| Column | Meaning |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tenant` | Tenant ID. |
| `state` | Live container state from Docker or Podman inspection. `unknown` means the runtime was unavailable, or a container with the cell's name exists but its Fleet ownership labels do not match the registry record (a collision or tampering signal — inspect it manually before acting). |
| `port` | Loopback host port mapped to the cell Gateway. |
| `image` | Recorded container image. |
| `created` | Cell creation time. |
Registry rows remain visible when Docker or Podman is unavailable; only live state becomes `unknown`.
## `fleet status`
Inspect one cell:
```bash
openclaw fleet status acme
openclaw fleet status acme --json
```
Status combines the fleet registry row, live container inspection, and a short best-effort request to:
```text
http://127.0.0.1:<host-port>/healthz
```
The health result is `ok`, `failed`, or `skipped`. `/healthz` proves Gateway liveness, not full readiness of every configured channel or plugin. The probe is skipped when there is no usable local endpoint to check.
## `fleet logs`
Stream a cell's container logs directly to the terminal:
```bash
openclaw fleet logs acme
openclaw fleet logs acme --follow
openclaw fleet logs acme --tail 200
openclaw fleet logs acme --since 10m
```
Fleet verifies the registered container's ownership labels before reading any logs, so it refuses a foreign container using the expected cell name. Press Ctrl-C to end `--follow` without treating the operator stop as a command failure. Log output is piped through a redaction filter that replaces the cell's current Gateway token with `<redacted>` before anything reaches the terminal.
`fleet logs` has no `--json` mode because container logs are a raw stdout/stderr stream. For scripts, bound the output with `--tail` and use ordinary shell redirection or pipelines.
## `fleet start`, `fleet stop`, and `fleet restart`
Control an existing cell with its recorded runtime:
```bash
openclaw fleet start acme
openclaw fleet stop acme
openclaw fleet restart acme
```
These commands operate on the registered container name. They fail if the tenant is unknown or the recorded runtime cannot perform the operation.
## `fleet upgrade`
Re-pull the recorded image and replace the cell container:
```bash
openclaw fleet upgrade acme
```
Move the cell to another image:
```bash
openclaw fleet upgrade acme --image ghcr.io/openclaw/openclaw:<version>
```
Upgrade pulls the target image, inspects the existing container and per-cell network, stops and removes the container, then recreates and starts it. The replacement preserves the same host port, data directories, per-cell bridge network, runtime profile, resource limits, restart policy, Fleet-managed environment, and values originally supplied with `--env`. Mounted state survives container replacement; image-default environment can change with the target image.
The replacement is committed only after its Gateway answers `/healthz` on the cell's loopback port, matching the health contract the official compose file uses. A replacement that exits, crash-loops, or fails to become healthy within about a minute is removed and the previous container is restored, so a broken image does not take down a working cell.
The Gateway token is intentionally not stored in the fleet registry. Before removing the old container, Fleet reads its environment and carries `OPENCLAW_GATEWAY_TOKEN` into the replacement. Do not manually remove the old container before an upgrade if the token exists nowhere else you control.
## `fleet backup` and `fleet restore`
Back up one stopped cell:
```bash
openclaw fleet stop acme
openclaw fleet backup acme --out ./acme.tgz
```
Restore that archive into the registered cell:
```bash
openclaw fleet restore acme --from ./acme.tgz
```
These are host-operator-privileged commands. Archives contain tenant state and auth secrets, are created with mode `0600`, and must be stored like credentials. Backup refuses a running cell so SQLite state is captured consistently. Restore refuses a running cell unless `--force` is supplied, replaces only that tenant's state, rotates the Gateway token, and prints the new token once. Fleet backs up one tenant at a time; all-tenant backup is a separate operator action.
Both commands accept `--max-bytes <bytes>` to bound archived or extracted file data, and both apply the same fixed one-million budget of archive path segments so metadata-only archive bombs cannot exhaust host inodes and every accepted backup stays restorable. Backup accepts `--out <path>` and both commands support `--json`.
Archives contain regular files and directories only. Backup never follows or stores symlinks, hard links, sockets, or device nodes; skipped counts are reported in the result. Restore rejects archives containing any other entry type. Recreatable symlink trees such as workspace `node_modules` must be reinstalled inside the cell after a restore.
## `fleet doctor`
Audit every cell or one tenant without changing runtime or filesystem state:
```bash
openclaw fleet doctor
openclaw fleet doctor acme --json
```
Doctor checks runtime locality, ownership labels, health, hardening, resource limits, loopback port binding, token presence, network ownership and egress mode, and private state-directory permissions. Warnings describe stopped cells or ownership differences; any failed finding sets a nonzero process exit code.
## `fleet rm`
Remove a stopped cell from the runtime and registry while keeping tenant data:
```bash
openclaw fleet rm acme
```
A running container requires `--force`:
```bash
openclaw fleet rm acme --force
```
Permanently remove the cell data as well:
```bash
openclaw fleet rm acme --purge-data --force
```
Fleet removes the cell container before removing its dedicated bridge network. `--purge-data` requires `--force`. Before recursive deletion, Fleet resolves both Fleet-owned roots and both per-tenant directories. Each target must be the exact expected tenant leaf, strictly inside its root, and not a symlink. These containment checks prevent a corrupted registry path or cross-tenant symlink from redirecting deletion elsewhere.
Purge is retryable when an exact expected tenant directory is already absent. This lets a later invocation finish cleanup after a partial filesystem failure without relaxing the path checks for directories that still exist.
## Storage and container layout
Cell state and auth-profile encryption keys use separate per-tenant host paths under the active OpenClaw state directory:
```text
<state-dir>/fleet/cells/<tenant>/
<state-dir>/fleet/auth-profile-secrets/<tenant>/
```
The first directory is mounted at `/home/node/.openclaw`. The second is mounted at `/home/node/.config/openclaw`, matching the official Docker setup's encryption-key mount. The encryption key is therefore not exposed beneath the ordinary state mount or included when only the cell-state directory is backed up or shared. Both directories survive normal removal and upgrade; `fleet rm --purge-data --force` deletes both after separate containment checks.
Before first start, Fleet initializes the cell config with `gateway.mode=local`, token auth, the LAN container bind, and Control UI origins for the allocated host port. The token value is not written to that config; it remains in the container environment.
Fleet pins the official image's container paths with these environment values:
| Variable | Container value |
| ------------------------ | ------------------------------------ |
| `HOME` | `/home/node` |
| `OPENCLAW_HOME` | `/home/node` |
| `OPENCLAW_STATE_DIR` | `/home/node/.openclaw` |
| `OPENCLAW_CONFIG_PATH` | `/home/node/.openclaw/openclaw.json` |
| `OPENCLAW_WORKSPACE_DIR` | `/home/node/.openclaw/workspace` |
| `OPENCLAW_GATEWAY_TOKEN` | Generated or supplied cell token |
The official image defaults to the non-root `node` user with UID 1000. Fleet keeps the private `0700` bind mounts writable without making them world-accessible. Rootful Docker runs the cell with the invoking non-root UID and GID; rootless Docker uses container UID 0, which maps to the invoking unprivileged host user inside the daemon's user namespace. Podman uses `keep-id` with the invoking UID and GID. When Fleet itself runs as root against a rootful runtime, it retains the image user and assigns the initial mount files to UID/GID 1000.
On SELinux hosts, Docker and Podman mounts receive a private `:Z` relabel. If you restore or relocate cell data, keep the bind-mounted paths writable by the effective container user. The profile is rootless-friendly, but Docker or Podman must already be configured for rootless operation on the host; Fleet does not convert a rootful daemon into a rootless one.
## Security profile
Fleet applies the following profile to every cell:
| Control | Applied profile | Why |
| -------------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------- |
| Linux capabilities | `--cap-drop=ALL` | The Gateway is a Node.js process and needs no added Linux capabilities. |
| Privilege escalation | `--security-opt no-new-privileges` | Prevents processes from gaining privileges through setuid or setgid binaries. |
| Init process | `--init` | Reaps descendant processes and forwards container lifecycle signals. |
| Process limit | `--pids-limit 512` by default | Bounds fork and process exhaustion. |
| Memory limit | `--memory 2g` by default | Bounds cell memory use. |
| CPU limit | `--cpus 2` by default | Bounds cell CPU use. |
| Writable-layer disk | Optional `--disk` | Bounds the container layer when the runtime storage backend supports quotas. |
| Restart policy | `--restart unless-stopped` | Restarts a failed cell without overriding an intentional stop. |
| Host publishing | `127.0.0.1:<host-port>:18789` only | Keeps the Gateway off wildcard host interfaces. |
| Cell network | One bridge or Podman internal network per cell | Separates container-IP traffic and optionally blocks Podman outbound egress. |
| Container identity | Host-matched user mapping | Keeps private bind mounts writable without granting world access. |
| Persistent state | Per-cell mounts; no shared state mount | Keeps tenant config, credentials, sessions, and workspaces in that tenant's data tree. |
| Container command | `node dist/index.js gateway --bind lan --port 18789` | Listens on the container network so the loopback-only host port mapping can reach it. |
Fleet never mounts `/var/run/docker.sock`, uses `--privileged` or host networking, or adds capabilities. The per-cell bridge is a cross-cell separation boundary, not an outbound firewall: cells retain the network egress needed for providers and channels. Front the loopback port with a proxy, SSH tunnel, or tailnet configuration that matches your deployment. `http://127.0.0.1:<port>` is directly reachable only from the Fleet host.
This profile separates tenant containers, but it does not protect tenants from the Fleet operator, the container runtime administrator, or a compromised host. See [Multi-tenant hosting](/gateway/multi-tenant-hosting) for the complete trust model and stronger isolation options.
## Token handling
By default, `fleet create` generates a cryptographically random 32-character hexadecimal Gateway token and prints it once in the create result. Store it in your approved secret manager and avoid capturing create output in logs.
`--gateway-token` places a custom token in the local process arguments, which may be retained in shell history or visible in process listings. Prefer the generated token unless an existing secret-management workflow requires a supplied value.
The token and every value passed with `--env` live in the container environment. Fleet writes them to a short-lived mode-`0600` environment file, passes only that file's path to Docker or Podman, and removes it after the runtime command finishes. Values explicitly typed in `openclaw fleet create --gateway-token ...` or `--env KEY=VALUE` can still be visible in the outer `openclaw` process arguments and shell history.
Container environment values are not hidden from the trusted host operator: Docker or Podman administrators can read them with container inspection. Fleet's "shown once" note describes normal CLI output, not resistance to a host administrator.
## Related
- [Multi-tenant hosting](/gateway/multi-tenant-hosting)
- [Docker](/install/docker)
- [Podman](/install/podman)
- [Gateway security](/gateway/security)