The 0.2.1-alpha.2 fix rendered `host` onto the container's own address
unconditionally. Every earlier release still validates it against the union of
the two literals 127.0.0.1 and 0.0.0.0, so those images now die in the boot
smoke test with
@deepseek-ai/dsh-host-webserver: ValidationError: invalid config:
- $.host expected "127.0.0.1" | "0.0.0.0" but got "172.17.0.22" (at host)
and their refresh rebuilds -- which the recipe-hash change re-queued anyway --
can no longer succeed. Probe the installed webserver schema instead: where it
rejects the unspecified address render the container's own IPv4, otherwise keep
0.0.0.0 so older versions build exactly as they did before. The probe is
overridable through DSH_INSTALL_ROOT so both branches are testable.
dsh 0.2.1-alpha.2 validates `webserver.host` in a schemastery transform and
throws on the unspecified address ("is an unspecified (wildcard) address,
which is not supported: binding every interface would expose remote code
execution to the network; bind one concrete IPv4 or IPv6 address of a local
interface instead"). The check is config-level, so the --patch opt-in that
carried 0.0.0.0 through the 0.1.x schema no longer works, and every build of
0.2.1-alpha.2 has failed the boot smoke test before the image could be pushed
(the tag exists, so the release page has no image and no notes).
The shipped patch becomes a template; docker-entrypoint.sh renders it onto the
container's first non-internal IPv4 (the same address the smoke test probes)
and passes the rendered copy to dsh, so a port published on the Docker host
still reaches the UI while the socket binds one concrete interface. Set
DSH_WEB_HOST to override the address. The substitution also accepts the old
literal `host: 0.0.0.0` template, so either file can land first.
Nothing repaired on a plain container update: the image defaulted to
`warn`, which reports lost grouping and stops there, so a project
directory an update erased stayed erased unless the operator set
DSH_WORKSPACE_DOCTOR=repair — a field an existing Unraid container never
receives, because Unraid does not add variables a template gained later.
Bake DSH_WORKSPACE_DOCTOR=repair into the image and make that the
entrypoint's fallback, so an update heals itself with no configuration:
missing project directories are recreated and the chats orphaned with
them are re-accounted at startup, with the registry backed up first and
session logs untouched. `warn` reports without repairing, `off` is
silent, and an unrecognised value repairs rather than silently doing
nothing.
README: the doctor is described as the default with the variable as an
opt-out, and the Unraid note now says an existing container needs no new
field.
The entrypoint called /usr/local/bin/dsh-workspace-doctor while the
Dockerfile installs /usr/local/bin/dsh-workspace-doctor.mjs, so every
start logged "...: not found" and the workspace doctor never ran: no
report, no repair. The CI smoke test checked the .mjs path with a direct
`docker exec`, which cannot see a mistake in the entrypoint's own
invocation, so the build stayed green while the container was broken.
Resolve the doctor by name (`command -v dsh-workspace-doctor.mjs`) in
both the root and the non-root path, and log an explicit "not installed
in this image" line when it cannot be found, instead of the shell's bare
"not found". The smoke test now reads the container's boot log: it
requires the doctor's own "dsh-workspace-doctor:" output to appear, and
fails if the entrypoint logs its "workspace doctor:" failure line — that
covers path, exec bit and invocation together, which is the class of
mistake that shipped.
README: manual repair through a one-off container with --entrypoint (dsh
stopped, so nothing races the registry write), and the note that an
existing Unraid container does not pick up the template's new Workspace
Doctor variable on its own.
Updating the container recreates it, so anything outside the mounted
/workspace and state volume is gone. dsh joins a session to its project
when the session is created (upstream's history bootstrap runs once per
state volume and never again), and the workspace registry stores an
absolute canonical directory path — so a project directory under
/home/node, where the workspace picker opens by default, loses its chats
to "Ungrouped" permanently on the next update.
Ship dsh-workspace-doctor.mjs and run it from the entrypoint before dsh
starts: it reports every project directory that no longer resolves and
every chat recoverable from one, and with DSH_WORKSPACE_DOCTOR=repair it
recreates the missing directories and re-accounts those sessions in the
workspace registry. Session logs are never read for writing and never
modified, no project is invented, no chat is moved into a project it did
not run in, and the registry is copied to workspace.json.bak-<stamp>
before the atomic, 0600, fsynced write dsh's own JSON backend performs.
The doctor refuses to write a registry version it does not know, is
idempotent, and defaults to report-only in the image (warn) so nothing
changes unasked; the Unraid template defaults to repair.
CI: the doctor joins the recipe hash and the push path list, and the
image smoke test asserts it is installed and runs against a fresh volume
(exit 2, the tool itself failing, is the only outcome that fails a build).
dsh 0.1.5 ships file upload under the loader id file-upload, which the
community dsh-file-upload plugin also registers. A profile that still carries
that plugin dies at boot with "duplicate loader entry id: file-upload" and the
stack trace never names the plugin. The entrypoint now compares the profile's
plugin layers (and the two hand-edited patch layers) against the installed
version's own bundle patches before every start and logs the offending plugin
with its remove command. Advisory only: the start is never blocked, and only
the profile manifest and patch YAML are read as data.
The 0.1.5+ release footer carries the caveat, and the publisher now re-renders
a release when its text changes rather than only when the digest does.
README: upgrade checklist covers this class of failure; the Highlights bullets
that still described the pre-mirror tag/release model are corrected.
A bind mount arrives with the host's ownership, so a freshly created appdata
directory killed dsh with EACCES on mkdir /home/node/.dsh/profiles/web. Only
root inside the container can fix that, so:
- docker-entrypoint.sh starts as root, aligns the node account with PUID/PGID
(default 1000:1000, Unraid convention), takes ownership of the state volume
when needed, then setpriv's down before exec'ing dsh. The network-reachable
process is never root and no setuid binary is left behind. The workspace
mount point is made writable but its CONTENTS are left alone unless
DSH_CHOWN_WORKSPACE=1 — never silently rewrite ownership across user data.
- Dropped USER node (root is required to chown); dsh is wrapped so a root
re-execs as PUID:PGID and cannot leave
root-owned files in the volume. Wrapper resolves the real bin.js at build
time and self-tests.
- Explicit --user is honored: ownership cannot be repaired, so the entrypoint
verifies writability and exits with one actionable line instead of a Node
stack trace.
- XDG_CONFIG_HOME pinned into the volume too; pnpm was reaching for
/root/.config when HOME was unset.
- CI: labels now carry a recipe hash, and the freshness check cuts a -rN
revision when the recipe changes — so an image fix reaches the pinned
deployment without waiting for an upstream release.
- README/template: chown step removed, PUID/PGID/DSH_CHOWN_WORKSPACE
documented, template pinned to 0.1.1-rc.2-r1.
Verified with a 23-case matrix: root-owned volume self-heals and serves 200,
already-correct volume skips the chown, 99:100 + PUID/PGID works, plugin
add/remove via root docker exec leaves no root-owned files, explicit --user
still works and fails clearly when it can't, both argument forms serve.
Co-Authored-By: Claude Fable 5 <[email protected]>
- Dockerfile: install pnpm (pinned via PNPM_VERSION arg) alongside dsh so
'dsh plugin --profile web add|update|remove' works in-container. Plain
'npm i -g' instead of corepack: corepack shims resolve versions at runtime
and write to a root-owned cache, breaking the uid-1000/read-only contract.
All pnpm writable dirs (store via PNPM_HOME, cache/state via XDG vars)
point into the /home/node/.dsh volume - verified zero rootfs writes.
- Dockerfile: ENTRYPOINT ['dsh'], CMD carries the patch flag AND --no-open
(a headless server must not open a browser; on 0.1.2+ that can burn the
one-time launch token). DSH_VERSION default pinned to 0.1.1-rc.2.
- CI: never publishes or moves 'latest' (deleted from the registry). Builds
only missing immutable version tags; a toolchain freshness check rebuilds
the pinned version as <ver>-rN when a newer pnpm or node base image ships.
Every build passes a smoke test before push: dsh/pnpm versions match and
the server boots with the baked patch, answering on the container's
non-loopback interface (200 pre-token, 303/401 on 0.1.2+).
- README: operating contract - pinned version rationale, PostArgs-replace-CMD
warning, 0.1.2 launch-token behavior, plugin management, lanmode
incompatibility, template pinned to :0.1.1-rc.2.
Co-Authored-By: Claude Fable 5 <[email protected]>
- Ship /opt/deepseek-harness/web.cordis.patch.yml and pass it via upstream's
--patch launcher flag (must precede the web command's own flags) instead of
seeding DSH_HOME; entrypoint accepts template Post Arguments verbatim.
- Add /workspace (agent WORKDIR) and pin DSH_HOME=/home/node/.dsh; drop the
implicit /home/node volume so templates mount .dsh and /workspace cleanly.
- README: Unraid template (repo/support/icon/versions corrected for this
image, lanmode plugin caution), first-run chown note, DeepSeek logo.
Verified: no-args, flags-only, and full template invocations all serve the
web UI; trust fence still 403s unknown Hosts; state persists to mounted .dsh.
Co-Authored-By: Claude Fable 5 <[email protected]>
dsh's webserver schema only accepts 127.0.0.1 or 0.0.0.0; the CLI refuses
0.0.0.0 by design. Seed $DSH_HOME/profiles/web/cordis.patch.yml (the
supported override) from the entrypoint instead. Verified: UI serves 200
through a published port, /api trust fence 403s unknown Hosts, and
--trusted-host pass-through lifts it.
Co-Authored-By: Claude Fable 5 <[email protected]>