Preinstall dsh plugins in CI images with lockfiles

Install & Get StartedPublished 2026-10-19Author: DeepSeek Plugin Market
DeepSeek HarnessDSH pluginCIcontainer imagelockfileversion pin
Preinstall dsh plugins in CI images: layer DeepSeek Harness, plugins and caches, pin versions with a lockfile, and verify offline.

A CI image that preinstalls dsh plugins is a version-pinning exercise, not a packaging trick. The useful question is not whether the plugin files fit, but whether the same build produces the same plugin set six weeks later with the network switched off.

Why a dsh plugin CI image needs separate layers

Layer order is the difference between a build that reuses its work and one that reinstalls everything after a single plugin bump.

Image build time is dominated by dependency downloads, and the three kinds of content in a plugin image change at different rates. The core runtime moves a few times a year, the plugin set follows the lockfile, and the package manager cache can change on every job. Collapsing them into one RUN instruction means a single plugin bump invalidates the runtime layer and forces a full download again.

Layer order is therefore a correctness decision as much as a speed decision. Stable steps belong near the top so reusable layers survive, and volatile steps belong at the bottom so they invalidate only themselves. Copying the lockfile before installing dependencies is the simplest version of this rule.

The split does not change runtime behavior. A plugin baked into the image may still call a model API or an external service, and that traffic needs its own network policy. Build-time availability and runtime availability are separate guarantees, and conflating them produces images that look self-contained but are not.

Pinning the DeepSeek Harness core in the base stage

The base stage should do one job: install a pinned runtime and the minimum environment needed to start it.

Writing latest or a bare major version looks harmless until a rebuild months later pulls a different runtime than the one you tested. A full semantic version in an ARG, with the Node major version pinned beside it, makes the upgrade an explicit change. The base stage should not carry community plugins, because that would tie the runtime layer to their release cadence.

dockerfile
FROM node:20-bookworm-slim

ARG DSH_VERSION=0.8.3
RUN npm install -g @deepseek-ai/dsh@${DSH_VERSION} \
    && dsh --version \
    && npm cache clean --force

WORKDIR /workspace

dsh --version is not decoration; it makes the build fail where version resolution went wrong instead of at runtime. Cleaning the npm cache in the same RUN keeps download residue out of the image layer, so the cleanup does not become a layer of garbage. If the team needs a shared plugin set, install it in a later stage rather than in this one.

Documenting the upgrade reason next to the ARG is a small habit with a long payoff. Six months later, the next maintainer can tell whether the pin was chosen for a feature or simply copied from a tutorial. A version without a reason tends to be bumped blindly.

Installing DSH plugins from a lockfile

A project-local install is easier to reproduce because the lockfile records the exact resolution, and npm ci refuses to pretend otherwise.

npm ci stops when the manifest and lockfile disagree, which is exactly the behavior a build should have. Global installs are fine for a small shared set, but they lack a natural version boundary and tend to drift across jobs. Do not mix global and project-local installs in one image, because conflict diagnosis becomes guesswork.

dockerfile
COPY package.json package-lock.json ./
RUN npm ci --omit=dev \
    && npx dsh plugin install --from-lockfile \
    && npx dsh plugin list

Copying both manifest and lockfile before the install step preserves the dependency layer cache when only application code changes. Printing the plugin list during the build writes the actual versions into the log, which is the record you want when a job behaves differently from its neighbor. That log line is also the baseline for the offline check later.

If plugins come from several sources, let the lockfile settle the versions before considering a forced overwrite. Forcing a version pushes the conflict to runtime, where it surfaces as a tool that silently stops working. Splitting the image is usually cheaper than debugging that class of failure. Confirm the entries against DSH Plugin Hub when a package name looks unfamiliar.

Cache mounts and the trade-offs of a DeepSeek Harness plugin build

A cache mount speeds up downloads without changing the installed result, which is what makes it safe to share across builds.

