Canary deployments with TypeScript: seamless release without fear of breaking production

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction: Why are you still afraid to press "Deploy"?

\n\n

You wrote the code. Tested it on your local machine. Tests passed. Everything is perfect. You press the coveted Deploy button... and 5 minutes later you see a 500 error, and users are panicking and writing to support. Sound familiar?

\n\n

Traditional deployment strategies (blue-green, rolling update) are good, but they have one drawback: they either include all the new code at once, or they don't include it at all. What if you could send a new feature to just 1% of users, look at the metrics, and only then, after ensuring stability, roll it out to 100%?

\n\n

That's exactly how canary deployments work. The name comes from an old mining practice — taking a canary into the mine to detect gas leaks in time. If the bird fell silent, the miners knew it was time to head up. In the IT world, the "canary" is a small group of users on whom the new code is tested. If everything is fine — the release rolls out further. If not — you roll back faster than you can say "hotfix".

\n\n

In this article, we will write a full-fledged canary deployment system in TypeScript. You will learn how to configure traffic balancing, how to automate the process via CI/CD, and how to monitor the "health" of your canary. Let's go!

\n\n

What is Canary Deployment and why do you need it?

\n\n

Canary deployment is a strategy for rolling out new code where the updated version of the application is first sent to a small percentage of users (usually 1-10%). The rest continue to work on the old, stable version. If the new version has no issues (errors, performance drops, increased latency), you gradually increase the percentage of traffic to it until it completely replaces the old one.

\n\n

Key advantages:

\n
    \n
  • Minimized risks — bugs affect only a small part of the audience.
  • \n
  • Fast rollback — just redirect traffic back to the stable version.
  • \n
  • A/B testing in real conditions — you can compare metrics of the old and new versions.
  • \n
  • Smooth rollout — you don't kill the server with a sudden load spike.
  • \n
\n\n

Now let's move from theory to practice. We will build a system consisting of three parts:

\n
    \n
  1. Backend in TypeScript (Express.js).
  2. \n
  3. Traffic balancer on Nginx.
  4. \n
  5. Automation via GitHub Actions.
  6. \n
\n\n

Step 1. Writing a TypeScript application with versioning support

\n\n

First, let's create a simple Express application that can work in two modes: stable and canary. The difference will only be in the response to the root endpoint, but in a real project, this could be any new feature.

\n\n
// src/app.ts\nimport express, { Request, Response } from 'express';\nimport dotenv from 'dotenv';\n\ndotenv.config();\n\nconst app = express();\nconst PORT = process.env.PORT || 3000;\nconst VERSION = process.env.VERSION || 'stable'; // 'stable' or 'canary'\n\napp.get('/', (req: Request, res: Response) => {\n  res.json({\n    message: 'Hello from API!',\n    version: VERSION,\n    timestamp: new Date().toISOString(),\n  });\n});\n\n// Endpoint for health check\napp.get('/health', (req: Request, res: Response) => {\n  res.status(200).send('OK');\n});\n\napp.listen(PORT, () => {\n  console.log(`Server running on port ${PORT} in ${VERSION} mode`);\n});\n
\n\n

Now let's create a Dockerfile to package the application into a container. This will simplify deployment and load balancing.

\n\n
# Dockerfile\nFROM node:18-alpine AS builder\nWORKDIR /app\nCOPY package*.json ./\nRUN npm ci\nCOPY . .\nRUN npm run build\n\nFROM node:18-alpine AS runner\nWORK                

Blogs

Book Recommendations