diff --git a/docs.json b/docs.json
index 1ebebe3e..88aceb15 100644
--- a/docs.json
+++ b/docs.json
@@ -316,6 +316,7 @@
"pages": [
"magic-containers/autoscaling",
"magic-containers/rolling-updates",
+ "magic-containers/deployment-strategies",
"magic-containers/graceful-shutdown",
"magic-containers/sandbox",
"magic-containers/multi-container",
@@ -413,7 +414,9 @@
"scripting/standalone/examples/return-json",
"scripting/standalone/examples/return-html",
"scripting/standalone/examples/send-email",
- "scripting/standalone/examples/redirect-domain"
+ "scripting/standalone/examples/redirect-domain",
+ "scripting/standalone/examples/weighted-routing",
+ "scripting/standalone/examples/geo-routing"
]
}
]
diff --git a/magic-containers/deployment-strategies.mdx b/magic-containers/deployment-strategies.mdx
new file mode 100644
index 00000000..838bea5e
--- /dev/null
+++ b/magic-containers/deployment-strategies.mdx
@@ -0,0 +1,198 @@
+---
+title: "Blue/Green & A/B Deployments"
+description: "Run multiple versions of your app at the same time and control how traffic is routed between them."
+---
+
+Magic Containers lets you run more than one version of an app simultaneously. That unlocks three well-known release strategies. Here's the short version of each, and how it maps to Magic Containers:
+
+- **[Blue/green](#bluegreen-deployment)** keeps two environments side by side, the current one (blue) and the new one (green), then switches _all_ traffic over in a single cutover. You get a clean release with no in-between state, and rollback is just pointing traffic back at blue.
+- **[A/B](#ab-deployment)** runs both versions at once and splits traffic between them by percentage, keeping each user pinned to the same version. It's how you put a new version in front of a slice of real users and compare how it performs.
+- **[Canary](#canary-deployment)** releases the new version to a narrow, targeted slice first, such as a single country, watches it under real traffic, then widens the audience as your confidence grows.
+
+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](/magic-containers/rolling-updates) is enough.
+
+| | Blue/Green | A/B | Canary |
+| ------------- | --------------------------------------------- | --------------------------------------- | --------------------------------------- |
+| Traffic | 100% to one version at a time | Split by percentage (e.g. 90/10) | Ramps from a targeted slice to 100% |
+| Goal | Safe release + instant rollback | Test a version on a slice of users | Validate a version on a targeted slice |
+| Routing | Pull zone origin points at the active version | Edge Script picks a version per request | Edge Script picks a version per request |
+| Configured in | Terraform / dashboard | Edge Scripting (+ two apps) | Edge Scripting (+ two apps) |
+
+## Why not just use rolling updates?
+
+A [rolling update](/magic-containers/rolling-updates) 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.
+
+
+
+ Create a second Magic Containers app (the "green" version) with the new image. Leave the current "blue" app running and serving production traffic.
+
+
+
+ Use a pull zone with a `ComputeContainer` origin that targets the currently
+ active app's container and endpoint.
+
+
+
+ When the new version is ready, switch the origin to the green app's container
+ and endpoint, then apply. All traffic moves at once.
+
+
+
+ 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:
+
+```hcl main.tf theme={null}
+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](/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](/scripting/standalone/overview) 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.
+
+```typescript
+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 => {
+ 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](/scripting/standalone/examples/weighted-routing).
+
+
+ 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](/scripting/standalone/overview) setup as A/B, routing on the visitor's country instead of a percentage. bunny.net adds the [`CDN-RequestCountryCode`](//cdn/cdn-acceleration#http-headers) 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.
+
+```typescript
+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 => {
+ 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](/scripting/standalone/examples/geo-routing).
+
+
+ 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.
+
diff --git a/scripting/standalone/examples/geo-routing.mdx b/scripting/standalone/examples/geo-routing.mdx
new file mode 100644
index 00000000..cc5e27f0
--- /dev/null
+++ b/scripting/standalone/examples/geo-routing.mdx
@@ -0,0 +1,30 @@
+---
+title: "Route by country"
+description: "Send visitors from specific countries to a different origin using the CDN-RequestCountryCode header."
+---
+
+bunny.net adds the [`CDN-RequestCountryCode`](/cdn/vary-cache) header to every request. This script reads it and routes the listed countries to a second origin.
+
+```typescript
+import * as BunnySDK from "@bunny.net/edgescript-sdk";
+
+const PRIMARY = "https://primary.example.com";
+const ALTERNATE = "https://alternate.example.com";
+
+// Countries routed to the alternate origin
+const ALTERNATE_COUNTRIES = new Set(["NZ", "FI"]);
+
+BunnySDK.net.http.serve(async (request: Request): Promise => {
+ const country = request.headers.get("CDN-RequestCountryCode") ?? "";
+ const variant = ALTERNATE_COUNTRIES.has(country) ? ALTERNATE : PRIMARY;
+
+ 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));
+});
+```
+
+Add or remove entries in `ALTERNATE_COUNTRIES` to change which countries get the alternate origin.
diff --git a/scripting/standalone/examples/weighted-routing.mdx b/scripting/standalone/examples/weighted-routing.mdx
new file mode 100644
index 00000000..1ac176a1
--- /dev/null
+++ b/scripting/standalone/examples/weighted-routing.mdx
@@ -0,0 +1,40 @@
+---
+title: "Weighted traffic splitting"
+description: "Send a set percentage of visitors to a second origin and keep each visitor on the same origin across requests."
+---
+
+This script hashes a stable key (the client IP) into a 0-99 bucket and routes by percentage. The same key always lands in the same bucket, so a visitor stays on one origin across requests.
+
+```typescript
+import * as BunnySDK from "@bunny.net/edgescript-sdk";
+
+const ORIGINS = {
+ a: "https://origin-a.example.com",
+ b: "https://origin-b.example.com",
+};
+
+const B_PERCENTAGE = 10; // send 10% of visitors to origin 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 => {
+ 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(ORIGINS[variant]);
+ target.protocol = origin.protocol;
+ target.host = origin.host;
+
+ return fetch(new Request(target, request));
+});
+```
+
+Adjust `B_PERCENTAGE` to change the split. To pin on something other than the client IP, such as a cookie or a header, hash that value instead.