Practical Blue-Green Deployments with Nginx and Systemd Sockets
Achieve zero-packet-loss blue-green releases on a single Linux VPS using dual systemd service instances and atomic Nginx upstream reloads.
Netbay Developer Relations
Netbay Engineering
On this page
Deploying application code updates by directly stopping and restarting a single service process invariably leads to HTTP 502 Bad Gateway errors for active users. While multi-node load balancers provide an obvious solution, maintaining multiple servers for small-to-medium web services introduces unnecessary infrastructure cost and maintenance overhead.
By configuring a dual-slot blue-green topology on a single Linux VPS using systemd template units and atomic Nginx upstream switching, you can achieve 100% zero-packet-loss releases. New code boots in the idle slot, executes automated smoke tests, and takes live production traffic via an instantaneous Nginx master configuration reload.
The Problem with Direct Service Restarts
When a deployment script executes systemctl restart myapp, the operating system sends SIGTERM to the active application process. Even if the service shuts down in 500 milliseconds and the new process boots in 2 seconds, any incoming TCP SYN packet arriving during that window will be rejected or wait until connection timeouts expire. Furthermore, if the newly deployed code contains a fatal syntax error, missing environment variable, or failed database migration, the service fails to boot, leaving production completely offline until an administrator notices the alert.
Blue-green deployments eliminate this risk by separating the deployment phase from the traffic activation phase. The new release boots and proves its health before a single production request touches it.
Creating Dual Systemd Template Services
Instead of defining two separate unit files, use systemd template units (app@.service) to manage the blue (port 8081) and green (port 8082) application slots:
# /etc/systemd/system/app@.service
[Unit]
Description=Application Service Instance %I
After=network.target
[Service]
Type=simple
User=www-data
WorkingDirectory=/srv/app/%I
EnvironmentFile=/srv/app/%I/.env
ExecStart=/usr/bin/node server.js
Restart=always
RestartSec=3s
LimitNOFILE=65535
# Process sandboxing
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/srv/app/%I/tmp /srv/app/%I/logs
[Install]
WantedBy=multi-user.targetIn /srv/app/blue/.env set PORT=8081, and in /srv/app/green/.env set PORT=8082. Both slots operate independently on local loopback addresses.
Configuring Dynamic Upstream Switching in Nginx
Configure Nginx to proxy traffic to an included upstream configuration file that points to a symbolic link:
# /etc/nginx/sites-available/app.conf
include /etc/nginx/conf.d/active-slot.conf;
server {
listen 80;
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
location / {
proxy_pass http://app_cluster;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 5s;
proxy_read_timeout 60s;
}
}The file /etc/nginx/conf.d/active-slot.conf is maintained as a symbolic link pointing to either /etc/nginx/conf.d/slot-blue.conf or /etc/nginx/conf.d/slot-green.conf:
# /etc/nginx/conf.d/slot-blue.conf
upstream app_cluster {
server 127.0.0.1:8081;
keepalive 32;
}
# /etc/nginx/conf.d/slot-green.conf
upstream app_cluster {
server 127.0.0.1:8082;
keepalive 32;
}The Atomic Deployment and Cutover Script
Create /usr/local/bin/deploy-slot.sh to automate pulling new code, updating the idle slot, testing endpoints, and reloading Nginx:
#!/usr/bin/env bash
set -euo pipefail
ACTIVE_TARGET=$(readlink -f /etc/nginx/conf.d/active-slot.conf)
if [[ "$ACTIVE_TARGET" == *"slot-blue.conf"* ]]; then
CURRENT="blue"
NEXT="green"
NEXT_PORT=8082
else
CURRENT="green"
NEXT="blue"
NEXT_PORT=8081
fi
echo "[$(date)] Current active slot: $CURRENT. Deploying to idle slot: $NEXT."
# Update code and dependencies in the idle directory
cd /srv/app/$NEXT
git pull origin main
npm ci --omit=dev
# Restart idle slot
sudo systemctl restart app@$NEXT
# Execute health smoke check against local port
echo "[$(date)] Smoke testing $NEXT on port $NEXT_PORT..."
for i in {1..10}; do
if curl -sf http://127.0.0.1:$NEXT_PORT/health > /dev/null; then
echo "Health check passed!"
break
fi
sleep 1
if [ "$i" -eq 10 ]; then
echo "Deployment failed! Smoke test timed out on slot $NEXT." >&2
exit 1
fi
done
# Atomically switch symlink and gracefully reload Nginx
sudo ln -sf /etc/nginx/conf.d/slot-$NEXT.conf /etc/nginx/conf.d/active-slot.conf
sudo nginx -s reload
echo "[$(date)] Successfully switched live traffic to $NEXT slot!"Nginx reloads its master process configuration gracefully: existing client requests finish processing on the old worker processes, while new TCP connections are immediately accepted by new workers routing to the freshly booted slot.
Verifying Deployments and Monitoring Drains
Verify that your cutover script completes cleanly and inspect active connections during releases:
# Run deployment script
sudo /usr/local/bin/deploy-slot.sh
# Confirm active symlink target
ls -l /etc/nginx/conf.d/active-slot.conf
# Check systemd process logs for both slots
journalctl -u app@blue.service -u app@green.service -fBlue-green deployments eliminate deployment anxiety and deliver a flawless user experience on single-node cloud environments.
Deploy high-availability single-node web services on Netbay Linux VPS in under 60 seconds with High-Speed SSD in Lucknow DC01 — netbayhosts.in.
Keep reading
Follow along on a real VPS
Deploy Linux in under 60 seconds
These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.
Deploy an instance