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(wbase) — 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.localna serwerze) — nigdy nie commitujemy sekretów.APP_SECRETwygenerujesz 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.yamlzrestart: unless-stoppedi konfiguracją ze zmiennych/sekretów, - ✅ CI/CD w GitHub Actions (push na
main→ deploy przez SSH) + zalecana bramkamake check, - ✅ bezpieczne migracje (
--allow-no-migration, zmiany kompatybilne wstecz), - ✅ działający stan: push na
mainbuduje 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.