Its key must follow the lockfile hash; otherwise an old dependency set can be reused after the lockfile has moved on. When the cache is corrupt, the correct response is a clean install rather than a longer cache lifetime. The cache is an optimization that must be removable without changing the outcome.

dockerfile
RUN --mount=type=cache,target=/root/.npm \
    npm ci --omit=dev

The trade-off is subtle: a cache hit can hide an installation step that only works because of leftover state. If clearing the cache makes the build fail, the build script has an undeclared dependency and the cache was masking it. Treat a cold-cache build as the reference result and a warm-cache build as the fast path.

Caching also makes retries cheap, which matters on CI networks that occasionally time out. Reusing downloaded packages is fine as long as verification still runs on every build. The cache is allowed to be disposable; the lockfile and the verification are not.

Proving the DeepSeek Harness plugin image works with no registry

The only convincing proof is a container that starts with no registry access, lists the expected plugins and runs a minimal task.

A successful build proves that packages were reachable during the build, not that they live in the image. Start the container with networking disabled, then compare the plugin list against the manifest printed earlier in the build log. If any step needs to fetch something, the image is not self-contained yet.

  1. Start the image with --network none and confirm the container itself boots.
  2. Run dsh plugin list and compare every entry with the build manifest.
  3. Execute a minimal task that depends on a single plugin and check the load path.
  4. Inspect plugin directory permissions as the CI user rather than as root.
  5. Store the commands and output as build artifacts for later comparison.
bash
docker run --rm --network none <image> \
  dsh plugin list

Run the check with the same profile, working directory and user as production. A root user in the default directory can mask permission problems that appear only under the CI account. Making verification part of the build, rather than a note in a wiki, is what keeps the image honest over time.

A verification that fails should fail the build, not produce a warning that nobody reads. The plugin list is short enough to compare mechanically, and a mismatch is exactly the kind of drift the image was supposed to prevent. Treat the check as the last layer of the build, with the same status as compilation.

Sources: DeepSeek Harness quickstart, npm @deepseek-ai/dsh, deepseek-harness repository.

FAQ

Should CI images preinstall dsh plugins globally or into the project directory?

Preinstall dsh plugins globally when every job shares the same set, and into the project directory when each repository needs lockfile reproducibility. Do not mix both in one image.

Why does a DeepSeek Harness plugin still need network access after being baked into the image?

A DeepSeek Harness plugin baked into an image may still need network access at runtime for models or external services. Preinstalling solves installation, not runtime dependencies.

If a lockfile pins dsh plugins versions, is the build cache still useful?

A lockfile pins dsh plugins versions for reproducibility, while the build cache speeds up installation. Key the cache by the lockfile hash so it invalidates when versions change.

How do I prove dsh plugins are really inside the CI image?

Prove dsh plugins are inside the image by starting the container without registry access and listing installed plugins. A correct list with the expected versions is the only hard evidence.

How do I handle version conflicts when preinstalling several dsh plugins?

Resolve version conflicts among dsh plugins with the lockfile first, then split the image layers if needed. Do not force-overwrite, because conflicts reappear at runtime.

Related Terms

base image (CI foundation)
A base image is the reusable layer that preinstalls DeepSeek Harness and common plugins for CI jobs. It should stay minimal and reproducible.— https://deepseek-harness.github.io/deepseek-harness/guide/quickstart
lockfile (dependency lock)
A lockfile records exact dependency versions and resolution results, which is what makes CI reproducible. Cache keys should follow its hash.— https://www.npmjs.com/package/@deepseek-ai/dsh
layer cache (image reuse)
Layer cache reuses unchanged image layers, so stable installation steps belong earlier. Volatile steps belong later to avoid full rebuilds.— https://github.com/deepseek-ai/deepseek-harness
offline verification (no-registry test)
Offline verification starts the image without network or registry access and checks plugins. It proves the packages are inside the image rather than downloaded at runtime.— https://deepseek-harness.github.io/deepseek-harness/guide/quickstart

Sources