DEV Community

pickuma
pickuma

Posted on • Originally published at pickuma.com

Blue-Green Deployments for Teams Without a Platform Engineer

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 sed line is the weakest link. If Nginx ever adds a second proxy_pass directive to the server block — for a new route, an internal redirect, or a misconfigured include — the regex will match the wrong one, or both, and Nginx will fail to reload. At that point, your site is down. Replace the sed with a templated config and a symlink swap: maintain two config files (app-blue.conf and app-green.conf), symlink the active one to app.conf, and reload. This is one extra file and one extra line of script, and it removes the regex fragility entirely.

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.


Originally published at pickuma.com. Subscribe to the RSS or follow @pickuma.bsky.social for new reviews.

Top comments (0)