From 5c81d387b6e6b1393051b160bb67022aed15f7d9 Mon Sep 17 00:00:00 2001 From: KodekaTeam Date: Thu, 30 Jul 2026 15:56:39 +0700 Subject: [PATCH] fix: preserve named docker volumes during rebuild --- src/server/routes/containers.ts | 22 ++++---------- .../services/app-install-rebuild.service.ts | 22 ++++---------- .../services/deployment-rollback.service.ts | 22 ++++---------- src/server/services/docker-inspect-format.ts | 29 +++++++++++++++++++ .../backend/docker-volume-validation.test.ts | 22 ++++++++++++++ 5 files changed, 69 insertions(+), 48 deletions(-) create mode 100644 src/server/services/docker-inspect-format.ts diff --git a/src/server/routes/containers.ts b/src/server/routes/containers.ts index dbb8326..fe6e1d9 100644 --- a/src/server/routes/containers.ts +++ b/src/server/routes/containers.ts @@ -38,6 +38,10 @@ import { startDeploymentLockHeartbeat, } from "../services/deployment-lock.service"; import { sanitizeDeploymentError } from "../services/deployment-error.service"; +import { + formatDockerInspectMountBindings, + type DockerInspectMount as DockerInspectRuntimeMount, +} from "../services/docker-inspect-format"; const DeploySourceTypeSchema = z.enum([ "APP_INSTALLER", @@ -972,11 +976,7 @@ type DockerInspectRuntime = { >; NetworkMode?: string | null; }; - Mounts?: Array<{ - Source?: string; - Destination?: string; - RW?: boolean; - }>; + Mounts?: DockerInspectRuntimeMount[]; }; type DockerPortBindings = Record< @@ -1005,16 +1005,6 @@ function formatEnvLines(env?: string[] | null): string { return (env ?? []).filter(Boolean).join("\n"); } -function formatMountBindings(mounts?: DockerInspectRuntime["Mounts"]): string { - return (mounts ?? []) - .map((mount) => { - if (!mount.Source || !mount.Destination) return ""; - return `${mount.Source}:${mount.Destination}${mount.RW === false ? ":ro" : ""}`; - }) - .filter(Boolean) - .join(","); -} - function formatPortBindings(bindings?: DockerPortBindings): string { if (!bindings) return ""; @@ -1130,7 +1120,7 @@ async function resolveContainerRuntimeConfig(container: { image: inspect.Config?.Image?.trim() || container.image, ports: formatPortBindings(inspect.HostConfig?.PortBindings), env: formatEnvLines(inspect.Config?.Env), - volumes: formatMountBindings(inspect.Mounts), + volumes: formatDockerInspectMountBindings(inspect.Mounts), network: inspect.HostConfig?.NetworkMode?.trim() || "bridge", restartPolicy: inspect.HostConfig?.RestartPolicy?.Name?.trim() || diff --git a/src/server/services/app-install-rebuild.service.ts b/src/server/services/app-install-rebuild.service.ts index 0bf05d7..096a1cd 100644 --- a/src/server/services/app-install-rebuild.service.ts +++ b/src/server/services/app-install-rebuild.service.ts @@ -11,6 +11,10 @@ import { releaseDeploymentLock, startDeploymentLockHeartbeat, } from "./deployment-lock.service"; +import { + formatDockerInspectMountBindings, + type DockerInspectMount, +} from "./docker-inspect-format"; type DockerInspectRuntime = { Config?: { @@ -26,11 +30,7 @@ type DockerInspectRuntime = { >; NetworkMode?: string | null; }; - Mounts?: Array<{ - Source?: string; - Destination?: string; - RW?: boolean; - }>; + Mounts?: DockerInspectMount[]; }; type DockerPortBindings = Record< @@ -42,16 +42,6 @@ function formatEnvLines(env?: string[] | null): string { return (env ?? []).filter(Boolean).join("\n"); } -function formatMountBindings(mounts?: DockerInspectRuntime["Mounts"]): string { - return (mounts ?? []) - .map((mount) => { - if (!mount.Source || !mount.Destination) return ""; - return `${mount.Source}:${mount.Destination}${mount.RW === false ? ":ro" : ""}`; - }) - .filter(Boolean) - .join(","); -} - function formatPortBindings(bindings?: DockerPortBindings): string { if (!bindings) return ""; @@ -122,7 +112,7 @@ async function resolveRuntimeConfig(install: { image: inspect.Config?.Image?.trim() || "", ports: formatPortBindings(inspect.HostConfig?.PortBindings), env: formatEnvLines(inspect.Config?.Env), - volumes: formatMountBindings(inspect.Mounts), + volumes: formatDockerInspectMountBindings(inspect.Mounts), network: inspect.HostConfig?.NetworkMode?.trim() || "bridge", restartPolicy: inspect.HostConfig?.RestartPolicy?.Name?.trim() || "unless-stopped", diff --git a/src/server/services/deployment-rollback.service.ts b/src/server/services/deployment-rollback.service.ts index 8e32384..b981b5b 100644 --- a/src/server/services/deployment-rollback.service.ts +++ b/src/server/services/deployment-rollback.service.ts @@ -15,6 +15,10 @@ import { startDeploymentLockHeartbeat, } from "./deployment-lock.service"; import { sanitizeDeploymentError } from "./deployment-error.service"; +import { + formatDockerInspectMountBindings, + type DockerInspectMount, +} from "./docker-inspect-format"; type RollbackRuntime = { image: string; @@ -43,11 +47,7 @@ type DockerInspectRuntime = { >; NetworkMode?: string | null; }; - Mounts?: Array<{ - Source?: string; - Destination?: string; - RW?: boolean; - }>; + Mounts?: DockerInspectMount[]; }; type DockerPortBindings = NonNullable< @@ -139,15 +139,6 @@ function formatPortBindings(bindings?: DockerPortBindings) { .join(","); } -function formatMountBindings(mounts?: DockerInspectRuntime["Mounts"]) { - return (mounts ?? []) - .flatMap((mount) => { - if (!mount.Source || !mount.Destination) return []; - return `${mount.Source}:${mount.Destination}${mount.RW === false ? ":ro" : ""}`; - }) - .join(","); -} - function formatEntrypoint(value?: string[] | string | null) { if (typeof value === "string") return value.trim() || undefined; return value?.[0]?.trim() || undefined; @@ -258,7 +249,7 @@ async function resolvePreviousRuntime(input: { image: inspect.Config?.Image?.trim() || input.container.image, ports: formatPortBindings(inspect.HostConfig?.PortBindings), env: (inspect.Config?.Env ?? []).filter(Boolean).join("\n"), - volumes: formatMountBindings(inspect.Mounts), + volumes: formatDockerInspectMountBindings(inspect.Mounts), network: inspect.HostConfig?.NetworkMode?.trim() || "bridge", restartPolicy: inspect.HostConfig?.RestartPolicy?.Name?.trim() || @@ -711,4 +702,3 @@ export async function rollbackContainerToDeployment(input: { } } - diff --git a/src/server/services/docker-inspect-format.ts b/src/server/services/docker-inspect-format.ts new file mode 100644 index 0000000..9473154 --- /dev/null +++ b/src/server/services/docker-inspect-format.ts @@ -0,0 +1,29 @@ +export type DockerInspectMount = { + Type?: string; + Name?: string; + Source?: string; + Destination?: string; + RW?: boolean; +}; + +function resolveMountSource(mount: DockerInspectMount) { + if (mount.Type === "volume") { + return mount.Name?.trim() || ""; + } + + return mount.Source?.trim() || ""; +} + +export function formatDockerInspectMountBindings( + mounts?: DockerInspectMount[] | null, +) { + return (mounts ?? []) + .flatMap((mount) => { + const source = resolveMountSource(mount); + const destination = mount.Destination?.trim(); + if (!source || !destination) return []; + + return `${source}:${destination}${mount.RW === false ? ":ro" : ""}`; + }) + .join(","); +} diff --git a/tests/backend/docker-volume-validation.test.ts b/tests/backend/docker-volume-validation.test.ts index b32aac9..7044c58 100644 --- a/tests/backend/docker-volume-validation.test.ts +++ b/tests/backend/docker-volume-validation.test.ts @@ -1,6 +1,7 @@ import assert from "node:assert/strict"; import test from "node:test"; +import { formatDockerInspectMountBindings } from "../../src/server/services/docker-inspect-format"; import { buildDockerRunCommand } from "../../src/server/services/ssh-services/docker-containers"; const BASE_OPTS = { @@ -57,3 +58,24 @@ test("volume validation only allows docker.sock when explicitly trusted", () => assert.match(command, /-v' '/); assert.match(command, /\/var\/run\/docker\.sock:\/var\/run\/docker\.sock/); }); + +test("docker inspect formatter preserves named volumes instead of host mountpoints", () => { + assert.equal( + formatDockerInspectMountBindings([ + { + Type: "volume", + Name: "nakama-data", + Source: "/var/lib/docker/volumes/nakama-data/_data", + Destination: "/nakama/data", + RW: true, + }, + { + Type: "bind", + Source: "/srv/doktainer/apps/site", + Destination: "/app", + RW: false, + }, + ]), + "nakama-data:/nakama/data,/srv/doktainer/apps/site:/app:ro", + ); +});