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,
+4
View File
@@ -79,6 +79,10 @@ When you update the local (gateway) node:
If the new container does not come up within 5 minutes, a timeout message appears with a manual reload option. If the gateway can detect that the update did not complete (for example, the image pull failed or the restart helper container could not spawn), the local node card surfaces a "Failed" badge with the underlying error within about 3 minutes instead of waiting for the full timeout.
<Note>
The self-update helper container inherits all bind mounts from the main Sencho container. If your `docker-compose.yml` references `env_file`, `configs`, or `secrets` outside the compose working directory, those paths must be mounted into the Sencho container using the same host and container path (1:1 rule). See [Troubleshooting](/operations/troubleshooting#local-self-update-fails-with-env-file-not-found) if you encounter "env file not found" errors during a local update.
</Note>
## What happens during an update
When an update is triggered on a node, Sencho:
+29
View File
@@ -386,6 +386,35 @@ docker compose pull && docker compose up -d
---
## Local self-update fails with "env file not found"
**Symptom:** After clicking **Update** on the local (gateway) node, the status changes to **Failed** and hovering over the badge shows an error like:
```
env file /path/to/env/globals.env not found: stat /path/to/env/globals.env: no such file or directory
```
**Cause:** When Sencho updates itself, it spawns a temporary helper container that runs `docker compose up -d --force-recreate` to recreate the main container. The helper needs access to every file referenced by your `docker-compose.yml`, including `env_file` entries, `configs`, and `secrets`. If any of those files live outside the compose working directory (e.g. in a shared config folder), the helper cannot reach them unless those paths are also mounted in the main Sencho container.
**Fix:** Mount any directory that your compose file references into the Sencho container, using the **same path** on both the host side and the container side (the 1:1 path rule). Sencho automatically forwards all of its own bind mounts to the helper container during a self-update.
For example, if your compose file references `env_file: /home/user/docker/env/globals.env`, your Sencho container needs:
```yaml
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /home/user/sencho/data:/app/data
- /home/user/docker:/home/user/docker # shared config directory
```
After adding the missing volume mount, restart Sencho and retry the update.
<Note>
The 1:1 path rule applies here: the host path and container path must match for any directory your compose file references. This ensures the helper container can resolve file paths exactly as they are written in the compose file.
</Note>
---
## First remote update always times out on old nodes
**Symptom:** After triggering a remote update on a node running a very old Sencho version (pre-v0.39.3), the node successfully restarts with the new version, but the dashboard shows **Timed out** or **Failed** instead of **Updated**.