forked from admin/VolcanicAgenticCore
Entrypoint drops the capability bounding set and sets no_new_privs. Security docs and spec no longer claim the agent can't reach the secrets; docker exec fallbacks use a clean environment. PLAYWRIGHT_ENABLED drops the nested compose default. Tests pin the Dockerfile's Playwright install step and the compose model fallback.
237 lines
16 KiB
Markdown
237 lines
16 KiB
Markdown
# Running the Telegram bot in Docker
|
|
|
|
The repo's `Dockerfile` and `docker-compose.yml` run the [Telegram bot](telegram-bot.md) as one container, on
|
|
Coolify or any Docker host. The bot uses long polling, so the container needs no open port and no domain.
|
|
|
|
Local run:
|
|
|
|
```bash
|
|
cp .env.example .env # fill in TELEGRAM_BOT_TOKEN and TELEGRAM_ALLOWED_USER_IDS
|
|
docker compose up -d --build
|
|
docker compose logs -f bot
|
|
```
|
|
|
|
`dotnet run` (see [telegram-bot.md](telegram-bot.md#setup)) stays the development path.
|
|
|
|
## What's in the image
|
|
|
|
- The .NET 10 runtime and the published host in `/app`. No .NET SDK, so the agent can't build .NET projects.
|
|
- Tools for the agent's `bash`: `bash`, `git`, `ripgrep`, `python3` (with `python-is-python3` and `python3-venv`),
|
|
`curl`, `ca-certificates`, `tzdata`, `jq`, `less`, `procps`, `openssh-client`.
|
|
- Node.js 24 from NodeSource, with `npm` and `npx`.
|
|
- The `claude` CLI (`@anthropic-ai/claude-code`), which the Claude Code provider drives.
|
|
- Optionally `@playwright/mcp` 0.0.82 (the `playwright-mcp` binary) and Chromium under `/opt/ms-playwright`
|
|
(`PLAYWRIGHT_BROWSERS_PATH`), for the browser tools in [mcp.md](mcp.md).
|
|
|
|
The bot and everything it starts run as `app` (uid/gid 1654, `HOME=/home/app`). The image's own user stays root:
|
|
the entrypoint `setpriv` drops to `app` before it starts `dotnet`, so PID 1 (`docker-init`, from compose's
|
|
`init: true`) is owned by root and the agent can't read the container's environment from `/proc/1/environ`. It also
|
|
empties the capability bounding set and sets `no_new_privs`, so nothing the agent runs regains root through a setuid
|
|
binary such as `su` or `mount`. Don't set `user:` in the compose file: `setpriv` needs root to switch users. For the
|
|
same reason `docker exec` opens a root shell; add `-u app` to act as the bot. Any command that writes under `HOME`
|
|
(`npm`, `npx`, `git config`, `gh auth`, `claude`) must run with `-u app`: as root it leaves root-owned files in
|
|
`/home/app` (`HOME` is set image-wide), and the bot's `npx` and `git` then fail with `EACCES` until the next redeploy.
|
|
An interactive `app` session should also start from a clean environment (see
|
|
[First run and Claude login](#first-run-and-claude-login)).
|
|
|
|
## Build arguments
|
|
|
|
| Argument | Default | Purpose |
|
|
|---|---|---|
|
|
| `INSTALL_PLAYWRIGHT` | `false` in the `Dockerfile`, `true` in `docker-compose.yml` | Adds Playwright MCP and Chromium, about 400 MB |
|
|
| `CLAUDE_CODE_VERSION` | `2.1` | npm version spec for `claude`: `2.1` is the newest 2.1.x at build time; an exact version such as `2.1.3` pins it |
|
|
| `NODE_MAJOR` | `24` | Node.js major version from NodeSource |
|
|
| `PLAYWRIGHT_MCP_VERSION` | `0.0.82` | Must equal the version in `PlaywrightPreset.DefaultPackage`; `PlaywrightPresetTests.Dockerfile_version_matches_DefaultPackage` fails when they differ. The compose file doesn't override it |
|
|
|
|
The compose file passes `INSTALL_PLAYWRIGHT` and `CLAUDE_CODE_VERSION` from `.env` (or Coolify's variables).
|
|
Without compose:
|
|
|
|
```bash
|
|
docker build --build-arg INSTALL_PLAYWRIGHT=true -t volcanic-agentic-core .
|
|
docker run -d --init --shm-size 1g --restart unless-stopped --env-file .env \
|
|
-e PLAYWRIGHT_ENABLED=true -e PLAYWRIGHT_HEADLESS=true -e PLAYWRIGHT_EXTRA_ARGS=--no-sandbox \
|
|
-e PLAYWRIGHT_COMMAND=playwright-mcp \
|
|
-v workspaces:/data/workspaces -v agent:/data/agent -v claude:/data/claude volcanic-agentic-core
|
|
```
|
|
|
|
`--init` is required: without it `dotnet` is PID 1, so nothing reaps the zombies that killed `claude` trees and `bash`
|
|
children leave behind, and PID 1 is no longer root's. `--shm-size 1g` keeps Chromium from crashing. The compose
|
|
file's defaults (the `-e` lines, which win over `--env-file`) don't apply to a plain `docker run`, and neither does
|
|
its `TELEGRAM_MODEL` / `LOG_LEVEL` mapping: pass `-e Telegram__Model=...` instead.
|
|
|
|
## Configuration
|
|
|
|
Everything is set through environment variables: `.env` locally, the resource's environment variables on Coolify.
|
|
The compose file maps them into the container.
|
|
|
|
| Variable | Default | Purpose | Agent's processes see it |
|
|
|---|---|---|---|
|
|
| `TELEGRAM_BOT_TOKEN` | — | Required. The token from @BotFather | No, and redacted from tool output |
|
|
| `TELEGRAM_ALLOWED_USER_IDS` | — | Required. Comma-separated Telegram user ids; the first one is the owner (the `🟢 Online.` message, Claude re-login prompts) | Yes |
|
|
| `TELEGRAM_MODEL` | `claude-code/claude-opus-5-5` | Model every chat uses; compose passes it as `Telegram__Model` | Yes, as `Telegram__Model` |
|
|
| `TZ` | `UTC` | The container's time zone | The agent's `bash` gets the chat's zone as `TZ` instead |
|
|
| `LOG_LEVEL` | `Information` | Passed as `Logging__LogLevel__Default` | Yes |
|
|
| `OPENAI_API_KEY` | — | Turns on speech to text for voice notes | No, and redacted |
|
|
| `STT_MODEL` | `gpt-4o-transcribe` | Transcription model | Yes |
|
|
| `GOOGLE_CLIENT_ID` | — | With the secret, turns on Google Calendar ([google-calendar.md](google-calendar.md)) | Yes |
|
|
| `GOOGLE_CLIENT_SECRET` | — | See above | No, and redacted |
|
|
| `DEFAULT_TIMEZONE` | the container's `TZ` | IANA zone for chats that never ran `/timezone`, e.g. `Europe/Berlin`. An unknown name stops startup | Yes |
|
|
| `PROCESS_ENV_ALLOW` | — | Comma-separated names or globs let through despite the deny rules, e.g. `GH_TOKEN` | Yes |
|
|
| `PLAYWRIGHT_ENABLED` | `true` | Browser tools on or off. Set it to `false` for an image built with `INSTALL_PLAYWRIGHT=false` | Yes |
|
|
| `PLAYWRIGHT_HEADLESS` | `true` | The container has no display | Yes |
|
|
| `PLAYWRIGHT_BROWSER` | `chromium` | Only Chromium is installed in the image | Yes |
|
|
| `PLAYWRIGHT_EXTRA_ARGS` | `--no-sandbox` | Extra `playwright-mcp` arguments. Keep `--no-sandbox` when you override it: Chromium's sandbox needs user namespaces, which Docker's default seccomp profile denies | Yes |
|
|
| `PLAYWRIGHT_COMMAND` | `playwright-mcp` | The preinstalled server, so the first browser call doesn't wait for an `npx` download | Yes |
|
|
|
|
`TELEGRAM_MODEL` and `LOG_LEVEL` are read by the compose file only, which passes them to the bot as `Telegram__Model`
|
|
and `Logging__LogLevel__Default`; `INSTALL_PLAYWRIGHT` and `CLAUDE_CODE_VERSION` are build arguments (see above).
|
|
|
|
> **Never set `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`, `ANTHROPIC_BASE_URL`, `CLAUDE_CODE_OAUTH_TOKEN`,
|
|
> `CLAUDE_CODE_USE_BEDROCK` or `CLAUDE_CODE_USE_VERTEX`.** The Claude Code provider runs on a Claude subscription only
|
|
> and refuses to run when one of them is present. In Coolify, check shared, project and team variables too: they are
|
|
> the likely way one gets in.
|
|
|
|
### What the agent can't see
|
|
|
|
The process environment filter removes variables from the environment of the agent's `bash`, scheduled scripts and
|
|
MCP servers:
|
|
|
|
- anything matching the default deny rules: `*_API_KEY`, `*_APIKEY`, `*_TOKEN`, `*_SECRET`, `*_PASSWORD` and their
|
|
`Section__Key` forms (`*__APIKEY`, `*__*TOKEN`, `*__*SECRET`, `*__*PASSWORD`), plus `TELEGRAM_BOT_TOKEN`,
|
|
`OPENAI_API_KEY`, `GOOGLE_CLIENT_SECRET`, `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN` and `CLAUDE_CODE_OAUTH_TOKEN`;
|
|
- any variable whose value is the bot token, the speech API key or the Google client secret, whatever its name.
|
|
|
|
Those values are also replaced by `[REDACTED]` in tool output. The startup log names the rules, never values:
|
|
`Process environment: 15 deny rules, allow: none`.
|
|
|
|
To give the agent a credential on purpose, add the variable in Coolify, add it to `environment:` in the compose file,
|
|
and list its name in `PROCESS_ENV_ALLOW` (`PROCESS_ENV_ALLOW=GH_TOKEN` lets `gh` work). A name that is on the deny
|
|
list by name, such as `TELEGRAM_BOT_TOKEN`, can't be allowed: trying stops startup.
|
|
|
|
### Other settings
|
|
|
|
Any setting of the bot is reachable as `Section__Key`: add the variable in Coolify and a matching line under
|
|
`environment:` in `docker-compose.yml`. For example `Telegram__ReconnectMaxDelay=00:02:00`, `Telegram__InputDebounce`,
|
|
`Telegram__Speech__Language`, `Scheduler__MaxJobsPerTarget`, `GoogleCalendar__*`, `ProcessEnvironment__Deny__0`, and
|
|
the provider's `VOLCANIC_CLAUDE_CODE_IDLE_TIMEOUT_MS` / `VOLCANIC_CLAUDE_CODE_TOTAL_TIMEOUT_MS`. The settings are
|
|
listed in [telegram-bot.md](telegram-bot.md#configuration) and [claude-code-provider.md](claude-code-provider.md#configuration).
|
|
|
|
The image sets these; normally leave them alone:
|
|
|
|
| Variable | Value | |
|
|
|---|---|---|
|
|
| `Telegram__WorkspaceRoot` | `/data/workspaces` | One directory per chat |
|
|
| `VOLCANIC_AGENT_DIR` | `/data/agent` | Sessions, settings, scheduler jobs, calendar tokens |
|
|
| `CLAUDE_CONFIG_DIR` | `/data/claude` | The Claude login |
|
|
| `HOME` | `/home/app` | |
|
|
| `DOTNET_EnableDiagnostics` | `0` | Turns off the .NET diagnostic socket, which would hand the bot's environment to the agent's processes. Unset it, together with `ProcessEnvironment__HardenHostProcess=false`, only to attach `dotnet-dump` or `dotnet-trace` while debugging |
|
|
| `DOTNET_CLI_TELEMETRY_OPTOUT`, `DOTNET_NOLOGO` | `1` | |
|
|
|
|
## Volumes
|
|
|
|
| Volume | Path | Contents |
|
|
|---|---|---|
|
|
| `workspaces` | `/data/workspaces` | One directory per chat (`/data/workspaces/<chatId>`): `AGENTS.md` or `CLAUDE.md`, `PERSONA.md`, `MEMORY.md`, `inbox/` (received files), `browser/` (screenshots, downloads), project-level `.volcanic/extensions/` |
|
|
| `agent` | `/data/agent` | Sessions (`sessions/telegram-<chatId>/`), `settings.json`, `extensions/*.dll`, chat time zones (`telegram/chat-settings.json`), `scheduler/jobs.json`, Google Calendar tokens (`google-calendar/`) |
|
|
| `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 <dir>`).
|
|
|
|
`HOME` (`/home/app`) isn't a volume: `~/.gitconfig`, `~/.ssh`, `gh` logins and the npm cache are lost on redeploy.
|
|
|
|
## Deploying on Coolify
|
|
|
|
1. Create a resource from this repository with the *Docker Compose* build pack.
|
|
2. Set `TELEGRAM_BOT_TOKEN` and `TELEGRAM_ALLOWED_USER_IDS` (owner first) and any optional variables from
|
|
[Configuration](#configuration). Leave the domain empty.
|
|
3. Deploy. The log shows lines such as `Using Claude Code at /usr/bin/claude, model claude-code/claude-opus-5-5` and
|
|
`Allowed users: <ids>. Workspaces under /data/workspaces`, and the owner gets `🟢 Online.`.
|
|
|
|
There is no healthcheck, because the bot serves no HTTP. Lost connectivity is retried with backoff inside the
|
|
process. A rejected token (`401`) exits with code 1 and invalid configuration exits with code 2;
|
|
`restart: unless-stopped` turns either into a restart loop you can see in the logs.
|
|
|
|
## First run and Claude login
|
|
|
|
1. The owner opens the bot, sends `/start`, then `/claude-code-login`.
|
|
2. The login link and the code prompt arrive as one message. Open the link, sign in, and paste the code shown on the
|
|
page back into the chat. The credentials are stored in `/data/claude`.
|
|
3. `/claude-code-doctor` should now report that you're logged in through `claude.ai` and that the bridge works.
|
|
|
|
When the login expires later, the owner gets the same prompt automatically and the paused request continues after
|
|
the code is pasted.
|
|
|
|
Without the bot:
|
|
|
|
```bash
|
|
docker exec -u app -it <container> env -i HOME=/home/app \
|
|
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin CLAUDE_CONFIG_DIR=/data/claude TERM=xterm \
|
|
claude auth login --claudeai
|
|
```
|
|
|
|
The `-u app` matters: `docker exec` runs as root by default, and credentials written by root can't be read by the bot.
|
|
So does `env -i`: a `docker exec` process gets the whole container environment, bot token and API keys included, and
|
|
while it runs as `app` the agent's `bash` or a scheduled watcher can read it from `/proc/<pid>/environ`.
|
|
|
|
## Updating
|
|
|
|
Redeploy to rebuild the image. The `claude` CLI can't update itself inside the container (its install directory is
|
|
owned by root), so a new version only arrives with a build. The `npm install` layer is cached, though: to pick up a
|
|
newer 2.1.x, build without cache (Coolify's force deploy without cache, or `docker compose build --no-cache`), or set
|
|
`CLAUDE_CODE_VERSION` to an exact version, which also pins it. A Claude Code upgrade invalidates the prompt cache
|
|
once.
|
|
|
|
Bumping Playwright MCP means changing `PlaywrightPreset.DefaultPackage` and `ARG PLAYWRIGHT_MCP_VERSION` in the
|
|
`Dockerfile` in the same commit; the unit test above catches a mismatch.
|
|
|
|
## Extensions and project context
|
|
|
|
Extension assemblies go into `/data/agent/extensions/` (every chat) or `/data/workspaces/<chatId>/.volcanic/extensions/`
|
|
(one chat), then restart:
|
|
|
|
```bash
|
|
docker cp MyExtension.dll <container>:/data/agent/extensions/
|
|
docker exec <container> chown app:app /data/agent/extensions/MyExtension.dll
|
|
docker restart <container>
|
|
```
|
|
|
|
Build them against the same `Volcanic.AgenticCore.Abstractions` version as the image. See [extensions.md](extensions.md).
|
|
|
|
Project context files (`AGENTS.md` or `CLAUDE.md`, `PERSONA.md`, `MEMORY.md`) live in the chat's workspace directory.
|
|
The agent can edit them, or you can with `docker exec -u app -it <container> env -i HOME=/home/app
|
|
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin TERM=xterm bash` (clean environment, for the reason
|
|
given under [First run and Claude login](#first-run-and-claude-login)).
|
|
|
|
## Troubleshooting
|
|
|
|
| Symptom | Cause and fix |
|
|
|---|---|
|
|
| `[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 <dir>` 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<token>/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` |
|
|
| The provider refuses to run because of an API key | One of the `ANTHROPIC_*` / `CLAUDE_CODE_*` variables above is set, possibly as a shared Coolify variable. Remove it |
|
|
|
|
## Security
|
|
|
|
The container is the isolation boundary. Inside it, the agent's `bash` runs as `app`, the same user as the bot: it
|
|
can read the workspaces, the agent directory with every chat's sessions, and the Claude login in `/data/claude`.
|
|
The configured secrets are kept out of its way: they are filtered out of its environment, the bot process is
|
|
non-dumpable (its `/proc/<pid>/environ` is unreadable and it can't be attached to), PID 1 is root's so
|
|
`/proc/1/environ` is unreadable too, the diagnostic socket is off, setuid binaries can't raise its privileges, and
|
|
secret values are redacted from tool output.
|
|
|
|
That stops secrets leaking by accident, not an agent that goes after them on purpose. `app` owns `/data/workspaces`
|
|
and `/data/agent`, so the agent can drop a DLL into `<workspace>/.volcanic/extensions/` (loaded into the bot process,
|
|
with its unfiltered environment, at the next `/new`) or add an MCP server whose `env` uses `${env:TELEGRAM_BOT_TOKEN}`
|
|
to `/data/agent/settings.json`, then kill the bot so that `restart: unless-stopped` brings it back with that server.
|
|
Treat everyone on the allow-list as able to obtain every secret you configure. None of this is a sandbox: anyone on
|
|
the allow-list can run any command in the container.
|
|
|
|
Details in [telegram-bot.md](telegram-bot.md#security).
|