18

Dzień 18 z 18

Deployment

Opublikowany

Ostatni dzień — wdrażamy Jobeet na produkcję. Zbudujemy wieloetapowy obraz Docker, przygotujemy overlay compose.prod.yaml, skonfigurujemy automatyczny deploy z GitHub Actions oraz omówimy sekrety i migracje. Wszystko w oparciu o realne pliki tego projektu.

Czego się nauczysz

  • Wieloetapowy Dockerfile: base → dev → prod
  • Overlay produkcyjny compose.prod.yaml (restart, zmienne)
  • Zmienne środowiskowe i sekrety (.env.local, GitHub Secrets)
  • Przygotowanie VPS: Docker + Nginx reverse proxy + SSL
  • CI/CD w GitHub Actions (push na main → deploy przez SSH)
  • Bezpieczne migracje przy deploymencie

Co zmieniło się w Symfony 8 vs 4.2

  • Multi-stage Docker build (nie istniał w oryginale z 2018)
  • GitHub Actions zamiast ręcznego deploymentu (Capifony/rsync)
  • Zmienne środowiskowe i Secrets Vault zamiast databases.yml
  • composer install --no-dev --optimize-autoloader w produkcji

Dzień 18: Deployment

To ostatni dzień. Jobeet jest napisany, przetestowany i zoptymalizowany — pora wypuścić go na produkcję. Zbudujemy wieloetapowy obraz Docker, przygotujemy overlay compose.prod.yaml, skonfigurujemy automatyczny deploy z GitHub Actions i omówimy sekrety oraz migracje. Wszystko w oparciu o pliki, które realnie znajdują się w tym projekcie.

Zmiana vs Symfony 4.2: oryginał z 2018 wdrażał aplikację ręcznie — przez rsync/FTP albo Capifony, z konfiguracją baz w config/databases.yml per-środowisko. Dziś standardem jest kontener Docker (multi-stage build), CI/CD w GitHub Actions i konfiguracja przez zmienne środowiskowe + sekrety, nigdy commitowane do repo.

Status: deployment tego projektu jest przygotowany, ale czeka na wybór hostingu. Pliki (Dockerfile, compose.prod.yaml, .github/workflows/deploy.yml, docker/setup-vps.sh) są gotowe do użycia — poniżej omawiamy je krok po kroku.


Wieloetapowy Dockerfile

Jeden Dockerfile opisuje trzy etapy o wspólnej bazie. Dzięki temu obraz produkcyjny nie zawiera narzędzi deweloperskich, a build jest szybki (współdzielona warstwa base):

FROM php:8.4-fpm-alpine AS base

RUN apk add --no-cache postgresql-dev icu-dev libzip-dev git unzip \
        nodejs npm chromium nss freetype harfbuzz ca-certificates ttf-freefont \
    && 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

Kluczowe różnice etapu prod:

  • composer install --no-dev — pomija zależności deweloperskie (PHPUnit, PHPStan itd.), mniejszy obraz.
  • dump-autoload --optimize — mapa klas dla szybszego autoloadingu (bez skanowania katalogów).
  • cache:warmup — rozgrzewa cache Symfony w trakcie budowania obrazu, nie przy pierwszym żądaniu.
  • opcache (instalowany w base) — kompiluje PHP do bajtkodu w pamięci; kluczowy dla wydajności prod.

Zmiana vs Symfony 4.2: multi-stage build nie istniał w oryginale (Docker dopiero raczkował). Dziś to standard — jeden plik, a --target dev / --target prod wybiera odpowiedni obraz.


Overlay produkcyjny — compose.prod.yaml

Nie duplikujemy całej konfiguracji Compose. Bazowy compose.yaml (dev) nakładamy overlayem compose.prod.yaml, który zmienia tylko to, co produkcyjne:

