diff --git a/src/lib/server/mount-dedupe.ts b/src/lib/server/mount-dedupe.ts index 5d2b372..ca56790 100644 --- a/src/lib/server/mount-dedupe.ts +++ b/src/lib/server/mount-dedupe.ts @@ -1,4 +1,5 @@ type HostConfigLike = { + Tmpfs?: Record | null; Binds?: string[] | null; Mounts?: Array<{ Target?: string | null }> | null; }; @@ -9,6 +10,41 @@ type InspectMountLike = { Destination?: string | null; }; +/** Destination path of a bind string ("src:/dst:ro" -> "/dst", "/dst" -> "/dst"). */ +function bindTarget(bind: string): string { + const parts = bind.split(':'); + return parts.length >= 2 ? parts[1] : parts[0]; +} + +/** + * Drop Config.Volumes entries whose path is already mounted, so recreate never sends a duplicate + * mount point (#1088 / #1363). The path can be occupied by Tmpfs, an existing bind, a bind we're + * ABOUT to add (getAdditionalVolumeBinds), or a volume that shows up only in inspect.Mounts (the + * shape a remote/hawser daemon returns) - the earlier fix checked Tmpfs/Binds only, so a named + * volume at an image-VOLUME path (e.g. cups /etc/cups) slipped through and collided with the bind + * re-added from inspect.Mounts. Checking all four sources in one place closes that gap. + */ +export function dedupeVolumesForRecreate( + volumes: Record | undefined | null, + hostConfig: HostConfigLike, + mounts: InspectMountLike[], + additionalBinds: string[] +): Record | undefined { + if (!volumes) return undefined; + const mounted = new Set(); + for (const p of Object.keys(hostConfig.Tmpfs || {})) mounted.add(p); + for (const b of hostConfig.Binds || []) mounted.add(bindTarget(b)); + for (const b of additionalBinds) mounted.add(bindTarget(b)); + for (const m of hostConfig.Mounts || []) { if (m?.Target) mounted.add(m.Target); } + for (const m of mounts || []) { if (m.Destination) mounted.add(m.Destination); } + + const kept: Record = {}; + for (const [path, val] of Object.entries(volumes)) { + if (!mounted.has(path)) kept[path] = val; + } + return Object.keys(kept).length > 0 ? kept : undefined; +} + /** Build extra bind strings for volume mounts missing from HostConfig. */ export function getAdditionalVolumeBinds( hostConfig: HostConfigLike, diff --git a/src/routes/api/self-update/+server.ts b/src/routes/api/self-update/+server.ts index 35084b8..b52efba 100644 --- a/src/routes/api/self-update/+server.ts +++ b/src/routes/api/self-update/+server.ts @@ -1,6 +1,6 @@ import { json } from '@sveltejs/kit'; import { authorize } from '$lib/server/authorize'; -import { getAdditionalVolumeBinds } from '$lib/server/mount-dedupe'; +import { getAdditionalVolumeBinds, dedupeVolumesForRecreate } from '$lib/server/mount-dedupe'; import { getOwnContainerId, getHostDockerSocket, @@ -169,6 +169,11 @@ function buildCreateConfig(inspectData: any, newImage: string): any { delete createConfig.Hostname; const additionalBinds = getAdditionalVolumeBinds(hostConfig, inspectData.Mounts || []); + // Drop image-VOLUME entries that collide with a bind/tmpfs/inspect mount, so create never + // sends a duplicate mount point (#1088 / #1363). + const kept = dedupeVolumesForRecreate(createConfig.Volumes, hostConfig, inspectData.Mounts || [], additionalBinds); + if (kept) createConfig.Volumes = kept; + else delete createConfig.Volumes; if (additionalBinds.length > 0) { createConfig.HostConfig = { ...createConfig.HostConfig,