From c7bfcadf02b232edcc2de1a1319b1931d7075f49 Mon Sep 17 00:00:00 2001 From: Ricardo Trevisan Date: Thu, 29 Jan 2026 19:49:33 -0300 Subject: [PATCH 1/2] fix(docker): atomized volume setup and migration robustness - src/infra/state-migrations.ts: Handle EBUSY errors on rename (Docker bind mount safe) - Dockerfile: Add global CLI symlink and fix volume permissions - docker-compose.yml: Use named volumes for isolation and explicit entrypoints --- Dockerfile | 7 +++++++ docker-compose.yml | 30 ++++++++++-------------------- docker-setup.sh | 12 ++++++------ src/infra/state-migrations.ts | 18 ++++++++++++------ 4 files changed, 35 insertions(+), 32 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9c6aa7036..6eaf5170c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,6 +32,13 @@ RUN pnpm ui:build ENV NODE_ENV=production +# Create global CLI symlink +RUN ln -s /app/moltbot.mjs /usr/local/bin/moltbot + +# Ensure state directories exist and are owned by node +RUN mkdir -p /var/lib/moltbot /home/node/clawd && \ + chown -R node:node /var/lib/moltbot /home/node/clawd + # Security hardening: Run as non-root user # The node:22-bookworm image includes a 'node' user (uid 1000) # This reduces the attack surface by preventing container escape via root privileges diff --git a/docker-compose.yml b/docker-compose.yml index 8ce610d6a..68412a5a2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,16 +1,18 @@ services: moltbot-gateway: + container_name: moltbot image: ${CLAWDBOT_IMAGE:-moltbot:local} environment: HOME: /home/node + MOLTBOT_STATE_DIR: /var/lib/moltbot TERM: xterm-256color CLAWDBOT_GATEWAY_TOKEN: ${CLAWDBOT_GATEWAY_TOKEN} CLAUDE_AI_SESSION_KEY: ${CLAUDE_AI_SESSION_KEY} CLAUDE_WEB_SESSION_KEY: ${CLAUDE_WEB_SESSION_KEY} CLAUDE_WEB_COOKIE: ${CLAUDE_WEB_COOKIE} volumes: - - ${CLAWDBOT_CONFIG_DIR}:/home/node/.clawdbot - - ${CLAWDBOT_WORKSPACE_DIR}:/home/node/clawd + - moltbot_data:/var/lib/moltbot + - moltbot_workspace:/home/node/clawd ports: - "${CLAWDBOT_GATEWAY_PORT:-18789}:18789" - "${CLAWDBOT_BRIDGE_PORT:-18790}:18790" @@ -19,27 +21,15 @@ services: command: [ "node", - "dist/index.js", + "/app/moltbot.mjs", "gateway", "--bind", "${CLAWDBOT_GATEWAY_BIND:-lan}", "--port", - "${CLAWDBOT_GATEWAY_PORT:-18789}" + "${CLAWDBOT_GATEWAY_PORT:-18789}", + "--allow-unconfigured" ] - moltbot-cli: - image: ${CLAWDBOT_IMAGE:-moltbot:local} - environment: - HOME: /home/node - TERM: xterm-256color - BROWSER: echo - CLAUDE_AI_SESSION_KEY: ${CLAUDE_AI_SESSION_KEY} - CLAUDE_WEB_SESSION_KEY: ${CLAUDE_WEB_SESSION_KEY} - CLAUDE_WEB_COOKIE: ${CLAUDE_WEB_COOKIE} - volumes: - - ${CLAWDBOT_CONFIG_DIR}:/home/node/.clawdbot - - ${CLAWDBOT_WORKSPACE_DIR}:/home/node/clawd - stdin_open: true - tty: true - init: true - entrypoint: ["node", "dist/index.js"] +volumes: + moltbot_data: + moltbot_workspace: diff --git a/docker-setup.sh b/docker-setup.sh index 0f7571e96..56545aaa9 100755 --- a/docker-setup.sh +++ b/docker-setup.sh @@ -21,11 +21,11 @@ if ! docker compose version >/dev/null 2>&1; then exit 1 fi -mkdir -p "${CLAWDBOT_CONFIG_DIR:-$HOME/.clawdbot}" -mkdir -p "${CLAWDBOT_WORKSPACE_DIR:-$HOME/clawd}" +mkdir -p "${CLAWDBOT_CONFIG_DIR:-$HOME/.moltbot}" +mkdir -p "${CLAWDBOT_WORKSPACE_DIR:-$HOME/moltbot}" -export CLAWDBOT_CONFIG_DIR="${CLAWDBOT_CONFIG_DIR:-$HOME/.clawdbot}" -export CLAWDBOT_WORKSPACE_DIR="${CLAWDBOT_WORKSPACE_DIR:-$HOME/clawd}" +export CLAWDBOT_CONFIG_DIR="${CLAWDBOT_CONFIG_DIR:-$HOME/.moltbot}" +export CLAWDBOT_WORKSPACE_DIR="${CLAWDBOT_WORKSPACE_DIR:-$HOME/moltbot}" export CLAWDBOT_GATEWAY_PORT="${CLAWDBOT_GATEWAY_PORT:-18789}" export CLAWDBOT_BRIDGE_PORT="${CLAWDBOT_BRIDGE_PORT:-18790}" export CLAWDBOT_GATEWAY_BIND="${CLAWDBOT_GATEWAY_BIND:-lan}" @@ -62,7 +62,7 @@ YAML if [[ -n "$home_volume" ]]; then printf ' - %s:/home/node\n' "$home_volume" >>"$EXTRA_COMPOSE_FILE" - printf ' - %s:/home/node/.clawdbot\n' "$CLAWDBOT_CONFIG_DIR" >>"$EXTRA_COMPOSE_FILE" + printf ' - %s:/home/node/.moltbot\n' "$CLAWDBOT_CONFIG_DIR" >>"$EXTRA_COMPOSE_FILE" printf ' - %s:/home/node/clawd\n' "$CLAWDBOT_WORKSPACE_DIR" >>"$EXTRA_COMPOSE_FILE" fi @@ -77,7 +77,7 @@ YAML if [[ -n "$home_volume" ]]; then printf ' - %s:/home/node\n' "$home_volume" >>"$EXTRA_COMPOSE_FILE" - printf ' - %s:/home/node/.clawdbot\n' "$CLAWDBOT_CONFIG_DIR" >>"$EXTRA_COMPOSE_FILE" + printf ' - %s:/home/node/.moltbot\n' "$CLAWDBOT_CONFIG_DIR" >>"$EXTRA_COMPOSE_FILE" printf ' - %s:/home/node/clawd\n' "$CLAWDBOT_WORKSPACE_DIR" >>"$EXTRA_COMPOSE_FILE" fi diff --git a/src/infra/state-migrations.ts b/src/infra/state-migrations.ts index f5e50740e..cc0ed5721 100644 --- a/src/infra/state-migrations.ts +++ b/src/infra/state-migrations.ts @@ -360,7 +360,13 @@ export async function autoMigrateLegacyStateDir(params: { try { fs.renameSync(legacyDir, targetDir); - } catch (err) { + } catch (err: any) { + if (err.code === "EBUSY") { + warnings.push( + `Legacy state dir (${legacyDir}) could not be renamed (EBUSY). This is common in Docker with bind mounts. Skipping automatic migration.`, + ); + return { migrated: false, skipped: true, changes: [], warnings }; + } warnings.push(`Failed to move legacy state dir (${legacyDir} → ${targetDir}): ${String(err)}`); return { migrated: false, skipped: false, changes, warnings }; } @@ -430,11 +436,11 @@ export async function detectLegacyStateMigrations(params: { : { store: {}, ok: true }; const legacyKeys = targetSessionParsed.ok ? listLegacySessionKeys({ - store: targetSessionParsed.store, - agentId: targetAgentId, - mainKey: targetMainKey, - scope: targetScope, - }) + store: targetSessionParsed.store, + agentId: targetAgentId, + mainKey: targetMainKey, + scope: targetScope, + }) : []; const legacyAgentDir = path.join(stateDir, "agent"); From 80ff97f44b2eff2dae96b047aa6be50bf8f7d661 Mon Sep 17 00:00:00 2001 From: Ricardo Trevisan Date: Thu, 29 Jan 2026 19:56:27 -0300 Subject: [PATCH 2/2] docs: add DOCKER_ENHANCEMENTS.md reference --- DOCKER_ENHANCEMENTS.md | 77 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 DOCKER_ENHANCEMENTS.md diff --git a/DOCKER_ENHANCEMENTS.md b/DOCKER_ENHANCEMENTS.md new file mode 100644 index 000000000..4df0b26ec --- /dev/null +++ b/DOCKER_ENHANCEMENTS.md @@ -0,0 +1,77 @@ +# Docker Enhancements & Migration Fixes + +This document details the technical improvements made to the `moltbot` Docker environment to resolve migration loops, permission errors, and usability issues. + +## 1. The Problem + +* **Migration Loop**: The core logic tried to rename `.clawdbot` to `.moltbot`. In Docker, these are often bind mounts from the host. Renaming a mount point fails with `EBUSY`, causing the container to crash and restart in a loop. +* **Permission Denied (EACCES)**: The container runs as a non-root user (`node`). If the host directories or Docker volumes were created by `root` (common during initial start), the app crashes when trying to write to `cron`, `canvas`, or `logs`. +* **Usability**: The `moltbot` CLI was not in the global `PATH` inside the container, requiring confusing commands like `node dist/index.js`. + +## 2. Technical Solution + +We implemented an **"Atomized"** architecture where the container's state and workspace are fully isolated from the host filesystem using Docker named volumes, ensuring consistent permissions and behavior. + +### A. Robust Migration Logic +**File**: `src/infra/state-migrations.ts` + +We patched the migration logic to gracefully handle the `EBUSY` error code. If the application encounters a legacy directory that it cannot rename (e.g., a bind mount), it now logs a warning and **skips** the migration instead of crashing. + +```typescript +try { + fs.renameSync(legacyDir, targetDir); +} catch (err: any) { + if (err.code === "EBUSY") { + // Log warning and skip migration + return { migrated: false, skipped: true, ... }; + } + throw err; +} +``` + +### B. Atomized & Isolated Storage +**File**: `docker-compose.yml` + +We replaced host bind mounts (which leak host permissions and state) with Docker **Named Volumes**. This guarantees the container manages its own state isolated from the host OS quirks. + +```yaml +services: + moltbot-gateway: + volumes: + - moltbot_data:/var/lib/moltbot # Isolated State + - moltbot_workspace:/home/node/clawd # Isolated Workspace +``` + +### C. Permission Fixes +**File**: `Dockerfile` + +We added an explicit build step to create the volume mount points and assign ownership to the `node` user *before* the container starts. + +```dockerfile +# Ensure state directories exist and are owned by node +RUN mkdir -p /var/lib/moltbot /home/node/clawd && \ + chown -R node:node /var/lib/moltbot /home/node/clawd + +USER node +``` + +### D. Developer Experience (DX) +**File**: `Dockerfile` & `docker-compose.yml` + +1. **Container Name**: Set `container_name: moltbot` for easy reference. +2. **Global CLI**: Added a symlink `/usr/local/bin/moltbot` -> `/app/moltbot.mjs`. +3. **Explicit Entrypoint**: Hardcoded `node /app/moltbot.mjs` to ensure the correct executable is always run. +4. **Zero-Config Start**: Added `--allow-unconfigured` to let the gateway start fresh without manual setup. + +## 3. Usage + +With these changes, the workflow is: + +```bash +# Start the container (detached) +docker compose up -d + +# Interactions (now intuitive) +docker exec -it moltbot moltbot status +docker exec -it moltbot moltbot onboard +```