diff --git a/Dockerfile b/Dockerfile index 7d0150416..0362770ab 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/apps/dokploy/__test__/docker/dockerfile-model-plugin.test.ts b/apps/dokploy/__test__/docker/dockerfile-model-plugin.test.ts new file mode 100644 index 000000000..0e2be5ac4 --- /dev/null +++ b/apps/dokploy/__test__/docker/dockerfile-model-plugin.test.ts @@ -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); + } + }); +});