Yet another one?!
Well, either my searching skills are getting rusty or my request was too specific while at the same time being pretty basic, or something would crash and break during the build process after following available online tutorials or downloading existing projects... but I couldn't find a single solution which I could reuse for my exact needs with just a couple of changes in the configuration.
I had to come up with my own, which is the result of bits and pieces taken from those that didn't fully work for me, as well as some knowledge and understanding of Docker I gained along the way. I'd like to share it; maybe it will be useful for someone else in this very format.
I've decided to use Docker for my local development because different projects I'm working on require different versions of PHP, NodeJS and even different versions of Composer... and switching/upgrading/downgrading those versions whenever I switched my focus on another project became cumbersome.
Docker can be really slow, but listing pros and cons of Docker is not the topic here.
I don't intend to get into the details of each line of code and explain all used commands. What I can do is encourage you into having a look at this great "Docker for local web development" series where you can learn much more. This was my main resource as well.
Let's get started
All my new projects will be using PHP 8 because of some great features I'd like to exploit which were not there prior to version 8. I think it's worth using the latest versions when starting fresh, so that's the stack I'm dealing with. Symfony 5.2 includes support for PHP 8 attributes to define routes and required dependencies, and that's one more argument in favour of it. I'll also use Apache for server and MariaDB for database.
On top of my Symfony project, I'll add PhpMyAdmin to save me some trouble when manually dealing with DB stuff.
I intend to have multiple projects locally with this exact stack, so I had to come up with something simple enough and reusable to get me started quick, but something I can build on top of and extend when necessary.
Prerequisites are installed Docker and Docker Compose (which, depending on your platform, might be a part of the Docker installation). If you're using Windows, make sure you have the WSL2 feature enabled.
Folder structure overview
Maybe it will be easier to follow if I provide the final folder structure first, so here it is:
.
├── codebase/
├── docker/
│ ├── db/
│ │ └── mariadb/
│ │ └── my.cnf
│ └── server/
│ ├── apache/
│ │ └── sites-enabled/
│ │ └── site.conf
│ ├── php/
│ │ └── php.ini
│ └── Dockerfile
├── .env
└── docker-compose.yml
All folder names are arbitrary. Make sure, if you're going to rename them, to rename them accordingly in the configurations.
codebase
folder will hold all our project code. Since this will be a Symfony app, we will have public
folder with index.php
file within it and that's what we'll rely on throughout the setup. For starters, codebase
folder is empty.
In .env
file, we'll have project-level Docker environment variables. As a part of this guide, I will not configure local machine's hosts
file to make site access more user friendly, but will access it over localhost:[PORT]. You must make sure that the port is not occupied already and map available ports from your local machine to container's port 80
. In the same manner, I'll map a port for PhpMyAdmin. Stick with some convention and be consistent.
E.G. use ports 8101
and 8102
for project A, 8103
and 8104
for project B and so on...
The same applies for DB port. You might already have default port 3306 occupied on your local machine.
I'm using APP_NAME
variable to avoid some extra copy-pasting in the configurations, but that's completely up to you.
MYSQL_*
config variables are pretty self-explanatory.
Here's what my .env
file looks like:
APP_NAME=symfony_project_2021
APP_PORT=8101
APP_DB_ADMIN_PORT=8102
DB_PORT=33016
MYSQL_ROOT_PASS=superSecr3t
MYSQL_USER=app_user
MYSQL_PASS=t3rceS
MYSQL_DB=symfony_project_2021
Docker Compose
docker-compose.yml
YAML file is where our services are defined and based on it, Docker Compose will take care of building the images and starting the containers. We will connect all our services to internal network symfony_project_2021_net
, which is also defined in docker-compose.yml
.
We will use 3 services:
server
db_server
-
db_admin
version: '3.9'
networks:
symfony_project_2021_net:
services:
server:
build:
context: .
dockerfile: ./docker/server/Dockerfile
container_name: '${APP_NAME}-server'
ports:
- '${APP_PORT}:80'
working_dir: /var/www/html
environment:
- 'DATABASE_URL=mysql://${MYSQL_USER}:${MYSQL_PASS}@db_server:3306/${MYSQL_DB}?serverVersion=10.5'
volumes:
- ./codebase:/var/www/html
- ./docker/server/apache/sites-enabled:/etc/apache2/sites-enabled
- ./docker/server/php/php.ini:/usr/local/etc/php/conf.d/extra-php-config.ini
depends_on:
db_server:
condition: service_healthy
networks:
- symfony_project_2021_net
db_server:
image: mariadb:10.5.9
container_name: '${APP_NAME}-db'
restart: always
ports:
- '${DB_PORT}:3306'
environment:
MYSQL_ROOT_PASSWORD: '${MYSQL_ROOT_PASS}'
MYSQL_USER: '${MYSQL_USER}'
MYSQL_PASSWORD: '${MYSQL_PASS}'
MYSQL_DATABASE: '${MYSQL_DB}'
volumes:
- db_data:/var/lib/mysql
- ./docker/db/mariadb/my.cnf:/etc/mysql/conf.d/my.cnf
healthcheck:
test: mysqladmin ping -h 127.0.0.1 -u root --password=$$MYSQL_ROOT_PASSWORD
interval: 5s
retries: 5
networks:
- symfony_project_2021_net
db_admin:
image: phpmyadmin/phpmyadmin:5
container_name: '${APP_NAME}-db-admin'
ports:
- '${APP_DB_ADMIN_PORT}:80'
environment:
PMA_HOST: db_server
depends_on:
db_server:
condition: service_healthy
volumes:
- db_admin_data:/var/www/html
networks:
- symfony_project_2021_net
volumes:
db_data:
db_admin_data:
We'll use official Docker images for building db_server
and db_admin
containers. You can find the official (and many other) container images on Docker Hub.
Server
We'll use our own Dockerfile to specify what the server
image looks like.
Content of /docker/server/Dockerfile
:
FROM php:8.0-apache
RUN a2enmod rewrite
RUN apt-get update && apt-get install -y git unzip zip
WORKDIR /var/www/html
COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/
RUN install-php-extensions gd pdo_mysql bcmath zip intl opcache
COPY --from=composer:2.0 /usr/bin/composer /usr/local/bin/composer
Docker PHP extension installer script will install all the required APT/APK packages. At the end of the script execution, packages that are no longer needed will be removed, so the image will be much smaller.
Using docker-php-ext-install
didn't always work for me, so I'm pulling this extra script. You can expand the list of extensions to install, if necessary.
After that, we'll also pull the Composer. What do you think about setting up a separate container for Composer?
Content of our local folder codebase
will be considered a volume mapped to /var/www/html
, which is the project root.
Apache
Site is defined on Apache side, in docker/server/apache/sites-enabled/site.conf
:
<VirtualHost *:80>
DocumentRoot /var/www/html/public
<Directory /var/www/html/public>
AllowOverride None
Order Allow,Deny
Allow from All
<IfModule mod_rewrite.c>
Options -MultiViews
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php [QSA,L]
</IfModule>
</Directory>
</VirtualHost>
Again, this is Symfony-specific (something very similar would be used for Laravel projects as well).
Configurations
Finally, there are some extra configurations for PHP and MariaDB in /docker/server/php/php.ini
and /docker/db/mariadb/my.cnf
, respectively.
You can configure and overwrite defaults in those files if necessary.
upload_max_filesize = 30M
post_max_size = 80M
short_open_tag = Off
memory_limit = 256M
[mysqld]
collation-server = utf8mb4_unicode_ci
character-set-server = utf8mb4
We'll use named volumes to persist data. We need such volumes because without them, every time the DB service container is destroyed the database is destroyed with it. This is what that final section in docker-compose.yml
is for. Volumes are referenced in db_server
and db_admin
services.
Order in which containers are started is important and that's what we've set with depends_on
.
Specifying environment server variables is also completely up to you; you're free to use another .env
file in the Symfony project root folder. DATABASE_URL
variable is Symfony-specific, and if you display it on the server, you should get
Variables defined like this will have precedence over those defined in .env
files (in Symfony project root folder).
You can do the same thing for Laravel project.
Ready, set, ...
In your terminal, run docker-compose up -d --build
to start the containers. The process will take some time, but only the first time you're building containers.
After this is done, check your containers by running docker-compose ps
in the terminal.
Also, in Docker Desktop app you should see:
Everything from now on should be a walk in the park. Or that's what I thought when following all online resources I could find 😃
Execute Bash on server
container (where server
is the name you've given to container) by running docker-compose exec server bash
We've set our working directory on the server to /var/www/html
. While at it, run composer create-project symfony/website-skeleton .
. If you prefer, you can install Symfony installer as well (add it to Dockerfile
).
This will install the Symfony project.
It's important that you do this directly on the server because your local Composer and PHP versions might be different and you could run into compatibility issues.
Now check http://localhost:8101/ and http://localhost:8102/. Your site and PhpMyAdmin should be available!
Was this guide "yet another" that didn't fully work for your local setup? What would you improve or change completely?
Which other containers do you usually use to get your projects up and running?
Top comments (4)
Great material and post!
We have a fairly similar setup and it works pretty good for local development when you add following several productivity/devUX tips. :)
Use local overrides: we heavily depend on
docker-compose.override.yml
to allow all local overrides, port exposures and additional services (ie., phpMyAdmin, exposing db, etc.).We provide
docker-compose.override.yml.dist
whose main purpose is to be a distribution file that can be c/p for local setup. Naturally,docker-compose.override.yml
is git-ignored. :)With this approach we allow developers to "flavor" their own local development: some devs use DataGrid to access db so they need exposed port, some use phpMyAdmin and access it through network, some like to query the db directly through the terminal; some want to mount volumes for everything they have, some refresh the whole project from fixtures every couple of days, etc.
Basically they can add whatever they want without risking an accidental addition to base setup when commiting their work. :)
Huge disclaimer: we also use this same setup on production, so we need to extract port exposing and similar functionalities from base docker-compose.yml :)
Pre-build your base images: if you run full installation of additional dependencies for PHP, you risk having different .dll versions between your developers just because they built their project a couple of days apart (this can be even a couple of months if it is a longer project due to the fact that most of us rarely rebuild everything). If you don't have any additional extensions for language, great, then this approach is perfect. :)
But if you have, consider building these images and deploy them to either a private registry or docker.io (it's free so, why not :) ).
We have a hybrid approach: our base image is prebuilt, but we allow additional expansions of local development through the same approach as you have. For instance, xDebug is an optional extension that we have inside of our Dockerfile behind a "feature flag" (we have some projects that where we feel heavy impact its presence even though listeners are turned off).
Create a Makefile (or something similar) to automate these things: having a simple
make up
command will allow automation of this whole process. And it is directly in your project :DIf you want, you can also organise it as a questionnaire with default values so it can speed up project setup.
Check out Traefik (traefik.io/): it's great in combination with
docker-compose.override.yml
since you can basically setup custom local domains and avoid a lot of port exposing and have multiple project running in parallel (if that is something that you need). It's based around custom labels on Docker containers so you can pre-populate these at the beginning of your project and have actual local domains. Additional plus: if you ever need to allow CORS if you're providing your backend to frontend developers, it is one label away (which can also be turned on by default). :DOf course, this works for us and isn't something that will definitely work for everyone, but there are some useful concepts that I think should be shared with the community. :)
Thanks a lot for your thorough comment and suggestions!
I think there's a lot I can exploit here right away:
point 4 - exactly what I am looking for at the moment, because of some issues and blockers I've encountered when locally integrating 3rd party social login providers where you need to register a domain on which requests will be redirected to, and
localhost
is not workingpoint 1 - I'm usually extracting a lot of variables into
.env
files and I admit it's starting to get annoying to build/run a container with--env-file=.env.local
option each time. I have a (non-versioned).env.local
file with a ton of Docker-related stuff and I think I can gain a lot from this.override.yaml
file.Also, automating the build process and adding a script in form of a wizard is something that's accomplished in the later stages of the referenced tutorial, and also in Laravel Sail which is recently definitely a way to go with Laravel's local setup, so I might give it a go, too.
Once again, thank you, this will be extremely helpful!
I'm time and again impressed by how much more complicated and complex things get when using Docker - yet people swear by it and say it's easier.
nice write-up. I agree that finding anything regarding PHP8, Docker, Symfony is a pain. I also had to just build out my own. I'm using Nginx myself along with the Alpine image.