Start a server that runs until the container stops, such as a development server, a tunnel, or a code kernel. Unlike a job, nothing waits for the server to exit. Your Durable Object starts it on first use, waits until it answers, and starts it again in each new container.
For a process that runs until it finishes, such as a build or an agent task, refer to Run background processes. If the sandbox runs one server and keeps no files that you need, you can make the server the main process instead, as in Preview a web application. When the main process exits, the instance stops, and its files end with it.
- A Worker with a Durable Object that starts a container with the Durable Object scheduling policy. To create one, refer to Run a Linux command.
-
Add shell scripts and functions that start a server, check it, and stop it:
src/index.jsjs const SERVERS = "/var/lib/servers"; const INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000; // Defines current(), which reads the process ID in a directory into $pid // when the process started in this instance. const CURRENT = `current() { read -r pid boot 2>/dev/null <"$1/pid" && [ "$boot" = "$(cat /proc/sys/kernel/random/boot_id)" ] }`; // Starts the server unless it already runs. The server runs in its own // process group, and writes its process ID and output to its directory. const SERVE = `${CURRENT} dir=$1; shift current "$dir" && kill -0 "$pid" 2>/dev/null && exit 0 mkdir -p "$dir" && rm -f "$dir/pid" setsid sh -c 'echo "$$ $(cat /proc/sys/kernel/random/boot_id)" >"$0/pid"; exec "$@"' \\ "$dir" "$@" >"$dir/log" 2>&1`; // Prints the end of the log and exits with 0 when the server has exited. const EXITED = `${CURRENT} current "$1" || exit 1 kill -0 "$pid" 2>/dev/null && exit 1 tail -n 20 "$1/log"`; // Stops the process group, with SIGKILL if the server still runs after // 10 seconds. Once the server exits, removes its process ID. const STOP = `${CURRENT} current "$1" || exit 0 kill -s TERM -- "-$pid" 2>/dev/null timeout 10 sh -c 'while kill -0 "$0" 2>/dev/null; do sleep 0.1; done' "$pid" || kill -s KILL -- "-$pid" 2>/dev/null while kill -0 "$pid" 2>/dev/null; do sleep 0.1; done rm -f "$1/pid"`; async function startServer(container, dir, argv, options = {}) { // Ignoring the output lets the server outlive this request. await container.exec(["sh", "-c", SERVE, "sh", dir, ...argv], { ...options, stdout: "ignore", stderr: "ignore", }); } async function serverExited(container, dir) { const check = await container.exec(["sh", "-c", EXITED, "sh", dir]); const output = await check.output(); return output.exitCode === 0 ? new TextDecoder().decode(output.stdout) : undefined; } async function waitForServer(container, dir, port, timeoutMs) { const server = container.getTcpPort(port); const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { try { const response = await server.fetch("http://server/", { signal: AbortSignal.timeout(1_000), }); await response.body?.cancel(); return server; } catch { const log = await serverExited(container, dir); if (log !== undefined) { throw new Error(`The server exited: ${log}`); } await scheduler.wait(500); } } throw new Error("The server did not answer in time"); } async function stopServer(container, dir) { const stop = await container.exec(["sh", "-c", STOP, "sh", dir]); const exitCode = await stop.exitCode; if (exitCode !== 0) { throw new Error(`Stopping the server failed with exit code ${exitCode}`); } }src/index.tsts const SERVERS = "/var/lib/servers"; const INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000; // Defines current(), which reads the process ID in a directory into $pid // when the process started in this instance. const CURRENT = `current() { read -r pid boot 2>/dev/null <"$1/pid" && [ "$boot" = "$(cat /proc/sys/kernel/random/boot_id)" ] }`; // Starts the server unless it already runs. The server runs in its own // process group, and writes its process ID and output to its directory. const SERVE = `${CURRENT} dir=$1; shift current "$dir" && kill -0 "$pid" 2>/dev/null && exit 0 mkdir -p "$dir" && rm -f "$dir/pid" setsid sh -c 'echo "$$ $(cat /proc/sys/kernel/random/boot_id)" >"$0/pid"; exec "$@"' \\ "$dir" "$@" >"$dir/log" 2>&1`; // Prints the end of the log and exits with 0 when the server has exited. const EXITED = `${CURRENT} current "$1" || exit 1 kill -0 "$pid" 2>/dev/null && exit 1 tail -n 20 "$1/log"`; // Stops the process group, with SIGKILL if the server still runs after // 10 seconds. Once the server exits, removes its process ID. const STOP = `${CURRENT} current "$1" || exit 0 kill -s TERM -- "-$pid" 2>/dev/null timeout 10 sh -c 'while kill -0 "$0" 2>/dev/null; do sleep 0.1; done' "$pid" || kill -s KILL -- "-$pid" 2>/dev/null while kill -0 "$pid" 2>/dev/null; do sleep 0.1; done rm -f "$1/pid"`; type ServerOptions = { cwd?: string; env?: Record<string, string> }; async function startServer( container: Container, dir: string, argv: string[], options: ServerOptions = {}, ): Promise<void> { // Ignoring the output lets the server outlive this request. await container.exec(["sh", "-c", SERVE, "sh", dir, ...argv], { ...options, stdout: "ignore", stderr: "ignore", }); } async function serverExited( container: Container, dir: string, ): Promise<string | undefined> { const check = await container.exec(["sh", "-c", EXITED, "sh", dir]); const output = await check.output(); return output.exitCode === 0 ? new TextDecoder().decode(output.stdout) : undefined; } async function waitForServer( container: Container, dir: string, port: number, timeoutMs: number, ): Promise<Fetcher> { const server = container.getTcpPort(port); const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { try { const response = await server.fetch("http://server/", { signal: AbortSignal.timeout(1_000), }); await response.body?.cancel(); return server; } catch { const log = await serverExited(container, dir); if (log !== undefined) { throw new Error(`The server exited: ${log}`); } await scheduler.wait(500); } } throw new Error("The server did not answer in time"); } async function stopServer(container: Container, dir: string): Promise<void> { const stop = await container.exec(["sh", "-c", STOP, "sh", dir]); const exitCode = await stop.exitCode; if (exitCode !== 0) { throw new Error(`Stopping the server failed with exit code ${exitCode}`); } }SERVEstarts nothing when the server in the directory already runs, and a new container, which has no server, gets one. This also covers a Durable Object that restarts while its container keeps running.The process ID file also records the boot ID, which changes in every instance. A snapshot restores the file, and a new instance reuses the same process IDs, so a restored process ID can belong to an unrelated process.
current()ignores a process ID from another instance.setsidstarts the server in a new process group, soSTOPalso stops the processes that the server starts. The shell that runsSERVEstays in the container until the server exits. Do not start the server with&instead: a shell starts background jobs withSIGINTignored.STOPremoves the process ID once the server exits, so a server started in the same directory right after it never reads as exited. A server that ignoresSIGTERMreceivesSIGKILLafter 10 seconds.waitForServer()treats any HTTP response as ready. If the server exits first, it throws with the end of the log. The log grows until the server stops, so keep the output of a busy server small, or send it to a mounted bucket. -
Add a constructor to your Durable Object. It sets the inactivity timeout again when a restarted Durable Object finds the container running:
src/index.tsts export class MyContainer extends DurableObject<Env> { constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); const container = ctx.container; if (container?.running) { void ctx.blockConcurrencyWhile(() => container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS), ); } } }Then add methods that send a request to the server, starting it first, and stop it:
src/index.tsts const WEB = `${SERVERS}/web`; const WEB_PORT = 8080; const WEB_SERVER = [ "node", "--eval", `require("node:http") .createServer((req, res) => res.end("hello from the server\\n")) .listen(${WEB_PORT})`, ]; export class MyContainer extends DurableObject<Env> { // ... // The server start that requests in progress share private starting?: Promise<Fetcher>; async fetchServer(request: Request): Promise<Response> { const container = this.ctx.container; if (!container) { throw new Error("No container is configured"); } if (!container.running) { container.start({ image: "cloudflare/debian-trixie", entrypoint: ["sleep", "infinity"], enableInternet: false, }); await container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS); } this.starting ??= startServer(container, WEB, WEB_SERVER) .then(() => waitForServer(container, WEB, WEB_PORT, 30_000)) .finally(() => { this.starting = undefined; }); const server = await this.starting; return server.fetch(request); } async stopWeb(): Promise<void> { const container = this.ctx.container; if (container?.running) { await stopServer(container, WEB); } } }Requests that arrive together share one start.
startServer()returns before the server records its process ID, so a secondSERVEin that window would start a second server. The second server fails on the port, overwrites the process ID file, and leavesSTOPunable to stop the first one. After the server answers, each request runsSERVEagain, which starts nothing while the server runs and restarts it if it exited.A running server does not keep the container running. Requests that reach the server through your Durable Object keep it running, as other requests do. When the inactivity timeout ends, the container stops, and the server stops with it. For more information, refer to Sandbox lifetime.
-
Add routes to your Worker that call these methods:
src/index.jsjs export default { async fetch(request, env) { const url = new URL(request.url); if (url.pathname !== "/server") { return new Response("Not found", { status: 404 }); } const sandbox = env.MY_CONTAINER.getByName("sandbox"); if (request.method === "DELETE") { await sandbox.stopWeb(); return new Response(null, { status: 202 }); } // getTcpPort() accepts only http:// URLs. const forward = new Request("http://container/", request); return sandbox.fetchServer(forward); }, };src/index.tsts export default { async fetch(request: Request, env: Env): Promise<Response> { const url = new URL(request.url); if (url.pathname !== "/server") { return new Response("Not found", { status: 404 }); } const sandbox = env.MY_CONTAINER.getByName("sandbox"); if (request.method === "DELETE") { await sandbox.stopWeb(); return new Response(null, { status: 202 }); } // getTcpPort() accepts only http:// URLs. const forward = new Request("http://container/", request); return sandbox.fetchServer(forward); }, } satisfies ExportedHandler<Env>; -
Deploy your Worker:
npx wrangler deployyarn wrangler deploypnpm wrangler deploy -
Send a request to the server. Replace the example hostname with the
workers.devURL that Wrangler prints:curl https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/serverThe first request starts the container and the server, and waits until the server answers:
hello from the server -
Stop the server, then send another request:
curl https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/server --request DELETE curl https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/serverThe
DELETErequest responds with202. The next request starts the server again and prints the same response.
- Run background processes: processes that run until they finish.
- Preview a web application: a web server as the main process.
- Sandbox lifetime
getTcpPort()