Using Docker for local development is a great way to introduce it into your workflow. Although it’s rare to only use one container, that is also probably the simplest way to begin. This guide is going to explain how to create a single container setup for Docker which implements services for PHP, NGINX and MariaDB. Following the single container setup I’ll explain how to split up your services into separate containers and connect them to each other via Docker networking. If you haven’t already, check out the Docker Get Started and Compose guide. At least skim it in order to get up to speed quickly.

For the complete code example check out the Github repository.

Single Container Setup

The example docker-compose.yml below contains three services, nginx, php and mariadb.

version: "3"

services:

  nginx:
    image: nginx
    volumes:
      - ./:/var/www/html
      - ./default.conf:/etc/nginx/conf.d/default.conf
    ports:
      - "8080:80"
  php:
    build: .
    volumes:
      - ./:/var/www/html

  mariadb:
    image: mariadb
    environment:
      MYSQL_ROOT_PASSWORD: password

Both NGINX and MariaDB are using the latest images however we are going to build a custom PHP-FPM image so that we have the PDO driver available. The build: . command tells Docker to build an image from a Dockerfile like the one below.

FROM php:fpm

RUN docker-php-ext-install pdo pdo_mysql

COPY ./ /var/www/html

Our Dockerfile is using the php:fpm image, installing the PDO driver, and copying the contents of the project directory into the html directory of the php service. Note that on development using the copy command is a wasted operation.

Looking again at our docker-compose we have created a temporary mounted volume - ./:/var/www/html for both nginx and php services. Now whenever our container is running we can edit, refresh the browser, and see the changes.

Nginx also requires configuration so mount a volume containing your favorite NGINX configuration - ./default.conf:/etc/nginx/conf.d/default.conf. Check out the Github repository for my nginx configuration. I chose to only supply a default configuration for NGINX but you can generate a complete NGINX configuration if you wish. If you do use an nginx.conf you will need to change the volume to something like - ./nginx.conf:/etc/nginx/nginx.conf.

The last part of configuring the nginx service is mapping port 80 to localhost 8080 so we can view the project in our browser.

Finally we set a root password for MariaDB.

Now you can cd into your project folder and run docker-compose up. Visit http://localhost:8080/ in your browser to see the new development server.

Bonus: Composer and Adminer

Let’s add a couple more services to make our lives easier. Below I’ve added services for adminer and composer (both the latest images) to our docker-composer.yml file.

version: "3"

services:

  ...

  adminer:
    image: adminer
    ports:
      - 33066:8080

  composer:
    image: composer
    command: install
    volumes:
      - ./:/app

Just like we mapped ports in our nginx service we are doing the same for adminer. Upon running the docker-compose up command, Composer will install any dependencies you have defined in your composer.json file. If you would like to install or update composer dependencies in an already runner container, you can issue the command docker-compose run --rm composer install.

Visit http://localhost:33066/ in your browser to access Adminer.

Tip: You can execute commands on your container to do things like backup and restore databases.

# Backup
docker exec CONTAINER /usr/bin/mysqldump -u root --password=root DATABASE > backup.sql
# Restore
cat backup.sql | docker exec -i CONTAINER /usr/bin/mysql -u root --password=root DATABASE

Multiple Container Setup

Running everything in a single container has it’s drawbacks probably most importantly it defeats the benefit of containerization. For instance having to run a MariaDB or Adminer service for each new project is redundant. Not to mention the difficulty to going from development to production environments. Lastly, a production database is a complete other article that I won’t be getting into here. So that being said, let’s discuss how to split up these services into multiple development containers.

Since the database is so often the backbone of any web app I’ll start with the MariaDB and Adminer container. Below is a docker-compose.yml example with two services – mariadb and adminer.

Database Container

version: "3"

services:

  mariadb:
    image: mariadb
    volumes:
       - ./sql:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: password
    networks:
      - default
      - database

  adminer:
    image: adminer
    ports:
      - 33066:8080

networks:
  database:
    external: false

The first big change that I’ve added to the bottom of the file is networking. This will allow us to connect our containers and services together. I’ve defined a new network, interally referred to as database, which uses the bridge network driver. In addition, every container has a default bridge network that every service is automatically linked together with.

The mariadb service is connected to the default and database networks. The adminer service is by de facto connected only to the default network. Note: If you define the networks for a service and don’t specify the default network, then you won’t be connected to it.

I’ve also created a volume for sql data to make it more easily visible within the project.

Run docker-compose up on the database container. Try to access Adminer http://localhost:33066/ or list the current networks via cli docker network ls.

NGINX, PHP and Composer Container

version: "3"

services:

  nginx:
    image: nginx
    volumes:
      - ./:/var/www/html
      - ./default.conf:/etc/nginx/conf.d/default.conf
    ports:
      - "8080:80"
  php:
    build: .
    volumes:
      - ./:/var/www/html
    networks:
      - default
      - database

  composer:
    image: composer
    command: install
    volumes:
      - ./:/app

networks:
  database:
    external:
      name: mariadb_database

Similar to the single container example with the big difference being networks that I’ve added to the bottom. Here we define a network, internally named database, that is connecting to the external mariadb_database network. Our php service is utilizing this network so that we have access to the database from our PHP project. Notice that the nginx and composer services do not need access to and by de facto are only connected to the default network.

Run docker-compose up on the nginx, php, composer container and try accessing it via http://localhost:8080/.

Conclusions

For development, the previous two setup methods work pretty well. However going into production further containerization might include splitting up PHP into it’s own scalable container. Other options include using a reverse proxy such as Træfik to load balance many smaller (microservices) on one server.


Sources: http://ctankersley.com/2016/07/27/my-php-docker-workflow/, http://geekyplatypus.com/dockerise-your-php-application-with-nginx-and-php7-fpm/