refactor: clarify docker setup cli phases

This commit is contained in:
Peter Steinberger
2026-03-24 16:44:47 -07:00
parent 1ba436b372
commit 33e9e485b8
3 changed files with 94 additions and 37 deletions

View File

@@ -55,6 +55,10 @@ Docker is **optional**. Use it only if you want a containerized gateway or to va
- generate a gateway token and write it to `.env`
- start the gateway via Docker Compose
During setup, pre-start onboarding and config writes run through
`openclaw-gateway` directly. `openclaw-cli` is for commands you run after
the gateway container already exists.
</Step>
<Step title="Open the Control UI">
@@ -94,7 +98,15 @@ If you prefer to run each step yourself instead of using the setup script:
```bash
docker build -t openclaw:local -f Dockerfile .
docker compose run --rm openclaw-cli onboard
docker compose run --rm --no-deps --entrypoint node openclaw-gateway \
dist/index.js onboard --mode local --no-install-daemon
docker compose run --rm --no-deps --entrypoint node openclaw-gateway \
dist/index.js config set gateway.mode local
docker compose run --rm --no-deps --entrypoint node openclaw-gateway \
dist/index.js config set gateway.bind lan
docker compose run --rm --no-deps --entrypoint node openclaw-gateway \
dist/index.js config set gateway.controlUi.allowedOrigins \
'["http://localhost:18789","http://127.0.0.1:18789"]' --strict-json
docker compose up -d openclaw-gateway
```
@@ -104,6 +116,13 @@ or `OPENCLAW_HOME_VOLUME`, the setup script writes `docker-compose.extra.yml`;
include it with `-f docker-compose.yml -f docker-compose.extra.yml`.
</Note>
<Note>
Because `openclaw-cli` shares `openclaw-gateway`'s network namespace, it is a
post-start tool. Before `docker compose up -d openclaw-gateway`, run onboarding
and setup-time config writes through `openclaw-gateway` with
`--no-deps --entrypoint node`.
</Note>
### Environment variables
The setup script accepts these optional environment variables:

View File

