Skip to main content
Magic Containers lets you run more than one version of an app simultaneously. Three routing patterns build on this:
  • Blue/green: switch all traffic from the current version to a new one in a single, deterministic cutover.
  • A/B: split traffic between two versions by percentage, keeping each user pinned to the same version.
  • Canary: route a targeted slice, such as a single country, to the new version first, then widen as it proves healthy.
Pick blue/green when you want a clean release with an instant rollback path. Pick A/B when you want to expose a new version to a fraction of real traffic. Pick canary when you want to validate a new version on a targeted slice before rolling it out everywhere. For everything else, a standard rolling update is enough.
Blue/GreenA/BCanary
Traffic100% to one version at a timeSplit by percentage (e.g. 90/10)Ramps from a targeted slice to 100%
GoalSafe release + instant rollbackTest a version on a slice of usersValidate a version on a targeted slice
RoutingPull zone origin points at the active versionEdge Script picks a version per requestEdge Script picks a version per request
Configured inTerraform / dashboardEdge Scripting (+ two apps)Edge Scripting (+ two apps)

Why not just use rolling updates?

A rolling update gradually replaces instances of a single app with the new image. During the rollout, both the old and new versions serve traffic at the same time, and across many regions a partial or stalled rollout can leave versions co-existing longer than you’d like. Blue/green sidesteps this by keeping the new version completely separate until you flip every request to it at once. There’s no in-between state, and rolling back is just pointing traffic at the previous version again.
Rolling updates are the right default for most apps. Reach for blue/green when a release must be atomic, such as a schema change where both versions need to serve the same traffic safely.

Blue/green deployment

The idea: deploy the new version as a separate container app while the current version keeps serving, then switch a pull zone’s origin from the old endpoint to the new one.
1

Deploy the new version as a separate app

Create a second Magic Containers app (the “green” version) with the new image. Leave the current “blue” app running and serving production traffic.
2

Point a pull zone at the active version

Use a pull zone with a ComputeContainer origin that targets the currently active app’s container and endpoint.
3

Cut over

When the new version is ready, switch the origin to the green app’s container and endpoint, then apply. All traffic moves at once.
4

Roll back if needed

To revert, point the origin back at the blue app. Because it’s still running, rollback is immediate.

Terraform example

This pull zone routes to one container version at a time. To cut over, comment out the active block, uncomment the other, and apply:
main.tf
resource "bunnynet_dns_zone" "app" {
  domain = "example.net"
}

resource "bunnynet_dns_record" "app" {
  zone  = bunnynet_dns_zone.app.id
  name  = "*.app"
  type  = "CNAME"
  value = "example.b-cdn.net"
}

resource "bunnynet_pullzone" "prod" {
  name = "my-app-prod"

  origin {
    type = "ComputeContainer"

    # Blue (current version): inactive
    # container_app_id      = "BLUE_APP_ID"
    # container_endpoint_id = "BLUE_APP_ID-http"

    # Green (new version): active
    container_app_id      = "GREEN_APP_ID"
    container_endpoint_id = "GREEN_APP_ID-http"
  }
}

resource "bunnynet_pullzone_hostname" "prod" {
  pullzone    = bunnynet_pullzone.prod.id
  name        = "*.app.example.net"
  tls_enabled = true
  force_ssl   = false
}
Replace BLUE_APP_ID and GREEN_APP_ID with your container app IDs. The cutover is a one-line change to which origin block is active.
For stateful workloads such as databases, both versions read and write the same data, so the two versions must be compatible with a shared schema. The version-selection logic typically lives in your application or database layer.

A/B deployment

A/B routing sends a percentage of traffic to each version while keeping individual users pinned to one version (stickiness). Edge Scripting handles the weighted split, running in front of two separate apps and choosing a version per request. The approach:
  1. Deploy two Magic Containers apps: version A and version B.
  2. Add a standalone Edge Script that derives a stable key per visitor (client IP works well for stickiness).
  3. Hash the key into a bucket and route to app A or app B based on your target percentage.
import * as BunnySDK from "@bunny.net/edgescript-sdk";

const VARIANTS = {
  a: "https://app-a.bunny.run",
  b: "https://app-b.bunny.run",
};

const B_PERCENTAGE = 10; // send 10% of visitors to version B

// Polynomial string hash (Java String.hashCode style), mapped into a 0-99 bucket
function bucket(key: string): "a" | "b" {
  let h = 0;
  for (let i = 0; i < key.length; i++) {
    h = (h * 31 + key.charCodeAt(i)) >>> 0;
  }
  return h % 100 < B_PERCENTAGE ? "b" : "a";
}

BunnySDK.net.http.serve(async (request: Request): Promise<Response> => {
  const ip = request.headers.get("x-forwarded-for") ?? "0.0.0.0";
  const variant = bucket(ip);

  const target = new URL(request.url);
  const origin = new URL(VARIANTS[variant]);
  target.protocol = origin.protocol;
  target.host = origin.host;

  return fetch(new Request(target, request));
});
Because the same IP always hashes to the same bucket, a given visitor consistently sees the same version. Adjust B_PERCENTAGE to change the split, and update the variant URLs to your two apps’ endpoints. For a standalone reference, see Weighted traffic splitting.
Use a key that’s stable for the session you want to pin. Client IP is the simplest. To weight by something else (a cookie, a header, a user ID), hash that value instead.

Canary deployment

A canary release sends the new version to a narrow, targeted audience first, lets you watch it under real traffic, then widens the audience as your confidence grows. Country makes a natural targeting key: ship to one or two markets, confirm the metrics look healthy, then add more. This uses the same two-app, standalone Edge Script setup as A/B, routing on the visitor’s country instead of a percentage. bunny.net adds the CDN-RequestCountryCode header to every request, so the script can read it directly. The approach:
  1. Deploy two Magic Containers apps: the stable version and the canary version.
  2. List the countries that should receive the canary.
  3. Route those countries to the canary app and everything else to stable.
import * as BunnySDK from "@bunny.net/edgescript-sdk";

const STABLE = "https://app-stable.bunny.run";
const CANARY = "https://app-canary.bunny.run";

// Countries that receive the canary version first
const CANARY_COUNTRIES = new Set(["NZ", "FI"]);

BunnySDK.net.http.serve(async (request: Request): Promise<Response> => {
  const country = request.headers.get("CDN-RequestCountryCode") ?? "";
  const variant = CANARY_COUNTRIES.has(country) ? CANARY : STABLE;

  const target = new URL(request.url);
  const origin = new URL(variant);
  target.protocol = origin.protocol;
  target.host = origin.host;

  return fetch(new Request(target, request));
});
To widen the rollout, add countries to CANARY_COUNTRIES and redeploy the script. To roll back, empty the set so all traffic returns to the stable app. For a standalone reference, see Route by country.
Combine this with the A/B bucket to ramp inside a country. Route a country to the canary, then send a growing percentage of its visitors to the new version as you gain confidence.