How to Deploy a Symfony Application with Docker and FrankenPHP

Written by
Avatar
Brayan Tiwa
Published on
Aug 22, 2026
How to Deploy a Symfony Application with Docker and FrankenPHP Cover

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:

Server
├── PHP
├── Composer
├── MariaDB
├── Nginx
└── Symfony application

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:

Server
└── Docker
├── FrankenPHP
└── MariaDB

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:

Nginx → PHP-FPM → Symfony

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:

Browser
│
▼
FrankenPHP
│
▼
Symfony
│
▼
MariaDB

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:

  1. FrankenPHP to run Symfony;
  2. MariaDB for the database.

Docker Compose will orchestrate both services.

The final architecture will look like this:

Internet
│
▼
FrankenPHP
│
▼
Symfony
│
▼
MariaDB

A 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.3

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:

RUN install-php-extensions \
pdo_mysql \
intl \
zip \
opcache

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:

COPY --from=composer:2 /usr/bin/composer /usr/bin/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:

WORKDIR /app

COPY . .

Our application will then be available at:

/app

We can install the Composer dependencies as part of the image build:

RUN composer install \
--no-dev \
--optimize-autoloader \
--no-interaction

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:

{
frankenphp
}

:80 {
root * /app/public

encode zstd gzip

php_server
}

The important part here is:

root * /app/public

Symfony uses the public/ directory as the application's entry point.

The:

php_server

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:

services:
php:
build:
context: .
dockerfile: docker/php/Dockerfile
ports:
- "80:80"
depends_on:
- database

database:
image: mariadb:11
environment:
MARIADB_DATABASE: app
MARIADB_USER: app
MARIADB_PASSWORD: password
MARIADB_ROOT_PASSWORD: root
volumes:
- database_data:/var/lib/mysql

volumes:
database_data:

We now have two services:

php
database

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:

volumes:
- database_data:/var/lib/mysql

and:

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:

database

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:

database:3306

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:

docker compose build

Then start the services:

docker compose up -d

To check their status:

docker compose ps

We should see something similar to:

NAME STATUS
my-project-php running
my-project-db running

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:

docker compose exec php php bin/console about

To install dependencies:

docker compose exec php composer install

To run migrations:

docker compose exec php php bin/console doctrine:migrations:migrate

And to clear the cache:

docker compose exec php php bin/console cache:clear

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:

APP_ENV=prod
APP_DEBUG=0

We 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 pull

we can rebuild the image:

docker compose build

and restart the services:

docker compose up -d

Then run any pending migrations:

docker compose exec php \
php bin/console doctrine:migrations:migrate --no-interaction

And clear the Symfony cache:

docker compose exec php \
php bin/console cache:clear

We can also install production dependencies with:

docker compose exec php composer install \
--no-dev \
--optimize-autoloader \
--no-interaction

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:

  1. point your domain to the server;
  2. configure the domain in your web server;
  3. make sure the required ports are accessible;
  4. allow Caddy/FrankenPHP to obtain the certificate;
  5. verify that certificate renewal works correctly.

The architecture can then look like:

https://example.com
│
▼
FrankenPHP
│
▼
Symfony

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:

could not find driver

check that the required PHP extension is installed in your Docker image:

RUN install-php-extensions pdo_mysql

Symfony cannot connect to MariaDB

Check your DATABASE_URL.

You generally shouldn't use:

127.0.0.1

Instead, use the Docker service name:

database

For example:

DATABASE_URL="mysql://app:password@database:3306/app"

The application returns a 502 error

Check the FrankenPHP logs:

docker compose logs php

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:

volumes:
- database_data:/var/lib/mysql

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:

Git push
│
▼
GitHub Actions
│
├── Run tests
├── Build Docker image
└── Deploy
│
▼
Production
│
├── FrankenPHP
└── MariaDB

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:

┌─────────────────┐
│ 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.


Tags: Symfony DevOps Tutorial