How to Get Started with Docker Compose and Symfony

May 24, 2021
Written by
Oluyemi Olususi
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

Developing software across teams is difficult - be it managing codebases or ensuring everything works for everyone at all times.

While Git has gone a long way in terms of version control and ensuring that everyone can work on a codebase without issues, there's still the risk of disruption as a result of the working environment.

Developers have varying preferences in terms of preferred operating systems (Windows, macOS, and Linux). The same is also true of the environments their applications are deployed to.

As a result, when certain errors occur, it becomes difficult to trace the source of the problem. In fact, it can be even more confusing if an error only occurs on one OS, leading to the famous phrase: It works on my machine.

Enter Docker

It adds predictability to applications by building them in containers, which are small and lightweight execution environments. Containers make shared use of the underlying operating system kernel, but otherwise, run in isolation from one another.

By integrating Docker into your Symfony project, you can be assured that whenever your application is running, the environment and its configuration will be the same—regardless of where it's deployed.

In this article, I will show you how to use Docker with a Symfony project. Nginx will be used as the webserver, PHP-FPM will process PHP requests, and MySQL will be the backend database. The application to be built will display famous quotes made by renowned historians over the years.

Prerequisites

  • Previous experience with Symfony, Twig, and ORMs (specifically Doctrine).
  • You will also need to understand some basic terms associated with Docker (such as container, image, network, and service). Jeff Hale wrote a brilliant series that explains these terms. Feel free to go through it if any of these terms is unfamiliar.
  • Composer globally installed.
  • Docker Desktop.
  • The Symfony CLI tool.

Getting started

To get started, create a new directory named symfony_docker and switch to it, using the commands below

mkdir symfony_docker
cd symfony_docker

Create the Docker Compose configuration

Because the different containers that compose our application need to communicate, we will use Docker Compose to define them. In the root of the symfony_docker directory, create a new file called docker-compose.yml using the command below.

touch docker-compose.yml

This file will hold all the configuration for the containers to be built in our application stack, from how the containers are to be built to the networks and volumes accessible to the containers.

In docker-compose.yml, add the configuration below.

version: '3.8'

services:

version refers to the schema version. services defines the list of containers our application stack will consist of.

Note: Services are really just “containers in production.”

In the following sections, we'll describe the containers for our MySQL database, PHP, and Nginx web server.

Define the database container

To define the database container, in docker-compose.yml, update the services element to match the following example:

services:
  database:
    container_name: database
    image: mysql:8.0
    command: --default-authentication-plugin=mysql_native_password
    environment:
      MYSQL_ROOT_PASSWORD: secret
      MYSQL_DATABASE: symfony_docker
      MYSQL_USER: symfony
      MYSQL_PASSWORD: symfony
    ports:
      - '4306:3306'
    volumes:
      - ./mysql:/var/lib/mysql

container_name sets the actual name of the container when it runs, rather than letting Docker Compose generate it.

image lets Docker know what image (blueprint) we want to build the container from. In this case, we've specified mysql:8.0 because we want to use version 8 of MySQL.

command specifies the authentication plugin to be used by MySQL for authenticating users. Using the environment key, we can specify environment variables such as the database's name, user, and password, as well as the root user's password.

We need a port to connect to our database. Using the ports key, we specify a port on our local development machine and map it to a port on the container which will be used to handle database connections.

Note: Port 4306 was specified in the event that MySQL service is already running on your computer.

Finally, we declare a volume, using the volume key. According to the Docker documentation:

Volumes are the preferred mechanism for persisting data generated by and used by Docker containers

We declare a volume in this case so that our database won't be lost when the containers are destroyed or rebuilt.

Define the PHP container

Unlike the database container, we need to specify some additional instructions to set up our PHP container. To do this, we will build the PHP container from a Dockerfile. In the root directory, symfony_docker, create a directory called php. Then, in symfony_docker/php, create a file named Dockerfile.

Note: this file has no extension.

mkdir php
touch php/Dockerfile

Then, in symfony_docker/php/Dockerfile, add the following.

FROM php:8.0-fpm

RUN apt update \
    && apt install -y zlib1g-dev g++ git libicu-dev zip libzip-dev zip \
    && docker-php-ext-install intl opcache pdo pdo_mysql \
    && pecl install apcu \
    && docker-php-ext-enable apcu \
    && docker-php-ext-configure zip \
    && docker-php-ext-install zip

WORKDIR /var/www/symfony_docker

RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

RUN curl -sS https://get.symfony.com/cli/installer | bash
RUN mv /root/.symfony/bin/symfony /usr/local/bin/symfony
RUN git config --global user.email "you@example.com" \ 
    && git config --global user.name "Your Name"

