Lifecycle of a Container Backend in Cloudflare Computer: Events, Monitoring, and Exit Handling
The container backend lifecycle consists of four phases: creation with generation tracking, monitor installation for exit detection, graceful destruction using destroyContainerExpectingExit, and structured exit recording via recordExit, with each phase managed through a generation counter to prevent race conditions.
The container backend in Cloudflare Computer provides FUSE-backed virtual filesystem capabilities and hosts the computerd process that powers remote shell and file synchronization features. Unlike the Durable Object (DO) layer—which maintains durable SQLite storage—the container backend is ephemeral and process-bound. Its lifecycle requires careful orchestration to ensure clean exits, proper logging, and state consistency when the DO restarts or the container is recycled.
Container Creation and Monitor Installation
When a Durable Object needs a container, it invokes ctx.container.start(), which returns a handle representing the running computerd instance. Immediately after successful startup, the system must arm the lifecycle monitor:
// From container-lifecycle.ts (lines 91-107)
installContainerMonitor(ctx, container);
This function, defined in [packages/computer/src/backends/container/container-lifecycle.ts](https://github.com/cloudflare/computer/blob/main/packages/computer/src/backends/container/container-lifecycle.ts), performs three critical operations:
- Increments the generation counter stored in a
WeakMapkeyed byDurableObjectState - Clears stale exit information from any previous container generation
- Attaches a promise handler to
container.monitor()that will trigger when the process exits
The monitor itself is a platform-provided promise that resolves when the underlying container process terminates—regardless of whether the exit is graceful (SIGTERM) or abnormal (OOM kill, crash).
Generation Tracking and Race Prevention
The lifecycle system uses a generation counter pattern to prevent stale exit handlers from corrupting state. The implementation stores per-DO state in LIFECYCLE, a module-level WeakMap<DurableObjectState, ContainerLifecycle>:
| Field | Type | Purpose |
|---|---|---|
currentGeneration |
number |
Monotonically increases with each installContainerMonitor call |
expectedExitGeneration |
number | null |
Set when destroyContainerExpectingExit is called, marking intentional shutdown |
exit |
ContainerExitInfo | null |
Most recent exit timestamp and reason |
currentMonitorSettled |
Promise<void> |
Awaits completion of the monitor's exit handler |
When recordExit processes a container termination, it validates:
// From container-lifecycle.ts (lines 50-81)
if (state.currentGeneration !== generation) {
// Stale monitor from previous generation—ignore silently
return;
}
This guard ensures that if a container is destroyed and a new one started before the old monitor resolves, the obsolete handler cannot overwrite the new generation's state.
Graceful Teardown with Expected Exits
Controlled container shutdown uses destroyContainerExpectingExit:
// Example: Graceful container restart
import { destroyContainerExpectingExit, installContainerMonitor } from './container-lifecycle';
async function restartContainer(ctx: DurableObjectState) {
const container = await ctx.container.get();
// Mark this generation as expecting exit before calling destroy
await destroyContainerExpectingExit(ctx, container);
// Start fresh container with new generation
const newContainer = await ctx.container.start({ /* config */ });
installContainerMonitor(ctx, newContainer);
}
The destroyContainerExpectingExit function:
- Records
expectedExitGeneration = currentGenerationin the lifecycle state - Invokes
container.destroy()to signal platform termination - Awaits
currentMonitorSettledto ensure exit logging completes before returning
This sequencing guarantees that the "expected" flag is set before the monitor resolves, ensuring recordExit classifies the termination correctly.
Container Exit Recording and Classification
The recordExit function implements the core telemetry logic:
function recordExit(
ctx: DurableObjectState,
generation: number,
exit: ContainerExitInfo
): void {
const state = LIFECYCLE.get(ctx);
if (!state || state.currentGeneration !== generation) return;
const expected = state.expectedExitGeneration === generation;
// Logging differs by expected vs unexpected
console[expected ? 'info' : 'warn']('workspace.container.exited', {
generation,
expected,
exitedAt: exit.exitedAt,
reason: exit.reason,
});
state.exit = exit;
state.currentMonitorSettledResolve?.();
}
Exit classification rules:
- Expected exit:
destroyContainerExpectingExitwas called for this generation → logged atinfolevel - Unexpected exit: Container died without prior
destroyContainerExpectingExit→ logged atwarnlevel
Applications can query exit information via containerExitInfo(ctx):
import { containerExitInfo } from './container-lifecycle';
function diagnoseContainer(ctx: DurableObjectState) {
const info = containerExitInfo(ctx);
if (!info) return 'No exit recorded—container may still be running';
const elapsed = Date.now() - info.exitedAt;
return `Exited ${elapsed}ms ago: ${info.reason}`;
}
Container-Lifecycle Events and DO Interactions
The container backend operates alongside three other architectural layers, as documented in docs/11_lifecycle.md#container-lifecycle:
| Event Sequence | Container State | DO State | Survival Characteristics |
|---|---|---|---|
| DO restart, container persists | Running, FUSE mounted, computerd active |
New #handle instantiation, fresh capnweb WebSocket |
Container VFS survives; DO rebuilds session |
| Container SIGTERM/restart | Destroyed, VFS lost, process terminated | Running with durable SQLite storage | DO storage survives; container state rebuilt from watermarks |
| Catastrophic failure (both die) | Destroyed | Destroyed | Only ctx.storage (SQLite) persists |
The capnweb WebSocket between DO and container is re-established on DO restart without container interruption. However, if the container restarts, the DO must perform a sync round to reconstruct the container's view from stored watermarks in SQLite.
Practical Monitoring Patterns
Pattern 1: Health-Check with Exit Awareness
import {
installContainerMonitor,
containerExitInfo,
destroyContainerExpectingExit
} from './container-lifecycle';
class WorkspaceDO {
async ensureHealthyContainer() {
const existing = await this.ctx.container.get();
const exitInfo = containerExitInfo(this.ctx.state);
// If we have exit info, previous container died
if (exitInfo && !existing) {
console.warn('Recovering from unexpected container exit', exitInfo);
}
const container = existing ?? await this.ctx.container.start({});
if (!existing) {
installContainerMonitor(this.ctx.state, container);
}
return container;
}
}
Pattern 2: Rolling Restart with Generation Validation
async function rollingRestart(ctx: DurableObjectState) {
const oldContainer = await ctx.container.get();
if (!oldContainer) throw new Error('No container to restart');
// Explicit expected-exit protocol
await destroyContainerExpectingExit(ctx, oldContainer);
// Verify old generation's monitor settled
const state = getLifecycleState(ctx); // internal helper
await state.currentMonitorSettled;
// Safe to start new generation
const newContainer = await ctx.container.start({
imageTag: 'latest' // or config changes
});
installContainerMonitor(ctx, newContainer);
return newContainer;
}
Summary
- Creation:
installContainerMonitorarms exit detection and increments generation counter - Monitoring: Promise-based
container.monitor()resolves on process termination;recordExitvalidates generation before logging - Graceful teardown:
destroyContainerExpectingExitmarks expected generation and await settlement - Classification: Exits matched to
expectedExitGenerationdetermine log severity (infovswarn) - DO coupling: Container survives DO restart; DO survives container restart via sync; only SQLite storage survives both dying
Frequently Asked Questions
How does the generation counter prevent race conditions?
The generation counter ensures that slow-resolving exit handlers from obsolete containers cannot corrupt current state. Each installContainerMonitor call increments currentGeneration; recordExit silently drops exits whose generation parameter mismatches. This pattern handles the case where a container is destroyed and replaced before the old monitor promise resolves.
What distinguishes expected from unexpected container exits?
An expected exit occurs when code explicitly calls destroyContainerExpectingExit before container.destroy(), setting expectedExitGeneration to the current value. All other exits—including platform-initiated SIGTERM, OOM kills, and process crashes—are unexpected. The distinction determines log severity and enables operational dashboards to filter routine maintenance from actionable incidents.
Can I retrieve historical exit information after DO restart?
No—exit information is stored in the LIFECYCLE WeakMap keyed by DurableObjectState, which is recreated on each DO instantiation. The containerExitInfo function only returns data from the current incarnation. For durable exit tracking, applications should capture the workspace.container.exited log events in external storage or subscribe to the DO's logging pipeline.
What happens if destroyContainerExpectingExit is not called before container.destroy()?
The exit will be classified as unexpected and logged at warn level. Functionally, the container still terminates, but observability systems will flag the event as potentially anomalous. Always use destroyContainerExpectingExit for deliberate shutdowns to maintain accurate operational metrics and reduce alert noise.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →