Merge 80ff97f44b into 4583f88626
This commit is contained in:
commit
f0153b4c2f
77
DOCKER_ENHANCEMENTS.md
Normal file
77
DOCKER_ENHANCEMENTS.md
Normal 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
|
||||
```
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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");
|
||||
|
||||
Loading…
Reference in New Issue
Block a user