Introduction: Why are you still afraid to press "Deploy"?
\n\nYou 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\nTraditional 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\nThat'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\nIn 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\nWhat is Canary Deployment and why do you need it?
\n\nCanary 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\nKey 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
Now let's move from theory to practice. We will build a system consisting of three parts:
\n- \n
- Backend in TypeScript (Express.js). \n
- Traffic balancer on Nginx. \n
- Automation via GitHub Actions. \n
Step 1. Writing a TypeScript application with versioning support
\n\nFirst, 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\nNow 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