Docker Compose setup for PHP project

Below I describe how I use Docker Compose for my PHP projects, although most of the concepts are helpful for any technology that you use. I will use Magento as an example, but there’s near-zero Magetno-specific stuff and this will work for any framework that you use.

I don’t pretend to be the author of all the techniques described here, I give all the thanks and credits to people that actually invented this.

Folder structure

This is how I organize files in the repo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.
├── bin
│   └── magento
├── docker
│   └── local
│   ├── nginx
│   │   ├── conf
│   │   │   ├── default.conf
│   │   └── Dockerfile
│   └── php
│   ├── conf
│   │   ├── custom-config.ini
│   └── Dockerfile
├── magento
│   ├── bin
│   ├── ...
│   ├── composer.json
│   └── composer.lock
├── .gitignore
├── docker-compose.override.example.yml
├── docker-compose.override.yml
├── docker-compose.yaml
└── README.md

The docker folder contains all docker-related files except for Docker Compose YAMLs.

I put all files for local (development) purpose into a local sub-folder. This is becasue you might want to keep different Dockerfiles and configs for diferent environments in your repository as well.

Say, you have a “Testing” environment. Then:

  • put Dockerfiles and configs to docker/testing folder.
  • create docker-compose.testing.yaml in the root folder.
  • bring your containers up on the testing environemnt by running:
    docker-compose -f docker-compose.testing.yaml up --build -d

As local environment is used most often, I prefer local YAML files not to have “.local” suffix, so that I don’t need to specify the list of files each time for docker-compose commands.

docker-compose.yaml

The contents should be self-explanatory. The only trick here is using YAML alias (codebase) not to repeat ourselves with mounting application volume to both nginx and php-fpm containers.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
version: '3'

services:

mage_web:
build:
context: ./docker/local/nginx
dockerfile: Dockerfile
restart: always
depends_on:
- mage_app
volumes:
- &codebase ./magento:/var/www/html
- ./docker/local/nginx/conf/default.conf:/etc/nginx/conf.d/default.conf

mage_app:
build:
context: ./docker/local/php
dockerfile: Dockerfile
restart: always
depends_on:
- mage_db
volumes:
- *codebase
- ./docker/local/php/conf/custom-config.ini:/usr/local/etc/php/conf.d/custom-config.ini

mage_db:
image: mariadb:10
restart: always
volumes:
- magento-dbdata:/var/lib/mysql
environment:
- MYSQL_DATABASE=magento
- MYSQL_ALLOW_EMPTY_PASSWORD=No
- MYSQL_ROOT_PASSWORD=demo
command: ['--character-set-server=utf8', '--collation-server=utf8_unicode_ci']

volumes:
magento-dbdata:

As you can see I also prefer to name my services in a more abstract way than just “php” or “nginx”. I like it more because I can change the underlying technology later without much pain (i.e. mariadb to mysql).

I also include the app name in the service name so that it is possible to run different, let’s say, php containers in the same network.

Using override file

Note that I don’t expose or map any ports in docker-compose.yaml above. I find this important because another developer can have ports that I use already taken on his machine. To manage this I use docker-compose.override.yaml which is loaded by default if present. Find out more about it at https://docs.docker.com/compose/extends/.

I keep this file ignorred by GIT and instead version docker-compose.override.example.yaml file, so that it is easy for everyone to start.

docker-compose.override.yaml

1
2
3
4
5
6
7
8
9
10
11
version: '3'

services:

mage_web:
ports:
- "127.0.0.1:8080:80"

mage_db:
ports:
- "3326:3306"

Here I map nginx container port 80 to local port 8080 so I can open my website in browser: http://magento.localhost:8080.

I also map mariadb container port 3306 to local port 3326 so that I can connect to the database from my local machine: mysql -h 127.0.0.1 -P 3326 -u root -p magento.

User permissions trick

By default docker containers are running from the root user. Sometimes they use custom user, like “nginx” for example. So files that are created in containers might appear owned by root or even unknown user in mounted folders on your local machine.

To overcome this inconvenience I use the following trick and change the user IDs inside containers to my local user.

  1. In your Dockerfile define the USER_ID and GROUP_ID args and modify the user from which the container is running:

For example for nginx we’ll have:

1
2
3
4
5
6
7
8
9
FROM nginx:1.15.2

ARG USER_ID=1000
ARG GROUP_ID=1000

RUN usermod -u ${USER_ID} nginx \
&& groupmod -g ${GROUP_ID} nginx

WORKDIR /var/www/html

For php-fpm:

1
2
3
4
5
6
7
8
9
10
11
FROM php:7.2-fpm

...

ARG USER_ID=1000
ARG GROUP_ID=1000

RUN usermod -u ${USER_ID} www-data \
&& groupmod -g ${GROUP_ID} www-data

...
  1. In your docker-compose.override.yaml define the actual user and group IDs that you want to use:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
version: '3'

services:

mage_web:
build:
args:
USER_ID: 1000
GROUP_ID: 1000
...

mage_app:
build:
args:
USER_ID: 1000
GROUP_ID: 1000
...

You can find out your current local user and group IDs with id -u and id -u respectively. Normally in Ubuntu these are both 1000.

Working with Composer

I only work with composer locally, never run it inside the container. This allows me not to care about copying ssh keys, composer repository credentials and other stuff.

As a disadvantage this leads to necessity of having PHP and Composer installed locally, but I always do have them latest anyway. However it is important to specify the PHP version that you use on the project for composer, so that it installs correct packages. You can do it like this in composer.json:

1
2
3
4
5
6
7
8
{
...
"config": {
"platform": {
"php": "7.2"
}
}
}

You can also specify PHP extensions versions here, see platform section documentation for more capabilities.

With properly configured platform in composer.json you can avoid adding “–ignore-platform-reqs” to your composer install or update command.

Bin helper

All modern frameworks have CLI interface for development purposes and it is being used quite often. Normally you must do this inside the container for it to work properly, but that’s not very convenient. The elegant way to improve this is to create a “bin helper” as I call it, which will pass the command you want to the container you need.

Below is the example for Magento, you can modify it for the framework of your choice:

bin/magento

1
2
3
4
5
#!/usr/bin/env bash
BIN_DIR=$(dirname "$0")
BASE_DIR=$(dirname "$BIN_DIR")

docker-compose exec --user=www-data mage_app /var/www/html/bin/magento "$@"

So now I just run this as if it would be the actual bin/magento file:

1
bin/magento cache:flush

That’s it for now.


Check out related topics: