Environment variables and secrets
Pass in environment variables and secrets to your container
Last updated Oct 12, 2026
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.
Configure bindings
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",
"upload_source_maps": true,
// Set this to today's date
"compatibility_date": "2026-10-27",
"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"
upload_source_maps = true
# Set this to today's date
compatibility_date = "2026-10-27"
[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:
npmyarnpnpm
npx wrangler secret put WORKER_SECRET
yarn wrangler secret put WORKER_SECRET
pnpm wrangler secret put WORKER_SECRET
npmyarnpnpm
npx wrangler kv key put --binding DEMO_KV KV_VALUE 'Hello from KV!' --remote
yarn wrangler kv key put --binding DEMO_KV KV_VALUE 'Hello from KV!' --remote
pnpm wrangler kv key put --binding DEMO_KV KV_VALUE 'Hello from KV!' --remote
WORKER_SECRET is available through the Worker environment without a vars entry. Store IDs identify stores; they are not store names.
Pass values at startup
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.
src/index.jsjs
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);
},
};
src/index.tsts
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.
Define the container
Save these files in the project root. The server listens on 0.0.0.0:8080 and exposes a readiness endpoint at /health.
server.mjsjs
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");
Dockerfiledockerfile
FROM node:24-bookworm-slim
WORKDIR /app
COPY server.mjs .
EXPOSE 8080
CMD ["node", "server.mjs"]
Test locally
For local development, create a .dev.vars file with test values. Do not commit this file.
.dev.varstxt
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:
npmyarnpnpm
npx wrangler secrets-store secret create <STORE_ID> --name SECRET_STORE_SECRET --scopes workers
yarn wrangler secrets-store secret create <STORE_ID> --name SECRET_STORE_SECRET --scopes workers
pnpm wrangler secrets-store secret create <STORE_ID> --name SECRET_STORE_SECRET --scopes workers
This command creates local data. It does not read or update the deployed secret.
Populate the local KV namespace:
npmyarnpnpm
npx wrangler kv key put --binding DEMO_KV KV_VALUE 'Hello from local KV!' --local
yarn wrangler kv key put --binding DEMO_KV KV_VALUE 'Hello from local KV!' --local
pnpm wrangler kv key put --binding DEMO_KV KV_VALUE 'Hello from local KV!' --local
Start a Docker-compatible engine. Install Wrangler in your project, using version 4.136.0 or later for local development.
npmyarnpnpmbun
npm i -D wrangler
yarn add -D wrangler
pnpm add -D wrangler
bun add -d wrangler
npmyarnpnpm
npx wrangler dev
yarn wrangler dev
pnpm wrangler dev
Visit http://localhost:8787/. The response shows demo and the local KV value, without exposing either secret.
Set build variables
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.
Was this helpful?
YesNo
On this page
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/containers/examples/env-vars-and-secrets/#page","headline":"Environment variables and secrets","description":"Pass in environment variables and secrets to your container","url":"https://developers.cloudflare.com/containers/examples/env-vars-and-secrets/","inLanguage":"en","image":"https://developers.cloudflare.com/containers/examples/env-vars-and-secrets/og.png?v=d15710cc7365b856","dateModified":"2026-10-12","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
description: Mount R2 buckets as filesystems using FUSE in Containers title: Mount R2 buckets with FUSE image: https://developers.cloudflare.com/containers/examples/r2-fuse-mount/og.png?v=b64ab882fb3a9b6a
Documentation Index
Fetch the complete documentation index at: https://developers.cloudflare.com/containers/llms.txt
Use this file to discover all available pages before exploring further.
Docs