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
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
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
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.
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
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.
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
docker-compose up on the nginx, php, composer container and try accessing it via http://localhost:8080/.
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.