This commit is contained in:
Ricardo Trevisan 2026-01-29 23:00:31 +00:00 committed by GitHub
commit f0153b4c2f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 112 additions and 32 deletions

77
DOCKER_ENHANCEMENTS.md Normal file
View File

@ -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
```

View File

@ -32,6 +32,13 @@ RUN pnpm ui:build
ENV NODE_ENV=production 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 # Security hardening: Run as non-root user
# The node:22-bookworm image includes a 'node' user (uid 1000) # The node:22-bookworm image includes a 'node' user (uid 1000)
# This reduces the attack surface by preventing container escape via root privileges # This reduces the attack surface by preventing container escape via root privileges

View File

@ -1,16 +1,18 @@
services: services:
moltbot-gateway: moltbot-gateway:
container_name: moltbot
image: ${CLAWDBOT_IMAGE:-moltbot:local} image: ${CLAWDBOT_IMAGE:-moltbot:local}
environment: environment:
HOME: /home/node HOME: /home/node
MOLTBOT_STATE_DIR: /var/lib/moltbot
TERM: xterm-256color TERM: xterm-256color
CLAWDBOT_GATEWAY_TOKEN: ${CLAWDBOT_GATEWAY_TOKEN} CLAWDBOT_GATEWAY_TOKEN: ${CLAWDBOT_GATEWAY_TOKEN}
CLAUDE_AI_SESSION_KEY: ${CLAUDE_AI_SESSION_KEY} CLAUDE_AI_SESSION_KEY: ${CLAUDE_AI_SESSION_KEY}
CLAUDE_WEB_SESSION_KEY: ${CLAUDE_WEB_SESSION_KEY} CLAUDE_WEB_SESSION_KEY: ${CLAUDE_WEB_SESSION_KEY}
CLAUDE_WEB_COOKIE: ${CLAUDE_WEB_COOKIE} CLAUDE_WEB_COOKIE: ${CLAUDE_WEB_COOKIE}
volumes: volumes:
- ${CLAWDBOT_CONFIG_DIR}:/home/node/.clawdbot - moltbot_data:/var/lib/moltbot
- ${CLAWDBOT_WORKSPACE_DIR}:/home/node/clawd - moltbot_workspace:/home/node/clawd
ports: ports:
- "${CLAWDBOT_GATEWAY_PORT:-18789}:18789" - "${CLAWDBOT_GATEWAY_PORT:-18789}:18789"
- "${CLAWDBOT_BRIDGE_PORT:-18790}:18790" - "${CLAWDBOT_BRIDGE_PORT:-18790}:18790"
@ -19,27 +21,15 @@ services:
command: command:
[ [
"node", "node",
"dist/index.js", "/app/moltbot.mjs",
"gateway", "gateway",
"--bind", "--bind",
"${CLAWDBOT_GATEWAY_BIND:-lan}", "${CLAWDBOT_GATEWAY_BIND:-lan}",
"--port", "--port",
"${CLAWDBOT_GATEWAY_PORT:-18789}" "${CLAWDBOT_GATEWAY_PORT:-18789}",
"--allow-unconfigured"
] ]
moltbot-cli: volumes:
image: ${CLAWDBOT_IMAGE:-moltbot:local} moltbot_data:
environment: moltbot_workspace:
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"]

View File

@ -21,11 +21,11 @@ if ! docker compose version >/dev/null 2>&1; then
exit 1 exit 1
fi fi
mkdir -p "${CLAWDBOT_CONFIG_DIR:-$HOME/.clawdbot}" mkdir -p "${CLAWDBOT_CONFIG_DIR:-$HOME/.moltbot}"
mkdir -p "${CLAWDBOT_WORKSPACE_DIR:-$HOME/clawd}" mkdir -p "${CLAWDBOT_WORKSPACE_DIR:-$HOME/moltbot}"
export CLAWDBOT_CONFIG_DIR="${CLAWDBOT_CONFIG_DIR:-$HOME/.clawdbot}" export CLAWDBOT_CONFIG_DIR="${CLAWDBOT_CONFIG_DIR:-$HOME/.moltbot}"
export CLAWDBOT_WORKSPACE_DIR="${CLAWDBOT_WORKSPACE_DIR:-$HOME/clawd}" export CLAWDBOT_WORKSPACE_DIR="${CLAWDBOT_WORKSPACE_DIR:-$HOME/moltbot}"
export CLAWDBOT_GATEWAY_PORT="${CLAWDBOT_GATEWAY_PORT:-18789}" export CLAWDBOT_GATEWAY_PORT="${CLAWDBOT_GATEWAY_PORT:-18789}"
export CLAWDBOT_BRIDGE_PORT="${CLAWDBOT_BRIDGE_PORT:-18790}" export CLAWDBOT_BRIDGE_PORT="${CLAWDBOT_BRIDGE_PORT:-18790}"
export CLAWDBOT_GATEWAY_BIND="${CLAWDBOT_GATEWAY_BIND:-lan}" export CLAWDBOT_GATEWAY_BIND="${CLAWDBOT_GATEWAY_BIND:-lan}"
@ -62,7 +62,7 @@ YAML
if [[ -n "$home_volume" ]]; then if [[ -n "$home_volume" ]]; then
printf ' - %s:/home/node\n' "$home_volume" >>"$EXTRA_COMPOSE_FILE" 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" printf ' - %s:/home/node/clawd\n' "$CLAWDBOT_WORKSPACE_DIR" >>"$EXTRA_COMPOSE_FILE"
fi fi
@ -77,7 +77,7 @@ YAML
if [[ -n "$home_volume" ]]; then if [[ -n "$home_volume" ]]; then
printf ' - %s:/home/node\n' "$home_volume" >>"$EXTRA_COMPOSE_FILE" 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" printf ' - %s:/home/node/clawd\n' "$CLAWDBOT_WORKSPACE_DIR" >>"$EXTRA_COMPOSE_FILE"
fi fi

View File

@ -360,7 +360,13 @@ export async function autoMigrateLegacyStateDir(params: {
try { try {
fs.renameSync(legacyDir, targetDir); 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)}`); warnings.push(`Failed to move legacy state dir (${legacyDir}${targetDir}): ${String(err)}`);
return { migrated: false, skipped: false, changes, warnings }; return { migrated: false, skipped: false, changes, warnings };
} }
@ -430,11 +436,11 @@ export async function detectLegacyStateMigrations(params: {
: { store: {}, ok: true }; : { store: {}, ok: true };
const legacyKeys = targetSessionParsed.ok const legacyKeys = targetSessionParsed.ok
? listLegacySessionKeys({ ? listLegacySessionKeys({
store: targetSessionParsed.store, store: targetSessionParsed.store,
agentId: targetAgentId, agentId: targetAgentId,
mainKey: targetMainKey, mainKey: targetMainKey,
scope: targetScope, scope: targetScope,
}) })
: []; : [];
const legacyAgentDir = path.join(stateDir, "agent"); const legacyAgentDir = path.join(stateDir, "agent");