i
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.
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.
:latest
(I strongly recommend unique tags).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
curl /
).If it fails, nothing changes—users keep hitting the old version and I get to debug without downtime.
:latest
is a great way to accidentally test rollback scenarios./
, it’s better than flipping blind.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/