Execute commands
Last updated Sep 30, 2026
Use exec() to start another process inside a running Container. The examples call this.ctx.container.exec() inside a class extending Container from @cloudflare/containers.
exec() does not start a stopped Container. In remote procedure call (RPC) methods, check this.ctx.container.running and call await this.start() when needed. You can also use the onStart() hook to run any series of commands whenever the Container starts.
Run a process after startup
The following hook runs a preparation command whenever the Container starts. You can execute any series of startup commands from this hook. output() buffers standard output and standard error as separate ArrayBuffer values.
import { Container } from "@cloudflare/containers";
export class MyContainer extends Container {
async onStart() {
const process = await this.ctx.container.exec([
"node",
"scripts/prepare.js",
]);
const output = await process.output();
const decoder = new TextDecoder();
if (output.exitCode !== 0) {
throw new Error(
`Container preparation failed: ${decoder.decode(output.stderr)}`,
);
}
console.log(decoder.decode(output.stdout));
}
}
import { Container } from "@cloudflare/containers";
export class MyContainer extends Container {
override async onStart() {
const process = await this.ctx.container.exec([
"node",
"scripts/prepare.js",
]);
const output = await process.output();
const decoder = new TextDecoder();
if (output.exitCode !== 0) {
throw new Error(
`Container preparation failed: ${decoder.decode(output.stderr)}`,
);
}
console.log(decoder.decode(output.stdout));
}
}
In an RPC method, ensure the Container is running before calling exec(). Standard output uses a readable stream by default.
import { Container } from "@cloudflare/containers";
export class MyContainer extends Container {
async readVersion() {
if (!this.ctx.container.running) {
await this.start();
}
const process = await this.ctx.container.exec(["node", "--version"]);
const stdout = process.stdout
? await new Response(process.stdout).text()
: "";
const exitCode = await process.exitCode;
return { pid: process.pid, stdout, exitCode };
}
}
import { Container } from "@cloudflare/containers";
export class MyContainer extends Container {
async readVersion() {
if (!this.ctx.container.running) {
await this.start();
}
const process = await this.ctx.container.exec(["node", "--version"]);
const stdout = process.stdout
? await new Response(process.stdout).text()
: "";
const exitCode = await process.exitCode;
return { pid: process.pid, stdout, exitCode };
}
}
The returned pid identifies the new process. The exitCode promise resolves when that process exits.
Pass arguments directly
The exec() operation starts the executable directly with the provided argument array. It does not start a shell first.
Each array item becomes one argument. Shell features such as pipes, redirects, globbing, and variable expansion do not run implicitly.
Invoke a shell when your command needs those features. Use ["bash", "-lc", "<COMMAND>"] if Bash exists in your image. Use ["sh", "-c", "<COMMAND>"] if the image only provides a Portable Operating System Interface (POSIX) shell. Pass untrusted values as separate arguments instead of interpolating them into a shell command string.
Send standard input
Pass a ReadableStream to send existing data. Setting stdout to "ignore" discards standard output.
import { Container } from "@cloudflare/containers";
export class MyContainer extends Container {
async importData(data) {
if (!this.ctx.container.running) {
await this.start();
}
const stdin = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode(data));
controller.close();
},
});
const process = await this.ctx.container.exec(["cat"], {
stdin,
stdout: "ignore",
});
const output = await process.output();
return {
stdoutBytes: output.stdout.byteLength,
exitCode: output.exitCode,
};
}
}
import { Container } from "@cloudflare/containers";
export class MyContainer extends Container {
async importData(data: string) {
if (!this.ctx.container.running) {
await this.start();
}
const stdin = new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(new TextEncoder().encode(data));
controller.close();
},
});
const process = await this.ctx.container.exec(["cat"], {
stdin,
stdout: "ignore",
});
const output = await process.output();
return {
stdoutBytes: output.stdout.byteLength,
exitCode: output.exitCode,
};
}
}
Ignored standard output produces an empty buffer from output(). Set stdin to "pipe" to write data over time.
import { Container } from "@cloudflare/containers";
export class MyContainer extends Container {
async concatenateInput() {
if (!this.ctx.container.running) {
await this.start();
}
const process = await this.ctx.container.exec(["cat"], {
stdin: "pipe",
});
const writer = process.stdin?.getWriter();
if (!writer) {
throw new Error("Standard input is unavailable");
}
const encoder = new TextEncoder();
await writer.write(encoder.encode("first\n"));
await writer.write(encoder.encode("second\n"));
await writer.close();
const output = await process.output();
return new TextDecoder().decode(output.stdout);
}
}
import { Container } from "@cloudflare/containers";
export class MyContainer extends Container {
async concatenateInput() {
if (!this.ctx.container.running) {
await this.start();
}
const process = await this.ctx.container.exec(["cat"], {
stdin: "pipe",
});
const writer = process.stdin?.getWriter();
if (!writer) {
throw new Error("Standard input is unavailable");
}
const encoder = new TextEncoder();
await writer.write(encoder.encode("first\n"));
await writer.write(encoder.encode("second\n"));
await writer.close();
const output = await process.output();
return new TextDecoder().decode(output.stdout);
}
}
Close the writer to send end-of-file (EOF). If you omit stdin, exec() closes standard input and sends EOF immediately.
Pass an RPC stream to standard input
RPC methods can accept byte-oriented ReadableStream values whose underlying source uses type: "bytes". A Request body meets this requirement. You can pass the received stream directly to exec() without buffering the entire stream in the Durable Object. For more information, refer to Streams over RPC.
import { Container, getContainer } from "@cloudflare/containers";
export class MyContainer extends Container {
async writeFile(input) {
if (!this.ctx.container.running) {
await this.start();
}
const process = await this.ctx.container.exec(["tee", "/tmp/upload.bin"], {
stdin: input,
stdout: "ignore",
});
return process.exitCode;
}
}
export default {
async fetch(request, env) {
if (!request.body) {
return new Response("Request body required", { status: 400 });
}
const container = getContainer(env.MY_CONTAINER, "upload-worker");
const exitCode = await container.writeFile(request.body);
return Response.json({ exitCode });
},
};
import { Container, getContainer } from "@cloudflare/containers";
export class MyContainer extends Container {
async writeFile(input: ReadableStream<Uint8Array>) {
if (!this.ctx.container.running) {
await this.start();
}
const process = await this.ctx.container.exec(
["tee", "/tmp/upload.bin"],
{
stdin: input,
stdout: "ignore",
},
);
return process.exitCode;
}
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
if (!request.body) {
return new Response("Request body required", { status: 400 });
}
const container = getContainer(env.MY_CONTAINER, "upload-worker");
const exitCode = await container.writeFile(request.body);
return Response.json({ exitCode });
},
};
RPC transfers ownership of the stream to the Durable Object. The calling Worker cannot read it after passing it to writeFile().
The following cat process exits because standard input is omitted:
import { Container } from "@cloudflare/containers";
export class MyContainer extends Container {
async verifyEndOfFile() {
if (!this.ctx.container.running) {
await this.start();
}
const process = await this.ctx.container.exec(["cat"]);
const output = await process.output();
return {
stdoutBytes: output.stdout.byteLength,
exitCode: output.exitCode,
};
}
}
import { Container } from "@cloudflare/containers";
export class MyContainer extends Container {
async verifyEndOfFile() {
if (!this.ctx.container.running) {
await this.start();
}
const process = await this.ctx.container.exec(["cat"]);
const output = await process.output();
return {
stdoutBytes: output.stdout.byteLength,
exitCode: output.exitCode,
};
}
}
Set the process context
Use cwd, env, and user to set the process context. The process receives only the variables in env, plus PATH. It does not inherit other variables from envVars or the image. PATH comes from envVars, or from the image when envVars does not set it. To give a command a value from envVars, pass it in env as well.
This example uses sh because it needs expansion and redirection. It also captures standard output and standard error separately.
import { Container } from "@cloudflare/containers";
export class MyContainer extends Container {
async inspectWorkspace() {
if (!this.ctx.container.running) {
await this.start();
}
const process = await this.ctx.container.exec(
[
"sh",
"-c",
'printf "%s:%s:%s" "$PWD" "$MODE" "$EXTRA_VALUE"; printf "diagnostic" >&2',
],
{
cwd: "/workspace",
env: {
MODE: "inspection",
EXTRA_VALUE: "added",
},
},
);
const output = await process.output();
const decoder = new TextDecoder();
return {
stdout: decoder.decode(output.stdout),
stderr: decoder.decode(output.stderr),
};
}
}
import { Container } from "@cloudflare/containers";
export class MyContainer extends Container {
async inspectWorkspace() {
if (!this.ctx.container.running) {
await this.start();
}
const process = await this.ctx.container.exec(
[
"sh",
"-c",
'printf "%s:%s:%s" "$PWD" "$MODE" "$EXTRA_VALUE"; printf "diagnostic" >&2',
],
{
cwd: "/workspace",
env: {
MODE: "inspection",
EXTRA_VALUE: "added",
},
},
);
const output = await process.output();
const decoder = new TextDecoder();
return {
stdout: decoder.decode(output.stdout),
stderr: decoder.decode(output.stderr),
};
}
}
The user option sets the numeric Linux user and group IDs for the process, as uid:gid. When omitted, the process runs as the image user. Before you rely on user to restrict a process, refer to exec().
Combine standard error
Set stderr to "combined" to merge standard error into standard output. Combined output requires stdout: "pipe".
```js import { Container } from "@cloudflare/containers";
export class MyContainer extends Container { async readCombinedOutput() { if (!this.ctx.container.running) { await this.start(); }
const process = await this.ctx.container.exec( [ "bash", "-lc", 'printf "standard output\n"; printf "standard error\n" >&2', ], { stdout: "pipe", stderr: "combined", }, ); const output = await process.output();
