fix(fleet): forward host bind mounts to self-update helper container (#509)

The self-update helper container runs `docker compose up -d
--force-recreate` to recreate the main Sencho container. Previously it
only mounted the docker socket, the compose working directory, and the
data directory. If the user's docker-compose.yml references env_file,
configs, or secrets at paths outside the compose working directory
(e.g. /opt/docker/env/globals.env), the helper could not resolve them
and compose failed with "env file not found".

Now during initialize(), SelfUpdateService collects all host bind mounts
from the container inspect data (filtered to Type=bind). In
triggerUpdate(), these are forwarded to the helper as read-only mounts
at their original host paths (source:source:ro), skipping the socket,
data dir, and compose working dir which are already mounted explicitly.
This lets docker compose resolve any host-path reference the user has
configured, without needing to parse compose files for specific
directives.
This commit is contained in:
Anso
2026-04-12 02:20:42 -04:00
committed by GitHub
parent d62f6244b2
commit 023e962a26
3 changed files with 68 additions and 10 deletions
+35 -10
View File
@@ -11,12 +11,18 @@ const execFileAsync = promisify(execFile);
// the NEW gateway process (which always mounts /app/data) can reach it.
const UPDATE_ERROR_FILE = '/app/data/.sencho-update-error';
interface HostMount {
source: string;
destination: string;
}
interface ComposeContext {
workingDir: string;
configFiles: string;
serviceName: string;
imageName: string;
dataDirHost: string | null;
hostBindMounts: HostMount[];
}
class SelfUpdateService {
@@ -73,16 +79,22 @@ class SelfUpdateService {
return;
}
// Find the host path backing /app/data. The helper container will bind-mount
// it at the same path so compose recreate errors can be persisted to a file
// that survives the gateway's restart and is readable by the NEW process.
const mounts = (info.Mounts ?? []) as Array<{ Source?: string; Destination?: string }>;
const dataDirHost = mounts.find(m => m.Destination === '/app/data')?.Source ?? null;
// Collect all host bind mounts so the helper container can forward them.
// This lets docker compose resolve env_file, configs, secrets, and any
// other host-path references that live outside the compose working dir.
const rawMounts = (info.Mounts ?? []) as Array<{
Type: string; Source: string; Destination: string;
}>;
const hostBindMounts: HostMount[] = rawMounts
.filter(m => m.Type === 'bind' && m.Source && m.Destination)
.map(m => ({ source: m.Source, destination: m.Destination }));
const dataDirHost = hostBindMounts.find(m => m.destination === '/app/data')?.source ?? null;
if (!dataDirHost) {
console.log('[SelfUpdate] /app/data mount not found - update error recovery will be unavailable');
}
this.composeContext = { workingDir, configFiles, serviceName, imageName, dataDirHost };
this.composeContext = { workingDir, configFiles, serviceName, imageName, dataDirHost, hostBindMounts };
this.canSelfUpdate = true;
console.log(`[SelfUpdate] Ready - service="${serviceName}" image="${imageName}" in ${workingDir}`);
@@ -128,7 +140,7 @@ class SelfUpdateService {
async triggerUpdate(): Promise<void> {
if (!this.composeContext) return;
const { workingDir, configFiles, serviceName, imageName, dataDirHost } = this.composeContext;
const { workingDir, configFiles, serviceName, imageName, dataDirHost, hostBindMounts } = this.composeContext;
const env = { ...process.env, PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' };
this.lastUpdateError = null;
@@ -168,13 +180,26 @@ class SelfUpdateService {
'exit $ec',
].join('; ');
const mountArgs: string[] = [
'-v', '/var/run/docker.sock:/var/run/docker.sock',
'-v', `${workingDir}:${workingDir}:ro`,
];
if (dataDirHost) {
mountArgs.push('-v', `${dataDirHost}:/app/data:rw`);
}
const alreadyMounted = new Set([
'/var/run/docker.sock', workingDir, ...(dataDirHost ? [dataDirHost] : []),
]);
for (const { source } of hostBindMounts) {
if (alreadyMounted.has(source) || source.startsWith(workingDir + '/')) continue;
mountArgs.push('-v', `${source}:${source}:ro`);
}
const args = [
'run', '--rm',
'--user', 'root',
'--entrypoint', 'sh',
'-v', '/var/run/docker.sock:/var/run/docker.sock',
'-v', `${workingDir}:${workingDir}:ro`,
...(dataDirHost ? ['-v', `${dataDirHost}:/app/data:rw`] : []),
...mountArgs,
'-w', workingDir,
imageName,
'-c', composeCmd,