diff --git a/Dockerfile b/Dockerfile index 3fbffd3..509dc79 100644 --- a/Dockerfile +++ b/Dockerfile @@ -65,7 +65,8 @@ RUN if [ "$INSTALL_PLAYWRIGHT" = "true" ]; then \ && rm -rf /var/lib/apt/lists/*; \ fi -# The entrypoint hard-codes uid/gid 1654. Named volumes copy this ownership on first mount. +# The entrypoint hard-codes uid/gid 1654. Named volumes copy this ownership on first mount; bind-mounted roots are +# chowned by the entrypoint (not recursively), since Docker creates missing host directories as root. RUN test "$(id -u app)" = 1654 && test "$(id -g app)" = 1654 \ && mkdir -p /data/workspaces /data/agent /data/claude /home/app \ && chown -R app:app /data /home/app @@ -85,4 +86,5 @@ ENV Telegram__WorkspaceRoot=/data/workspaces \ # No USER: PID 1 (docker-init with compose's init: true) must stay root, or the agent could read every secret from # /proc/1/environ. setpriv drops to app and execs dotnet, so Environment.ProcessPath is the muxer the bridge needs. # An empty bounding set and no_new_privs keep the agent's processes from regaining root through setuid binaries. -ENTRYPOINT ["setpriv", "--reuid=1654", "--regid=1654", "--init-groups", "--inh-caps=-all", "--bounding-set=-all", "--no-new-privs", "--", "dotnet", "Volcanic.AgenticCore.Telegram.dll"] +# The shell only chowns the mount roots and execs setpriv, so the process tree is unchanged. +ENTRYPOINT ["/bin/sh", "-c", "chown 1654:1654 /data/workspaces /data/agent /data/claude; exec setpriv --reuid=1654 --regid=1654 --init-groups --inh-caps=-all --bounding-set=-all --no-new-privs -- dotnet Volcanic.AgenticCore.Telegram.dll"] diff --git a/docker-compose.yml b/docker-compose.yml index c21c384..f08a990 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -32,12 +32,8 @@ services: - PLAYWRIGHT_BROWSER=${PLAYWRIGHT_BROWSER:-} - PLAYWRIGHT_EXTRA_ARGS=${PLAYWRIGHT_EXTRA_ARGS:---no-sandbox} - PLAYWRIGHT_COMMAND=${PLAYWRIGHT_COMMAND:-playwright-mcp} + # Bind mounts on the host. Docker creates missing directories as root; the entrypoint hands the three roots to app. volumes: - - workspaces:/data/workspaces - - agent:/data/agent - - claude:/data/claude - -volumes: - workspaces: - agent: - claude: + - /media/disk1/containers/volcanic_secretary_core/workspaces:/data/workspaces + - /media/disk1/containers/volcanic_secretary_core/agent:/data/agent + - /media/disk1/containers/volcanic_secretary_core/claude:/data/claude diff --git a/docs/docker.md b/docs/docker.md index 8fbfbb6..d853002 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -129,15 +129,20 @@ The image sets these; normally leave them alone: ## Volumes +The compose file binds them to directories under `/media/disk1/containers/volcanic_secretary_core/` on the host +(`workspaces/`, `agent/`, `claude/`); edit the three `volumes:` lines to put them elsewhere. + | Volume | Path | Contents | |---|---|---| | `workspaces` | `/data/workspaces` | One directory per chat (`/data/workspaces/`): `AGENTS.md` or `CLAUDE.md`, `PERSONA.md`, `MEMORY.md`, `inbox/` (received files), `browser/` (screenshots, downloads), project-level `.volcanic/extensions/`, the Google Calendar token (`.volcanic/google-calendar/token.json`) | | `agent` | `/data/agent` | Sessions (`sessions/telegram-/`), `settings.json`, `extensions/*.dll`, chat time zones (`telegram/chat-settings.json`), `scheduler/jobs.json` | | `claude` | `/data/claude` | The Claude login (credentials and onboarding state) | -Named volumes survive redeploys. Coolify prefixes them with the resource's uuid and deletes them only when you delete -the resource together with its volumes. Bind mounts work too if the host directories belong to uid 1654 -(`chown -R 1654:1654 `). +Bind mounts survive redeploys and deleting the resource. Docker creates missing host directories as root; the +entrypoint chowns the three mount roots (not their contents) to uid 1654 before it drops to `app`, so files copied in +from elsewhere need `chown -R 1654:1654 ` on the host. Named volumes (`-v workspaces:/data/workspaces`, as in +the `docker run` example) work too. On an NTFS disk (`fuseblk`, as `/media/disk1` is) ownership and modes aren't +stored: everything shows as root with mode 777, `chown` is a silent no-op, and `app` can still write. `HOME` (`/home/app`) isn't a volume: `~/.gitconfig`, `~/.ssh`, `gh` logins and the npm cache are lost on redeploy. @@ -211,7 +216,7 @@ given under [First run and Claude login](#first-run-and-claude-login)). | `[executable_missing]` in a reply, or `Claude Code (claude) was not found on PATH` at startup | The image has no working `claude`. Rebuild and check that `claude --version` ran in the build log | | `[auth]` in a reply | The Claude login is missing or expired: `/claude-code-login`, or the `docker exec -u app` command above | | `[mcp_startup]` in a reply | The bridge files are missing from `/app`: `Volcanic.AgenticCore.Extensions.ClaudeCode.Bridge.dll` and `Volcanic.AgenticCore.Extensions.ClaudeCode.Bridge.runtimeconfig.json`. The build checks for both, so rebuild from a clean tree | -| `Permission denied` under `/data` | Bind-mounted directories owned by another user: `chown -R 1654:1654 ` on the host | +| `Permission denied` under `/data` | Files in the bind-mounted directories owned by another user: `chown -R 1654:1654 /media/disk1/containers/volcanic_secretary_core` on the host | | `Telegram reports another poller or a webhook for this bot (409)` | Another instance polls with the same token (an old container, a local `dotnet run`), or the bot has a webhook. Stop the other instance; remove a webhook with `https://api.telegram.org/bot/deleteWebhook` | | `Telegram rejected the bot token (401 Unauthorized)`, restart loop | Wrong or revoked `TELEGRAM_BOT_TOKEN` | | Browser tools missing or failing | The image was built with `INSTALL_PLAYWRIGHT=false`, `PLAYWRIGHT_ENABLED` is off, or `PLAYWRIGHT_EXTRA_ARGS` lost `--no-sandbox`. Chromium crashes also come from a small `/dev/shm`; keep `shm_size: 1gb` |