20

Rozdział 20 z 21

Deployment

Opublikowany

Wdrażamy blog na produkcję. Zbudujemy wieloetapowy obraz Docker, przygotujemy overlay compose.prod.yaml, skonfigurujemy automatyczny deploy z GitHub Actions oraz omówimy bezpieczne migracje przy wdrożeniu.

Czego się nauczysz

  • Wieloetapowy Dockerfile (base → dev → prod)
  • Overlay produkcyjny compose.prod.yaml
  • CI/CD w GitHub Actions (push na main → deploy)
  • Bezpieczne migracje przy deploymencie

Rozdział 20: Deployment

Blog jest napisany, przetestowany i udokumentowany — czas wypuścić go na produkcję. Zbudujemy wieloetapowy obraz Docker, przygotujemy overlay produkcyjny, skonfigurujemy automatyczny deploy z GitHub Actions i omówimy bezpieczne migracje.

Stan wejściowy: działający blog ze środowiskiem dev w Dockerze (Rozdział 1). Tu dokładamy warstwę produkcyjną.


Wieloetapowy Dockerfile

Jeden Dockerfile opisuje trzy etapy o wspólnej bazie — obraz produkcyjny nie zawiera narzędzi deweloperskich:

FROM php:8.4-fpm-alpine AS base
RUN apk add --no-cache postgresql-dev icu-dev libzip-dev git unzip \
    && docker-php-ext-install pdo_pgsql intl zip opcache
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/html

# --- etap deweloperski ---
FROM base AS dev
COPY composer.json composer.lock symfony.lock ./
RUN composer install --no-scripts --no-autoloader --no-interaction
COPY . .
RUN composer dump-autoload && composer run-script post-install-cmd --no-interaction

# --- etap produkcyjny ---
FROM base AS prod
ENV APP_ENV=prod
COPY composer.json composer.lock symfony.lock ./
RUN composer install --no-dev --no-scripts --no-autoloader --no-interaction
COPY . .
RUN composer dump-autoload --optimize \
    && composer run-script post-install-cmd --no-interaction \
    && php bin/console cache:warmup
  • composer install --no-dev — pomija zależności deweloperskie (PHPUnit, PHPStan…), mniejszy obraz.
  • dump-autoload --optimize — mapa klas dla szybszego autoloadingu.
  • cache:warmup — rozgrzewa cache Symfony w trakcie budowania obrazu, nie przy pierwszym żądaniu.
  • opcache (w base) — kompiluje PHP do bajtkodu; kluczowe dla wydajności prod.

Overlay produkcyjny

Bazowy compose.yaml (dev) nakładamy overlayem compose.prod.yaml, który zmienia tylko to, co produkcyjne:

# compose.prod.yaml
services:
  app:
    build:
      target: prod                    # buduj z etapu prod
    environment:
      APP_ENV: prod
      APP_SECRET: ${APP_SECRET}
      DATABASE_URL: postgresql://app:${POSTGRES_PASSWORD}@database:5432/app?serverVersion=16
    restart: unless-stopped
  nginx:
    ports: ["80:80"]
    restart: unless-stopped
  database:
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - database_data:/var/lib/postgresql/data:rw
    restart: unless-stopped

Uruchomienie łączy oba pliki (kolejność ma znaczenie — prod nadpisuje):

docker compose -f compose.yaml -f compose.prod.yaml up -d --build
  • restart: unless-stopped — kontenery wstają po awarii i po restarcie serwera.
  • Wartości ${...} przychodzą ze zmiennych środowiskowych (.env.local na serwerze) — nigdy nie commitujemy sekretów. APP_SECRET wygenerujesz np. openssl rand -hex 16.

CI/CD w GitHub Actions

Automatyczny deploy uruchamia się przy każdym push na main — workflow łączy się z serwerem przez SSH i aktualizuje aplikację:

# .github/workflows/deploy.yml
name: Deploy to Production
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Deploy via SSH
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.VPS_HOST }}
          username: ${{ secrets.VPS_USER }}
          key: ${{ secrets.VPS_SSH_KEY }}
          script: |
            set -e
            cd /var/www/app
            git pull origin main
            docker compose -f compose.yaml -f compose.prod.yaml up -d --build --remove-orphans
            docker compose -f compose.yaml -f compose.prod.yaml exec -T app \
              php bin/console doctrine:migrations:migrate --no-interaction --allow-no-migration
            docker compose -f compose.yaml -f compose.prod.yaml exec -T app \
              php bin/console cache:clear --env=prod
            docker image prune -f

Wymagane GitHub Secrets: VPS_HOST, VPS_USER, VPS_SSH_KEY.

Warto dodać bramkę jakości — osobny workflow CI, który uruchamia make check (PHPStan, PHP-CS-Fixer, testy) zanim cokolwiek trafi na produkcję. Dzięki testom z Rozdziałów 18–19 masz czym bramkować.


Bezpieczne migracje

Migracje uruchamiamy po starcie nowych kontenerów, z flagą chroniącą przed błędem, gdy nic nie ma do zrobienia:

php bin/console doctrine:migrations:migrate --no-interaction --allow-no-migration
  • --no-interaction — bez pytań (deploy jest automatyczny),
  • --allow-no-migration — nie przerywaj, gdy brak nowych migracji (deploy bez zmian w bazie).

Strategia bez przestojów: pisz migracje kompatybilne wstecz (najpierw dodaj kolumnę jako nullable, wdróż kod, dopiero potem ją uszczelnij) — stara i nowa wersja mogą chwilę współistnieć podczas wdrożenia.


Podsumowanie

Blog jest gotowy do produkcji:

  • ✅ wieloetapowy Dockerfile (base → dev → prod) z --no-dev, --optimize, cache:warmup, OPcache,
  • ✅ overlay compose.prod.yaml z restart: unless-stopped i konfiguracją ze zmiennych/sekretów,
  • ✅ CI/CD w GitHub Actions (push na main → deploy przez SSH) + zalecana bramka make check,
  • ✅ bezpieczne migracje (--allow-no-migration, zmiany kompatybilne wstecz),
  • ✅ działający stan: push na main buduje obraz, migruje bazę i wdraża blog na serwer.

W Rozdziale 21 — ostatnim — domkniemy SEO: sitemap XML, hreflang/canonical (z Rozdziału 14) oraz meta tagi Open Graph.

Narzędzia / paczki

Docker multi-stage GitHub Actions Nginx + Certbot

Spis treści