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 wconfig/databases.ymlper-ś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 wbase) — 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 prodwybiera 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_SECRETto losowy 32-znakowy ciąg (podpisuje ciasteczka/CSRF) — wygenerujopenssl rand -hex 16.- Nigdy nie commituj
.env.localani 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:
- instaluje Docker + Compose plugin,
- instaluje Nginx + Certbot,
- klonuje repozytorium do
/var/www/factorycode, - konfiguruje Nginx jako proxy na
localhost:80(kontener aplikacji), - 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 namain= 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:warmupi OPcache, - ✅ overlay
compose.prod.yamlzrestart: unless-stoppedi 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 bramkamake 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!