i
- 2025-08-28 - 2025/08/28/Poor-Man-s-Blue-Green-Deployment/

I’ve been tinkering with containerized deployments for my side projects, and recently I landed on a setup that I think strikes a good balance between simple enough to manage myself and robust enough not to take down my site every time I push an update. That setup is a blue-green deployment strategy, stitched together with Docker, Nginx, and a little deploy.sh script that after a lot of trial and error—finally works!

If you’re curious about how to build something like this yourself, or if you just enjoy seeing someone else go through the painful process so you don’t have to, read on.

Why Blue-Green?

Blue-green deployments are a fancy way of saying: keep two versions of your app running side by side, swap traffic between them, and always have a fallback. Instead of stopping one container, updating it, and crossing your fingers that it starts cleanly, you spin up the new one in parallel, test it, and then flip the switch.

In my case, I run two versions of the app: one on port 3000 (blue) and one on port 3001 (green). Nginx sits in front as a reverse proxy, directing traffic to whichever version is currently live.

Blue-Green Deployment Diagram

The Gotchas

  • If you redeploy the same tag, Docker will complain that a container with that name already exists. You either need to stop/remove it explicitly or accept that you can’t do rolling updates with :latest (I strongly recommend unique tags).
  • If your deploy fails halfway through, you can end up with Nginx pointing at a dead port. That’s where the “bulletproofing” comes in: the script needs to check health and only update Nginx if the new container is good.
  • Nginx reloads are cheap, but forgetting to update the upstream block leaves you in 502 Bad Gateway purgatory.

The Script

Here’s the final version of my deploy.sh. It’s not short, but it handles all the little failure cases that tripped me up.

#!/bin/bash
set -euo pipefail

TAG=$1
IMAGE="myreponame/myimagename:$TAG"
BLUE_PORT=3000
GREEN_PORT=3001

# Determine which port is currently live by looking at nginx config
LIVE_PORT=$(grep 'proxy_pass http://127.0.0.1' /etc/nginx/sites-enabled/example.conf | sed -E 's/.*:(\d+);/\1/')
if [ "$LIVE_PORT" == "$BLUE_PORT" ]; then
  IDLE_PORT=$GREEN_PORT
  IDLE_COLOR=green
else
  IDLE_PORT=$BLUE_PORT
  IDLE_COLOR=blue
fi

echo "*** Live port is $LIVE_PORT ($([ "$LIVE_PORT" == "$BLUE_PORT" ] && echo blue || echo green))"
echo "*** Pulling $IMAGE"
docker pull $IMAGE

# Stop any container already using the idle port
docker ps -q --filter "publish=$IDLE_PORT" | xargs -r docker stop || true
docker ps -aq --filter "publish=$IDLE_PORT" | xargs -r docker rm || true

# Run the new container
docker run -d -p $IDLE_PORT:3000 --name example-$IDLE_COLOR $IMAGE

# Wait a few seconds, then check health
sleep 5
if curl -fs http://127.0.0.1:$IDLE_PORT/ > /dev/null; then
  echo "*** New container healthy on port $IDLE_PORT"
  sed -i "s|proxy_pass http://127.0.0.1:$LIVE_PORT;|proxy_pass http://127.0.0.1:$IDLE_PORT;|" /etc/nginx/sites-enabled/vpdf.conf
  nginx -s reload
  echo "*** Traffic switched to $IDLE_COLOR"
  # Optional: clean up old container
docker stop example-$([ "$IDLE_PORT" == "$BLUE_PORT" ] && echo green || echo blue) || true
docker rm example-$([ "$IDLE_PORT" == "$BLUE_PORT" ] && echo green || echo blue) || true
else
  echo "!!! New container on port $IDLE_PORT failed health check, aborting"
  docker logs example-$IDLE_COLOR
  exit 1
fi

How It Works

  1. Figure out which port Nginx is currently sending traffic to.
  2. Pick the other port as the “idle” one.
  3. Pull the new image and start it on the idle port.
  4. Health check it (right now I just do a simple curl /).
  5. If it passes, update the Nginx config and reload.
  6. Kill the old container.

If it fails, nothing changes—users keep hitting the old version and I get to debug without downtime.

Lessons Learned

  • Always tag your images uniquely. Deploying :latest is a great way to accidentally test rollback scenarios.
  • Health checks matter. Even if it’s just hitting /, it’s better than flipping blind.
  • Automate cleanup. Otherwise you’ll wake up to a server full of dead containers.

This script isn’t Kubernetes, but for a single VPS running side projects, it’s more than enough. And the best part: I can deploy with a single command like this:

./deploy.sh 2025-08-28-10-43

…and have confidence I won’t nuke my live site.

Author: S. Eric Asberry
URL: https://blog.ericasberry.com/2025/08/28/Poor-Man-s-Blue-Green-Deployment/
keyboard_arrow_up