How to Deploy a Symfony Application with Docker and FrankenPHP
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.3 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:
Each 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.3 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:
The 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:
Instead, 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:
We 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:
A possible project structure is:
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:
I'm using PHP 8.3 here simply because this is the version used in 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:
The 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:
We 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:
Our application will then be available at:
We can install the Composer dependencies as part of the image build:
For 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:
The important part here is:
Symfony uses the public/ directory as the application's entry point.
The:
directive tells FrankenPHP to serve the PHP application.
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:
We now have two services:
The 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:
and:
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:
Inside Docker, 127.0.0.1 refers to the current container, not the MariaDB container.
We therefore use the name of the Docker service:
Here:
is the service defined in compose.yaml.
Docker automatically provides DNS resolution between services on the same network.
Symfony can therefore connect to MariaDB using:
This 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:
Then start the services:
To check their status:
We should see something similar to:
Our 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:
To install dependencies:
To run migrations:
And to clear the cache:
The 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:
We should also avoid committing passwords and other secrets to Git.
For example:
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:
we can rebuild the image:
and restart the services:
Then run any pending migrations:
And clear the Symfony cache:
We can also install production dependencies with:
Once 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:
The 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:
check that the required PHP extension is installed in your Docker image:
Symfony cannot connect to MariaDB
Check your DATABASE_URL.
You generally shouldn't use:
Instead, use the Docker service name:
For example:
The application returns a 502 error
Check the FrankenPHP logs:
Docker 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:
Without 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:
At 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:
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.