Note: In the last two lines of symfony_docker/php/Dockerfile, please replace "you@example.com" with your email address and "Your Name" with your actual name.

In addition to scaffolding a container from the PHP-FPM image, we will do the following:

  1. Install the PHP extensions Symfony depends on.
  2. Set the working directory of the container to /var/www/symfony_docker
  3. Install composer
  4. Install the Symfony CLI

Next, add the example below to docker-compose.yml after the database configuration.

  php:
    container_name: php
    build:
      context: ./php
    ports:
      - '9000:9000'
    volumes:
      - ./app:/var/www/symfony_docker
    depends_on:
      - database

As can be seen, the PHP container is defined differently. Instead of specifying an image, we specify a build context. This way, when the docker-compose command is run, the instructions declared in php/Dockerfile will be used to build the container.

Port 9000 on the computer is mapped to port 9000 on the container, just as we mapped a port on the computer to a port on the container for the MySQL database.

We declare a volume again to persist the data generated by the container. In this case, our Symfony application will be created in the /var/www/symfony_docker directory of the PHP container. However, it will be persisted in the app directory in the project.

Finally, the depends_on key was used. This creates a dependency between the PHP and database containers instructing Docker to build and start the database container before the PHP container.

With the PHP container defined, create the app directory in the root directory of the project, which the container requires, using the command below.

mkdir app

Define the Nginx container

Before we build the Nginx container, let's write the default configuration for the server. In the root of the project, create a directory called nginx and in it create a configuration file named default.conf using the commands below.

mkdir -p nginx/default.conf

Add the configuration below to nginx/default.conf.

server {

    listen 80;
    index index.php;
    server_name localhost;
    root /var/www/symfony_docker/public;
    error_log /var/log/nginx/project_error.log;
    access_log /var/log/nginx/project_access.log;

    location / {
        try_files $uri /index.php$is_args$args;
    }

    location ~ ^/index\\.php(/|$) {
        fastcgi_pass php:9000;
        fastcgi_split_path_info ^(.+\\.php)(/.*)$;
        include fastcgi_params;

        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        fastcgi_param DOCUMENT_ROOT $realpath_root;

        fastcgi_buffer_size 128k;
        fastcgi_buffers 4 256k;
        fastcgi_busy_buffers_size 256k;

        internal;
    }

    location ~ \\.php$ {
        return 404;
    }

}

This is a basic Nginx configuration required for running a Symfony project. The only thing done differently is the specification for fastcgi_pass. Notice that in this case we specify port 9000 of the PHP container, because this is the default port that PHP-FPM listens on for requests..

Next, add the Nginx container's configuration to docker-compose.yml, in the example below, after the PHP container's configuration.

  nginx:
    container_name: nginx
    image: nginx:stable-alpine
    ports:
      - '8080:80'
    volumes:
      - ./app:/var/www/symfony_docker
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - php
      - database

Build the containers

With this in place, we can finally build our containers by running the command below.

docker-compose up -d --build

When the containers are built, you will see something similar to the screenshot below in your terminal.

The Docker Compose configuration built successfully

If you open Docker Desktop, you should see your newly created container as shown in the screenshot below.

The symfony_docker configuration in Docker Desktop

Create the Symfony application

To create the Symfony application, we need to initiate a terminal in our PHP container. There are two ways of doing this:

  • Using Docker Desktop. By expanding the symfony_docker application, you get to see the containers it is composed of. You can initiate the CLI by clicking on the button highlighted in the screenshot below. This runs the docker exec command and opens a terminal for you to interact with.

List of containers in symfony docker
  • Run the docker exec command. To do that, run the command below
docker-compose exec php /bin/bash

Regardless of the approach you chose, in the newly opened terminal, ensure that your setup meets the requirements for a Symfony application by running the following command.

symfony check:requirements

If it meets the requirements, you will see the following output in the terminal.

OK]                                             
Your system is ready to run Symfony projects

Next, create a new Symfony project by running the following command.

symfony new .

If successful, you will see the following text in the terminal output:

OK] Your project is now ready in /var/www/symfony_docker

With the application created, navigate to http://localhost:8080/ and you will see the default Symfony index page, which you can see an example of below. Also, if you look in the app directory, you will see that the Symfony project has been persisted there.

Default Homepage for Symfony

Back in our PHP container CLI, let's add some development dependencies for the application we are building. Run the following command to do so.

composer req --dev maker ormfixtures fakerphp/faker

