Valentin Cojocaru Web developer · Cluj-Napoca EN

Valentin
Cojocaru

Construiesc aplicații web și mă ocup de serverul, DNS-ul și baza de date din spatele lor.

blog

DevOps 9 min de citit

Deploy fără downtime pentru o aplicație Yii2, cu Docker și Nginx

Majoritatea aplicațiilor Yii2 pe care le-am întâlnit se publică la fel: intri prin SSH pe server, rulezi git pull, poate o migrație, cureți cache-ul și reîmprospătezi browserul să vezi dacă s-a stricat ceva. Dacă migrația durează treizeci de secunde, utilizatorii tăi plătesc acele treizeci de secunde cu erori.

Există o cale mai bună. Cu Docker și două containere care se schimbă între ele în spatele lui Nginx, poți publica cod nou fără nicio întrerupere. Containerul vechi servește cererile cât timp pornește cel nou, iar comutarea se face la nivel de proxy, după ce containerul nou trece de health check.

Ideea de bază

Rulezi două containere pe același server, pe porturi diferite. Nginx trimite traficul doar către unul dintre ele. La deploy, pornești containerul nou pe portul liber, aștepți să fie sănătos, apoi îi spui lui Nginx să comute. Containerul vechi primește un răgaz ca să termine cererile în curs, apoi se oprește.

Iată cum arată partea relevantă din docker-compose.yml:

services:
  app_blue:
    image: yourapp:${VERSION}
    ports:
      - "9001:9000"
    environment:
      - APP_ENV=production
    healthcheck:
      test: ["CMD", "php", "yii", "health/check"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 30s

  app_green:
    image: yourapp:${VERSION}
    ports:
      - "9002:9000"
    environment:
      - APP_ENV=production
    healthcheck:
      test: ["CMD", "php", "yii", "health/check"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 30s

Configurarea Nginx

Ține upstream-ul activ într-un mic fișier inclus, pe care îl schimbi atomic:

upstream app_active {
    include /etc/nginx/conf.d/active_upstream.conf;
    keepalive 32;
}

server {
    listen 80;
    server_name yourdomain.com;

    location / {
        proxy_pass         http://app_active;
        proxy_set_header   Host $host;
        proxy_set_header   X-Real-IP $remote_addr;
        proxy_read_timeout 60s;
    }
}

Fișierul active_upstream.conf conține o singură linie: server 127.0.0.1:9001; pentru blue sau server 127.0.0.1:9002; pentru green.

Scriptul de deploy

#!/usr/bin/env bash
set -euo pipefail

VERSION=$1
CURRENT=$(cat /var/deploy/active_slot)  # "blue" or "green"
NEXT=$( [ "$CURRENT" = "blue" ] && echo "green" || echo "blue" )
NEXT_PORT=$( [ "$NEXT" = "blue" ] && echo "9001" || echo "9002" )

echo "Deploying $VERSION to $NEXT slot (port $NEXT_PORT)"

# Pull and start new container
VERSION=$VERSION docker compose up -d app_$NEXT

# Wait for health check
echo "Waiting for health check..."
until docker inspect --format "{{.State.Health.Status}}" app_$NEXT | grep -q healthy; do
    sleep 3
done

# Run migrations (idempotent, fast)
docker exec app_$NEXT php yii migrate --interactive=0

# Switch Nginx
echo "server 127.0.0.1:$NEXT_PORT;" > /etc/nginx/conf.d/active_upstream.conf
nginx -s reload

# Save state
echo $NEXT > /var/deploy/active_slot

# Stop old container after grace period
sleep 10
docker compose stop app_$CURRENT

echo "Deploy complete. Active slot: $NEXT"

Comanda de health check în Yii2

Ai nevoie de o comandă de consolă pe care Docker s-o poată apela pentru health check. Ar trebui să verifice că baza de date răspunde și că serviciile critice merg:

<?php
namespace consolecontrollers;

use yiiconsoleController;
use Yii;

class HealthController extends Controller
{
    public function actionCheck(): int
    {
        // Check DB
        Yii::$app->db->createCommand("SELECT 1")->execute();

        // Check cache
        Yii::$app->cache->set("health_check", time(), 60);

        return 0;
    }
}

Migrațiile în timpul unui deploy fără downtime

Aici dau oamenii de probleme. Dacă migrația șterge o coloană pe care codul vechi încă o citește, containerul vechi începe să arunce erori din clipa în care rulează migrația. Soluția sunt migrațiile compatibile înapoi: adaugi coloanele înainte să publici codul care le folosește și le ștergi abia după ce codul care le folosea a dispărut din toate containerele.

În majoritatea proiectelor Yii2 asta nu e o problemă în practică. Migrațiile de obicei doar adaugă, iar intervalul dintre rularea migrației și oprirea containerului vechi e sub zece secunde.

Ce câștigi

Poți face release-uri la orice oră, fără să programezi o fereastră de mentenanță. Ai o evidență clară a versiunii care rulează pe fiecare slot. Faci rollback rulând din nou scriptul de deploy cu versiunea anterioară. Iar utilizatorii tăi nu simt nimic în ce era înainte partea cea mai riscantă a săptămânii tale.

#Docker #Nginx #CI/CD #Yii2 #Deployment

Zero to Production: Yii2