Files
dsh-relay 0af110766d
Sync upstream and build images / sync-and-build (push) Successful in 38m42s
ci: pick the webserver bind address from the installed schema
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.
2026-10-13 21:09:56 -04:00

123 lines
6.0 KiB
Docker

# Trusted container build of DeepSeek Harness (dsh).
#
# Upstream (https://github.com/deepseek-ai/deepseek-harness) publishes no
# official container image — only the @deepseek-ai/dsh npm package, released
# from the tagged source mirrored in this repository's `master` branch. This
# image installs that exact pinned release from registry.npmjs.org, so its
# provenance chain is: upstream tag -> upstream npm release -> this Dockerfile.
#
# No upstream code is patched. The bind address comes from the `--patch`
# launcher layer (/opt/deepseek-harness/web.cordis.patch.yml), which the
# entrypoint renders at startup: dsh 0.2.1-alpha.2+ rejects the unspecified
# address in the webserver schema and gets the container's own IPv4 instead,
# while releases whose schema still only accepts 127.0.0.1 or 0.0.0.0 keep
# 0.0.0.0. The UI is reachable only through ports you explicitly publish on the
# host.
FROM node:22-bookworm-slim
# DSH_VERSION default: the current release candidate, for local builds. CI
# passes every version it builds explicitly and derives "current" from the
# registry, so this default never steers the pipeline.
ARG DSH_VERSION=0.1.5-rc.2
# pnpm is required on PATH by dsh's plugin manager (`dsh plugin --profile web
# add|update|remove`). CI passes the current release; the scheduled freshness
# check rebuilds a -rN revision tag when a newer pnpm or base image ships.
ARG PNPM_VERSION=12.3.4
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates git \
&& rm -rf /var/lib/apt/lists/*
# pnpm is a fixed npm global rather than a corepack shim: corepack resolves
# versions at runtime and can attempt downloads/writes outside the state
# volume (its cache is root-owned), which breaks the non-root uid-1000 /
# read-only-rootfs contract. A pinned global install is deterministic,
# offline, and readable by uid 1000.
RUN npm install -g "@deepseek-ai/dsh@${DSH_VERSION}" "pnpm@${PNPM_VERSION}" \
&& npm cache clean --force
COPY web.cordis.patch.yml /opt/deepseek-harness/web.cordis.patch.yml
COPY docker-entrypoint.sh run-as-runtime-user.sh dsh-workspace-doctor.mjs /usr/local/bin/
# Replace npm's bin symlinks for dsh and the pnpm family with the
# privilege-dropping wrapper, resolving each real target at build time (never
# guessing paths across versions) and self-testing the result. Each symlink
# must be removed before writing, or the redirect would clobber the package's
# own entry point through it.
RUN set -eux; \
for cmd in dsh pnpm pnpx pn pnx; do \
[ -e "/usr/local/bin/${cmd}" ] || continue; \
target="$(readlink -f "/usr/local/bin/${cmd}")"; \
test -x "$target"; \
rm "/usr/local/bin/${cmd}"; \
sed "s|__TARGET__|${target}|" /usr/local/bin/run-as-runtime-user.sh > "/usr/local/bin/${cmd}"; \
chmod 0755 "/usr/local/bin/${cmd}"; \
done; \
rm /usr/local/bin/run-as-runtime-user.sh; \
chmod 0755 /usr/local/bin/docker-entrypoint.sh; \
chmod 0755 /usr/local/bin/dsh-workspace-doctor.mjs; \
mkdir -p /workspace /home/node/.dsh; \
chown node:node /workspace /home/node/.dsh; \
dsh --version; \
pnpm --version
# Everything writable at runtime (dsh state, pnpm store/cache for plugin
# installs) lives inside the mounted state volume, never the rootfs.
# PUID/PGID: the ids dsh runs as; the entrypoint aligns the state volume to
# them at startup. Unraid's share default is 99:100.
# DSH_WORKSPACE_DOCTOR: the entrypoint's workspace-grouping step. `repair` is
# the default so an update heals the projects whose directories the update
# erased; `warn` reports without repairing, `off` is silent.
ENV DSH_HOME=/home/node/.dsh \
PNPM_HOME=/home/node/.dsh/.pnpm \
XDG_CACHE_HOME=/home/node/.dsh/.cache \
XDG_STATE_HOME=/home/node/.dsh/.state \
XDG_CONFIG_HOME=/home/node/.dsh/.config \
DSH_WORKSPACE_DOCTOR=repair \
PUID=1000 \
PGID=1000
EXPOSE 3080
# No `USER` directive on purpose: the entrypoint needs root to repair
# bind-mount ownership, then drops to PUID:PGID via setpriv before exec'ing
# dsh. The dsh process — the one reachable from the network — is never root.
WORKDIR /workspace
ARG DSH_VERSION
ARG PNPM_VERSION
# BASE_DIGEST: resolved digest of the FROM image at build time; the scheduled
# freshness check compares it (and pnpm-version) against current releases.
# GIT_SHA / CI_RUN / BUILD_DATE: build provenance, read back by the release
# publisher so each release points at the commit that actually built it.
ARG BASE_DIGEST=""
ARG GIT_SHA=""
ARG CI_RUN=""
ARG BUILD_DATE=""
# RECIPE_HASH: digest of the build inputs (this Dockerfile, the entrypoint,
# the wrapper, the bind patch). The freshness check cuts a -rN revision of the
# current version when it changes, so an image-recipe fix actually reaches the
# deployment instead of waiting for the next upstream release.
ARG RECIPE_HASH=""
LABEL org.opencontainers.image.title="deepseek-harness" \
org.opencontainers.image.description="DeepSeek Harness (dsh) web UI, built from the official npm release" \
org.opencontainers.image.version="${DSH_VERSION}" \
org.opencontainers.image.revision="${GIT_SHA}" \
org.opencontainers.image.created="${BUILD_DATE}" \
org.opencontainers.image.source="https://gitea.milesward.dev/mward4/deepseek-harness" \
org.opencontainers.image.url="https://github.com/deepseek-ai/deepseek-harness" \
org.opencontainers.image.licenses="MIT" \
dev.milesward.dsh-version="${DSH_VERSION}" \
dev.milesward.pnpm-version="${PNPM_VERSION}" \
dev.milesward.base-digest="${BASE_DIGEST}" \
dev.milesward.ci-run="${CI_RUN}" \
dev.milesward.recipe-hash="${RECIPE_HASH}"
ENTRYPOINT ["docker-entrypoint.sh"]
# --patch must come immediately after `web` (launcher flag), --no-open so a
# headless server never launches a browser (on 0.1.2+ an in-container browser
# could consume the one-time launch token). Unraid Post Arguments REPLACE this
# CMD entirely — operators must repeat the patch flag and --no-open.
CMD ["web", "--patch", "/opt/deepseek-harness/web.cordis.patch.yml", "--no-open"]