To persist and retrieve quotes in and from the database we’ll need an ORM, Doctrine ORM to be precise, and the Twig template engine will be required to render the front end. Add these dependencies by running the command below

composer req doctrine twig

Next, create a .env.local file from the existing .env file which Symfony generated during the creation of the project. To do that, run the command below.

cp .env .env.local

Finally, update the database parameters in .env.local to allow your application to connect to your database container. Replace the current DATABASE_URL entry in the file with the version below.

DATABASE_URL="mysql://root:secret@database:3306/symfony_docker?serverVersion=8.0"

Create the quote entity

Next, we need to create an ORM entity that will handle interacting with the MySQL database. Use the Symfony Maker bundle to do that, by running the command below.

symfony console make:entity Quote

Answer the questions asked as shown below.

New property name (press <return> to stop adding fields):
 > quote

 Field type (enter ? to see all types) [string]:
 > text

 Can this field be null in the database (nullable) (yes/no) [no]:
 > no

 updated: src/Entity/Quote.php

 Add another property? Enter the property name (or press <return> to stop adding fields):
 > historian

 Field type (enter ? to see all types) [string]:
 > string

 Field length [255]:
 > 25

 Can this field be null in the database (nullable) (yes/no) [no]:
 > no

 updated: src/Entity/Quote.php

 Add another property? Enter the property name (or press <return> to stop adding fields):
 > year

 Field type (enter ? to see all types) [string]:
 > string

 Field length [255]:
 > 5

 Can this field be null in the database (nullable) (yes/no) [no]:
 > no

 updated: src/Entity/Quote.php

 Add another property? Enter the property name (or press <return> to stop adding fields):
 >

An entity named Quote.php will be created in the src/Entity directory. Open src/Entity/Quote.php and add a constructor function as shown below.

public function __construct($quote, $historian, $year) {
    $this->quote = $quote;
    $this->historian = $historian;
    $this->year = $year;
}

Create a migration to update your database using the following command.

symfony console make:migration

Run your migrations using the following command.

symfony console doctrine:migrations:migrate

Answer with "yes" when prompted and your database will be updated with a new table, named "quote".

To confirm that it has been created, open a new terminal window and run the following command, from within the symfony_docker directory.

docker-compose exec database /bin/bash

When the terminal is opened, connect to your database with the following command.

mysql -u root -p symfony_docker

Provide your MYSQL_ROOT_PASSWORD when prompted. Because we specified the database we intend to connect to, we can query for the tables using the following command.

> show tables;

You should see something similar to the screenshot below:

mysql> show tables;
+-----------------------------+
| Tables_in_symfony_docker    |
+-----------------------------+
| doctrine_migration_versions |
| quote                       |
+-----------------------------+
2 rows in set (0.00 sec)

Create the quote fixture

Next, we need to load the database with a set of generated quotes so that, when the app is ready, we'll have quotes to retrieve and display. To do that, in the PHP container, run the following command.

symfony console make:fixture QuoteFixture

The fixture will be located in src/DataFixtures/QuoteFixture.php Open the file and update it to match the code below.

<?php

namespace App\DataFixtures;

use App\Entity\Quote;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;
use Faker\Factory;

class QuoteFixture extends Fixture {

    private $faker;

    public function __construct() {

        $this->faker = Factory::create();
    }

    public function load(ObjectManager $manager) {

        for ($i = 0; $i < 50; $i++) {
            $manager->persist($this->getQuote());
        }
        $manager->flush();
    }

    private function getQuote() {

        return new Quote(
            $this->faker->sentence(10),
            $this->faker->name(),
            $this->faker->year()
        );
    }
}

To load the fixtures in our database, run the following command in the PHP container.

symfony console doctrine:fixtures:load

Respond with "yes" when prompted to load the fixtures in the database. Following that, confirm that the quotes have been loaded by running the following command in the database container.

> SELECT * FROM quote

Your table should look like the one shown below.

