Zero-downtime deployment z Nginx — blue-green, rolling update, health checks i rollback
Opublikowano: 9 kwietnia 2026 · Kategoria: VPS / DevOps
Deploy o 3 w nocy żeby uniknąć użytkowników? Okno maintenance w niedzielę? To już przeszłość. Zero-downtime deployment pozwala wdrażać nowe wersje aplikacji w ciągu dnia roboczego, bez błędów 502 i bez utraty sesji użytkownika. W tym przewodniku omówimy mechanizm graceful reload Nginx, blue-green deployment, rolling update z health checkami i automatyczny rollback gdy coś pójdzie nie tak.
Nginx graceful reload — jak to działa
Zanim przejdziemy do złożonych strategii — zrozum fundamentalny mechanizm. nginx -s reload
nie restartuje serwera, lecz zastępuje worker procesy zachowując ciągłość obsługi:
# Sprawdź PID master procesu przed reload ps aux | grep "nginx: master" cat /var/run/nginx.pid # Przetestuj konfigurację (ZAWSZE przed reload) sudo nginx -t # Graceful reload — zero dropped connections sudo nginx -s reload # lub: sudo systemctl reload nginx # Sprawdź PID po reload — master PID się nie zmienia! cat /var/run/nginx.pid # Obserwuj stare workery kończące obsługę żądań watch -n 0.5 "ps aux | grep nginx"
Stary worker może żyć kilka sekund lub minut (zależy od aktywnych WebSocket i long-polling
połączeń). Parametr worker_shutdown_timeout ustawia limit:
# /etc/nginx/nginx.conf worker_processes auto; worker_shutdown_timeout 30s; # Maksymalny czas na graceful shutdown workerów
Rolling update — wymiana instancji po jednej
Rolling update działa gdy masz load balancer i wiele instancji aplikacji. Wyłączasz backend z rotacji Nginx, wdrażasz nową wersję, weryfikujesz zdrowie, przywracasz do rotacji:
# /etc/nginx/sites-available/app.conf
upstream app_backend {
server 127.0.0.1:3001; # instancja 1
server 127.0.0.1:3002; # instancja 2
keepalive 32;
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://app_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_connect_timeout 5s;
proxy_read_timeout 30s;
# Retry na zdrowy backend gdy jeden padnie
proxy_next_upstream error timeout http_502 http_503;
proxy_next_upstream_tries 2;
}
# Health check endpoint (Nginx Plus lub przez limit_except)
location /nginx-health {
return 200 "healthy\n";
add_header Content-Type text/plain;
}
} Skrypt rolling update — wdrożenie po jednej instancji:
#!/bin/bash
# rolling-deploy.sh
set -euo pipefail
APP_DIR="/var/www/app"
PORTS=(3001 3002)
NGINX_CONF="/etc/nginx/sites-available/app.conf"
HEALTH_URL="http://localhost"
NEW_VERSION=${1:-"latest"}
log() { echo "[$(date '+%H:%M:%S')] $1"; }
health_check() {
local port=$1
local retries=10
while [ $retries -gt 0 ]; do
if curl -sf "http://localhost:${port}/health" > /dev/null 2>&1; then
return 0
fi
sleep 2
retries=$((retries - 1))
done
return 1
}
for PORT in "${PORTS[@]}"; do
log "=== Wdrażam na port $PORT ==="
# 1. Wyłącz instancję z load balancera
log "Wyłączam port $PORT z rotacji Nginx..."
sudo sed -i "s/server 127.0.0.1:${PORT};/server 127.0.0.1:${PORT} down;/" "$NGINX_CONF"
sudo nginx -t && sudo nginx -s reload
sleep 5 # Daj czas na drenaż aktywnych połączeń
# 2. Zatrzymaj starą instancję
log "Zatrzymuję starą instancję..."
pm2 stop "app-${PORT}" || true
# 3. Wdróż nowy kod
log "Wdrażam $NEW_VERSION..."
cd "$APP_DIR"
git pull origin main
npm ci --production
PORT=$PORT pm2 start ecosystem.config.js --only "app-${PORT}"
# 4. Health check nowej instancji
log "Sprawdzam zdrowie na porcie $PORT..."
if ! health_check "$PORT"; then
log "BŁĄD: Health check nieudany na porcie $PORT — rollback!"
sudo sed -i "s/server 127.0.0.1:${PORT} down;/server 127.0.0.1:${PORT};/" "$NGINX_CONF"
sudo nginx -s reload
exit 1
fi
# 5. Przywróć do rotacji
log "Przywracam port $PORT do rotacji..."
sudo sed -i "s/server 127.0.0.1:${PORT} down;/server 127.0.0.1:${PORT};/" "$NGINX_CONF"
sudo nginx -t && sudo nginx -s reload
log "Port $PORT zaktualizowany pomyślnie."
sleep 10 # Stabilizacja przed przejściem do kolejnej instancji
done
log "=== Rolling deploy zakończony ===" Blue-green deployment
Blue-green to dwa kompletne środowiska. Aktywne środowisko (blue) obsługuje ruch produkcyjny. Nowa wersja wdrażana jest na green, testowana, a następnie load balancer przełącza 100% ruchu:
# Struktura katalogów
/var/www/
app-blue/ # Blue environment — port 3001
app-green/ # Green environment — port 3002
app-current -> app-blue # Symlink na aktywne środowisko
# /etc/nginx/conf.d/blue-green.conf
# ACTIVE=blue (zmień na green żeby przełączyć)
upstream app_active {
server 127.0.0.1:3001; # blue
# server 127.0.0.1:3002; # green (odkomentuj po wdrożeniu)
} #!/bin/bash
# blue-green-switch.sh
set -euo pipefail
NGINX_CONF="/etc/nginx/conf.d/blue-green.conf"
BLUE_PORT=3001
GREEN_PORT=3002
# Wykryj aktualnie aktywne środowisko
if grep -q "server 127.0.0.1:${BLUE_PORT};" "$NGINX_CONF"; then
ACTIVE="blue"
ACTIVE_PORT=$BLUE_PORT
STANDBY="green"
STANDBY_PORT=$GREEN_PORT
else
ACTIVE="green"
ACTIVE_PORT=$GREEN_PORT
STANDBY="blue"
STANDBY_PORT=$BLUE_PORT
fi
echo "Aktywne: $ACTIVE (port $ACTIVE_PORT)"
echo "Standby: $STANDBY (port $STANDBY_PORT)"
# 1. Wdróż na standby
echo "Wdrażam na $STANDBY..."
cd "/var/www/app-${STANDBY}"
git pull origin main
npm ci --production
pm2 restart "app-${STANDBY}" --update-env
# 2. Sprawdź zdrowie standby
echo "Health check na porcie $STANDBY_PORT..."
sleep 5
HEALTH=$(curl -sf "http://localhost:${STANDBY_PORT}/health" || echo "FAIL")
if [ "$HEALTH" = "FAIL" ]; then
echo "Health check nieudany! Przerywam bez przełączenia."
exit 1
fi
# 3. Przełącz ruch (atomic swap)
echo "Przełączam ruch z $ACTIVE na $STANDBY..."
sed -i "s/server 127.0.0.1:${ACTIVE_PORT};/server 127.0.0.1:${STANDBY_PORT};/" "$NGINX_CONF"
nginx -t && nginx -s reload
# 4. Aktualizuj symlink
ln -sfn "/var/www/app-${STANDBY}" /var/www/app-current
echo "Przełączono na $STANDBY. Poprzednie środowisko $ACTIVE pozostaje gotowe do rollback."
echo "Rollback: bash blue-green-switch.sh (przełączy z powrotem)" Health checks — endpointy aplikacji
Każda aplikacja powinna eksponować endpoint /health sprawdzający stan wewnętrznych
zależności:
// health.ts — endpoint Node.js/Express
app.get('/health', async (req, res) => {
const checks = {
uptime: process.uptime(),
timestamp: Date.now(),
db: 'unknown' as string,
redis: 'unknown' as string,
};
try {
// Sprawdź połączenie DB
await db.query('SELECT 1');
checks.db = 'ok';
} catch {
checks.db = 'error';
}
try {
// Sprawdź Redis
await redis.ping();
checks.redis = 'ok';
} catch {
checks.redis = 'error';
}
const isHealthy = checks.db === 'ok' && checks.redis === 'ok';
const status = isHealthy ? 200 : 503;
res.status(status).json({
status: isHealthy ? 'ok' : 'degraded',
checks,
});
}); Porównanie strategii deployment
| Strategia | Downtime | Zasoby | Rollback | Kiedy używać |
|---|---|---|---|---|
| In-place restart | Tak (sekundy) | 1× | Trudny | Nigdy na produkcji z ruchem |
| Rolling update | Nie | 1× | Możliwy | Standardowy deploy z LB i 2+ instancjami |
| Blue-green | Nie | 2× | Natychmiastowy | Gdy potrzebujesz gwarantowanego rollback w 30s |
| Canary | Nie | 1.1×–2× | Natychmiastowy | Ryzykowne zmiany, testowanie na % ruchu |