Sync upstream and build images / sync-and-build (push) Successful in 1m45s
Verifying the pushed -r1 image found one hole: only dsh was wrapped, so a root 'docker exec <c> pnpm ...' created PNPM_HOME/XDG_CONFIG_HOME owned by root inside the state volume — which the unprivileged dsh could not then use for plugin installs (self-healing on the next restart, since the entrypoint re-chowns, but broken until then). The wrapper is now generic (execs the resolved target directly; dsh's bin.js is +x with a node shebang and pnpm's bin is an ELF, so no node prefix is needed) and is installed over dsh, pnpm, pnpx, pn and pnx. Renamed dsh-wrapper.sh -> run-as-runtime-user.sh to match what it does. Verified: root 'docker exec pnpm --version' + 'pnpm store path' + 'dsh plugin add' leave zero root-owned files in the volume; 23-case matrix still green. Ships as 0.1.1-rc.2-r2. Co-Authored-By: Claude Fable 5 <[email protected]>
114 lines
5.5 KiB
Docker
114 lines
5.5 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 all-interfaces bind goes through upstream's
|
|
# supported `--patch` launcher layer (/opt/deepseek-harness/web.cordis.patch.yml);
|
|
# the UI is only reachable through ports you explicitly publish on the host.
|
|
|
|
FROM node:22-bookworm-slim
|
|
|
|
# DSH_VERSION default is the pinned, known-good deployment version: 0.1.2+
|
|
# introduced launch-token auth and the @Remote gateway (breaks lanmode).
|
|
# Moving off it is a deliberate template edit, never a side effect of CI.
|
|
ARG DSH_VERSION=0.1.1-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 /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; \
|
|
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.
|
|
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 \
|
|
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 dsh wrapper, the bind patch). The freshness check cuts a -rN revision of
|
|
# the pinned 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"]
|