OpenClaw Fleet CLI: Provision and Manage Per-Tenant Cells
Reference for the openclaw fleet command, which provisions and manages isolated per-tenant OpenClaw cells. Covers quick start, tenant ID rules, and Docker/Podman support for Linux and macOS.
Read this when
- You host multiple tenant trust domains on one machine
- You need to create, inspect, upgrade, or remove fleet cells
openclaw fleet
openclaw fleet handles full OpenClaw deployments, referred to as cells. Every cell runs its own Gateway, state, credentials, channel accounts, container, and loopback-only host port. Assign one cell per tenant trust boundary; never rely on a single shared Gateway as a hostile multi-tenant boundary.
Fleet is experimental. Command names, flags, output shapes, and the container profile may shift between releases without any deprecation period.
Both Docker and Podman are supported by Fleet. The default image is ghcr.io/openclaw/openclaw:latest.
Linux and macOS hosts are where Fleet has been tested. Windows hosts have not been tested yet.
Quick start
openclaw fleet create acme
openclaw fleet status acme
openclaw fleet list
fleet create outputs the generated Gateway token once, along with the cell URL. Save the token right away, then set up each tenant's channel accounts within that tenant's cell.
Tenant IDs
Tenant IDs must conform to:
^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$
This permits 1 to 40 lowercase letters, digits, and internal hyphens. An ID must begin and conclude with a letter or digit. Uppercase letters, underscores, slashes, dots, whitespace, and traversal strings like ../acme are not accepted.
The ID is incorporated into the container name: openclaw-cell-<tenant>.
fleet create
To create and start a cell:
openclaw fleet create acme
To create a Podman cell on a fixed port without starting it:
openclaw fleet create acme \
--runtime podman \
--port 19125 \
--no-start
Pass tenant-specific environment variables by repeating --env:
openclaw fleet create acme \
--env TZ=America/Los_Angeles \
--env OPENCLAW_DISABLE_BONJOUR=1
Environment keys are composed of letters, digits, and underscores, and cannot begin 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.
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. |
--no-start | Cell starts | Create the container without starting it. |
--json | Human-readable output | Print machine-readable output. |
Automatic allocation picks the first unused registry port at or above 19100. Duplicate tenant IDs and explicit ports already assigned to another cell are rejected by Fleet.
Image references are passed as a single container-runtime argument. Empty references and values starting with - are rejected so an image cannot be treated as a Docker or Podman option.
The chosen Docker or Podman endpoint must be local. Remote Docker contexts, DOCKER_HOST endpoints, and remote Podman services are rejected by Fleet before a port is reserved or local state is created. Remote cell hosts are not supported.
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
Show cells ordered by tenant ID:
openclaw fleet list
openclaw fleet ls
openclaw fleet list --json
The columns break down as follows:
| Column | Meaning |
|---|---|
tenant | The tenant identifier. |
state | Current container state pulled from Docker or Podman inspection. unknown signals that the runtime could not be reached, or that a container bearing the cell's name exists but its Fleet ownership labels disagree with the registry entry (this hints at a collision or tampering, so check it by hand before taking action). |
port | The loopback host port assigned to the cell Gateway. |
image | The image recorded for the container. |
created | When the cell was created. |
Even when Docker or Podman is offline, registry entries stay listed; only the live status turns into unknown.
fleet status
Look at a single cell:
openclaw fleet status acme
openclaw fleet status acme --json
State is built from the fleet registry row, the live container inspection, and a brief best-effort probe sent to:
http://127.0.0.1:<host-port>/healthz
Health comes back as ok, failed, or skipped. A result of /healthz confirms the Gateway is reachable, but it does not guarantee that every configured channel or plugin is fully operational. When no usable local endpoint exists, the probe is omitted.
fleet logs
Pipe a cell's container logs straight to the terminal:
openclaw fleet logs acme
openclaw fleet logs acme --follow
openclaw fleet logs acme --tail 200
openclaw fleet logs acme --since 10m
Before any log data is read, Fleet checks the ownership labels on the registered container, so a foreign container that borrows the expected cell name is rejected. Log streaming is tied to the inspected container ID, meaning a container swapped in later cannot hijack the stream to a different generation. Ending the stream with Ctrl-C sets --follow without marking the operator stop as a command failure. Log output passes through a redaction filter that swaps the cell's current Gateway token for <redacted> prior to reaching the terminal.
fleet logs offers no --json mode, since container logs arrive as a raw stdout/stderr stream. For scripted use, cap the output with --tail and rely on normal shell redirection or pipelines.
fleet start, fleet stop, and fleet restart
Drive an existing cell using its recorded runtime:
openclaw fleet start acme
openclaw fleet stop acme
openclaw fleet restart acme
These commands target the registered container name. They error out when the tenant is unknown or the recorded runtime cannot carry out the operation.
fleet upgrade
Fetch the recorded image again and swap in a fresh cell container:
openclaw fleet upgrade acme
Point the cell at a different image:
openclaw fleet upgrade acme --image ghcr.io/openclaw/openclaw:<version>
Upgrade pulls the desired image, examines the current container and the per-cell network, halts and deletes the container, then rebuilds and launches it. The new container keeps the same host port, data directories, per-cell bridge network, runtime profile, resource limits, restart policy, Fleet-managed environment, and any values originally passed with --env. Mounted data survives the container swap, while image-default environment settings may shift with the target image.
The swap is finalized only once the Gateway answers /healthz on the cell's loopback port, following the health contract used by the official compose file. If the replacement exits, crash-loops, or fails to reach healthy within roughly a minute, it is discarded and the prior container is brought back, so a faulty image cannot knock out a healthy cell.
The Gateway token is deliberately absent from the fleet registry. Prior to deleting the old container, Fleet pulls its environment and transfers OPENCLAW_GATEWAY_TOKEN into the replacement. If you have no other copy of the token, do not delete the old container manually before an upgrade.
fleet backup and fleet restore
Create a backup of one halted cell:
openclaw fleet stop acme
openclaw fleet backup acme --out ./acme.tgz
Load that archive back into the registered cell:
openclaw fleet restore acme --from ./acme.tgz
These commands require host-operator privileges. Archives hold tenant state and auth secrets, are written with mode 0600, and need to be handled like credentials. Backup will not run against a live cell, so SQLite state is captured consistently. Restore also refuses a running cell unless --force is given, overwrites only that tenant's state, rotates the Gateway token, and prints the new token a single time. Fleet backs up one tenant per invocation; backing up all tenants is a separate operator task.
Restore depends on an existing stopped container, because its inspected runtime profile provides the replacement limits, user mapping, environment provenance, and image. If the registered container was deleted out of band, start with fleet rm <tenant> --force without --purge-data, recreate the cell using the intended image and --no-start, then attempt restore again. The initial removal leaves both tenant data directories untouched.
Both commands take --max-bytes <bytes> to cap archived or extracted file data, and both enforce the same fixed one-million budget of archive path segments, so metadata-only archive bombs cannot exhaust host inodes and every accepted backup remains restorable. Backup accepts --out <path>, and both commands support --json.
Archives hold only regular files and directories. Backup never follows or stores symlinks, hard links, sockets, or device nodes, and skipped counts appear in the result. Restore rejects archives with any other entry type, ignores archived ownership, and clamps restored file and directory modes before applying the cell runtime owner. Recreatable symlink trees, such as workspace node_modules, must be reinstalled inside the cell after a restore.
fleet doctor
Inspect every cell or a single tenant without touching runtime or filesystem state:
openclaw fleet doctor
openclaw fleet doctor acme --json
Doctor verifies runtime locality, ownership labels, health, hardening, resource limits, loopback port binding, token presence, network ownership and egress mode, and private state-directory permissions. Warnings flag stopped cells or ownership mismatches, and any failed finding produces a nonzero process exit code.
fleet rm
Take a halted cell out of the runtime and registry while preserving tenant data:
openclaw fleet rm acme
A running container calls for --force:
openclaw fleet rm acme --force
Delete the cell data permanently as well:
openclaw fleet rm acme --purge-data --force
Fleet drops the cell container before it removes the dedicated bridge network. --purge-data requires --force. Prior to 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 stop a corrupted registry path or cross-tenant symlink from redirecting deletion elsewhere.
Purge can be retried when an exact expected tenant directory is already missing. That 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 live in separate per-tenant host paths under the active OpenClaw state directory:
<state-dir>/fleet/cells/<tenant>/
<state-dir>/fleet/auth-profile-secrets/<tenant>/
The first directory mounts at /home/node/.openclaw. The second mounts at /home/node/.config/openclaw, matching the encryption-key mount in the official Docker setup. The encryption key therefore never appears beneath the ordinary state mount, nor is it included when only the cell-state directory is backed up or shared. Both directories survive normal removal and upgrade; fleet rm --purge-data --force removes 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 into that config, staying instead 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 |
In its official form, the image runs under the non-root node account, which carries UID 1000. The private 0700 bind mounts stay writable for Fleet without being exposed to all users. When Docker operates in rootful mode, the cell adopts the invoking non-root UID and GID; in rootless mode, the container uses UID 0, which the daemon's user namespace maps back to the unprivileged host user who started it. Podman relies on keep-id together with the invoking UID and GID. If Fleet itself runs as root against a rootful runtime, it preserves the image user and gives the initial mount files UID/GID 1000.
On hosts with SELinux, Docker and Podman mounts get a dedicated :Z relabel. Should you restore or move cell data, ensure the bind-mounted paths remain writable for the effective container user. The profile works under rootless setups, but Docker or Podman must already be configured for rootless operation on the host; Fleet cannot turn a rootful daemon into a rootless one.
Security profile
Every cell receives the following profile from Fleet:
| Control | Applied profile | Why |
|---|---|---|
| Linux capabilities | --cap-drop=ALL | The Gateway runs as a Node.js process and does not require extra Linux capabilities. |
| Privilege escalation | --security-opt no-new-privileges | Stops processes from gaining privileges via setuid or setgid binaries. |
| Init process | --init | Reaps descendant processes and forwards container lifecycle signals. |
| Process limit | --pids-limit 512 by default | Limits fork and process exhaustion. |
| Memory limit | --memory 2g by default | Limits cell memory use. |
| CPU limit | --cpus 2 by default | Limits cell CPU use. |
| Writable-layer disk | Optional --disk | Limits 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 acts as a separation boundary between cells, not as an outbound firewall: cells keep the network egress needed for providers and channels. Put a proxy, SSH tunnel, or tailnet configuration that fits your deployment in front of the loopback port. http://127.0.0.1:<port> is reachable only from the Fleet host.
This profile isolates tenant containers, but it does not shield tenants from the Fleet operator, the container runtime administrator, or a compromised host. See Multi-tenant hosting for the full 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.