LAMP Stack
Dev Environment with Docker
Today I moved a local website project from XAMPP to a docker based development environment. Here is a summary how to setup Apache, PHP and MySQL with docker container.
Disclaimer
I use this setup for local development only. Please don't use this in production unless you know what you are doing.
Why
Until now my local development environment was based on a single XAMPP installation. This worked pretty well but as new projects came up with new requirements, it became very hard to manage all this with one XAMPP installation. So I decided to find another solution.
I searched for a way to
- set PHP and MySQL version and configurations per project
- change a projects environment without breaking another
- create a new environment fast
In other words I wanted to have an independent development environment for each website project. So I deceided to give Docker a try.
Docker Basics
It is a good idea to get a basic understanding of what docker is and how it works. I would refer to the official docs. It will also be helpful to have a basic understanding of docker images
, container
, services
and volumes
.
The installation is pretty easy. Just go to https://docker.com and download the latest version for your OS.
Dockerfile & Docker Compose
A Docker container is created from an image which is defined by a Dockerfile
. Images can inherit from other images. While you can create your own images, most of the time you will start with already existing images from Docker Hub.
Since every service (MySQL, Apache+PHP) runs in its own containter, the classic LAMP stack is a good use-case for Docker Compose. Docker Compose is a
...tool for defining and running multi-container Docker applications.
It uses a configuration file named docker-compose.yml
to define all the services.
MySQL
The creation of a MySQL container is pretty straight forward. There is an official MySQL image on Docker Hub which can be used. In the docker-compose.yml
file I created a new mysql
service (or whatever name you like). Then I denfined the image which I would like to use. In this case it is mysql
version 5.6
.
The data in a container is not persistent by default. To preserve the MySQL data during a container shutdown you have to define a volume by adding it to the volumes
section. Here I named the volume mysql
. Then I mapped a directory path from the service to this volume. The MySQL data is stored in /var/lib/mysql
, so I mapped this directory to the created volume name.
More config options and other ways to make the data persistent can be found on the images Docker Hub page https://hub.docker.com/_/mysql.
Setting the root password and create a database and a user can be done via the corresponding environment variables.
If you want to access the database from your host system (eg. with a client like Sequel Pro Ace) you have to map the MySQL port (default is 3306) from the container to a host port. Then you can connect to the database via 127.0.0.1:3306
using the credentials set earlier.
#docker-compose.yml
version: "3.1"
services:
mysql:
image: mysql:5.6 #install mysql:version
container_name: project-name-mysql
working_dir: /www
volumes:
- mysql:/var/lib/mysql #map volume to path to persist data VOLUME_NAME:MAPPED_PATH
environment:
MYSQL_ROOT_PASSWORD: "secret"
MYSQL_DATABASE: "MyDatabase" #create database
MYSQL_USER: "MyDatabaseUser" #create user for database
MYSQL_PASSWORD: "secret"
ports:
- "3306:3306" #map port to host HOST:CONTAINER
volumes: #define volume name 'mysql'
mysql:
Apache & PHP
Like the MySQL image there is an official image for PHP with Apache on Docker Hub. This time I've created a seperate Dockerfile
. This allows me to change the default Apache/PHP configuration (eg. enable Apache modules, install PHP modules) during container startup. The FROM
line sets the base image.
#./docker/php-apache/Dockerfile
#set base image
FROM php:7.2-apache
This file can now be used in the docker-compose.yml
. I added a php-apache service and set the build context to the path of the Dockerfile
.
In the volumes
section I mapped the default Apache document root /var/www/html
to a directory on my local file system. Here I used a directory called www
relative to the directory of the docker-compose.yml
file. Like the database the webserver port has to be mapped to a port on the host so that it can be accessed through the browser. I used the standard HTTP port 80
but you can use whatever you like.
By default docker container can't talk to each other. You have to link the services first. I linked both since the PHP application needs to access the MySQL container to connect to the database. This is done by adding the name of the database service ('mysql'
) to the links
section.
#docker-compose.yml
version: "3.1"
services:
mysql:
image: mysql:5.6 #install mysql:version
container_name: project-name-mysql
working_dir: /www
volumes:
- mysql:/var/lib/mysql #volume to persist data
environment:
MYSQL_ROOT_PASSWORD: "secret"
MYSQL_DATABASE: "MyDatabase" #create database
MYSQL_USER: "MyDatabaseUser" #create user for database
MYSQL_PASSWORD: "secret"
ports:
- "3306:3306" #map port to host
php-apache:
build:
context: ./docker/php-apache #install from Dockerfile
ports:
- 80:80 #map port to host
volumes:
- ./www:/var/www/html #set directory './www' as Document Root
links:
- 'mysql' #make mysql service available to php-apache
volumes: #define volume
mysql:
Enable Apache Modules
Sometimes you need to enable a specific Apache module or add a PHP module to run your application. You can install pretty much everthing by simply adding a RUN
command followed by the linux command to the ./docker/php-apache/Dockerfile
.
Apache modules can be enabled by using the RUN
command followed by a2enmod <module>
.
#./docker/php-apache/Dockerfile
#set base image
FROM php:7.2-apache
#apache mod_rewrite
RUN a2enmod rewrite
pdo, pdo_mysql, mysqli...
The image provides a function called docker-php-ext-install
for installing and enabling PHP modules. To install pdo, pdo_mysql and mysqli I added the following RUN
command.
#./docker/php-apache/Dockerfile
#set base image
FROM php:7.2-apache
#apache mod_rewrite
RUN a2enmod rewrite
#add php mysql modules
RUN docker-php-ext-install pdo pdo_mysql mysqli
GD
The gd module requires some depdencies which have to be installed. The Apache PHP image is based on Ubuntu so software can be installed by using the package manager apt-get
.
#./docker/php-apache/Dockerfile
#set base image
FROM php:7.2-apache
#apache mod_rewrite
RUN a2enmod rewrite
#add php mysql modules
RUN docker-php-ext-install pdo pdo_mysql mysqli
#install php gd
RUN apt-get update && \
apt-get install -y libfreetype6-dev libjpeg62-turbo-dev libpng-dev && \
docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ && \
docker-php-ext-install gd
XDebug
XDebug is a pecl extension and can be installed by calling the pecl install
command. The yes |
part ensures that the installation works without user interaction. The echo ... > ...xdebug.ini
part writes the path to the extension into xdebug.ini
.
Update XDebug 3.x
Since XDebug 3.x the configuration has changed. I've left the docker block for using 2.x
commented.
#./docker/php-apache/Dockerfile
#set base image
FROM php:7.2-apache
#apache mod_rewrite
RUN a2enmod rewrite
#add php mysql modules
RUN docker-php-ext-install pdo pdo_mysql mysqli
#install php gd
RUN apt-get update && \
apt-get install -y libfreetype6-dev libjpeg62-turbo-dev libpng-dev && \
docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ && \
docker-php-ext-install gd
#php xdebug 2.x
#RUN yes | pecl install xdebug \
# && echo "zend_extension=$(find /usr/local/lib/php/extensions/ -name xdebug.so)" > /usr/local/etc/php/conf.d/xdebug.ini;
#php xdebug 3.x
RUN yes | pecl install xdebug && docker-php-ext-enable xdebug
RUN echo "xdebug.mode=debug" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini;
RUN echo "xdebug.start_with_request=yes" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini;
To make XDebug work in VSCode I had to add the XDEBUG_CONFIG
environment variable which sets the remote host and port for the debugging server.
#docker:compose.yml
...
php-apache:
build:
context: ./docker/php-apache #install from Dockerfile
ports:
- 80:80 #map port to host
volumes:
- ./www:/var/www/html #set directory './www' as Document Root
environment:
#XDEBUG_CONFIG: remote_host=host.docker.internal remote_port=9000 remote_enable=1 #XDebug 2.x
XDEBUG_CONFIG: client_host=host.docker.internal client_port=9000 #XDebug 3.x
links:
- 'mysql' #make mysql service available to php-apache
...
Also a pathMapping has to be added to the debugger launch configuration.
//.vscode/launch.json
{
"name": "Listen for XDebug",
"type": "php",
"request": "launch",
"port": 9000,
"pathMappings": {
"/var/www/html": "${workspaceFolder}/"
},
},
Build and Run Container
To start all services run docker-compose up
command from the directory where your placed the docker-comopse.yml
. If you need to make changes to your container configration (eg. add a php module) you need to rebuild the container with docker-compose up --build
.
With docker-compose stop
you can stop the running container, while docker-compose down
stops and removes containers created by the up
command. Volumes
are not beeing removed (unless the --volumes
option is used).
Whenever you change your docker-compose.yml
configuration and do a docker-compose up
a new container is created. This can lead to several unused containers on your disk. You can delete unused containers and data with docker system prune command. Read the docs before using it!
Complete Dockerfile and docker-compose.yml
So here is my complete docker-compose.yml
and php-apache Dockerfile
with MySQL, XDebug, GD and mod_rewrite.
#./docker/php-apache/Dockerfile
#set base image
FROM php:7.2-apache
#apache mod_rewrite
RUN a2enmod rewrite
#add php mysql modules
RUN docker-php-ext-install pdo pdo_mysql mysqli
#install php gd
RUN apt-get update && \
apt-get install -y libfreetype6-dev libjpeg62-turbo-dev libpng-dev && \
docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ && \
docker-php-ext-install gd
#php xdebug 2.x
#RUN yes | pecl install xdebug \
# && echo "zend_extension=$(find /usr/local/lib/php/extensions/ -name xdebug.so)" > /usr/local/etc/php/conf.d/xdebug.ini;
#php xdebug 3.x
RUN yes | pecl install xdebug && docker-php-ext-enable xdebug
RUN echo "xdebug.mode=debug" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini;
RUN echo "xdebug.start_with_request=yes" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini;
#docker_compose.yml
version: "3.1"
services:
mysql:
image: mysql:5.6 #install mysql:version
container_name: project-name-mysql
working_dir: /www
volumes:
- mysql:/var/lib/mysql #volume to persist data
environment:
MYSQL_ROOT_PASSWORD: "secret"
MYSQL_DATABASE: "MyDatabase" #create database
MYSQL_USER: "MyDatabaseUser" #create user for database
MYSQL_PASSWORD: "secret"
ports:
- "3306:3306" #map port to host
php-apache:
build:
context: ./docker/php-apache #install from Dockerfile
ports:
- 80:80 #map port to host
volumes:
- ./www:/var/www/html #set directory './www' as Document Root
environment:
#XDEBUG_CONFIG: remote_host=host.docker.internal remote_port=9000 remote_enable=1 #XDebug 2.x
XDEBUG_CONFIG: client_host=host.docker.internal client_port=9000 #XDebug 3.x
links:
- 'mysql' #make mysql service available to php-apache
volumes: #define volume
mysql:
Conclusion
It took me some time and reading to get a basic understanding of how Docker, container, services, volumes and all that works. But now that I have a working docker_compose.yml
setting up a new development environments is a matter of minutes.