mysql> SELECT * FROM quote limit 10;
+----+----------------------------------------------------------------------------------------------------+----------------------+------+
| id | quote                                                                                              | historian            | year |
+----+----------------------------------------------------------------------------------------------------+----------------------+------+
|  1 | Aut eaque aut quos autem incidunt ut est quod tempore sed aut placeat.                             | Einar Lebsack DDS    | 2001 |
|  2 | Eum autem sed aut quos impedit cupiditate harum voluptatem aut qui qui sunt ad.                    | Aliza Morissette     | 1990 |
|  3 | Deserunt consequatur et sunt architecto enim quia deleniti consectetur est reprehenderit.          | Zora Bailey          | 1973 |
|  4 | Pariatur adipisci voluptatem rerum id adipisci doloremque porro maxime unde placeat ad autem sint. | Dallin Erdman        | 1974 |
|  5 | Nobis magni eius voluptatibus blanditiis sequi praesentium aut aperiam et.                         | Pearlie Cremin PhD   | 1982 |
|  6 | Aperiam labore cum delectus aut consequatur animi in.                                              | Stone Harvey         | 2008 |
|  7 | Reiciendis non quia libero omnis quis quae quo aut odit tempora aut.                               | Aurore Graham        | 1988 |
|  8 | Earum aliquid quia reiciendis repudiandae non consequatur aliquid.                                 | Lyric Towne          | 1977 |
|  9 | Eos illo et unde sint esse tenetur.                                                                | Mr. Demarcus Klein V | 1988 |
| 10 | Maxime soluta veniam qui debitis sit maiores sint dolores culpa architecto aliquam est facere.     | Francesca O'Connell  | 1970 |
+----+----------------------------------------------------------------------------------------------------+----------------------+------+
10 rows in set (0.00 sec)

Creating the quote controller

Next, we need to create a new controller that will use the ORM entity to retrieve the quotes from the database and render them in the view for us to see. To do that, in the php container, run the following command.

symfony console make:controller QuoteController

When the command completes, a new controller, named QuoteController.php, will have been created in the src/Controller directory. Open the file and update it to match the code below.

<?php

namespace App\Controller;

use App\Repository\QuoteRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\\Annotation\Route;

class QuoteController extends AbstractController {
    #[Route('/', name: 'index')]
    public function index(
        QuoteRepository $quoteRepository
    )
    : Response {

        return $this->render(
            'quote/index.html.twig',
            [
                'quotes' => $quoteRepository->findAll(),
            ]
        );
    }
}

Style the view

Finally, let's make the view look professional and easy to read. To do that, we'll style it using Bootstrap. Update app/templates/base.html.twig to match the code below.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link
            href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/css/bootstrap.min.css"
            rel="stylesheet"
            integrity="sha384-eOJMYsd53ii+scO/bJGFsiCZc+5NDVN2yr8+0RDqr0Ql0h+rP48ckxlpbzKgwra6"
            crossorigin="anonymous">

    <title>{% block title %}Welcome!{% endblock %}</title>
    {% block stylesheets %}
    {% endblock %}

    {% block javascripts %}
    {% endblock %}
</head>
<body>
{% block body %}{% endblock %}
<script
        src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-JEW9xMcG8R+pH31jmWH6WWP0WintQrMb4s7ZOdauHnUtxwoG2vI5DkLtS3qm9Ekf"
        crossorigin="anonymous">
</script>
</body>
</html>

Next, open app/templates/quote/index.html.twig and edit its content to match the following.

{% extends 'base.html.twig' %}

{% block title %}Quotes{% endblock %}

{% block body %}
    <style>
        .wrapper {
            margin: 1em auto;
            width: 95%;
        }
    </style>

    <div class="wrapper">
        <h1>Great Quotes</h1>
        <table class="table table-striped table-hover table-bordered">
            <thead>
            <tr>
                <th scope="col">#</th>
                <th scope="col">Quote</th>
                <th scope="col">Historian</th>
                <th scope="col">Year</th>
            </tr>
            </thead>
            <tbody>
            {% for quote in quotes %}
                <tr>
                    <td>{{ loop.index }}</td>
                    <td>{{ quote.quote }}</td>
                    <td>{{ quote.historian }}</td>
                    <td>{{ quote.year }}</td>
                </tr>
            {% endfor %}
            </tbody>
        </table>
    </div>
{% endblock %}

Test the application

Now that the application has been completed, it's time to test it. Reload your index page to see the famous quotes stored in your database.

Quotes in the database

 

That's how to set up a Symfony project using Docker

Not only were we able to build containers from images and Dockerfiles, but we were also able to make them communicate with one another, thus allowing us to run our Symfony application and database in separate containers.

By building containers from the same specifications the development team is guaranteed to be not only working with the same code base but working in the same environment.

The entire codebase for this tutorial is available here on GitHub. Feel free to explore further. Happy coding!

Oluyemi is a tech enthusiast with a background in Telecommunication Engineering. With a keen interest to solve day-to-day problems encountered by users, he ventured into programming and has since directed his problem-solving skills at building software for both web and mobile.

A full-stack software engineer with a passion for sharing knowledge, Oluyemi has published a good number of technical articles and content on several blogs on the internet. Being tech-savvy, his hobbies include trying out new programming languages and frameworks.