feat: remount — recreate containers preserving writable layer#71
feat: remount — recreate containers preserving writable layer#71
Conversation
There was a problem hiding this comment.
Good idea — remount fills a real gap. The commit-image-then-recreate approach is the right mechanism. Three things to look at:
1. No error handling if docker commit fails — supervisor ends up in bad state
src/host/supervisor.ts (new remount()):
try { execSync(`docker stop ${cname}`, { stdio: 'ignore', timeout: 15_000 }); } catch {}
execSync(`docker commit ${cname} ${cname}`, { stdio: 'ignore', timeout: 60_000 });
try { execSync(`docker rm -f ${cname}`, { stdio: 'ignore' }); } catch {}
this.creature = null;
this.currentSHA = getCurrentSHA(this.dir);
this.status = 'starting'; // ← set unconditionally
await this.spawnCreature();docker commit has a timeout but no try/catch. If it throws (disk full, daemon busy), execution stops mid-method: the container is stopped but not committed or removed, and this.status is never set to 'starting' (the exception propagates before that line). The HTTP handler returns a 400 to the caller. That part is fine.
But the next time anything calls spawnCreature() (e.g. the restart watcher), it'll call docker start on the existing stopped container — effectively recovering. So the unhappy path isn't catastrophic, just confusing. A log line on the catch would help operators know what happened:
try {
execSync(`docker commit ${cname} ${cname}`, { stdio: 'ignore', timeout: 60_000 });
} catch (err) {
console.error(`[${this.name}] remount: commit failed — container preserved, aborting`, err);
this.expectingExit = false;
throw err;
}2. remount() doesn't call evictCreatureTokenCache — stale auth tokens survive
I flagged this same gap on PR #16: stop() calls evictCreatureTokenCache(this.name) but rebuild() doesn't, and now remount() doesn't either. Any HMAC token minted before a remount remains valid after the container comes back up. Given remount is specifically intended for picking up new env vars and mounts (security-adjacent configuration changes), this gap is worth closing — same one-liner fix as stop().
3. Dashboard: creatureAction('remount') fires immediately with no loading state
CreatureDetail.tsx:
if (confirm(`Remount...`)) {
api.creatureAction(name, 'remount'); refresh();
}refresh() runs before the remount completes (the API call is not awaited). restart has the same pattern — so this is consistent with existing code. Just worth knowing: the UI will show the creature as still-running briefly, then flip state on the next poll. Acceptable but could confuse operators who expect instant feedback.
None of these are blockers — the feature is sound and the happy path works correctly. The commit-failure error handling (point 1) is the most concrete thing to address before merge.
d433ef9 to
005e29f
Compare
…layer Adds a `remount` action that commits the container's current state into its image, removes the old container, then creates a fresh one from the updated image. The new container picks up any new mounts, env vars, or network config from createContainer() while preserving installed packages and configs from the writable layer. Useful when createContainer() gains new volume mounts (like /board) and existing creatures need to pick them up without losing their self-installed tools (supervisord, python packages, etc). - supervisor: remount() method (docker commit + rm + spawnCreature) - orchestrator: POST /api/creatures/:name/remount endpoint - dashboard: remount button with confirmation dialog Made-with: Cursor
005e29f to
b28e39b
Compare
|
Addressed all three points: 1. Commit error handling — Fixed. 2. 3. Loading state on action buttons — Fixed. All creature action buttons (wake, restart, rebuild, remount, archive) now show loading feedback ( |
There was a problem hiding this comment.
Point 1 addressed — docker commit is now wrapped with error logging and expectingExit = false reset before rethrowing. That was the only real concern. The other two items (token eviction, dashboard fire-and-forget) were non-blocking and consistent with existing patterns.
Looks good to merge.
Problem
Docker doesn't support adding volume mounts to existing containers. When
createContainer()gains new mounts (like the/boarddirectory from #65), existing creatures can't pick them up without a full rebuild — which destroys their writable layer (self-installed packages like supervisord, python libraries, custom configs, etc.).restartpreserves the writable layer but keeps the old mounts.rebuildpicks up new mounts but wipes the writable layer.There's no middle ground.
Solution
Adds a
remountcommand that:docker commitspawnCreature()flowThe new container picks up any changes to volume mounts, env vars, or network config in
createContainer(), while the committed image preserves everything the creature installed or configured.How to trigger
POST /api/creatures/:name/remountcurl -X POST http://localhost:7770/api/creatures/wondrous/remountWhen to use
createContainer()(e.g./board)What it doesn't do
rebuildfor that)/creature, node_modules) are unaffected — they're external storageChanges
supervisor.ts:remount()methodindex.ts:remountCreature()+POST /api/creatures/:name/remountrouteCreatureDetail.tsx: remount button with confirmation dialogMade with Cursor