services:
  app:
    build:
      target: prod                    # buduj z etapu prod Dockerfile
    environment:
      APP_ENV: prod
      APP_SECRET: ${APP_SECRET}
      DATABASE_URL: postgresql://app:${POSTGRES_PASSWORD}@database:5432/app?serverVersion=16
      MAILER_DSN: ${MAILER_DSN:-null://null}
    restart: unless-stopped

  worker:
    build:
      target: 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.
  • worker (Messenger z Dnia 13) też działa w prod — wysyła maile z kolejki.
  • Wartości ${...} przychodzą ze zmiennych środowiskowych — nie ma ich w repo.

Zmienne środowiskowe i sekrety

Produkcyjne wartości trzymamy poza repozytorium. W repo jest tylko szablon .env.prod.example:

APP_ENV=prod
APP_SECRET=zmien_na_losowy_32_znakowy_string
POSTGRES_PASSWORD=zmien_na_silne_haslo
MAILER_DSN=null://null

Na serwerze kopiujesz go do .env.local (plik ignorowany przez Git) i uzupełniasz realnymi wartościami:

cp .env.prod.example .env.local
nano .env.local          # wpisz prawdziwy APP_SECRET i hasło do bazy

Zasady:

  • APP_SECRET to losowy 32-znakowy ciąg (podpisuje ciasteczka/CSRF) — wygeneruj openssl rand -hex 16.
  • Nigdy nie commituj .env.local ani haseł. Sekrety CI trzymaj w GitHub Secrets.
  • Do wrażliwych danych w repo Symfony ma też Secrets Vault (secrets:set) — szyfrowane sekrety w kontroli wersji z kluczem trzymanym osobno.

Zmiana vs Symfony 4.2: dawniej konfiguracja środowisk siedziała w plikach YAML (databases.yml, app.yml) i łatwo było przez pomyłkę wypchnąć hasło. Dziś standardem jest .env + zmienne środowiskowe (12-factor app) oraz szyfrowany Secrets Vault.


Przygotowanie serwera (VPS)

Skrypt docker/setup-vps.sh przygotowuje świeży serwer Ubuntu jednym poleceniem — instaluje Dockera, Nginx jako reverse proxy i certyfikat SSL przez Let's Encrypt:

bash docker/setup-vps.sh twoja-domena.pl

Skrypt w skrócie:

  1. instaluje Docker + Compose plugin,
  2. instaluje Nginx + Certbot,
  3. klonuje repozytorium do /var/www/factorycode,
  4. konfiguruje Nginx jako proxy na localhost:80 (kontener aplikacji),
  5. wystawia HTTPS komendą certbot --nginx.

Nginx na hoście terminuje SSL i przekazuje ruch do kontenera — dzięki temu aplikacja w Dockerze nie musi znać certyfikatów.


CI/CD w GitHub Actions

Automatyczny deploy uruchamia się przy każdym push na main. Workflow .github/workflows/deploy.yml łączy się z serwerem przez SSH i aktualizuje aplikację:

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/factorycode
            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 + typecheck + testy) zanim cokolwiek trafi na produkcję:

# .github/workflows/ci.yml (zalecane uzupełnienie)
name: CI
on: [push, pull_request]
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: docker compose build
      - run: docker compose run --rm app make check

Zmiana vs Symfony 4.2: deploy w 2018 był ręczny (Capifony, symfony sync, rsync). Dziś push na main = deploy — powtarzalny, zautomatyzowany, z pełną historią w Actions.


Migracje przy deploymencie

Migracje bazy uruchamiamy po starcie nowych kontenerów, 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).

Bezpieczna strategia: pisz migracje kompatybilne wstecz (najpierw dodaj kolumnę jako nullable, wdróż kod, dopiero potem ją uszczelnij). Dzięki temu stara i nowa wersja aplikacji mogą chwilę współistnieć podczas wdrożenia, bez przerwy w działaniu.


Podsumowanie

Jobeet jest gotowy do produkcji:

  • ✅ wieloetapowy Dockerfile (base → dev → prod) z --no-dev, --optimize, cache:warmup i OPcache,
  • ✅ overlay compose.prod.yaml z restart: unless-stopped i konfiguracją ze zmiennych,
  • ✅ sekrety poza repo (.env.local, GitHub Secrets, opcjonalnie Secrets Vault),
  • ✅ przygotowanie VPS skryptem setup-vps.sh (Docker + Nginx reverse proxy + SSL),
  • ✅ CI/CD w GitHub Actions (push na main → deploy przez SSH) + zalecana bramka make check,
  • ✅ bezpieczne migracje (--allow-no-migration, zmiany kompatybilne wstecz).

To już koniec 🎉

Przeszliśmy przez pełny cykl życia aplikacji Symfony 8 — od symfony new (Dzień 1), przez model danych, kontrolery, formularze, bezpieczeństwo, API, maile, tłumaczenia i testy, aż po wdrożenie na produkcję. Masz teraz kompletny, nowoczesny punkt odniesienia do własnych projektów.

Dziękujemy za przejście całego tutoriala Jobeet — powodzenia we własnym kodzie!

Narzędzia / paczki

Docker multi-stage GitHub Actions Nginx + Certbot PHP 8.4 OPcache

Spis treści