This directory deploys DS Bot as a Docker image: a dsh web server (DeepSeek
Harness) with the ds-bot plugin installed, ready for a NAS or any always-on
box. The container is the sandbox boundary — see Permissions.
For local source development with frontend rebuilds and host restarts, see
the development Compose setup.
- Docker 24+ with Compose v2.24+ (Docker Desktop, or Docker Engine + the
compose plugin —
env_file.requiredneeds 2.24). - Linux containers, x86-64. arm64 builds should work (
node:24-bookworm-slimis multi-arch) but are untested. - A DeepSeek API key (or another Anthropic-Messages-compatible endpoint through
DEEPSEEK_BASE_URL).
The image is a Debian Bookworm userland, so the host's glibc does not decide
anything: this is also the supported route on hosts too old to run the desktop
app or a host-installed dsh (CentOS 7, glibc 2.17 — see known
issues). The container brings its own DSH and native
libraries, so keep the host's libghostty-vt.so and other system libraries
out of it; the only host requirement is a Docker (or Podman) that can run a
Bookworm image.
From the repository root, edit docker-compose.yml: fill in
DEEPSEEK_API_KEY, add the NAS IP to DSH_TRUSTED_HOST, and set PUID/PGID
to the share owner's IDs. Environment variables, ports and paths are all
configured directly in this file; no .env is needed. Do not commit real keys.
docker compose up -d --build
docker compose logs ds-botThis builds the local image without pulling a published DS Bot image. The root
Compose file publishes host port 13080, and stores data and workspace files
in docker/data and docker/workspace.
The log prints a line like dsh web: http://127.0.0.1:3080/?token=… (LAN: http://172.x.x.x:3080/?token=…). Take the token from it and open
http://<NAS IP>:13080/?token=… once in your browser: the token sets a signed
cookie (valid 30 days, bound to that authority), after which
http://<NAS IP>:13080/ opens the UI directly. The printed port is the container's;
use the host port from ports: when opening it. The token is a fresh
process credential on every start. It exchanges itself for a signed cookie
(valid 30 days), whose key persists in /data, so later restarts keep the
browser logged in. Bookmark the root URL without ?token=…. The first time
the team opens, DS Bot creates a Main Bot called Chief.
An alternative for users who prefer .env is still available in docker/:
cd docker
cp .env.example .env
# Edit .env: DEEPSEEK_API_KEY, DSH_TRUSTED_HOST, PUID/PGID.
docker compose up -d --buildThat file publishes host port 3080 by default. Choose one deployment method; do not run both against the same data directory.
Stop it with docker compose down (run in the directory that holds the
Compose file you started). The container gets 15 s to flush sessions; data and
workspace stay in their host folders.
The container is the only sandbox. DSH's own kernel sandbox (bubblewrap /
Landlock) does not run inside Docker's default seccomp profile or on most NAS
kernels, so the image offers exactly one permission preset, Full access:
every Bot command runs without asking. Only mount folders the Bots may change
into /workspace, and run the container with PUID/PGID of the share owner
so file ownership stays right.
On dsh 0.2.2, the entrypoint passes one concrete address to --host.
The default DSH_HOST=0.0.0.0 selects the container's own address. Inside a
container the publish rule is the real boundary, and the trust fence below
still applies. The UI checks the Host header of every /api request:
loopback is always trusted, and every other name or IP you open it at must be
listed in DSH_TRUSTED_HOST (no port — a bare entry trusts it on any port),
or the API answers 403. DSH_HOST=127.0.0.1 narrows the bind to loopback
(e.g. a reverse proxy in the same network namespace); published ports then no
longer reach it.
Alternatives:
- Host network: set
network_mode: hostincompose.ymland dropports:. The server then binds the NAS's interfaces directly onDSH_LISTEN_PORT(default 3080);DSH_HOST=127.0.0.1narrows it to loopback again. - Reverse proxy / HTTPS: the proxy must preserve the browser-visible
Hostheader and forward WebSocket upgrades; add the public host name toDSH_TRUSTED_HOST. The printed login URL is the container's — replace its host with the public one when logging in the first time.
HTTPS is recommended, not required. The UI works over plain
http://<LAN IP> on this dsh release, but the page is not a secure context
there: API keys and secret values you type cross the LAN in clear text. Put a
reverse proxy in front for regular use.
In the root docker-compose.yml, edit container variables under environment,
the published port under ports, and host directories under volumes.
The alternative docker/compose.yml reads these settings from .env;
see docker/.env.example for its full list. The main variables are:
| Variable | Purpose |
|---|---|
DEEPSEEK_API_KEY |
API key for the model endpoint |
DEEPSEEK_BASE_URL |
Override the endpoint (Anthropic-Messages-compatible) |
DSH_TRUSTED_HOST |
Comma-separated host names / IPs the UI answers to |
DSH_PORT |
Alternative .env deployment's host port (default 3080); edit ports directly in the root Compose file |
DSH_HOST |
Bind address: 0.0.0.0 selects the container address (default); 127.0.0.1 selects loopback; a concrete host address is useful with network_mode: host |
DSH_LISTEN_PORT |
Listen port inside the container (default 3080); only relevant with network_mode: host |
PUID / PGID |
User/group the server runs as and new files belong to |
DSH_TELEMETRY_DISABLED |
1 turns off dsh's anonymous telemetry |
Extra dsh CLI flags can be appended to the container command.
For an OpenAI-compatible relay (or another provider DSH speaks), create
DATA_DIR/cordis.patch.yml before the first start — DSH's home-level patch
layer, applied after the profile's own — with an llm-pi-ai provider and an
agent-default-model row, and pass the key as an env var:
- id: llm-pi-ai
config:
providers:
relay:
apiKeyEnv: RELAY_API_KEY
api: openai-completions # or openai-responses / anthropic-messages
baseURL: http://host.docker.internal:3425/v1
models:
- id: my-model
contextWindow: 131072
maxTokens: 8192
# New Bots made from the Create Bot form start on DSH's default model;
# point it at the relay too, or they start on deepseek-official without a key.
- id: agent-default-model
config:
provider: relay
model: my-modelAdd RELAY_API_KEY under environment in the root Compose file, or to .env
for the alternative deployment. The alternative is Settings → Models in the
UI, which writes the same config.
/data(DATA_DIR) isDSH_HOME: the team and chats (bot/,sessions/), profiles, settings and the login cookie key (.credentials.yaml)./workspace(WORKSPACE_DIR) is the team workspace where the Bots work.
A rebuild reuses the persisted data and workspace. Keep their container
destinations /data and /workspace unchanged when you rebuild or move the
host directories; the team records those destinations, not the host paths.
Backup: stop the container, then copy both DATA_DIR and WORKSPACE_DIR.
Restore by putting both folders back and starting again.
On every start, entries in /data with a different owner or group are assigned
to PUID/PGID, including existing profiles and restored files. This scans the
data directory, so large histories can take longer to start. It does not follow
symbolic links or descend into other filesystems. A populated /workspace keeps
its ownership; if you change PUID/PGID, grant the new user access to that
share on the host.
docker compose build # or: docker build -f docker/Dockerfile -t ds-bot:local .
docker compose up -dA rebuild changes the image and the DSH it contains. An existing persisted
profile is reused, and the entrypoint does not manage plugin upgrades.
The packed plugin stays at /opt/ds-bot/ds-bot.tgz for later dependency
installs. /tmp/ds-bot.tgz links to it for profiles created by older images.
A manual dependency install can therefore pick up the plugin bundled in the
current image; restarting alone does not update the persisted profile.
The image is built and tested with dsh 0.2.2-rc.1, the version
DS Bot is verified with. The entrypoint translates the default wildcard setting
to the concrete container address this release requires. A DSH_VERSION build
arg exists, but other releases are untested.
GitHub Actions publishes the image to GHCR after the branch CI succeeds:
main as latest, dev as dev, and every successful push also as
sha-<full commit SHA>. Only the tested linux/amd64 image is published. The
first publication happens only after this workflow is merged and a push
succeeds. The maintainer must set the GHCR package visibility to Public once
before anonymous pulls work.
Save this as compose.yml in the directory you want to deploy from. Fill in
the API key and your NAS host names before starting it. Running these commands
from the repository root would use the local root Compose file instead.
services:
ds-bot:
image: ghcr.io/fei2411/ds-bot:dev
ports:
- "13080:3080"
environment:
DEEPSEEK_API_KEY: ""
PUID: "1000"
PGID: "1000"
DSH_TRUSTED_HOST: "localhost,127.0.0.1"
volumes:
- ./data:/data
- ./workspace:/workspace
extra_hosts:
- "host.docker.internal:host-gateway"
restart: unless-stopped
stop_grace_period: 15sdocker compose pull
docker compose up -dThe root docker-compose.yml and docker/compose.yml still build the local
image. Choose one setup; do not run both against the same data directory. An
existing persisted profile is not upgraded automatically.
- No Bot browser /
read_browsertool: those exist in dsh Desktop and the desktop app only. - Over plain http, API keys and secret values you type cross the network in clear text — use HTTPS off-box (see Network).
- Full access is the only permission mode: a Bot can change everything the container can see.
After building the image, run node docker/test.mjs ds-bot:local from the
repository root. The test uses isolated, network-disabled containers and
temporary Docker volumes, with no host directory mounts or model calls. It
checks entrypoint permissions, first start, UID/GID changes, restored files,
workspace ownership and the authenticated DS Bot API. It also checks that the
runtime user can read and repack the profile's plugin archive offline, and
that legacy archive references work without rewriting the persisted profile.
All test containers and volumes are removed when it finishes.
CI builds the image from the Linux checkout and runs this test as part of the
required Test result check.