feat(docker): pin Docker Model Runner CLI plugin

## What is this PR about?

Pins the Docker Model Runner CLI package already installed in the Dokploy production image so rebuilds do not silently float to a different plugin version.

`get.docker.com` with Docker 28.5.2 already installs `docker-model-plugin` unpinned. Current Dokploy images therefore already contain the plugin; this PR makes that dependency explicit and reproducible.

## Changes

- Pins `docker-model-plugin` to `1.2.6-1~debian.12~bookworm`
- Keeps the pin in the same Docker layer as the Docker installer to avoid duplicate plugin bytes if the upstream candidate becomes newer
- Verifies the exact installed version with `dpkg-query`
- Runs a bounded `docker model version` smoke check
- Cleans apt package lists after installation
- Adds a focused Dockerfile contract test

## Multi-architecture

Verified on both supported image architectures:

- `linux/amd64`
- `linux/arm64`

Both install the exact pinned package and successfully execute the Model Runner CLI.

## Safety

The package is CLI-only and does not install or start the Model Runner service.

The smoke test intentionally uses `docker model version`.

It does not run commands such as:

- `docker model status`
- `docker model list`
- `docker model pull`
- `docker model install-runner`

which may initialize or modify Model Runner state.

No Docker socket is required for the build-time smoke check.

## Supply chain

The plugin continues to come from Docker's existing signed Debian repository configured by `get.docker.com`.

This PR introduces no new download origin or third-party binary source.

## Scope

This PR only makes the Docker Model Runner CLI package deterministic in the production image.

It does not add:

- Model Runner lifecycle management
- model pulling or execution
- API or UI changes
- model catalog support
- hardware/model-fit logic
- runner networking changes

## Testing

Verified with:

- focused Dockerfile contract tests
- real package validation on amd64 and arm64
- exact package-version assertion
- bounded `docker model version` smoke test
- newer-candidate downgrade simulation
- missing-version failure validation
- image/layer size comparison
- `pnpm typecheck`
- `pnpm dokploy:build`
- Biome
- `git diff --check`
This commit is contained in:
EgerDev 2026-09-11 15:46:25 -07:00
parent 853ca33659
commit 36fdb1cad6
2 changed files with 74 additions and 2 deletions

View File

@ -45,8 +45,16 @@ COPY --from=build /prod/dokploy/components.json ./components.json
COPY --from=build /prod/dokploy/node_modules ./node_modules
# Install docker
RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh --version 28.5.2 && rm get-docker.sh && curl https://rclone.org/install.sh | bash
# Install docker. get.docker.com already pulls docker-model-plugin unpinned
# for Engine 28.2+; pin it in this same layer so rebuilds stay deterministic
# without a second-layer downgrade keeping both binaries.
RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh --version 28.5.2 && rm get-docker.sh \
&& apt-get update \
&& apt-get install -y --no-install-recommends --allow-downgrades docker-model-plugin=1.2.6-1~debian.12~bookworm \
&& dpkg-query -W -f='${Version}\n' docker-model-plugin | grep -Fx '1.2.6-1~debian.12~bookworm' \
&& timeout -k 5s 15s docker model version >/dev/null \
&& rm -rf /var/lib/apt/lists/* \
&& curl https://rclone.org/install.sh | bash
# Install Nixpacks and tsx
# | VERBOSE=1 VERSION=1.21.0 bash

View File

@ -0,0 +1,64 @@
import { readFileSync } from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
const dockerfile = readFileSync(
path.resolve(__dirname, "../../../../Dockerfile"),
"utf8",
);
const PLUGIN_PIN = "docker-model-plugin=1.2.6-1~debian.12~bookworm";
const PLUGIN_VERSION = "1.2.6-1~debian.12~bookworm";
const dockerInstallRun = () => {
const lines = dockerfile.split("\n");
const hit = lines.findIndex((line) =>
line.includes("https://get.docker.com"),
);
expect(hit).toBeGreaterThanOrEqual(0);
let start = hit;
while (start > 0 && !lines[start]?.startsWith("RUN ")) start--;
let end = start;
while (end < lines.length && lines[end]?.endsWith("\\")) end++;
return lines.slice(start, end + 1).join("\n");
};
describe("Dockerfile docker-model-plugin pin", () => {
it("pins the plugin in the same RUN as get.docker.com", () => {
const run = dockerInstallRun();
expect(run).toContain(PLUGIN_PIN);
expect(run).toContain("--allow-downgrades");
expect(run).toContain("--no-install-recommends");
expect(run.indexOf("get.docker.com")).toBeLessThan(
run.indexOf("apt-get update"),
);
expect(run.indexOf("apt-get update")).toBeLessThan(run.indexOf(PLUGIN_PIN));
});
it("asserts the installed dpkg version and a bounded plugin smoke check", () => {
const run = dockerInstallRun();
expect(run).toContain(
"dpkg-query -W -f='${Version}\\n' docker-model-plugin",
);
expect(run).toContain(`grep -Fx '${PLUGIN_VERSION}'`);
expect(run).toContain("timeout -k 5s 15s docker model version");
expect(run).toContain("rm -rf /var/lib/apt/lists/*");
});
it("does not download the plugin from GitHub or run mutating docker model commands", () => {
expect(dockerfile).not.toMatch(
/github\.com\/docker\/model-runner|releases\/download/,
);
const run = dockerInstallRun();
for (const cmd of [
"docker model status",
"docker model list",
"docker model ls",
"docker model inspect",
"docker model pull",
"docker model install-runner",
]) {
expect(run).not.toContain(cmd);
}
});
});