@@ -108,7 +108,7 @@ ensure_control_ui_allowed_origins() {
local current_allowed_origins
allowed_origin_json="$(printf '["http://localhost:%s","http://127.0.0.1:%s"]' "$OPENCLAW_GATEWAY_PORT" "$OPENCLAW_GATEWAY_PORT")"
current_allowed_origins="$(
run_setup_cli config get gateway.controlUi.allowedOrigins 2>/dev/null || true
run_prestart_cli config get gateway.controlUi.allowedOrigins 2>/dev/null || true
)"
current_allowed_origins="${current_allowed_origins//$'\r'/}"
@@ -117,26 +117,53 @@ ensure_control_ui_allowed_origins() {
return 0
fi
run_setup_cli config set gateway.controlUi.allowedOrigins "$allowed_origin_json" --strict-json \
run_prestart_cli config set gateway.controlUi.allowedOrigins "$allowed_origin_json" --strict-json \
>/dev/null
echo "Set gateway.controlUi.allowedOrigins to $allowed_origin_json for non-loopback bind."
}
sync_gateway_mode_and_bind() {
run_setup_cli config set gateway.mode local >/dev/null
run_setup_cli config set gateway.bind "$OPENCLAW_GATEWAY_BIND" >/dev/null
run_prestart_cli config set gateway.mode local >/dev/null
run_prestart_cli config set gateway.bind "$OPENCLAW_GATEWAY_BIND" >/dev/null
echo "Pinned gateway.mode=local and gateway.bind=$OPENCLAW_GATEWAY_BIND for Docker setup."
}
run_setup_cli() {
run_prestart_gateway() {
docker compose "${COMPOSE_ARGS[@]}" run --rm --no-deps "$@"
}
run_prestart_cli() {
# During setup, avoid the shared-network openclaw-cli service because it
# requires the gateway container's network namespace to already exist. That
# creates a circular dependency for config writes that are needed before the
# gateway can start cleanly.
docker compose "${COMPOSE_ARGS[@]}" run --rm --no-deps --entrypoint node openclaw-gateway \
run_prestart_gateway --entrypoint node openclaw-gateway \
dist/index.js "$@"
}
run_runtime_cli() {
local compose_scope="${1:-current}"
local deps_mode="${2:-with-deps}"
shift 2
local -a compose_args
local -a run_args=(run --rm)
case "$compose_scope" in
current) compose_args=("${COMPOSE_ARGS[@]}") ;;
base) compose_args=("${BASE_COMPOSE_ARGS[@]}") ;;
*) fail "Unknown runtime CLI compose scope: $compose_scope" ;;
esac
case "$deps_mode" in
with-deps) ;;
no-deps) run_args+=(--no-deps) ;;
*) fail "Unknown runtime CLI deps mode: $deps_mode" ;;
esac
docker compose "${compose_args[@]}" "${run_args[@]}" openclaw-cli "$@"
}
contains_disallowed_chars() {
local value="$1"
[[ "$value" == *$'\n'* || "$value" == *$'\r'* || "$value" == *$'\t'* ]]
@@ -464,7 +491,7 @@ echo "==> Fixing data-directory permissions"
# ownership of all user project files on Linux hosts.
# After fixing the config dir, only the OpenClaw metadata subdirectory
# (.openclaw/) inside the workspace gets chowned, not the user's project files.
docker compose "${COMPOSE_ARGS[@]}" run --rm --no-deps --user root --entrypoint sh openclaw-gateway -c \
run_prestart_gateway --user root --entrypoint sh openclaw-gateway -c \
'find /home/node/.openclaw -xdev -exec chown node:node {} +; \
[ -d /home/node/.openclaw/workspace/.openclaw ] && chown -R node:node /home/node/.openclaw/workspace/.openclaw || true'
@@ -477,7 +504,7 @@ echo "Gateway token: $OPENCLAW_GATEWAY_TOKEN"
echo "Tailscale exposure: Off (use host-level tailnet/Tailscale setup separately)."
echo "Install Gateway daemon: No (managed by Docker Compose)"
echo ""
run_setup_cli onboard --mode local --no-install-daemon
run_prestart_cli onboard --mode local --no-install-daemon
echo ""
echo "==> Docker gateway defaults"
@@ -561,17 +588,17 @@ fi
if [[ -n "$SANDBOX_ENABLED" ]]; then
# Enable sandbox in OpenClaw config.
sandbox_config_ok=true
if ! docker compose "${COMPOSE_ARGS[@]}" run --rm --no-deps openclaw-cli \
if ! run_runtime_cli current no-deps \
config set agents.defaults.sandbox.mode "non-main" >/dev/null; then
echo "WARNING: Failed to set agents.defaults.sandbox.mode" >&2
sandbox_config_ok=false
fi
if ! docker compose "${COMPOSE_ARGS[@]}" run --rm --no-deps openclaw-cli \
if ! run_runtime_cli current no-deps \
config set agents.defaults.sandbox.scope "agent" >/dev/null; then
echo "WARNING: Failed to set agents.defaults.sandbox.scope" >&2
sandbox_config_ok=false
fi
if ! docker compose "${COMPOSE_ARGS[@]}" run --rm --no-deps openclaw-cli \
if ! run_runtime_cli current no-deps \
config set agents.defaults.sandbox.workspaceAccess "none" >/dev/null; then
echo "WARNING: Failed to set agents.defaults.sandbox.workspaceAccess" >&2
sandbox_config_ok=false
@@ -585,7 +612,7 @@ if [[ -n "$SANDBOX_ENABLED" ]]; then
else
echo "WARNING: Sandbox config was partially applied. Check errors above." >&2
echo " Skipping gateway restart to avoid exposing Docker socket without a full sandbox policy." >&2
if ! docker compose "${BASE_COMPOSE_ARGS[@]}" run --rm --no-deps openclaw-cli \
if ! run_runtime_cli base no-deps \
config set agents.defaults.sandbox.mode "off" >/dev/null; then
echo "WARNING: Failed to roll back agents.defaults.sandbox.mode to off" >&2
else
@@ -601,7 +628,7 @@ else
# Keep reruns deterministic: if sandbox is not active for this run, reset
# persisted sandbox mode so future execs do not require docker.sock by stale
# config alone.
if ! docker compose "${COMPOSE_ARGS[@]}" run --rm openclaw-cli \
if ! run_runtime_cli current with-deps \
config set agents.defaults.sandbox.mode "off" >/dev/null; then
echo "WARNING: Failed to reset agents.defaults.sandbox.mode to off" >&2
fi

View File

@@ -114,6 +114,26 @@ function runDockerSetup(
});
}
async function resetDockerLog(sandbox: DockerSetupSandbox) {
await writeFile(sandbox.logPath, "");
}
async function readDockerLog(sandbox: DockerSetupSandbox) {
return readFile(sandbox.logPath, "utf8");
}
async function readDockerLogLines(sandbox: DockerSetupSandbox) {
return (await readDockerLog(sandbox)).split("\n").filter(Boolean);
}
function isGatewayStartLine(line: string) {
return line.includes("compose") && line.includes(" up -d") && line.includes("openclaw-gateway");
}
function findGatewayStartLineIndex(lines: string[]) {
return lines.findIndex((line) => isGatewayStartLine(line));
}
async function runDockerSetupWithUnsetGatewayToken(
sandbox: DockerSetupSandbox,
suffix: string,
@@ -204,7 +224,7 @@ describe("scripts/docker/setup.sh", () => {
expect(extraCompose).toContain("openclaw-home:/home/node");
expect(extraCompose).toContain("volumes:");
expect(extraCompose).toContain("openclaw-home:");
const log = await readFile(activeSandbox.logPath, "utf8");
const log = await readDockerLog(activeSandbox);
expect(log).toContain("--build-arg OPENCLAW_DOCKER_APT_PACKAGES=ffmpeg build-essential");
expect(log).toContain(
"run --rm --no-deps --entrypoint node openclaw-gateway dist/index.js onboard --mode local --no-install-daemon",
@@ -224,16 +244,12 @@ describe("scripts/docker/setup.sh", () => {
it("avoids shared-network openclaw-cli before the gateway is started", async () => {
const activeSandbox = requireSandbox(sandbox);
await writeFile(activeSandbox.logPath, "");
await resetDockerLog(activeSandbox);
const result = runDockerSetup(activeSandbox);
expect(result.status).toBe(0);
const log = await readFile(activeSandbox.logPath, "utf8");
const lines = log.split("\n").filter(Boolean);
const gatewayStartIdx = lines.findIndex(
(line) =>
line.includes("compose") && line.includes(" up -d") && line.includes("openclaw-gateway"),
);
const lines = await readDockerLogLines(activeSandbox);
const gatewayStartIdx = findGatewayStartLineIndex(lines);
expect(gatewayStartIdx).toBeGreaterThanOrEqual(0);
const prestartLines = lines.slice(0, gatewayStartIdx);
@@ -286,7 +302,7 @@ describe("scripts/docker/setup.sh", () => {
expect(sessionsDirStat.isDirectory()).toBe(true);
// Verify that a root-user chown step runs before setup.
const log = await readFile(activeSandbox.logPath, "utf8");
const log = await readDockerLog(activeSandbox);
const chownIdx = log.indexOf("--user root");
const onboardIdx = log.indexOf("onboard");
expect(chownIdx).toBeGreaterThanOrEqual(0);
@@ -350,7 +366,7 @@ describe("scripts/docker/setup.sh", () => {
it("treats OPENCLAW_SANDBOX=0 as disabled", async () => {
const activeSandbox = requireSandbox(sandbox);
await writeFile(activeSandbox.logPath, "");
await resetDockerLog(activeSandbox);
const result = runDockerSetup(activeSandbox, {
OPENCLAW_SANDBOX: "0",
@@ -360,7 +376,7 @@ describe("scripts/docker/setup.sh", () => {
const envFile = await readFile(join(activeSandbox.rootDir, ".env"), "utf8");
expect(envFile).toContain("OPENCLAW_SANDBOX=");
const log = await readFile(activeSandbox.logPath, "utf8");
const log = await readDockerLog(activeSandbox);
expect(log).toContain("--build-arg OPENCLAW_INSTALL_DOCKER_CLI=");
expect(log).not.toContain("--build-arg OPENCLAW_INSTALL_DOCKER_CLI=1");
expect(log).toContain("config set agents.defaults.sandbox.mode off");
@@ -368,7 +384,7 @@ describe("scripts/docker/setup.sh", () => {
it("resets stale sandbox mode and overlay when sandbox is not active", async () => {
const activeSandbox = requireSandbox(sandbox);
await writeFile(activeSandbox.logPath, "");
await resetDockerLog(activeSandbox);
await writeFile(
join(activeSandbox.rootDir, "docker-compose.sandbox.yml"),
"services:\n openclaw-gateway:\n volumes:\n - /var/run/docker.sock:/var/run/docker.sock\n",
@@ -381,14 +397,14 @@ describe("scripts/docker/setup.sh", () => {
expect(result.status).toBe(0);
expect(result.stderr).toContain("Sandbox requires Docker CLI");
const log = await readFile(activeSandbox.logPath, "utf8");
const log = await readDockerLog(activeSandbox);
expect(log).toContain("config set agents.defaults.sandbox.mode off");
await expect(stat(join(activeSandbox.rootDir, "docker-compose.sandbox.yml"))).rejects.toThrow();
});
it("skips sandbox gateway restart when sandbox config writes fail", async () => {
const activeSandbox = requireSandbox(sandbox);
await writeFile(activeSandbox.logPath, "");
await resetDockerLog(activeSandbox);
const socketPath = join(activeSandbox.rootDir, "sandbox.sock");
await withUnixSocket(socketPath, async () => {
@@ -402,15 +418,10 @@ describe("scripts/docker/setup.sh", () => {
expect(result.stderr).toContain("Failed to set agents.defaults.sandbox.scope");
expect(result.stderr).toContain("Skipping gateway restart to avoid exposing Docker socket");
const log = await readFile(activeSandbox.logPath, "utf8");
const gatewayStarts = log
.split("\n")
.filter(
(line) =>
line.includes("compose") &&
line.includes(" up -d") &&
line.includes("openclaw-gateway"),
);
const log = await readDockerLog(activeSandbox);
const gatewayStarts = (await readDockerLogLines(activeSandbox)).filter((line) =>
isGatewayStartLine(line),
);
expect(gatewayStarts).toHaveLength(2);
expect(log).toContain(
"run --rm --no-deps openclaw-cli config set agents.defaults.sandbox.mode non-main",