Skip to content

Wrangler is not creating containers in local dev environment when doing wrangler dev #211

@RahiReja

Description

@RahiReja

Summary

When adding two containers to a wrangler.jsonc file and running wrangler dev, only the last container image in the file is created. In the below provided wrangler configuration only the EgressTest1Container image is created. However, the image build logs show both images are created but when we run docker images only one image is shown.

Here is the github repo for reproducible code - https://github.com/RahiReja/egress-tests

{
  "name": "egress-tests",
  "main": "src/index.ts",
  "compatibility_date": "2026-02-06",
  "compatibility_flags": ["nodejs_compat"],
  "observability": {
    "enabled": true
  },
  "containers": [
    {
      "image": "./Dockerfile",
      "class_name": "EgressTestContainer",
      "name": "egress-test-container",
      "max_instances": 2
    },
    {
      "image": "./Dockerfile",
      "class_name": "EgressTest1Container",
      "name": "egress-test1-container",
      "max_instances": 2
    }
  ],
  "durable_objects": {
    "bindings": [
      {
        "class_name": "EgressTestContainer",
        "name": "CONTAINER"
      },
      {
        "class_name": "EgressTest1Container",
        "name": "CONTAINER1"
      }
    ]
  },
  "migrations": [
    {
      "tag": "v1",
      "new_sqlite_classes": ["EgressTestContainer", "EgressTest1Container"]
    }
  ]
}

Expected Behaviour

When we do wrangler dev both container image has to be created. Not only the last one.

Environment

Package: @cloudflare/containers@0.3.4
Wrangler: 4.81.0
Worker compatibility date: 2026-04-18
enableInternet = true
interceptHttps = true

Minimal shape of the setup

import { Container, getContainer, ContainerProxy } from "@cloudflare/containers";

export { ContainerProxy };

export class EgressTestContainer extends Container {
  defaultPort = 8080;
  sleepAfter = "3m";
  enableInternet = false;
  interceptHttps = true;

  // allowedHosts gates everything: only these hosts can reach outbound/internet.
  // 'by-host.com' is included so the outboundByHost handler can run.
  allowedHosts = ["allowed.com", "by-host.com", "*.globtest.com"];
  deniedHosts = ["denied.com"];

  constructor(ctx: any, env: any) {
    super(ctx, env);
    this.entrypoint = ["node", "server.js"];
    this.envVars = {
      NODE_EXTRA_CA_CERTS: "/etc/cloudflare/certs/cloudflare-containers-ca.crt"
    };
  }
}
export class EgressTest1Container extends Container {
  defaultPort = 8080;
  sleepAfter = "3m";
  enableInternet = false;
  interceptHttps = true;

  // allowedHosts gates everything: only these hosts can reach outbound/internet.
  // 'by-host.com' is included so the outboundByHost handler can run.
  allowedHosts = ["allowed.com", "by-host.com", "*.globtest.com"];
  deniedHosts = ["denied.com"];

  constructor(ctx: any, env: any) {
    super(ctx, env);
    this.entrypoint = ["node", "server.js"];
    this.envVars = {
      NODE_EXTRA_CA_CERTS: "/etc/cloudflare/certs/cloudflare-containers-ca.crt"
    };
  }
}
EgressTestContainer.outboundByHost = {
  "by-host.com": (_req: Request) => {
    const res = new Response("outboundByHost: by-host.com");
    // res.headers.append("Set-Cookie", "cookie3=value3; Path=/");
    // res.headers.append("Set-Cookie", "cookie4=value4; Path=/");
    res.headers.append("Set-Cookie", "cookie3=value3; Path=/");
    //console.log(res); // Logs both cookies
    return res;
  },
  "*.globtest.com": (req: Request) => {
    return new Response("outboundByHost glob: " + new URL(req.url).hostname);
  }
};

EgressTestContainer.outbound = (req: Request) => {
  return new Response("catch-all: " + new URL(req.url).hostname);
};

export default {
  async fetch(request: Request, env: { CONTAINER: DurableObjectNamespace<EgressTestContainer> }): Promise<Response> {
    try {
      const url = new URL(request.url);
      const id = url.searchParams.get("id") || "singleton";
      const container = getContainer(env.CONTAINER, id);

      if (url.pathname === "/proxy") {
        return await container.containerFetch(request);
      }
      if (url.pathname === "/proxy_https") {
        const res = await container.containerFetch(request);
        // return await container.containerFetch(request);
        console.log("Handler result:", res); // Logs both cookies if present
        // for (const key of res.headers.keys()) {
        //   console.log(`Header: ${key} = ${res.headers.get(key)}`);
        // }
        return res;
      }
      if (url.pathname === "/config/deny-host") {
        const hostname = url.searchParams.get("hostname");
        if (!hostname) {
          return new Response("hostname is required", { status: 400 });
        }

        await container.denyHost(hostname);
        return new Response("OK");
      }
      if (url.pathname === "/destroy") {
        await container.destroy();
        return new Response("Container killed");
      }
      if (url.pathname === "/status") {
        const state = await container.getState();
        return new Response(JSON.stringify(state, null, 2));
      }

      return new Response("Not Found");
    } catch (e) {
      console.error("Worker fetch error:", e);
      return new Response(`Worker error: ${e instanceof Error ? e.message : String(e)}`, {
        status: 500
      });
    }
  }
};

This is the same code as in the Cloudflare/Containers package example egress-test with some modifications. I have added one more container EgressTest1Container and runs wrangler dev. It only creates the last container image not all container images.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions