Środowisko deweloperskie — od lokalnego devu do produkcji w dwóch kontenerach
symfony,php,docker,ddev,raspberry-pi,homelabCzęść 5 z 5
- 19 lat z WordPressem — dlaczego w końcu zrezygnowałem
- Symfony jako generator stron statycznych — jak działa holas.pl
- Zabezpieczenie jedynego dynamicznego endpointu strony statycznej — formularz kontaktowy
- Drzewo decyzyjne narzędzi dla Claude Code z globalną pamięcią
- Środowisko deweloperskie — od lokalnego devu do produkcji w dwóch kontenerach
To część 4 serii o migracji holas.pl z WordPressa do własnego generatora stron statycznych opartego na Symfony. Część 3 opisuje bezpieczeństwo formularza kontaktowego.
Jednym z celów dla holas.pl było środowisko deweloperskie tak proste jak produkcyjne. Brak bazy danych do uruchomienia, brak ręcznej konfiguracji sieci Docker, brak pięciominutowej sekwencji startowej. Efektem jest konfiguracja oparta na DDEV uruchamiająca się jednym poleceniem i stos produkcyjny działający w dwóch kontenerach.
Development z DDEV
DDEV zarządza lokalnym środowiskiem deweloperskim. Cała konfiguracja jest w .ddev/config.yaml:
name: holas-pl
type: php
php_version: "8.5"
webserver_type: nginx-fpm
nodejs_version: "22"
omit_containers: [db]
web_environment:
- APP_ENV=dev
Linia omit_containers: [db] jest znacząca — nie ma bazy danych, więc nie ma kontenera bazy danych. Domyślna konfiguracja MySQL/MariaDB DDEV jest całkowicie pominięta. ddev start uruchamia nginx + PHP-FPM i nic więcej.
W developmencie drzewo treści jest przebudowywane przy każdym żądaniu, więc edycja pliku Markdown i odświeżenie przeglądarki od razu pokazuje zmianę. Brak cache do czyszczenia. Pasek narzędzi debugowania Symfony jest dostępny. Wpisy draft są widoczne.
Polecenie build
ddev build uruchamia pełny pipeline:
rm -rf var/dart-sass # usuwa binarny plik specyficzny dla architektury (patrz niżej)
php bin/console cache:clear
php bin/console sass:build # kompiluje SCSS przez dart-sass
php bin/console asset-map:compile # fingerprinting assetów
php bin/console app:build # renderuje wszystkie URL do public/static/
npx pagefind --site public/static --output-path public/pagefind
Po tym public/static/ zawiera kompletną stronę jako pliki HTML. nginx serwuje z tego katalogu. Krok dart-sass usuwa binarny plik specyficzny dla platformy przed buildem, aby wymusić świeże pobranie — więcej o tym poniżej.
Kontrola jakości kodu
ddev code-check uruchamia cztery sprawdzenia po kolei:
composer validate --strict
composer audit --no-dev # sprawdza znane CVE w zależnościach
vendor/bin/php-cs-fixer fix --dry-run --diff
vendor/bin/phpstan analyse # poziom 6
ddev code-fix automatycznie naprawia problemy PHP CS Fixer i ponownie uruchamia sprawdzenie. PHPStan poziom 6 wyłapuje brakujące type hinty, złe typy argumentów i nieznane metody przed dotarciem do produkcji.
Styleguide
Strona dostępna tylko w deweloperskim środowisku pod adresem /styleguide/ dokumentuje każdy komponent UI używając rzeczywistych klas CSS — nie wrapperów ani migawek. Strona jest serwowana tylko w środowisku dev (kontroler rzuca 404 w produkcji) i nie jest uwzględniana w statycznym buildzie.
Wartość styleguide'a jest w developmencie: zmiana SCSS komponentu natychmiast aktualizuje styleguide. Nie ma osobnego systemu designu do synchronizowania.
Pipeline assetów bez Node.js
SCSS jest kompilowany przez symfonycasts/sass-bundle, który opakowuje dart-sass i nie wymaga instalacji Node.js. Jeden punkt wejścia assets/styles/app.scss importuje wszystkie pliki częściowe. W developmencie kompiluje w locie. W produkcji php bin/console sass:build uruchamia się przed buildem.
Moduły JavaScript są obsługiwane przez AssetMapper Symfony — bez webpacka, bez Vite, bez rollupa. Skrypty są ładowane jako zwykłe tagi <script src="..."> z nazwami plików zawierającymi hash treści. Import map rejestruje tylko główny punkt wejścia app.js; inne skrypty (formularz kontaktowy, wyszukiwanie, baner cookies) są ładowane osobno przez {{ asset('script.js') }} w szablonach.
Problem z binarnym plikiem dart-sass: symfonycasts/sass-bundle pobiera binarny plik dart-sass specyficzny dla platformy do var/dart-sass/. Plik binarny skompilowany na maszynie deweloperskiej (x86_64) nie uruchomi się w kontenerze produkcyjnym Docker (również x86_64 w tym przypadku, ale ścieżka i wersja binarna mogą się różnić). Rozwiązanie jest proste: usuń plik binarny przed każdym buildem i pozwól dart-sass pobrać właściwy dla bieżącej platformy.
Produkcja: dwa kontenery
Produkcyjny docker-compose.yaml:
services:
nginx:
image: nginx:alpine
volumes:
- ./docker/nginx.conf:/etc/nginx/conf.d/default.conf:ro
- .:/app:ro
depends_on:
- php
php:
build:
context: .
dockerfile: docker/Dockerfile
user: "${UID:-1000}:${GID:-1000}"
volumes:
- .:/app
Dwa kontenery: nginx i PHP-FPM. Brak kontenera bazy danych. nginx montuje projekt jako tylko do odczytu; PHP-FPM działa jako użytkownik hosta, aby uniknąć problemów z uprawnieniami plików cache Symfony.
Obraz PHP (docker/Dockerfile) to php:8.5-fpm-alpine z tylko tym, co potrzebne: icu-dev (Symfony intl), nodejs i npm (dla kroku budowania Pagefind), unzip, git i Composer.
Deployment
Cały deployment to jeden skrypt ./deploy.sh --prod:
docker compose downdocker compose build --pull— przebudowuje obraz PHP od zeradocker compose up -dcomposer install --no-dev --optimize-autoloaderphp bin/console cache:clearrm -rf var/dart-sass— usuwa plik binarny specyficzny dla architekturyphp bin/console sass:buildphp bin/console asset-map:compilephp bin/console app:build— renderuje cały statyczny HTMLnpx --yes pagefind --site public/static --output-path public/pagefind
Build uruchamia się wewnątrz kontenera PHP, gdzie znana jest poprawna architektura CPU. Po kroku 10 nginx już serwuje statyczne pliki poprzedniego buildu. Nowe pliki zastępują je na poziomie systemu plików. Istnieje krótkie okno, w którym częściowy build jest live, ale dla portfolio z niewielkim ruchem jest to akceptowalne bez złożoności blue-green deploymentu.
Brak pipeline'u CI/CD, brak środowiska stagingowego. Deployment to ssh server, cd holas.pl, git pull, ./deploy.sh --prod.
Porównanie z WordPressem
Pełny produkcyjny stos WordPress wymagał: PHP-FPM, MySQL, warstwy cache (Redis lub system plików), uruchamiania zaplanowanych zadań dla wp-cron i wystarczająco dużo RAM, żeby utrzymać MySQL w pamięci podręcznej. Aktualizacje dowolnego komponentu wymagały przestoju lub starannego sekwencjonowania.
Obecny stos to dwa kontenery. Raspberry Pi 5 obsługuje je bez presji na pamięć. Deployment to skrypt shell. Cała baza kodu, treść i konfiguracja mieszczą się w jednym repozytorium git. Backup to git push.
Kolejny wpis z serii opisuje najbardziej niekonwencjonalną część tego projektu: budowanie całej strony z Claude Code jako AI pair programmerem i używanie narzędzi MCP do lokalnego generowania obrazów.