Pass runtime environment variables through env in ctx.container.start(). Read Worker secrets, Secrets Store values, and KV data inside the Durable Object before starting its Container.
This example passes four values to the container process: ENV_VAR, WORKER_SECRET, SECRET_STORE_SECRET, and KV_VALUE. The HTTP response includes only the two non-secret values.
Create a KV namespace and a Secrets Store secret. Use a secret named SECRET_STORE_SECRET with the workers scope. Replace the namespace and store IDs in this configuration with the IDs returned during setup.
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "container-environment",
"main": "src/index.ts",
// Set this to today's date
"compatibility_date": "2026-10-05",
"observability": {
"enabled": true
},
"containers": [
{
"class_name": "MyContainer",
"scheduling_policy": "durable_object",
"images": {
"base": {
"dockerfile": "./Dockerfile"
}
}
}
],
"durable_objects": {
"bindings": [
{
"name": "MY_CONTAINER",
"class_name": "MyContainer"
}
]
},
"exports": {
"MyContainer": {
"type": "durable-object",
"storage": "sqlite"
}
},
"vars": {
"ENV_VAR": "demo"
},
"kv_namespaces": [
{
"binding": "DEMO_KV",
"id": "<KV_NAMESPACE_ID>"
}
],
"secrets_store_secrets": [
{
"binding": "SECRET_STORE",
"store_id": "<STORE_ID>",
"secret_name": "SECRET_STORE_SECRET"
}
]
}name = "container-environment"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-10-05"
[observability]
enabled = true
[[containers]]
class_name = "MyContainer"
scheduling_policy = "durable_object"
[containers.images.base]
dockerfile = "./Dockerfile"
[[durable_objects.bindings]]
name = "MY_CONTAINER"
class_name = "MyContainer"
[exports.MyContainer]
type = "durable-object"
storage = "sqlite"
[vars]
ENV_VAR = "demo"
[[kv_namespaces]]
binding = "DEMO_KV"
id = "<KV_NAMESPACE_ID>"
[[secrets_store_secrets]]
binding = "SECRET_STORE"
store_id = "<STORE_ID>"
secret_name = "SECRET_STORE_SECRET"Create the Worker secret, then populate the KV key for deployment:
npx wrangler secret put WORKER_SECRETyarn wrangler secret put WORKER_SECRETpnpm wrangler secret put WORKER_SECRETnpx wrangler kv key put --binding DEMO_KV KV_VALUE 'Hello from KV!' --remoteyarn wrangler kv key put --binding DEMO_KV KV_VALUE 'Hello from KV!' --remotepnpm wrangler kv key put --binding DEMO_KV KV_VALUE 'Hello from KV!' --remoteWORKER_SECRET is available through the Worker environment without a vars entry. Store IDs identify stores; they are not store names.
start() initiates startup. The Worker waits for a successful /health response before forwarding traffic. Concurrent requests share that readiness check, and a failed check can be retried by the next request.
import { DurableObject } from "cloudflare:workers";
const INACTIVITY_TIMEOUT_MS = 60 * 1000;
export class MyContainer extends DurableObject {
starting;
constructor(ctx, env) {
super(ctx, env);
const container = ctx.container;
if (container.running) {
void ctx.blockConcurrencyWhile(() =>
container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS),
);
}
}
async fetch(request) {
// Concurrent requests share startup and readiness checks.
this.starting ??= this.startAndWaitForPort().finally(() => {
this.starting = undefined;
});
await this.starting;
const url = new URL(request.url);
url.protocol = "http:";
url.host = "container";
const forwarded = new Request(url, request);
forwarded.headers.delete("host");
return this.ctx.container.getTcpPort(8080).fetch(forwarded);
}
async startAndWaitForPort() {
const container = this.ctx.container;
if (!container.running) {
const [storedSecret, kvValue] = await Promise.all([
this.env.SECRET_STORE.get(),
this.env.DEMO_KV.get("KV_VALUE"),
]);
if (storedSecret === null || kvValue === null) {
throw new Error(
"Create the secret and KV value before starting the container",
);
}
container.start({
image: container.images.base,
instance: "lite",
enableInternet: false,
env: {
ENV_VAR: this.env.ENV_VAR,
WORKER_SECRET: this.env.WORKER_SECRET,
SECRET_STORE_SECRET: storedSecret,
KV_VALUE: kvValue,
},
});
}
await container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS);
const port = container.getTcpPort(8080);
let lastError;
for (let attempt = 0; attempt < 100; attempt++) {
try {
const response = await port.fetch("http://container/health", {
signal: AbortSignal.timeout(1000),
});
await response.body?.cancel();
if (!response.ok) {
throw new Error(`Health check returned ${response.status}`);
}
return;
} catch (error) {
lastError = error;
await scheduler.wait(200);
}
}
throw new Error("Container did not become ready on port 8080", {
cause: lastError,
});
}
}
export default {
fetch(request, env) {
return env.MY_CONTAINER.getByName("demo").fetch(request);
},
};import { DurableObject } from "cloudflare:workers";
interface Env {
MY_CONTAINER: DurableObjectNamespace<MyContainer>;
ENV_VAR: string;
WORKER_SECRET: string;
SECRET_STORE: SecretsStoreSecret;
DEMO_KV: KVNamespace;
}
const INACTIVITY_TIMEOUT_MS = 60 * 1000;
export class MyContainer extends DurableObject<Env> {
private starting: Promise<void> | undefined;
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
const container = ctx.container!;
if (container.running) {
void ctx.blockConcurrencyWhile(() =>
container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS),
);
}
}
async fetch(request: Request): Promise<Response> {
// Concurrent requests share startup and readiness checks.
this.starting ??= this.startAndWaitForPort().finally(() => {
this.starting = undefined;
});
await this.starting;
const url = new URL(request.url);
url.protocol = "http:";
url.host = "container";
const forwarded = new Request(url, request);
forwarded.headers.delete("host");
return this.ctx.container!.getTcpPort(8080).fetch(forwarded);
}
private async startAndWaitForPort(): Promise<void> {
const container = this.ctx.container!;
if (!container.running) {
const [storedSecret, kvValue] = await Promise.all([
this.env.SECRET_STORE.get(),
this.env.DEMO_KV.get("KV_VALUE"),
]);
if (storedSecret === null || kvValue === null) {
throw new Error(
"Create the secret and KV value before starting the container",
);
}
container.start({
image: container.images.base,
instance: "lite",
enableInternet: false,
env: {
ENV_VAR: this.env.ENV_VAR,
WORKER_SECRET: this.env.WORKER_SECRET,
SECRET_STORE_SECRET: storedSecret,
KV_VALUE: kvValue,
},
});
}
await container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS);
const port = container.getTcpPort(8080);
let lastError: unknown;
for (let attempt = 0; attempt < 100; attempt++) {
try {
const response = await port.fetch("http://container/health", {
signal: AbortSignal.timeout(1000),
});
await response.body?.cancel();
if (!response.ok) {
throw new Error(`Health check returned ${response.status}`);
}
return;
} catch (error) {
lastError = error;
await scheduler.wait(200);
}
}
throw new Error("Container did not become ready on port 8080", {
cause: lastError,
});
}
}
export default {
fetch(request: Request, env: Env): Promise<Response> {
return env.MY_CONTAINER.getByName("demo").fetch(request);
},
} satisfies ExportedHandler<Env>;Each named Durable Object reads the values when starting its Container. A running process keeps its startup environment. Updating a secret or KV value does not change that process's environment; the updated value is read on its next start.
Save these files in the project root. The server listens on 0.0.0.0:8080 and exposes a readiness endpoint at /health.
import { createServer } from "node:http";
createServer((request, response) => {
if (request.url === "/health") {
response.writeHead(200).end();
return;
}
response.setHeader("Content-Type", "application/json");
response.end(
JSON.stringify({
message: "Hello from a Container",
environment: process.env.ENV_VAR,
kvValue: process.env.KV_VALUE,
}),
);
}).listen(8080, "0.0.0.0");FROM node:24-bookworm-slim
WORKDIR /app
COPY server.mjs .
EXPOSE 8080
CMD ["node", "server.mjs"]For local development, create a .dev.vars file with test values. Do not commit this file.
WORKER_SECRET="local-worker-secret"Create a local Secrets Store value using the same store ID and secret name as the configuration. Enter a test value at the prompt:
npx wrangler secrets-store secret create <STORE_ID> --name SECRET_STORE_SECRET --scopes workersyarn wrangler secrets-store secret create <STORE_ID> --name SECRET_STORE_SECRET --scopes workerspnpm wrangler secrets-store secret create <STORE_ID> --name SECRET_STORE_SECRET --scopes workersThis command creates local data. It does not read or update the deployed secret.
Populate the local KV namespace:
npx wrangler kv key put --binding DEMO_KV KV_VALUE 'Hello from local KV!' --localyarn wrangler kv key put --binding DEMO_KV KV_VALUE 'Hello from local KV!' --localpnpm wrangler kv key put --binding DEMO_KV KV_VALUE 'Hello from local KV!' --localStart a Docker-compatible engine. Install Wrangler in your project, using version 4.136.0 or later for local development.
npm i -D wrangleryarn add -D wranglerpnpm add -D wranglerbun add -d wranglernpx wrangler devyarn wrangler devpnpm wrangler devVisit http://localhost:8787/. The response shows demo and the local KV value, without exposing either secret.
Use build_vars on a named image for non-secret Docker build arguments. For example, replace the containers entry in the configuration and declare ARG APP_VERSION in your Dockerfile:
{
"$schema": "./node_modules/wrangler/config-schema.json",
"containers": [
{
"class_name": "MyContainer",
"scheduling_policy": "durable_object",
"images": {
"base": {
"dockerfile": "./Dockerfile",
"build_vars": {
"APP_VERSION": "1.0.0"
}
}
}
}
]
}[[containers]]
class_name = "MyContainer"
scheduling_policy = "durable_object"
[containers.images.base]
dockerfile = "./Dockerfile"
build_vars = { APP_VERSION = "1.0.0" }Build arguments do not automatically become runtime environment variables. Keep secrets out of build arguments and container images.