Post-incident hardening: pnpm, pinned versions, immutable tags, no more :latest
Sync upstream and build images / sync-and-build (push) Successful in 2m4s

- 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]>
This commit is contained in:
Miles WardandClaude Fable 5 committed 2026-09-06 02:43:10 -04:00
1 parent 768de4ac75
commit 2d033c323b
4 files changed
+246 -115

No files matched your search

+116 -47
View File
@@ -1,12 +1,19 @@
name: Sync upstream and build image
name: Sync upstream and build images
# Keeps this repository current with the official upstream
# (https://github.com/deepseek-ai/deepseek-harness):
# (https://github.com/deepseek-ai/deepseek-harness) and the registry stocked
# with IMMUTABLE version tags:
# 1. Mirrors upstream master and dsh-v* tags into this repo.
# 2. Reads the official npm dist-tag "latest" of @deepseek-ai/dsh.
# 3. If the Gitea container registry does not yet hold an image for that
# version, builds one from the pinned official npm release and pushes
# gitea.milesward.dev/mward4/deepseek-harness:<version> and :latest.
# 2. Builds a new <version> tag when a new @deepseek-ai/dsh appears on npm
# and the registry lacks it.
# 3. Freshness check for the PINNED default version (ARG DSH_VERSION in the
# Dockerfile): if a newer pnpm release or a patched node base image has
# shipped since the newest existing build of that version, builds a
# <version>-r<run> revision tag with the updated toolchain.
#
# This workflow NEVER pushes or moves `latest`, and never re-pushes an
# existing tag: every upgrade of the running deployment is a deliberate
# template edit to a new immutable tag.
on:
schedule:
@@ -16,24 +23,19 @@ on:
branches: [main]
paths:
- Dockerfile
- docker-entrypoint.sh
- web.cordis.patch.yml
- .gitea/workflows/sync-upstream.yml
env:
UPSTREAM: https://github.com/deepseek-ai/deepseek-harness.git
REGISTRY: gitea.milesward.dev
IMAGE: mward4/deepseek-harness
BASE_IMAGE: node:22-bookworm-slim
jobs:
sync-and-build:
runs-on: ubuntu-latest
steps:
- name: Runner environment
run: |
uname -a
docker version || echo "NO DOCKER CLI"
node --version || true
- name: Checkout CI branch
uses: actions/checkout@v4
with:
@@ -50,48 +52,115 @@ jobs:
git push mirror '+refs/heads/master:refs/heads/master'
git push mirror '+refs/tags/dsh-v*:refs/tags/dsh-v*'
- name: Resolve official release version
id: version
run: |
set -eu
VERSION=$(npm view @deepseek-ai/dsh@latest version)
echo "official npm latest: $VERSION"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Check registry for existing image
id: exists
- name: Plan builds (new dsh version + pinned-version freshness)
id: plan
env:
TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: |
set -eu
CODE=$(curl -s -o /dev/null -w '%{http_code}' \
-u "${{ github.repository_owner }}:${TOKEN}" \
-H 'Accept: application/vnd.oci.image.index.v1+json' \
-H 'Accept: application/vnd.oci.image.manifest.v1+json' \
-H 'Accept: application/vnd.docker.distribution.manifest.list.v2+json' \
-H 'Accept: application/vnd.docker.distribution.manifest.v2+json' \
"https://${REGISTRY}/v2/${IMAGE}/manifests/${{ steps.version.outputs.version }}")
echo "manifest HTTP $CODE"
if [ "$CODE" = "200" ]; then
echo "exists=true" >> "$GITHUB_OUTPUT"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
DSH_LATEST=$(npm view @deepseek-ai/dsh@latest version)
PNPM_LATEST=$(npm view pnpm@latest version)
PINNED=$(sed -n 's/^ARG DSH_VERSION=//p' Dockerfile | head -1)
docker pull -q "$BASE_IMAGE" > /dev/null
BASE_DIGEST=$(docker inspect --format '{{index .RepoDigests 0}}' "$BASE_IMAGE")
echo "dsh latest=$DSH_LATEST pinned=$PINNED pnpm=$PNPM_LATEST base=$BASE_DIGEST"
ACCEPT='application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.oci.image.manifest.v1+json,application/vnd.docker.distribution.manifest.v2+json'
tag_exists() {
[ "$(curl -s -o /dev/null -w '%{http_code}' -u "${{ github.repository_owner }}:${TOKEN}" \
-H "Accept: $ACCEPT" "https://${REGISTRY}/v2/${IMAGE}/manifests/$1")" = 200 ]
}
# Labels of a single-arch manifest's config blob ("pnpm|base"); empty
# output (index manifests, missing labels) means "treat as stale".
tag_toolchain() {
MAN=$(curl -s -u "${{ github.repository_owner }}:${TOKEN}" \
-H 'Accept: application/vnd.docker.distribution.manifest.v2+json,application/vnd.oci.image.manifest.v1+json' \
"https://${REGISTRY}/v2/${IMAGE}/manifests/$1") || return 0
CFG=$(printf '%s' "$MAN" | grep -o '"digest": *"sha256:[a-f0-9]*"' | head -1 | grep -o 'sha256:[a-f0-9]*') || return 0
[ -n "$CFG" ] || return 0
BLOB=$(curl -s -u "${{ github.repository_owner }}:${TOKEN}" "https://${REGISTRY}/v2/${IMAGE}/blobs/$CFG") || return 0
P=$(printf '%s' "$BLOB" | grep -o '"dev.milesward.pnpm-version":"[^"]*"' | cut -d'"' -f4 || true)
B=$(printf '%s' "$BLOB" | grep -o '"dev.milesward.base-digest":"[^"]*"' | cut -d'"' -f4 || true)
echo "${P}|${B}"
}
BUILDS=""
# 1. New official release the registry lacks -> new version tag.
if ! tag_exists "$DSH_LATEST"; then
BUILDS="$DSH_LATEST=$DSH_LATEST"
fi
# 2. Pinned version: missing entirely -> build it; otherwise compare
# the newest existing build's toolchain labels and cut a -rN
# revision when pnpm or the base image moved.
if [ "$PINNED" != "$DSH_LATEST" ] && ! tag_exists "$PINNED"; then
BUILDS="$BUILDS $PINNED=$PINNED"
elif tag_exists "$PINNED"; then
NEWEST=$(curl -s -H "Authorization: token ${TOKEN}" \
"https://${REGISTRY}/api/v1/packages/${{ github.repository_owner }}?type=container&q=deepseek-harness&limit=100" \
| tr ',' '\n' | sed -n 's/.*"version": *"\([^"]*\)".*/\1/p' \
| grep -E "^${PINNED}(-r[0-9]+)?$" | sort -V | tail -1)
TOOL=$(tag_toolchain "$NEWEST")
echo "newest build of pinned: $NEWEST toolchain='$TOOL'"
if [ "$TOOL" != "${PNPM_LATEST}|${BASE_DIGEST}" ]; then
echo "toolchain stale (want ${PNPM_LATEST}|${BASE_DIGEST}) -> revision build"
BUILDS="$BUILDS ${PINNED}-r${{ github.run_number }}=$PINNED"
fi
fi
- name: Build and push image
if: steps.exists.outputs.exists == 'false'
echo "planned builds: '${BUILDS}'"
{
echo "builds=${BUILDS}"
echo "pnpm=${PNPM_LATEST}"
echo "base_digest=${BASE_DIGEST}"
} >> "$GITHUB_OUTPUT"
- name: Build, smoke-test, and push
if: steps.plan.outputs.builds != ''
env:
# Dedicated write:package token; the ephemeral GITHUB_TOKEN can push
# git refs but is not accepted by the container registry.
TOKEN: ${{ secrets.REGISTRY_TOKEN }}
VERSION: ${{ steps.version.outputs.version }}
PNPM_VERSION: ${{ steps.plan.outputs.pnpm }}
BASE_DIGEST: ${{ steps.plan.outputs.base_digest }}
run: |
set -eux
echo "$TOKEN" | docker login "$REGISTRY" -u "${{ github.repository_owner }}" --password-stdin
docker build \
--build-arg "DSH_VERSION=${VERSION}" \
-t "${REGISTRY}/${IMAGE}:${VERSION}" \
-t "${REGISTRY}/${IMAGE}:latest" \
.
docker push "${REGISTRY}/${IMAGE}:${VERSION}"
docker push "${REGISTRY}/${IMAGE}:latest"
for entry in ${{ steps.plan.outputs.builds }}; do
TAG=${entry%%=*}
VER=${entry#*=}
REF="${REGISTRY}/${IMAGE}:${TAG}"
docker build \
--build-arg "DSH_VERSION=${VER}" \
--build-arg "PNPM_VERSION=${PNPM_VERSION}" \
--build-arg "BASE_DIGEST=${BASE_DIGEST}" \
-t "$REF" .
# Smoke: versions match, and the web server actually boots with
# the baked patch and answers HTTP on its non-loopback interface.
# 200 = pre-token versions; 303/401 = 0.1.2+ launch-token auth.
SMOKE="dsh-smoke-${{ github.run_id }}"
docker rm -f "$SMOKE" 2>/dev/null || true
docker run -d --name "$SMOKE" "$REF"
sleep 3
[ "$(docker exec "$SMOKE" dsh --version)" = "$VER" ]
[ "$(docker exec "$SMOKE" pnpm --version)" = "$PNPM_VERSION" ]
OK=""
for i in $(seq 1 30); do
if docker exec "$SMOKE" node -e '
const os = require("os");
const nic = Object.values(os.networkInterfaces()).flat()
.find(i => i && i.family === "IPv4" && !i.internal);
fetch("http://" + nic.address + ":3080/").then(r => {
process.exit([200, 303, 401].includes(r.status) ? 0 : 1);
}).catch(() => process.exit(2));'
then OK=1; break; fi
sleep 2
done
if [ -z "$OK" ]; then
echo "::error::smoke test failed for $REF (server never answered on the container interface)"
docker logs "$SMOKE" | tail -50
docker rm -f "$SMOKE"
exit 1
fi
docker rm -f "$SMOKE"
docker push "$REF"
done
+38 -10
View File
@@ -12,36 +12,64 @@
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/*
RUN npm install -g "@deepseek-ai/dsh@${DSH_VERSION}" \
# 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 /usr/local/bin/docker-entrypoint.sh
RUN chmod 0755 /usr/local/bin/docker-entrypoint.sh \
# /workspace: the agent's working directory (mount your project here);
# /home/node/.dsh: dsh state (settings, credentials, sessions).
&& mkdir -p /workspace /home/node/.dsh \
# /workspace: the agent's working directory (mount your project here);
# /home/node/.dsh: dsh state (settings, credentials, sessions, plugins).
RUN mkdir -p /workspace /home/node/.dsh \
&& chown node:node /workspace /home/node/.dsh
ENV DSH_HOME=/home/node/.dsh
# Everything writable at runtime (dsh state, pnpm store/cache for plugin
# installs) lives inside the mounted state volume, never the rootfs.
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
EXPOSE 3080
USER node
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.
ARG BASE_DIGEST=""
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.source="https://gitea.milesward.dev/mward4/deepseek-harness" \
org.opencontainers.image.url="https://github.com/deepseek-ai/deepseek-harness" \
org.opencontainers.image.licenses="MIT"
org.opencontainers.image.licenses="MIT" \
dev.milesward.dsh-version="${DSH_VERSION}" \
dev.milesward.pnpm-version="${PNPM_VERSION}" \
dev.milesward.base-digest="${BASE_DIGEST}"
ENTRYPOINT ["docker-entrypoint.sh"]
CMD []
ENTRYPOINT ["dsh"]
# --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"]
+92 -27
View File
@@ -10,13 +10,13 @@ with a locally-built, auditable container image for homelab use (Unraid).
| Branch | Contents |
|----------|----------|
| `master` | Exact mirror of upstream `master` (plus all `dsh-v*` tags). Never edited locally. |
| `main` | This branch: Dockerfile, entrypoint, and the sync/build workflow. |
| `main` | This branch: Dockerfile, patch file, and the sync/build workflow. |
## Image
```
gitea.milesward.dev/mward4/deepseek-harness:<version>
gitea.milesward.dev/mward4/deepseek-harness:latest
gitea.milesward.dev/mward4/deepseek-harness:<version> e.g. 0.1.1-rc.2
gitea.milesward.dev/mward4/deepseek-harness:<version>-r<N> toolchain refresh of the same version
```
Built from `node:22-bookworm-slim` + the **official** `@deepseek-ai/dsh` npm
@@ -24,6 +24,17 @@ release, pinned to the exact version. Upstream publishes no container image;
this repo exists so the image provenance is:
upstream tag → upstream npm release → this Dockerfile → this registry.
**There is no `:latest` tag, deliberately.** This deployment once tracked
`latest`; a routine restart silently moved it from dsh 0.1.1-rc.2 to the
0.1.2 line, which introduced launch-token auth and replaced the ApiProxy
with the @Remote gateway (breaking the lanmode plugin). Every tag here is
immutable and every upgrade is an explicit template edit.
**Pinned default: `0.1.1-rc.2`** (`ARG DSH_VERSION` in the Dockerfile) — the
last version verified against this deployment: no launch-token auth, and
`@goodandready/dsh-lanmode` is compatible. Newer versions get their own tags
built by CI (see below) but nothing repoints until you edit the template.
No upstream code is modified. Upstream deliberately refuses `--host 0.0.0.0`
on the CLI (the web UI can execute code, so exposing it to a network is a
deliberate opt-in); the supported opt-in is a `--patch` launcher layer, which
@@ -39,11 +50,60 @@ Paths inside the container:
| `/workspace` | The agent's working directory (container `WORKDIR`) |
| `/opt/deepseek-harness/web.cordis.patch.yml` | Baked web-server bind patch |
Entrypoint behavior: no arguments → `dsh web --patch <baked patch> --no-open` (the `--patch` launcher flag must precede the web command’s own flags);
arguments starting with a flag (e.g. `--trusted-host tower.local`) are
appended to that default; a full command line (e.g.
`web --patch /opt/deepseek-harness/web.cordis.patch.yml --no-open`) runs as
given. Runs as user `node` (UID/GID 1000).
Runs as user `node` (UID/GID 1000). Everything writable at runtime — dsh
state and pnpm's store/cache for plugin installs — lives inside the
`/home/node/.dsh` volume, never the rootfs.
### Command contract
```
ENTRYPOINT ["dsh"]
CMD ["web", "--patch", "/opt/deepseek-harness/web.cordis.patch.yml", "--no-open"]
```
- `--patch` must come **immediately after `web`** (it's a launcher flag; after
the web command's own flags it fails with "unknown option").
- `--no-open` is mandatory on a server: on 0.1.2+ an in-container browser
launch can consume the one-time launch token before you do.
- **Unraid "Post Arguments" REPLACE this CMD entirely.** If you set Post
Arguments you must repeat the whole command yourself:
```
web --patch /opt/deepseek-harness/web.cordis.patch.yml --no-open --trusted-host <host> [--trusted-host <host2>]
```
- `--trusted-host` is deployment-specific and is never baked into the image.
### Plugin management
pnpm ships in the image (dsh's plugin manager requires it), so plugin
operations work through dsh's own tooling against the state volume:
```sh
docker exec <container> dsh plugin --profile web add <pkg>
docker exec <container> dsh plugin --profile web update <pkg>
docker exec <container> dsh plugin --profile web remove <pkg>
```
Plugins live in the runtime volume (`/home/node/.dsh/profiles/web`), not in
the image — image rebuilds never touch them.
### dsh 0.1.2+ launch-token auth (read before upgrading)
From 0.1.2, the Web UI requires a one-time launch token:
- The tokened URL is printed in the startup log: `docker logs <container>`.
- The token **rotates on every restart** of the container.
- Opening the tokened URL exchanges it for a ~30-day cookie **bound to the
exact origin you opened it on** — do the exchange on the hostname you
actually intend to use (e.g. your tunnel domain, not the LAN IP).
- Bare requests without token/cookie get "dsh web authentication required".
**Known incompatibility:** `@goodandready/dsh-lanmode` does not work with
dsh 0.1.2+ (the @Remote gateway replaced the ApiProxy it monkey-patches;
symptom: "web boot: 26 entries did not activate ... waiting for services:
remote.*"). Remove or update it *before* moving to 0.1.2:
`docker exec <container> dsh plugin --profile web remove @goodandready/dsh-lanmode`.
## Unraid template
@@ -61,7 +121,7 @@ Otherwise dsh fails with `EACCES` on `/home/node/.dsh`.
<?xml version="1.0"?>
<Container version="2">
<Name>DeepSeek-Harness</Name>
<Repository>gitea.milesward.dev/mward4/deepseek-harness:latest</Repository>
<Repository>gitea.milesward.dev/mward4/deepseek-harness:0.1.1-rc.2</Repository>
<Registry>https://gitea.milesward.dev/mward4/deepseek-harness</Registry>
<Network>bridge</Network>
<Shell>sh</Shell>
@@ -76,12 +136,15 @@ FIRST RUN (required): the container runs as UID 1000. Before starting, from the
chown -R 1000:1000 /mnt/user/appdata/deepseek-harness
Otherwise dsh fails with EACCES on /home/node/.dsh.
SECURITY - READ BEFORE EXPOSING: the dsh Web UI has no authentication or TLS, and its web API can execute code on this container. Anything that can reach the port can run commands. Keep it on a trusted LAN, or put an authenticated reverse proxy (e.g. Cloudflare Access, Authelia) in front. Never port-forward it directly to the internet.
SECURITY - READ BEFORE EXPOSING: the dsh Web UI has no authentication or TLS (before 0.1.2), and its web API can execute code on this container. Anything that can reach the port can run commands. Keep it on a trusted LAN, or put an authenticated reverse proxy (e.g. Cloudflare Access, Authelia) in front. Never port-forward it directly to the internet.
REMOTE ACCESS: dsh rejects non-loopback requests (HTTP 403 on /api) unless the address you browse from is trusted. To use the Web UI from another machine, edit Post Arguments and append: --trusted-host YOUR-UNRAID-IP-OR-HOSTNAME
The Settings/credentials pages are additionally restricted to loopback pages by upstream design; edit them via SSH tunnel (ssh -L 3080:127.0.0.1:3080 root@unraid-ip, then browse http://127.0.0.1:3080). The @goodandready/dsh-lanmode community plugin can lift that restriction, but it is third-party code from an unaffiliated author that disables an upstream security fence - review it yourself before trusting it with your credentials.
POST ARGUMENTS REPLACE THE IMAGE COMMAND ENTIRELY. Always include the patch flag and --no-open, then your trusted hosts:
web --patch /opt/deepseek-harness/web.cordis.patch.yml --no-open --trusted-host YOUR-HOST [--trusted-host YOUR-HOST2]
Without a matching --trusted-host, non-loopback requests get HTTP 403 on /api - that is upstream's DNS-rebinding/CSRF fence working as intended.
VERSIONS: this registry publishes immutable version tags AND a :latest tag that CI moves to each new official npm release. dsh is a developer preview with breaking changes between releases - pin a version tag if you want to update deliberately.
The Settings/credentials pages are additionally restricted to loopback pages by upstream design; edit them via SSH tunnel (ssh -L 3080:127.0.0.1:3080 root@unraid-ip, then browse http://127.0.0.1:3080). The @goodandready/dsh-lanmode community plugin can lift that restriction on dsh 0.1.1 (it is NOT compatible with 0.1.2+, and it is third-party code that disables an upstream security fence - review it before trusting it with your credentials).
VERSIONS: this registry publishes immutable version tags only - NO :latest. dsh is a developer preview with breaking changes between releases (0.1.2 adds mandatory launch-token auth: token in docker logs, rotates every restart). Upgrade by editing this tag deliberately. Tags like 0.1.1-rc.2-rN are the same dsh version rebuilt with updated pnpm/base-image security patches.
Your DeepSeek API key can be set below (stored in the container env) or entered later in Settings once running.
</Overview>
@@ -98,26 +161,28 @@ Your DeepSeek API key can be set below (stored in the container env) or entered
</Container>
```
To reach the UI from your LAN, append your Unraid host to Post Arguments,
e.g.:
```
web --patch /opt/deepseek-harness/web.cordis.patch.yml --no-open --trusted-host tower.local --trusted-host 192.168.1.50
```
(port-less entries match any port). Without it, anything but a loopback Host
header gets a 403 from `/api` — that's upstream's DNS-rebinding/CSRF fence,
working as intended.
## Keeping current
[`.gitea/workflows/sync-upstream.yml`](.gitea/workflows/sync-upstream.yml)
runs every 6 hours on the Gitea Actions runners:
1. Force-syncs upstream `master` and `dsh-v*` tags into this repo.
2. Resolves the official npm `latest` version of `@deepseek-ai/dsh`.
3. If the registry lacks an image for that version, builds and pushes it,
moving `:latest` along with it.
2. Builds a **new immutable version tag** when a new `@deepseek-ai/dsh`
appears on npm and the registry lacks it. Existing tags are never
re-pushed, and `latest` is never published or moved — a new upstream
release cannot change what a running deployment executes.
3. **Toolchain freshness check** for the pinned default version: if a newer
pnpm release or an updated `node:22-bookworm-slim` base image (which is
where `npm` itself comes from) has shipped since the newest existing
build, CI builds a `<version>-r<N>` revision tag — same dsh, patched
toolchain. Adopting it is still a deliberate template edit.
Every build must pass a smoke test before it is pushed: `dsh --version` and
`pnpm --version` match the intended versions, and the web server boots with
the baked patch and answers HTTP on the container's non-loopback interface
(200 on pre-token versions; 303/401 on 0.1.2+, which proves the server is up
behind its launch-token gate). A dsh release the Cordis patch no longer
applies to fails here, loudly, instead of at runtime.
## Validating an image
-31
View File
@@ -1,31 +0,0 @@
#!/bin/sh
# Container entrypoint for dsh.
#
# The image ships /opt/deepseek-harness/web.cordis.patch.yml, which binds the
# web server to 0.0.0.0:3080 through upstream's supported `--patch` launcher
# layer (the CLI's own --host flag deliberately refuses 0.0.0.0). Exposure is
# still controlled entirely by which ports you publish on the Docker host, and
# the /api browser-trust fence refuses Host headers you haven't allowed with
# --trusted-host.
#
# Argument handling:
# (no args) -> dsh web --no-open --patch <baked patch>
# --trusted-host x ... -> flags are appended to that default invocation
# web --patch ... / any -> a full dsh command line is executed as given
# subcommand (Unraid templates pass one via Post Arguments)
set -eu
DSH_HOME="${DSH_HOME:-${HOME}/.dsh}"
export DSH_HOME
PATCH=/opt/deepseek-harness/web.cordis.patch.yml
if [ "$#" -eq 0 ]; then
set -- web --patch "$PATCH" --no-open
else
case "$1" in
-*) set -- web --patch "$PATCH" --no-open "$@" ;;
esac
fi
exec dsh "$@"