This repository is a template for Symfony 7.3 API services running in Docker with a full set of infrastructure:
- PHP‑FPM 8.4 (Alpine) with Symfony 7.3 (API‑only skeleton)
- Nginx as HTTP entrypoint
- PostgreSQL 16
- Redis
- RabbitMQ
- Doctrine ORM + migrations
- Symfony Messenger (Doctrine transports + failure transport)
- Observability stack: Prometheus, Grafana, Loki, Promtail, exporters
- k6 for HTTP load testing
The goal is to provide a production‑like environment for local development and experiments with metrics, logging and messaging.
app/– Symfony application (Symfony 7.3 skeleton)src/Entity/Product.php– simpleProductentityRepository/ProductRepository.phpController/ProductController.php– sample API endpoints
config/– Symfony configuration (Doctrine, Messenger, Framework, Monolog, etc.)migrations/– Doctrine migrations (organized by year & month)bin/– console tools (bin/console,bin/phpunit)tools/– isolated Composer tools (viabamarni/composer-bin-plugin)rector,php-cs-fixer,phpstan
docker/php/– PHP‑FPM Dockerfile and configs (php.ini,php-fpm.conf,www.conf,xdebug.ini)nginx/– Nginx config + vhostpostgres/– tunedpostgresql.conf, queries for exporter,init.sql(pg_stat_statements)prometheus/– Prometheus configuration (scraping app exporters)grafana/– provisioned datasources and dashboards (HTTP, Redis, RabbitMQ)loki/– Loki configurationpromtail/– Promtail configuration for Docker log scrapingk6/load.js– k6 load script for/productsAPI
docker-compose.yml– all services (application + infra)Makefile– helper commands for running the stack and tools.env– root env vars (ports, resource sizes, etc.)app/.env– Symfony app env (DB & Messenger DSNs)
Defined in docker-compose.yml:
php– PHP 8.4 FPM (Alpine)- Built from
docker/php/Dockerfile - Uses
install-php-extensions(intl, opcache, pdo_pgsql, zip, xdebug in dev) - Runs as
www-data, working dir/var/www/app - Mounts
./appas project root
- Built from
nginx– Nginx 1.27 (Alpine)- Configured via
docker/nginx/nginx.confanddocker/nginx/conf.d/app.conf - Listens on port
8080in the container - Exposed as
APP_HTTP_PORT(default8080) on the host - Access log in JSON to stdout (used by Loki)
- Configured via
db– PostgreSQL 16 (Alpine)- Data volume:
db-data - Tuned with
docker/postgres/postgresql.conf pg_stat_statementsenabled viainit.sql- Healthcheck via
pg_isready
- Data volume:
redis– Redis 7 (Alpine)maxmemorytaken fromAPP_REDIS_MEMORY_LIMIT
rabbitmq– RabbitMQ 3 management- Default user/pass:
app/app - Ports:
- AMQP:
APP_RABBITMQ_PORT(default5672) - Management UI:
APP_RABBITMQ_MGMT_PORT(default15672)
- AMQP:
- Default user/pass:
- Exporters & observability
postgres-exporter– PostgreSQL metrics (Prometheus)redis-exporter– Redis metrics (oliver006/redis_exporter)rabbitmq-exporter– RabbitMQ metrics (kbudde/rabbitmq-exporter)prometheus– metrics storage (docker/prometheus/prometheus.yml)grafana– dashboards (Redis, RabbitMQ, HTTP, etc.)loki– log storagepromtail– collects Docker logs → Loki
k6– Grafana k6 image for load testing
- DB URL in
app/.env:
DATABASE_URL="postgresql://app:app@db:5432/app?serverVersion=16&charset=utf8"- Doctrine ORM mapping:
- Attributes in
src/Entity - Config in
app/config/packages/doctrine.yaml
- Attributes in
- Migrations:
- Config:
app/config/packages/doctrine_migrations.yamlorganize_migrations: BY_YEAR_AND_MONTH
- Example migration:
app/migrations/Version20250101000000.php(createsproducttable)
- Config:
Entity: App\Entity\Product
Fields:
id– integer, PKname– stringprice– decimal(10, 2) (stored as string)createdAt–DateTimeImmutableupdatedAt– nullableDateTimeImmutable
Controller: App\Controller\ProductController
Routes:
-
GET /products- Returns last 50 products ordered by
id DESC. - Response example:
[ { "id": 1, "name": "Product-ABC", "price": "19.99", "createdAt": "2025-01-01T12:00:00+00:00", "updatedAt": null } ]
- Returns last 50 products ordered by
-
POST /products- Request body:
{ "name": "Product name", "price": 9.99 } - On success:
201 Createdwith created product. - On invalid payload:
400 Bad Request.
- Request body:
Package: symfony/messenger
Config: app/config/packages/messenger.yaml
-
Bus:
messenger.bus.default- Custom middleware stack:
reject_redelivered_message_middlewarevalidationdoctrine_ping_connectionadd_bus_name_stamp_middleware: ['messenger.bus.default']dispatch_after_current_bussend_messagefailed_message_processing_middlewarehandle_messagedoctrine_close_connection
-
Failure transport:
failure_transport: failedfailedtransport usesdoctrine://default?queue_name=failed
-
Transports (all customizable via env):
async,notifications,search_index,modules_bell_async,scheduler_default
-
Env (
app/.env):
MESSENGER_TRANSPORT_DSN=doctrine://default
MESSENGER_MODULES_BELL_ASYNC_TRANSPORT_DSN=doctrine://default- Test override (
when@test):asyncandnotificationsusetest://DSNs.
Isolated via bamarni/composer-bin-plugin with target directory tools:
- Rector:
app/tools/rector/vendor/bin/rector- Config:
app/rector.php
- PHP CS Fixer:
app/tools/php-cs-fixer/vendor/bin/php-cs-fixer- Config:
app/.php-cs-fixer.dist.php
- PHPStan:
app/tools/phpstan/vendor/bin/phpstan- Config:
app/phpstan.neon.dist
- Installed as dev dependency in
app/composer.json. - Config:
app/phpunit.dist.xml. - Run inside PHP container:
make php→php bin/phpunit
Scrape configs in docker/prometheus/prometheus.yml:
prometheusitself- PostgreSQL exporter (
postgres-exporter:9187) - Redis exporter (
redis-exporter:9121) - RabbitMQ exporter (
rabbitmq-exporter:9419)
Provisioned via:
- Datasources:
docker/grafana/provisioning/datasources/prometheus.ymldocker/grafana/provisioning/datasources/loki.yml
- Dashboards:
- Redis –
docker/grafana/dashboards/redis.json - RabbitMQ –
docker/grafana/dashboards/rabbitmq.json - HTTP RPS & latency (via Loki) –
docker/grafana/dashboards/http.json
- Redis –
Grafana runs on APP_GRAFANA_PORT (default 3000), default admin credentials: admin/admin.
- Loki:
- Config:
docker/loki/config.yml - Exposed on
APP_LOKI_PORT(default3100)
- Config:
- Promtail:
- Config:
docker/promtail/config.yml - Scrapes Docker logs via
/var/run/docker.sock - Adds labels
service,container,compose_project,stream.
- Config:
PHP & Nginx are configured to write logs to stdout/stderr:
- Monolog:
- Dev/test:
php://stdout - Prod:
php://stderr(JSON)
- Dev/test:
- Nginx:
- JSON access logs to
stdoutwithrequest_timeandupstream_response_time
- JSON access logs to
In Grafana → Explore → Loki you can query:
{service="nginx"}– HTTP logs{service="php"}– Symfony/PHP logs
Script: docker/k6/load.js
- Scenario:
vus: 10,duration: 30s- For each VU:
POST /products(create product)GET /products(list products)sleep(1)
Base URL:
- Inside Docker network:
http://nginx:8080 - Configurable via env var
BASE_URL.
Service k6 in docker-compose.yml uses the official grafana/k6 image and mounts docker/k6 as /scripts.
From the repository root:
- Start stack:
make up- App URL is printed (uses
APP_HTTP_PORTfrom.env)
- Rebuild only PHP container:
make php-rebuild
- Shell inside PHP container:
make php
Static analysis / code style (run inside PHP container via Docker):
make phpstan– runs PHPStan withphpstan.neon.distmake cs-fix– runs PHP CS Fixer with.php-cs-fixer.dist.phpmake rector– runs Rector withrector.php
Load testing:
make k6– runs k6 withdocker/k6/load.jsagainst the running stack.
Prerequisites:
- Docker + Docker Compose
- Make (optional but recommended)
Steps:
-
Clone the repo:
git clone <this-repo-url> cd symfony-template-docker
-
Start the stack:
make up
-
Apply database migrations:
make php php bin/console doctrine:migrations:migrate
-
Test the API:
curl http://localhost:8080/products curl -X POST http://localhost:8080/products \ -H 'Content-Type: application/json' \ -d '{"name":"Test","price":9.99}'
-
Run a basic load test:
make k6
-
Explore metrics and logs:
- Prometheus:
http://localhost:9090 - Grafana:
http://localhost:3000 - Loki via Grafana → Explore (logs from
nginx,php, etc.)
- Prometheus:
This template is intentionally minimal on domain code and heavy on infrastructure.
You are expected to:
- Add your own entities, message handlers and routing.
- Switch Messenger transports to RabbitMQ (AMQP) if needed.
- Extend Grafana dashboards and alerting rules for your use‑cases.
Use this as a starting point for new Symfony API projects with Docker‑first, observability‑ready setup.