Szybki start w Symfony: Dockerfile i Makefile

Spis treści
Wstęp#
Ostatnio sporo czasu poświęciłem na optymalizację mojego środowiska lokalnego. Zauważyłem, że sam wpadłem w pułapkę nadmiernej komplikacji. Tworzyłem rozbudowane pliki docker-compose i ukrywałem logikę w skryptach, przez co traciłem z oczu to, co najważniejsze. Postanowiłem wrócić do korzeni i postawić na prostotę. Moja obecna konfiguracja dla Symfony opiera się na dwóch filarach: czystym Dockerze i pliku Makefile. W samym obrazie deweloperskim postawiłem na najnowszą wersję PHP oraz wsparcie dla SQLite. Dzięki temu mogę wystartować z developmentem błyskawicznie, bez tracenia czasu na konfigurowanie ciężkich kontenerów z bazami danych.
Dockerfile#
Zacznijmy od samego obrazu. Zdecydowałem się na podejście minimalistyczne.
FROM php:8.4-fpm-alpine
# Build arguments
ARG PORT=8000
ENV PORT=$PORT
# Composer environment variables
ENV COMPOSER_ALLOW_SUPERUSER=1
ENV COMPOSER_NO_INTERACTION=1
# Install system dependencies
RUN apk add --no-cache \
bash \
curl \
git \
make \
linux-headers \
libzip-dev \
oniguruma-dev \
icu-dev \
sqlite \
sqlite-dev \
&& docker-php-ext-install \
pdo \
pdo_sqlite \
zip \
mbstring \
intl
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Install Gitleaks
RUN wget https://github.com/gitleaks/gitleaks/releases/download/v8.18.2/gitleaks_8.18.2_linux_x64.tar.gz
RUN tar -xvzf gitleaks_8.18.2_linux_x64.tar.gz
RUN mv gitleaks /usr/bin/gitleaks
RUN chmod +x /usr/bin/gitleaks
# Set working directory
WORKDIR /app
# Create non-root user
RUN addgroup -g 1000 appuser && adduser -u 1000 -D -G appuser appuser
# Switch to non-root user
USER appuser
# Expose port for built-in PHP server
EXPOSE $PORT
CMD ["sh", "-c", "php -S 0.0.0.0:$PORT -t public"]
Dlaczego właśnie tak? Po pierwsze, wybrałem Alpine. Zależy mi na tym, żeby obraz był lekki i szybko się budował. Po drugie, kluczowa jest kwestia uprawnień. Uruchamiam aplikację jako użytkownik systemowy, a nie root. Dzięki temu unikam tych irytujących problemów z dostępem do plików na podmontowanych wolumenach, które wszyscy pewnie znamy.
Jednak najważniejsza jest dla mnie idea startowania z absolutnym minimum. W tym obrazie mam tylko to, co niezbędne, by zacząć pisać kod, stąd wybieram serwer http dostarczony przez PHP i SQLite. Oczywiście zdaję sobie sprawę, że w miarę rozwoju projektu pewnie dojdzie tu jakiś bardziej zaawansowny serwer http, wyspecjalizowane bazy danych jak MySQL czy Redis a dla przetwarzania równoległego system kolejek. I to jest w porządku. To podejście cenię właśnie za to, że pozwala mi wystartować natychmiast. Nie chcę na dzień dobry przytłaczać się „kombajnem”, który wprowadza niepotrzebną złożoność, zanim jeszcze napiszę pierwszą linię logiki biznesowej. Wolę rozbudowywać stack krok po kroku, dokładnie wtedy, gdy faktycznie tego potrzebuję.
Makefile#
Drugim elementem tej układanki jest wygoda codziennej pracy. Zamiast pisać długie komendy w terminalu, sięgnąłem po starego, dobrego make. Traktuję go tutaj jako zbiór skrótów do często używanych, czasem złożonych poleceń. Pozwala mi to zamknąć długie ciągi argumentów w krótkich, czytelnych hasłach typu make up czy make test, co znacząco przyspiesza pracę w terminalu.
.PHONY: help build up stop sh composer phpcsfixer-fix phpcsfixer-dry-run lint security phpstan phpunit qa rebuild init
# Variables
APP_CONTAINER = symfony-app
PORT = 8000
DOCKER_EXEC = docker exec -t $(APP_CONTAINER)
RUN_COMPOSER = $(DOCKER_EXEC) composer
##@ Help
help: ## Display this help
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-20s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
##@ Docker
build: ## Build the Docker image
docker build -t $(APP_CONTAINER) --build-arg PORT=$(PORT) . --no-cache
up: ## Start the app container
docker run --rm -d -v $(PWD):/app --name $(APP_CONTAINER) -p $(PORT):$(PORT) $(APP_CONTAINER)
stop: ## Stop the container
-docker stop $(APP_CONTAINER)
sh: ## Enter the container shell
docker exec -it $(APP_CONTAINER) sh
##@ Development
composer: ## Install Composer dependencies
$(RUN_COMPOSER) install --no-interaction
phpcsfixer-fix: ## Auto-fix code style (PHP-CS-Fixer)
$(DOCKER_EXEC) vendor/bin/php-cs-fixer fix --allow-risky=yes --diff
phpcsfixer-dry-run: ## Check code style (PHP-CS-Fixer in dry-run mode)
$(DOCKER_EXEC) vendor/bin/php-cs-fixer fix --allow-risky=yes --dry-run --diff
##@ Quality Assurance
lint: ## Run PHP linting (cache:clear, lint:yaml, lint:container)
$(DOCKER_EXEC) bin/console cache:clear
$(DOCKER_EXEC) bin/console lint:yaml config/
$(DOCKER_EXEC) bin/console lint:container
security: ## Run security checks (Composer audit + validate + gitleaks)
$(RUN_COMPOSER) audit
$(RUN_COMPOSER) validate --strict
$(RUN_COMPOSER) update roave/security-advisories --dry-run
$(DOCKER_EXEC) gitleaks protect --source .
$(DOCKER_EXEC) gitleaks detect --source .
phpstan: ## Run PHPStan static analysis
$(DOCKER_EXEC) vendor/bin/phpstan analyze src --memory-limit=-1
phpunit: ## Run PHPUnit tests
$(DOCKER_EXEC) vendor/bin/phpunit
qa: phpcsfixer-dry-run lint security phpstan phpunit ## Run full quality checks
##@ Automation
rebuild: stop build up composer lint security phpstan phpcsfixer-dry-run phpunit ## Full rebuild
##@ Setup
init: ## Initialize project (build, start, install dependencies)
@cp -n .env.example .env 2>/dev/null || true
@$(MAKE) stop
@$(MAKE) build
@$(MAKE) up
@sleep 2
@$(MAKE) composer
@echo ""
@echo "Done! App running at http://localhost:$(PORT)"
Podsumowanie#
Projektując ten plik, nie startowałem od zera. Dużą inspiracją był dla mnie artykuł The perfect MakeFile for Symfony ze strony Strangebuzz. To stamtąd zaczerpnąłem np. świetny mechanizm auto-dokumentacji (komenda help), choć samą zawartość mocno uprościłem i dostosowałem pod kątem mojej filozofii “jednego kontenera”
Tutaj również kieruję się zasadą pełnej transparentności. Zrezygnowałem ze skryptów w composer.json. Wolę widzieć wprost, co się dzieje. Kiedy uruchamiam analizę statyczną czy testy, chcę mieć jasność, jakie flagi są używane, na przykład te dotyczące limitu pamięci czy trybu ryzykownego.
Makefile pełni u mnie rolę centrum dowodzenia. Mam jeden plik, który obsługuje budowanie obrazu, uruchamianie kontenera i wszystkie narzędzia quality assurance. Dzięki temu nie muszę pamiętać długich komend ani zastanawiać się, co robi dany skrypt “pod maską”. To rozwiązanie jest też bardzo elastyczne każdy może dostosować je do własnych potrzeb.
To podejście daje mi pełną kontrolę i zrozumienie mojego środowiska. Oczywiście, to baza, którą można rozbudować, ale jako punkt wyjścia sprawdza się znakomicie. Jest prosto, szybko i przejrzyście.