For a long time, I deployed my Symfony applications the traditional way: PHP installed directly on the server, a web server such as Nginx or Apache, Composer installed on the machine, and a database configured separately.
It worked well. In fact, this is how I deployed many of my projects.
Then, in a professional environment, Docker became part of the required workflow. I had to rethink the way I approached deployment. Instead of treating the server as an environment that had to be manually configured, I could define that environment directly in the project.
That’s when Docker started to make much more sense to me.
In this article, I’ll show how I deploy a Symfony application using Docker, FrankenPHP, MariaDB, and Docker Compose.
The goal is not to present the only correct way to deploy Symfony. It is simply a practical approach that can be adapted to your own projects. In this example, I use PHP 8.4 and MariaDB, but you can of course use another supported PHP version or database engine depending on your application’s requirements.
1. Why use Docker for Symfony?
Without Docker, a Symfony server might look something like this:
Server
├── PHP
├── Composer
├── MariaDB
├── Nginx
└── Symfony applicationEach component needs to be installed and configured directly on the server.
This can become problematic when your development and production environments are not identical.
For example, you might develop with PHP 8.5 while the server uses PHP 8.2. A PHP extension may be installed locally but missing in production. Database configuration can also differ between environments.
With Docker, we can define the environment using configuration files:
Server
└── Docker
├── FrankenPHP
└── MariaDBThe environment becomes reproducible.
That’s probably one of the main benefits I look for when using Docker: I don’t have to rely as much on manually configured servers.
2. Why FrankenPHP?
The interesting part of this setup is that we’re not going to use the traditional architecture:
Nginx → PHP-FPM → SymfonyInstead, we’ll use FrankenPHP.
FrankenPHP is a modern application server built on top of Caddy that can directly serve PHP applications.
This allows us to simplify our architecture:
Browser
│
▼
FrankenPHP
│
▼
Symfony
│
▼
MariaDBWe don’t need an additional Nginx container just to forward requests to PHP-FPM.
For a Symfony application, this can be a very convenient approach, especially when you want a relatively simple containerized architecture.
3. Project architecture
For this example, we’ll use two main services:
- FrankenPHP to run Symfony;
- MariaDB for the database.
Docker Compose will orchestrate both services.
The final architecture will look like this:
Internet
│
▼
FrankenPHP
│
▼
Symfony
│
▼
MariaDBA possible project structure is:
my-project/
├── docker/
│ └── php/
│ └── Dockerfile
├── public/
├── src/
├── config/
├── migrations/
├── composer.json
├── compose.yaml
└── ...The Dockerfile defines our PHP/FrankenPHP environment, while compose.yaml describes the services that make up our application.
4. Creating the FrankenPHP image
Let’s start with the Dockerfile.
We can use the official FrankenPHP image:
FROM dunglas/frankenphp:php8.4I’m using PHP 8.4 here simply because this is the version used for my example. You can replace it with another PHP version supported by your Symfony application.
We can then install the PHP extensions required by the project:
RUN install-php-extensions \
pdo_mysql \
intl \
zip \
opcacheThe exact extensions will depend on your application.
For a Symfony application using Doctrine with MariaDB, pdo_mysql is one of the important extensions.
We can also install Composer:
COPY --from=composer:2 /usr/bin/composer /usr/bin/composerWe now have an image containing PHP, FrankenPHP and Composer.
5. Adding the Symfony application
We need to copy our Symfony application into the container.
In the Dockerfile:
WORKDIR /appCOPY . .Our application will then be available at:
/appWe can install the Composer dependencies as part of the image build:
RUN composer install \
--no-dev \
--optimize-autoloader \
--no-interactionFor a production image, this prevents development-only dependencies from being installed.
6. Configuring FrankenPHP
FrankenPHP uses Caddy as its web server, so we can use a Caddyfile to configure how Symfony should be served.
For example:
{
frankenphp
}:80 {
root * /app/public encode zstd gzip php_server
}
The important part here is:
root * /app/publicSymfony uses the public/ directory as the application's entry point.
The:
php_serverdirective tells FrankenPHP to serve the PHP application. You can learn more about it on this page: Configuring a Web Server (Symfony Docs).
We therefore don’t need to manually configure Nginx and PHP-FPM.
7. Adding MariaDB with Docker Compose
Now let’s define our database in compose.yaml.
A simplified configuration could look like this:
services:
php:
build:
context: .
dockerfile: docker/php/Dockerfile
ports:
- "80:80"
depends_on:
- databasedatabase:
image: mariadb:11
environment:
MARIADB_DATABASE: app
MARIADB_USER: app
MARIADB_PASSWORD: password
MARIADB_ROOT_PASSWORD: root
volumes:
- database_data:/var/lib/mysqlvolumes:
database_data:
We now have two services:
php
databaseThe first one contains Symfony and FrankenPHP.
The second one contains MariaDB.
I’m using MariaDB 11 here as an example. There is nothing Docker-specific about this choice: you can use another MariaDB version or another database engine if it better fits your project.
8. Why use a volume for MariaDB?
A container is designed to be easily removed and recreated.
That’s fine for an application container, but it’s obviously different for a database.
If we remove a MariaDB container without persistent storage, we can lose its data.
That’s why we have:
volumes:
- database_data:/var/lib/mysqland:
volumes:
database_data:Docker stores the database data in a volume that is independent from the lifecycle of the container.
An important rule to remember is:
Containers can be recreated. Data must be persistent.
9. Connecting Symfony to MariaDB
This is one of the first things that can be confusing when starting with Docker.
Outside Docker, we might have:
DATABASE_URL="mysql://app:password@127.0.0.1:3306/app"Inside Docker, 127.0.0.1 refers to the current container, not the MariaDB container.
We therefore use the name of the Docker service:
DATABASE_URL="mysql://app:password@database:3306/app"Here:
databaseis the service defined in compose.yaml.
Docker automatically provides DNS resolution between services on the same network.
Symfony can therefore connect to MariaDB using:
database:3306This is a small detail, but understanding it makes Docker networking much easier to work with.
10. Starting the environment
Once the configuration is ready, we can build the image:
docker compose buildThen start the services:
docker compose up -dTo check their status:
docker compose psWe should see something similar to:
NAME STATUS
my-project-php running
my-project-db runningOur Symfony application is now being served by FrankenPHP.
11. Running Symfony commands inside Docker
Another advantage of this setup is that we don’t need to install PHP or Composer directly on the production server.
We can execute commands inside the container.
For example:
docker compose exec php php bin/console aboutTo install dependencies:
docker compose exec php composer installTo run migrations:
docker compose exec php php bin/console doctrine:migrations:migrateAnd to clear the cache:
docker compose exec php php bin/console cache:clearThe host server therefore doesn’t need to have the complete PHP environment installed.
Everything required by the application lives inside the container.
12. Preparing Symfony for production
Production configuration obviously needs to be different from development.
For example:
APP_ENV=prod
APP_DEBUG=0We should also avoid committing passwords and other secrets to Git.
For example:
DATABASE_URL=...
APP_SECRET=...should be provided through environment variables or an appropriate secrets management solution.
The principle is simple:
Code can be versioned. Secrets shouldn’t be.
13. Deploying a new version
Once the application is containerized, deploying a new version becomes much more predictable.
After retrieving the latest version:
git pullwe can rebuild the image:
docker compose buildand restart the services:
docker compose up -dThen run any pending migrations:
docker compose exec php \
php bin/console doctrine:migrations:migrate --no-interactionAnd clear the Symfony cache:
docker compose exec php \
php bin/console cache:clearWe can also install production dependencies with:
docker compose exec php composer install \
--no-dev \
--optimize-autoloader \
--no-interactionOnce this manual workflow works correctly, it can be automated using a CI/CD solution such as GitHub Actions.
14. What about HTTPS?
In production, the application obviously shouldn’t be exposed only through HTTP.
One of the advantages of FrankenPHP is that it is built on top of Caddy, which can handle HTTPS and TLS certificates in an appropriate production configuration.
In practice, you need to:
- point your domain to the server;
- configure the domain in your web server;
- make sure the required ports are accessible;
- allow Caddy/FrankenPHP to obtain the certificate;
- verify that certificate renewal works correctly.
The architecture can then look like:
https://example.com
│
▼
FrankenPHP
│
▼
SymfonyThe exact configuration will depend on your infrastructure, especially if you already have a reverse proxy or another web server in front of Docker.
15. Common problems
Docker doesn’t eliminate deployment problems. It changes where you need to look for them.
could not find driver
If Symfony displays:
could not find drivercheck that the required PHP extension is installed in your Docker image:
RUN install-php-extensions pdo_mysqlSymfony cannot connect to MariaDB
Check your DATABASE_URL.
You generally shouldn’t use:
127.0.0.1Instead, use the Docker service name:
databaseFor example:
DATABASE_URL="mysql://app:password@database:3306/app"The application returns a 502 error
Check the FrankenPHP logs:
docker compose logs phpDocker logs should be one of the first places you look when a container isn’t behaving as expected.
MariaDB data disappeared
Check that you have a persistent volume:
volumes:
- database_data:/var/lib/mysqlWithout correctly configured persistent storage, recreating the database container can result in data loss.
16. Taking it further with CI/CD
Once the Docker deployment works manually, the next logical step is automation.
A possible workflow is:
Git push
│
▼
GitHub Actions
│
├── Run tests
├── Build Docker image
└── Deploy
│
▼
Production
│
├── FrankenPHP
└── MariaDBAt this point, deploying a new version doesn’t have to mean manually running several commands on the server.
The same Docker configuration can be used to build, test and deploy the application.
This is where Docker becomes particularly useful as part of a CI/CD workflow.
Conclusion
Docker can initially feel unnecessarily complex when you’re used to installing PHP, MariaDB and a web server directly on a machine.
That’s exactly how I felt before I started using it.
But its value becomes much clearer when working across different environments or multiple projects.
With Docker, the application doesn’t depend as heavily on what happens to be installed and configured on the server. The environment becomes part of the project itself.
And with FrankenPHP, we can simplify the traditional PHP architecture even further by combining the web server and PHP application server into one service.
For a Symfony application, the resulting setup can be relatively straightforward:
┌─────────────────┐
│ Internet │
└────────┬────────┘
│
▼
┌─────────────────┐
│ FrankenPHP │
│ │
│ Symfony │
└────────┬────────┘
│
▼
┌─────────────────┐
│ MariaDB │
└─────────────────┘Of course, FrankenPHP is not the only way to run Symfony, just as MariaDB is not the only database you can use. You could adapt this setup to another PHP version, PostgreSQL, MySQL, or another infrastructure depending on your project’s requirements.
The important part is the principle: define your application environment, make it reproducible, and automate the deployment as much as possible.
Docker doesn’t necessarily make your first deployment easier. It makes subsequent deployments much more predictable.
Комментарии (0)
Пока нет комментариев — будьте первым.