Implementing Canary Deployments on Vercel

Canary deployments are a strategy that allows developers to introduce a new version of an application to a subset of users, minimizing downtime and risk. This involves maintaining a stable existing production environment while deploying a new version (Canary) for testing. This guide will walk you through setting up canary deployments on Vercel, utilizing its features like Skew Protection, Edge Config, and Middleware in Next.js for a seamless transition.

Demo

Explore a live demo of canary deployments here.

Getting Started

  • Deploy Your Project: You can begin with deploying our template that is configured for canary deployments.
  • Activate Skew Protection: This ensures that users stick to the assigned canary or existing deployments throughout their session, providing a consistent user experience across Vercel’s global CDN and serverless function infrastructure.
  • Activate Deployment Protection Bypass: This allows your deployments to smoothly transition without being hindered by standard protections.
  • Disable Auto-assign Custom Production Domains: Prevent Vercel from auto-assigning custom domains to new production deployments, creating a production-like environment for staging instead.
  • Create an Edge Config: Define how traffic is managed between your existing and canary deployments using the following configuration:
{
"canary-configuration": {
"deploymentDomainExisting": "https://existing-production-url.com",
"deploymentDomainCanary": "https://canary-version-url.com",
"trafficCanaryPercent": 10
}
}

Implementing Middleware for Traffic Management

Use Next.js Middleware to direct traffic based on your canary configuration, requiring the configuration to be fetched from Edge Config and applied accordingly.

Example middleware in your Next.js application:

import { get } from "@vercel/edge-config";
import { NextRequest, NextResponse } from "next/server";
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
"/((?!api|_next/static|_next/image|favicon.ico).*)",
],
};
// Configuration stored in Edge Config.
interface CanaryConfig {
deploymentDomainExisting: string;
deploymentDomainCanary: string;
trafficCanaryPercent: number;
}
export async function middleware(req: NextRequest) {
// We don't want to run canary during development.
if (process.env.NODE_ENV !== "production") {
return NextResponse.next();
}
// We only want to run canary for GET requests that are for HTML documents.
if (req.method !== "GET") {
return NextResponse.next();
}
if (req.headers.get("sec-fetch-dest") !== "document") {
return NextResponse.next();
}
// Skip if the request is coming from Vercel's deployment system.
if (/vercel/i.test(req.headers.get("user-agent") || "")) {
return NextResponse.next();
}
// Skip if the middleware has already run.
if (req.headers.get("x-deployment-override")) {
return getDeploymentWithCookieBasedOnEnvVar();
}
if (!process.env.EDGE_CONFIG) {
console.warn("EDGE_CONFIG env variable not set. Skipping canary.");
return NextResponse.next();
}
// Get the canary configuration from Edge Config.
const canaryConfig = await get<CanaryConfig>(
"canary-configuration"
);
if (!CanaryConfig) {
console.warn("No canary configuration found");
return NextResponse.next();
}
const servingDeploymentDomain = process.env.VERCEL_URL;
const selectedDeploymentDomain =
selectCanaryDomain(canaryConfig);
console.info(
"Selected deployment domain",
selectedDeploymentDomain,
canaryConfig
);
if (!selectedDeploymentDomain) {
return NextResponse.next();
}
// The selected deployment domain is the same as the one serving the request.
if (servingDeploymentDomain === selectedDeploymentDomain) {
return getDeploymentWithCookieBasedOnEnvVar();
}
// Fetch the HTML document from the selected deployment domain and return it to the user.
const headers = new Headers(req.headers);
headers.set("x-deployment-override", selectedDeploymentDomain);
headers.set(
"x-vercel-protection-bypass",
process.env.VERCEL_AUTOMATION_BYPASS_SECRET || "unknown"
);
const url = new URL(req.url);
url.hostname = selectedDeploymentDomain;
return fetch(url, {
headers,
redirect: "manual",
});
}
// Selects the deployment domain based on the canary configuration.
function selectCanaryDomain(canaryConfig: CanaryConfig) {
const random = Math.random() * 100;
const selected =
random < canaryConfig.trafficCanaryPercent
? canaryConfig.deploymentDomainCanary
: canaryConfig.deploymentDomainExisting || process.env.VERCEL_URL;
if (!selected) {
console.error("Canary configuration error", canaryConfig);
}
if (/^http/.test(selected || "")) {
return new URL(selected || "").hostname;
}
return selected;
}
function getDeploymentWithCookieBasedOnEnvVar() {
console.log(
"Setting cookie based on env var",
process.env.VERCEL_DEPLOYMENT_ID
);
const response = NextResponse.next();
// We need to set this cookie because next.js does not do this by default, but we do want
// the deployment choice to survive a client-side navigation.
response.cookies.set("__vdpl", process.env.VERCEL_DEPLOYMENT_ID || "", {
sameSite: "strict",
httpOnly: true,
maxAge: 60 * 60 * 24, // 24 hours
});
return response;
}

