dsh-container
DeepSeek Harness (dsh) container image: universal dev-container base, dsh auto-update on boot, compose + Quadlet examples
- Stars
- 1
- Language
- Shell
- Created
- Aug 13, 2026
- Updated
- Sep 17, 2026
Introduction
dsh Container Image
DeepSeek Harness (dsh) as a batteries-included
container — agent, full toolchain, and a reverse proxy in one image, built from the official source tags.
English | 中文
Quick start
Docker Compose (Linux)
docker compose -f examples/compose.yaml up -d
docker compose logs dsh | grep 'dsh web:'
# open http://127.0.0.1:3081/ in your local browser (the proxy bootstraps the login)
Podman Quadlet (Linux, recommended)
sudo mkdir -p /etc/containers/systemd
sudo cp examples/dsh.container /etc/containers/systemd/
sudo systemctl daemon-reload
sudo systemctl enable --now dsh.service
Both examples publish 127.0.0.1:3081 and mount one volume at /home/dsh — the user layer
(~/.dsh, caches, user-installed tools) survives image upgrades while the system layer comes from
the image. There is no login step: the proxy bootstraps the dsh session automatically.
What's inside
| Component | Description |
|---|---|
| Base image | debian:13-slim (pinned; overridable via the BASE_IMAGE build arg) |
| Toolchain | Node.js 22 LTS, pnpm, uv, Rust/cargo, git, build-essential, Caddy, podman, gh — image-owned real binaries, upgraded with the image |
| dsh | Built from the official source tag into /opt/deepseek-harness (DSH_TAG pinnable); no runtime auto-update |
| Exposure | Caddy reverse proxy (0.0.0.0:3081 → dsh's 127.0.0.1:3080) with optional basic auth |
| Supervisor | dsh web auto-restarts on exit; docker exec dsh dsh-restart restarts it manually |
| Remote compatibility | One container-adapt plugin (container/plugin/, mounted via dsh --patch): session-cookie bootstrap inside dsh, headless-hostile "Open config file" button hidden (describe reports no local document), browser-side isLoopback patch script |
| Observability | OCI labels, HEALTHCHECK (curl 3080 + 3081) |
| Runtime user | uid 1000 (dsh), passwordless sudo; /home/dsh is the persisted user layer |
Container-adapt plugin
All container-side adaptation of upstream dsh lives in one Cordis plugin, shipped with the
image at /opt/dsh-container-plugin and mounted into the web profile via dsh --patch:
- Session-cookie bootstrap — the plugin exchanges dsh's one-time login token inside the dsh process and writes the cookie for the proxy to inject; browsers never see a token.
- Hidden settings-document button — "Open config file" has no headless fallback upstream and
would spawn
xdg-openinto nothing in a container. The plugin makessettings/describereporthasDocument: false, so the button never renders (upstream's own UI logic). The document itself stays at~/.dsh/settings.yamlon the mounted volume. - Browser-side
isLoopbackpatch —scripts/patch-client.jsmakes settings/credentials work through the proxy (applied at image build and before everydsh webstart).
This plugin is the single adaptation maintenance point. When upstream ships an API that makes part of it redundant, the release pipeline deletes that part automatically and says so in the release notes (see docs/upstream-contract.md § Simplification triggers).
Networking & security
- Port model —
dsh weblistens on127.0.0.1:3080(upstream rejects--host 0.0.0.0); the exposed port is3081, published on host loopback by the examples. - The proxy is the security boundary — Caddy rewrites
Host/Originto loopback, so remote browsers pass dsh's/apitrust fence, including settings/credentials methods that are otherwise loopback-only. Anyone who can reach3081gets full control: enable basic auth (DSH_PROXY_USER/DSH_PROXY_PASSWORD, set together or the entrypoint refuses to start) and keep the port firewalled. - Session bootstrapped — the container-adapt plugin exchanges dsh's one-time login token inside the dsh process at startup and the proxy injects the session cookie into every proxied request; browsers never see a token.
- Streams & compression — SSE/WebSocket pass through unbuffered (verified against Caddy 2.6); UI assets are gzip-compressed by dsh's own webserver (≈1.3 MB → ≈360 KB).
- Telemetry off by default —
DSH_TELEMETRY_MODE=DISABLEDis set by the entrypoint; no feedback/telemetry data leaves the container unless you opt back in. - Client patch — the container-adapt plugin's
patch-client.jsmakes settings/credentials usable through the proxy (applied at build time and before everydsh webstart; if upstream changes the bundle strings it warns and skips instead of blocking startup). - Extra args — pass
dsh webarguments through the container command, e.g.["--port", "8080"](internal port only; exposed port stays3081).
For WAN access, terminate TLS in front of 3081 (the docs include a working nginx config with
WebSocket headers and raised timeouts) — see docs/deployment.md and
docs/security.md.
Environment variables
| Variable | Default | Description |
|---|---|---|
DSH_PROXY_USER / DSH_PROXY_PASSWORD | (empty) | Basic auth on the exposed proxy (recommended for any non-loopback deployment); set both or neither |
DSH_TELEMETRY_MODE | DISABLED | dsh feedback/telemetry upload policy; FEEDBACK_ONLY restores the upstream default (uploads on explicit feedback), DISABLED keeps everything local |
Everything else uses built-in defaults — dsh data at ~/.dsh, cwd $HOME, writable caches under
~/.cargo / ~/.local/share, image-owned tools in /usr/local/bin and /opt/rust. The whole
/home/dsh is the persistence boundary: mount it as one volume; image upgrades replace the
toolchain, never the data. Details in docs/build.md and
docs/deployment.md.
Documentation
| Document | Contents |
|---|---|
| docs/deployment.md | Deployment & maintenance: Compose, Quadlet, remote access, offline use, FAQ |
| docs/security.md | Security notes: network exposure tradeoff, credentials, trusted workloads |
| docs/build.md | Build configuration: build args, source tag pinning, reproducible builds |
| docs/releasing.md | Release automation: upstream tag watcher, contract check, agent repair, auto-publish |
| docs/upstream-contract.md | The upstream behaviors this image depends on, and how drift is detected |
| docs/development.md | Directory structure and local development |