pickuma.
Infrastructure

Blue-Green Deployments for Teams Without a Platform Engineer

Blue-green deployments do not require Kubernetes, a service mesh, or a dedicated platform team. Here is a working setup using nothing more than a reverse proxy, two ports, and a shell script.

7 min read

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:

  1. Start the green instance.
  2. Health check green.
  3. Mark the blue backend as draining in HAProxy.
  4. Wait for in-flight connections to drop to zero (HAProxy exposes this as a metric).
  5. 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?
No. Both blue and green instances should point to the same database. The deployment risk is not data corruption — both instances run the same application with the same database credentials. The risk is schema incompatibility: if green runs migrations that rename a column, the still-running blue instance will fail queries against that column. Run additive-only migrations (new columns, new tables, no renames, no drops) until both blue and green are on the same schema version, then clean up in a follow-up migration after blue is shut down.
What is the difference between blue-green and rolling deployments?
A rolling deployment replaces instances one at a time — instance 1 goes down, instance 1 comes back up with the new version, then instance 2, and so on. During the rollout, some users hit the old version and some hit the new. This is fine for stateless services where version mismatches do not matter, but it creates bugs when old and new versions talk to each other or to a database that only one version understands. Blue-green avoids the overlap entirely: all traffic switches at once from one version to the other.
How do I handle static assets during a blue-green deploy?
If your static assets are versioned by filename (JavaScript bundles with content hashes, CSS files with fingerprinting), both blue and green can reference the same CDN without conflict — new assets exist alongside old ones. If your assets are not versioned and you overwrite them on deploy, the old blue instance will serve broken pages because its HTML references assets that no longer exist. The fix is to always version your static assets by content hash, which most bundlers do by default, or to serve assets from a CDN that keeps multiple versions.

Related tools

Some links above are affiliate links. We may earn a commission if you sign up. See our disclosure for details.

Related reading

See all Infrastructure articles →

Get the best tools, weekly

One email every Friday. No spam, unsubscribe anytime.