This commit is contained in:
jarek
2026-02-16 12:46:28 +01:00
parent ae42baa67c
commit 3a7b856047
4 changed files with 95 additions and 33 deletions
+87 -25
View File
@@ -1404,6 +1404,17 @@ export async function recreateContainerFromInspect(
const oldContainerId = inspectData.Id;
const wasRunning = inspectData.State?.Running;
// Detect shared/special network modes where network manipulation must be skipped
const networkMode = hostConfig.NetworkMode || '';
const isSharedNetwork = networkMode.startsWith('container:') ||
networkMode.startsWith('service:') ||
networkMode === 'host' ||
networkMode === 'none';
if (isSharedNetwork) {
log?.(`Shared network mode detected: ${networkMode} — skipping network manipulation`);
}
// 1. Stop the container
if (wasRunning) {
log?.('Stopping container...');
@@ -1419,24 +1430,27 @@ export async function recreateContainerFromInspect(
).then(r => { if (!r.ok) throw new Error('Failed to rename old container'); });
// 3. Disconnect all networks from old container (frees static IPs)
// Skip for shared network modes (container:X, host, none) — Docker manages these
// Capture the first network for use during container creation
let initialNetworkName: string | null = null;
let initialNetworkConfig: any = null;
for (const [netName, netConfig] of Object.entries(networks)) {
const networkId = (netConfig as any).NetworkID;
if (networkId) {
try {
await disconnectContainerFromNetwork(networkId, oldContainerId, true, envId);
} catch {
// Best effort - network may already be disconnected
if (!isSharedNetwork) {
for (const [netName, netConfig] of Object.entries(networks)) {
const networkId = (netConfig as any).NetworkID;
if (networkId) {
try {
await disconnectContainerFromNetwork(networkId, oldContainerId, true, envId);
} catch {
// Best effort - network may already be disconnected
}
}
}
// Use first network for creation
if (!initialNetworkName) {
initialNetworkName = netName;
initialNetworkConfig = netConfig;
// Use first network for creation
if (!initialNetworkName) {
initialNetworkName = netName;
initialNetworkConfig = netConfig;
}
}
}
@@ -1452,10 +1466,12 @@ export async function recreateContainerFromInspect(
).catch(() => {});
// Reconnect networks using full EndpointSettings from inspect
for (const [, netConfig] of Object.entries(networks)) {
const nc = netConfig as any;
if (nc.NetworkID) {
await connectContainerToNetworkRaw(nc.NetworkID, oldContainerId, nc, envId).catch(() => {});
if (!isSharedNetwork) {
for (const [, netConfig] of Object.entries(networks)) {
const nc = netConfig as any;
if (nc.NetworkID) {
await connectContainerToNetworkRaw(nc.NetworkID, oldContainerId, nc, envId).catch(() => {});
}
}
}
@@ -1475,6 +1491,48 @@ export async function recreateContainerFromInspect(
HostConfig: hostConfig
};
// container:<name> mode shares the network namespace — Docker rejects
// networking-related fields on the dependent container since they're
// owned by the network provider container
if (networkMode.startsWith('container:')) {
delete createConfig.Hostname;
delete createConfig.Domainname;
delete createConfig.ExposedPorts;
delete createConfig.MacAddress;
// HostConfig fields that conflict with container network mode
if (createConfig.HostConfig) {
delete createConfig.HostConfig.PortBindings;
delete createConfig.HostConfig.PublishAllPorts;
delete createConfig.HostConfig.DNS;
delete createConfig.HostConfig.DNSOptions;
delete createConfig.HostConfig.DNSSearch;
delete createConfig.HostConfig.ExtraHosts;
delete createConfig.HostConfig.Links;
}
// Resolve container ID references to names for resilience.
// Docker stores NetworkMode with the full container SHA ID, but if that container
// gets recreated (new ID), the reference goes stale. Using the container name
// instead makes the reference survive recreation.
const containerRef = networkMode.slice('container:'.length);
const isHexId = /^[0-9a-f]{12,64}$/.test(containerRef);
if (isHexId) {
try {
const refInspect = await inspectContainer(containerRef, envId);
// Container exists — switch from ID to name for resilience
const refName = (refInspect as any).Name?.replace(/^\//, '');
if (refName) {
createConfig.HostConfig.NetworkMode = `container:${refName}`;
log?.(`Resolved network container ID to name: ${refName}`);
}
} catch {
// Container ID is stale — the referenced container was likely recreated
// with a new ID. We can't resolve without knowing the original name.
log?.(`WARNING: Network reference container:${containerRef.slice(0, 12)}... is stale (container not found). The container may fail to start if the referenced container was recreated.`);
}
}
}
// Preserve anonymous volumes from Mounts not in HostConfig.Binds
const existingBinds = new Set((hostConfig.Binds || []).map((b: string) => {
const parts = b.split(':');
@@ -1498,8 +1556,9 @@ export async function recreateContainerFromInspect(
// Docker can only connect to one network at creation. Pass the first network
// from the old container's settings to avoid getting a random bridge IP.
// Skip for shared network modes — EndpointsConfig conflicts with container:/host/none modes.
// Clear MacAddress for Docker API < 1.44 compatibility.
if (initialNetworkName && initialNetworkConfig) {
if (!isSharedNetwork && initialNetworkName && initialNetworkConfig) {
const endpointConfig = { ...initialNetworkConfig };
delete endpointConfig.MacAddress;
createConfig.NetworkingConfig = {
@@ -1529,15 +1588,18 @@ export async function recreateContainerFromInspect(
}
// 6. Connect additional networks using full EndpointSettings from inspect
for (const [netName, netConfig] of Object.entries(networks)) {
if (netName === initialNetworkName) continue; // Already connected at creation
// Skip for shared network modes — Docker manages networking via the parent container
if (!isSharedNetwork) {
for (const [netName, netConfig] of Object.entries(networks)) {
if (netName === initialNetworkName) continue; // Already connected at creation
const nc = netConfig as any;
if (nc.NetworkID) {
try {
await connectContainerToNetworkRaw(nc.NetworkID, newContainerId, nc, envId);
} catch (netError: any) {
log?.(`Warning: Failed to connect to network "${netName}": ${netError.message}`);
const nc = netConfig as any;
if (nc.NetworkID) {
try {
await connectContainerToNetworkRaw(nc.NetworkID, newContainerId, nc, envId);
} catch (netError: any) {
log?.(`Warning: Failed to connect to network "${netName}": ${netError.message}`);
}
}
}
}
@@ -569,7 +569,7 @@ export async function runContainerUpdate(
// =============================================================================
log(`Recreating container with full config passthrough...`);
const success = await recreateContainer(containerName, envId, log);
const success = await recreateContainer(containerName, envId, log, imageNameFromConfig);
if (success) {
await updateAutoUpdateLastUpdated(containerName, envId);
@@ -626,7 +626,8 @@ export async function runContainerUpdate(
export async function recreateContainer(
containerName: string,
envId?: number,
log?: (msg: string) => void
log?: (msg: string) => void,
imageNameOverride?: string
): Promise<boolean> {
try {
const containers = await listContainers(true, envId);
@@ -638,7 +639,7 @@ export async function recreateContainer(
}
const inspectData = await inspectContainer(container.id, envId) as any;
const imageName = inspectData.Config?.Image;
const imageName = imageNameOverride || inspectData.Config?.Image;
log?.(`Recreating container: ${containerName} (image: ${imageName})`);
@@ -464,7 +464,7 @@ export const POST: RequestHandler = async (event) => {
message: `Recreating ${containerName}...`
});
updateSuccess = await recreateContainer(containerName, envIdNum, logProgress);
updateSuccess = await recreateContainer(containerName, envIdNum, logProgress, imageName);
if (updateSuccess) {
const updatedContainers = await listContainers(true, envIdNum);
const updatedContainer = updatedContainers.find(c => c.name === containerName);
+3 -4
View File
@@ -1760,17 +1760,16 @@
{#if container.systemContainer === 'dockhand'}
{#if hasUpdate}
<div class="space-y-2">
<p class="font-medium text-sm flex items-center gap-1.5">
<p class="font-medium text-sm flex items-center gap-1.5 whitespace-nowrap">
<CircleArrowUp class="w-4 h-4 text-amber-500" />
Update available
</p>
<p class="text-muted-foreground text-xs">Update Dockhand from the About page:</p>
<a
href="/settings?tab=about"
class="text-primary hover:underline text-xs flex items-center gap-1"
class="text-primary hover:underline text-xs flex items-center gap-1 whitespace-nowrap"
onclick={(e) => e.stopPropagation()}
>
Settings &gt; About &gt; Update now
Settings &gt; About
</a>
</div>
{:else}