Blue-Green vs Canary Deployments on a Budget (No Kubernetes)

Photo by Albert Stoynov

Photo by Albert Stoynov
Most articles about deployment strategies assume you have Kubernetes, Istio, and a platform team. Most teams I work with have two or three developers, a couple of VPSs, nginx, and Docker. The good news nobody tells them: blue-green and canary deployments are routing patterns, not Kubernetes features. Everything you need to run both safely exists in the nginx and Docker Compose you already operate.
I have shipped both patterns for small Indonesian SMB clients and on my own Docker Swarm boxes. In this post I will define each strategy precisely, show minimal working implementations on a single VPS, compare them on the criteria that actually matter at small scale — cost, rollback speed, and observability requirements — and give you a decision rule that fits on a sticky note.
Blue-green
Run two identical environments. Blue serves all traffic while you deploy and smoke-test green in private. Cutover is one router change sending 100 percent of traffic to green; rollback is switching back. Martin Fowler documented the pattern long before container orchestrators existed.
Canary
Deploy the new version next to the old one, route a small slice of real users to it, watch error rates and latency, then progressively widen the slice until the old version drains. Rollback is rerouting the slice back to stable. Risk is reduced by exposure control, not by an instant switch.
Notice what both definitions share: two versions running simultaneously and a router deciding who sees what. That router does not have to be a service mesh. On a budget, it is nginx with an upstream block — which is exactly the implementation we will build.
My minimal blue-green is two Docker Compose projects, app-blue on port 3001 and app-green on port 3002, behind a single nginx upstream. The deploy script is short enough to read in full:
# Two compose projects, one nginx upstream
# /etc/nginx/conf.d/app-upstream.conf
upstream app_backend {
server 127.0.0.1:3001; # blue (live)
# server 127.0.0.1:3002; # green (idle)
}
# deploy.sh — the whole "platform"
docker compose -p app-green up -d --build # start new version on :3002
./smoke-test.sh http://127.0.0.1:3002 # verify before any user sees it
sed -i 's/3001/3002/' /etc/nginx/conf.d/app-upstream.conf
nginx -t && nginx -s reload # atomic cutover
docker compose -p app-blue stop # keep it around for rollbackThree properties make this worth the modest extra RAM of running two app instances briefly:
Canary releases sound like they need Istio's traffic splitting, but nginx has shipped weighted load balancing for two decades. The same two-instance layout becomes a canary with one directive:
# Weighted canary with plain nginx — no service mesh
upstream app_backend {
server 127.0.0.1:3001 weight=9; # stable: 90% of traffic
server 127.0.0.1:3002 weight=1; # canary: 10% of traffic
}
# promote by shifting weights: 9/1 -> 5/5 -> 0/1
# rollback = comment out the canary line and reloadPromotion is editing weights and reloading: I typically go 10 percent for an hour, 50 percent for a few hours, then 100. The catch nobody mentions: a weighted canary is only as good as your ability to compare the two versions. If you cannot tell canary errors from stable errors, you are not canarying — you are gambling slowly. I label metrics and logs by upstream port in Prometheus and Loki so the comparison is one Grafana panel: error rate and p95 latency, stable versus canary, side by side.
Sticky sessions are the classic small-team canary bug: with plain round-robin weights, the same user can bounce between versions on every request. If your app keeps server-side session state or your frontend hashes differ per version, add ip_hash or a sticky cookie so each user consistently sees one version during the rollout.
Both patterns beat deploy-and-pray. They differ in what they cost you and what they demand of your monitoring:
| Criterion | Blue-green | Canary |
|---|---|---|
| Extra infrastructure | Second environment during deploys only; can stop blue after a soak period. | Both versions run for the whole rollout window, hours to days. |
| Rollback speed | Seconds — one upstream edit and reload. | Seconds for the canary slice; affected users limited to the slice from the start. |
| Blast radius of a bad deploy | Everyone, instantly, until you switch back. | Only the canary percentage — 10 percent of users at 10 percent weight. |
| Monitoring needed | A smoke test and basic uptime checks suffice. | Per-version error and latency metrics, or the whole exercise is theater. |
| Best for | Most small-team releases; schema-coupled changes; teams without per-version metrics. | Risky changes on busy services: query rewrites, runtime upgrades, payment flows. |
Both strategies put two app versions against one database, so the schema must support both at once. Fowler's advice predates Kubernetes and still beats every tool: decouple schema changes from code deploys with expand-and-contract. The sequence is mechanical:
Pro tip: rehearse the rollback, not just the deploy. Once a quarter I switch production back to the previous version for five minutes during a quiet window. The first rehearsal found a hardcoded port in a health check that would have turned a clean rollback into a 20-minute outage during a real incident.
Deployment safety is a property of your routing and your discipline, not your orchestrator's price tag. A 4-dollar VPS running nginx in front of two Compose projects gives you the same fundamental guarantees that companies buy service meshes for: zero-downtime cutover, instant rollback, and controlled exposure.
Start with the blue-green script above — it is an afternoon of work including the smoke test. Add weighted canaries the day you ship something that genuinely scares you. And whichever you choose, do the expand-and-contract dance on the database, because that is where deployment strategies actually fail.
Sources and further reading