Blue-Green Deployments Without a Platform Team
No Kubernetes and no service mesh. A working setup built from a reverse proxy, two ports, and a shell script.
The phrase “blue-green deployment” conjures images of Kubernetes Ingress controllers, weighted traffic splitting in Istio, and a platform engineer who owns the rollout pipeline. That version exists, and it works well at scale, but it is not the only version. A blue-green deployment is fundamentally two copies of your application running side by side — one serving traffic, one waiting — and a switch that flips which one is active. You can build the whole thing with nothing more than Nginx, two ports, and a script that health-checks a container before cutting over.
For a team of three developers running production on a handful of VPS instances, the heavyweight approach costs more in tooling complexity than it saves in deployment safety. The lightweight version costs a few hours of setup and earns back every minute you would have spent rolling back a bad deploy at 11 p.m.
What blue-green actually does (and does not do)
Blue-green is not zero-downtime deployment. It is near-zero-downtime deployment. The switch from blue to green takes however long your reverse proxy takes to reload its configuration — typically under a second, but not zero. If your application has in-flight requests that span several seconds, the ones that started on blue will fail when blue shuts down unless you drain connections properly. The switch is fast, but it is not atomic.
Blue-green also does not handle database migrations. If your green deployment runs migrations that alter a table schema, the still-running blue deployment will break because it cannot read the new schema. You either need to run backward-compatible migrations (additive only, no renames, no drops) or accept that blue will throw errors for the few seconds between migration and shutdown. The latter is usually acceptable for internal tools and low-traffic apps. For a payments API, it is not.
What blue-green does well is give you a fully validated, production-warm copy of your application before you switch traffic to it. You run health checks against the green instance, smoke-test a few endpoints, and only then cut over. If green fails health checks, traffic stays on blue. The rollback is instantaneous because blue never stopped running.
The manual version that costs nothing
Here is the simplest possible blue-green setup on a single VPS. You run two copies of your application, one on port 3000 (blue), one on port 3001 (green). Nginx sits in front, proxying to whichever port is marked active.
Step one: a config file that tracks the active port.
# /etc/app/active-port
3000
Step two: a deployment script that does the following, in order:
#!/bin/bash
set -e
CURRENT=$(cat /etc/app/active-port)
if [ "$CURRENT" = "3000" ]; then
NEXT=3001
else
NEXT=3000
fi
# Start the new instance on the inactive port
docker compose -p app-"$NEXT" up -d --build
# Health check loop — up to 30 seconds
for i in $(seq 1 30); do
if curl -sf http://localhost:"$NEXT"/health; then
break
fi
sleep 1
done
# Switch Nginx to the new port
sed -i "s/proxy_pass http:\/\/127.0.0.1:$CURRENT/proxy_pass http:\/\/127.0.0.1:$NEXT/" /etc/nginx/sites-enabled/app
nginx -s reload
# Update the active port marker
echo "$NEXT" > /etc/app/active-port
# Drain old instance (wait for in-flight requests to finish)
sleep 5
# Stop old instance
docker compose -p app-"$CURRENT" down
That is under 30 lines of shell. No Kubernetes, no service mesh, no separate staging environment. It handles health checks, traffic switching, connection draining, and cleanup. The only external dependency is Docker and Nginx.
The mid-weight version with a reverse proxy that reloads gracefully
If your application has WebSocket connections or long-lived requests that Nginx’s proxy_pass switch will sever, you need a reverse proxy that can drain connections before switching. HAProxy and Caddy both handle this better than Nginx does out of the box.
HAProxy supports a drain state for backends: when you mark a server as draining, it stops sending new connections to it but keeps existing connections alive until they finish naturally. The deployment flow becomes:
- Start the green instance.
- Health check green.
- Mark the blue backend as draining in HAProxy.
- Wait for in-flight connections to drop to zero (HAProxy exposes this as a metric).
- Add the green backend and remove blue entirely.
This preserves WebSocket sessions through the deployment and avoids the connection-reset errors that Nginx’s reload causes. The cost is that HAProxy’s configuration language is less familiar to most developers than Nginx’s, and the draining step adds 10 to 30 seconds to each deployment cycle. For an app where users stay connected for minutes at a time, the trade is worthwhile.
When to add tooling (and when not to)
If your team already runs Kubernetes, use its native rollout mechanisms. Kubernetes Deployments with strategy: RollingUpdate and readinessProbe give you blue-green semantics without the manual scripting. The tooling is already paid for.
If you are on a platform that handles this for you — Fly.io, Railway, Render — let the platform do it. Fly’s fly deploy spins up a new VM, health-checks it, and switches traffic atomically. Railway does the same with its deployment pipeline. The labor cost of building your own is higher than the platform markup, and the platform has already debugged the edge cases you have not hit yet.
If you are on bare VPS instances and do not want Kubernetes, the 30-line shell script above works. It will not scale to 50 services across 12 machines, but a team of three with three services does not need that scale. The right amount of tooling is the smallest amount that prevents a bad deploy from waking someone up.
FAQ
Do I need two separate databases for blue-green?
What is the difference between blue-green and rolling deployments?
How do I handle static assets during a blue-green deploy?
Related tools
Beehiiv
Newsletter platform with built-in ad network and Boost referrals.
Try Beehiiv →
Webflow
Visual site builder with real CSS export and a CMS that scales.
Try Webflow →
Some links above are affiliate links. We may earn a commission if you sign up. See our disclosure for details.
Related reading
2026-09-02
OpenAI Deployment Layer: The Assistants API Precedent
OpenAI shipped the Assistants API in November 2023 and marked it for sunset 16 months later. That precedent is how to price the new deployment stack.
2026-08-21
701 Bad Internal Links Before 49 Good Ones
A phrase-matching script inserted 750 links across 269 MDX articles. Here is where they failed, and how claim-matching cut the review pile to 128.
2026-08-21
Cloudflare Web Analytics via GraphQL: the siteTag Filter
Why accountTag and siteTag differ in rumPageloadEventsAdaptiveGroups, how the limit argument truncates silently, and which fields we did not verify.
2026-08-21
Bing Webmaster API's 100-URL Cap: 289 URLs, Three Days
Quota is 100 a day against 1300 a month, GetQueryStats was still empty at the end, and two error shapes will kill a cron job.
2026-08-19
IndexNow Batch Mode: 286 URLs Per Publish Down to 0
Bing flags full-sitemap submissions as batch mode. The ~40-line lastmod diff that fixes it, the precondition it needs, and two ways it silently breaks.
Get the best tools, weekly
One email every Friday. No spam, unsubscribe anytime.