The middleware filters only production GET requests for HTML documents, excluding API routes, static files, image optimization files, and the favicon.

Setting Up CI/CD Integrations for Deployment Management

Begin Traffic Splitting Upon New Canary Deployment

Using GitHub Actions as an example, we can create a workflow to automate the process of updating your Edge Config settings when a new staged deployment is successful. We initiate the traffic splitting based of the configuration you specify. In this case we are starting the traffic at 10%.

name: Create Canary Deployment
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
VERCEL_PROD_URL: ${{ secrets.VERCEL_PRODUCTION_DOMAIN }}
on:
deployment_status:
jobs:
create-canary-deployment:
if: github.event_name == 'deployment_status' && github.event.deployment_status.state == 'success'
runs-on: ubuntu-latest
steps:
- name: Update Edge Config
run: |
curl -X 'PATCH' 'https://api.vercel.com/v1/edge-config/${{ secrets.VERCEL_EDGE_CONFIG_ID }}/items?teamId=${{ secrets.VERCEL_ORG_ID }}' \
-H 'Authorization: Bearer ${{ secrets.VERCEL_TOKEN }}' \
-H 'Content-Type: application/json' \
-d $'{ "items": [ { "operation": "upsert", "key": "canary-configuration", "value": { "deploymentDomainExisting": "${{ env.VERCEL_PROD_URL }}", "deploymentDomainCanary": "${{ github.event.deployment_status.environment_url }}", "trafficCanaryPercent": 10 } } ] }'

Promote a Build After Approval

Set up another workflow to manage the promotion of the canary deployment to production upon approval of the deployment. This specific workflow allows for manual triggers, providing flexibility in deployment management, but this can also be automated based on your organizational needs.

name: Promote Canary Deployment
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
VERCEL_PROD_URL: ${{ secrets.VERCEL_PRODUCTION_DOMAIN }}
on:
# Allow manual runs
workflow_dispatch
jobs:
promote-canary-deployment:
runs-on: ubuntu-latest
steps:
- name: Install Vercel CLI
run: npm install --global vercel
- name: Read Edge Config
run: |
curl 'https://edge-config.vercel.com/${{ secrets.VERCEL_EDGE_CONFIG_ID }}/item/canary-configuration \
-H 'Authorization: Bearer ${{ secrets.EDGE_CONFIG_SECRET }}' \
-o workflow.json
- name: Parse Staged URL
id: canary
run: |
echo "url=$(cat workflow.json | jq -c '.deploymentDomainCanary')" >> $GITHUB_OUTPUT
- name: Promote Vercel Deployment
run: vercel promote ${{ steps.canary.outputs.url }} --token=${{ secrets.VERCEL_TOKEN }} --scope jasonwikerentdemo
- name: Update Edge Config
run: |
curl -X 'PATCH' 'https://api.vercel.com/v1/edge-config/${{ secrets.VERCEL_EDGE_CONFIG_ID }}/items?teamId=${{ secrets.VERCEL_ORG_ID }}' \
-H 'Authorization: Bearer ${{ secrets.VERCEL_TOKEN }}' \
-H 'Content-Type: application/json' \
-d $'{ "items": [ { "operation": "upsert", "key": "canary-configuration", "value": { "deploymentDomainExisting": "${{ env.VERCEL_PROD_URL }}", "deploymentDomainCanary": "${{ steps.canary.outputs.url }}", "trafficCanaryPercent": 0 } } ] }'

This workflow fetches the current Edge Config, identifies the deployment to promote, and updates the Edge Config to the new state.

Conclusion

Canary deployments on Vercel provide a strategic approach for safely updating applications with minimal user disruption. Utilizing Vercel's features developers can finely control the rollout process, ensuring smooth and reliable application updates. Follow this guide to implement your canary deployments, and explore the demo for a hands-on example.

Couldn't find the guide you need?