Following this tutorial you will be able to create a LAMP environment using the Docker technology.
- Root permissions
- Basic knowledge of Docker
- # – requires given linux commands to be executed with root privileges either
directly as a root user or by use of
- $ – requires given linux commands to be executed as a regular non-privileged user
Other Versions of this Tutorial
Docker is an open source project aimed at providing software inside
containers. You can think of a container as a sort of “package”, an isolated environment which shares the kernel with the host machine and contains everything the application needs. All containers are built using
images (the central images repository for them being Dockerhub).
In this tutorial, we will see how to create a LAMP stack based on dockerized components: following the “one service per container” philosophy, we will assemble the environment using
docker-compose, a tool to orchestrate container compositions.
One service vs multiple service for container
There are several advantages in using one service per container, instead of running multiple services in the same one. Modularity, for example, (we can reuse a container for different purposes), or a better maintainability: it’s easier to focus on a specific piece of an environment instead of considering all of them at once. If we want to respect this philosophy, we must create a container for each component of our LAMP stack: one for apache-php and one for the database. The different containers must be able to speak to each other: to easily orchestrate linked containers we will use
Before proceeding we need to install
docker-compose on our system:
# apt-get install docker docker-compose
The packages will be installed in few seconds, and the
docker service will be automatically started. We can now proceed into creating a directory for our project and inside of it, another one to hold the pages that will be served by Apache. DocumentRoot would be a meaningful name for it; in this case the only page that will be served it’s
$ mkdir -p dockerized-lamp/DocumentRoot $ echo "<?php phpinfo(); ?>" > dockerized-lamp/DocumentRoot/index.php
Here our code consists simply in the
phpinfo function: it’s output (a page showing information about php, in case you don’t know) will be what our server will display by default. Now let’s use our favorite editor to create the
docker-compose.yml file for our project.
We can now start providing instruction about building and connecting our containers into the docker-compose file. This is a file which uses the
yaml syntax. All definitions must be provided into the
version: '3' services: php-apache: image: php:7.2.1-apache ports: - 80:80 volumes: - ./DocumentRoot:/var/www/html links: - 'mariadb'
Let’s take a look at what we just done here. The first line we inserted into the file,
version, specifies what docker-compose syntax version we are going to use, in this case the version
3, the latest main version available. Inside the
services section, we started describing our service by specifying its name,
php-apache (an arbitrary name, you can use whatever you want), then the instructions for building it.
image keyword lets docker know what image we want to use to build our container: in this case I used
7.2.1-apache which will provide us php 7.2.1 together with the apache web server. Need another php version? you just need to choose from the many provided in the image page on dockerhub.
The second instruction we provided is
ports: we are telling docker to map the port
80 on our host, to the port
80 on the container: this way will appear as we were running the web server directly on our system.
We then used the
volumes instruction to specify a
bind mount. Since during development the code changes a lot and fast, there would be no sense in putting the code directly inside a container: this way we should rebuild it every time we make some modifications. Instead, what we are going to do is to tell docker to bind-mount the
DocumentRoot directory, at
/var/www/html inside the container. This directory represents the main apache
VirtualHost document root, therefore the code we put inside it, will be immediately available.
Finally we used the
link keyword specifying
mariadb as its argument. This keyword it’s not needed, as it may seem, to create a connection between the two containers: even without specifying it, the
mariadb service would be reachable from inside the container built for the
apache-php service, by using its name as an hostname. The keyword does two things: first let us optionally specify an
alias we can use to reference a service in addition to its name. So, for example, by writing:
the service could also be reached using
database-service. The second thing
link does, is specify a dependency: in this case the
php-apache service will be considered as dependent from the
mariadb one, so the latter will be started before the former when building or starting the environment.
Install php extensions
The default php-apache dockerfile does not include some php extensions, like mysqli or pdo. To install them we have to build our own dockerfile, based on it. To do that, we create a directory inside of our project named php-apache (this will be our
build context) and inside of it, our dockerfile. Paste and save the code below as php-apache/Dockerfile:
FROM php:7.2.1-apache MAINTAINER egidio docile RUN docker-php-ext-install pdo pdo_mysql mysqli
As you can see, with the
FROM instruction, we specified that this dockerfile should be based on the default one. Then we included a
RUN instruction: using the script provided in the image itself,
docker-php-ext-install, we include the extensions needed to use pdo and mysqli. At this point, if we want to use our custom dockerfile, we have to slightly change the php-apache section in our docker-compose.yml, this way:
version: '3' services: php-apache: build: context: ./php-apache ports: - 80:80 volumes: - ./DocumentRoot:/var/www/html links: - 'mariadb'
What has changed? Instead of directly specifying the remote image to use, we provided the
context instruction, inside the
build section, so that the dockerfile contained in the directory we created and here provided as the argument, will be automatically used. The context directory is imported by the docker daemon when building the image, so if we want to add additional files we have to put them also there.
The database service
A database in an essential part of a LAMP environment, it is used for persistence. In this case we are going to use
mariadb: image: mariadb:10.1 volumes: - mariadb:/var/lib/mysql environment: TZ: "Europe/Rome" MYSQL_ALLOW_EMPTY_PASSWORD: "no" MYSQL_ROOT_PASSWORD: "rootpwd" MYSQL_USER: 'testuser' MYSQL_PASSWORD: 'testpassword' MYSQL_DATABASE: 'testdb'
We already know what the
image keyword is for. The same goes for the
volumes instruction, except for the fact that this time we didn’t declared a
bind mount, instead, we referenced a
named volume, for persistence. It’s important to focus on the difference between the two for a moment.
As said before, a
bind mount is a quick way to mount an host directory inside a container, so that the files contained in said directory become accessible from inside the restricted environment: to specify a bind mount, the
short syntax is:
The host path can be a relative (to the docker-compose file) or an absolute path, while the mountpoint inside the container must be specified in absolute form.
named volume is something different: it is a proper
docker volume used for persistence, and it is generally to be preferred over a bind mount, because it doesn’t depend on the host file structure (one of the many advantages of containers it’s their portability). The syntax to use to reference a
named volume inside a service definition is:
named volume life cycle is independent from that of a container which uses it, and must be declared in the
volumes section of the docker-compose file, as we will see in a moment.
Back to the definition of the service now. The last keyword we used is
environment: it lets us set some environment variables which will influence the behavior of the service. First we used
TZ to specify our database timezone: in this case I used “Europe/Rome”. The names of the other variables say everything about their purpose: by using them we set important details as the name of the default database to be created (testdb), the user to be created and its password. We also set the root user password and decided to don’t allow empty passwords.
The volumes section
In this section we must declare the
named volume we referenced from the
mariadb server definition:
At the end, this is how our file will look in its entirety:
version: '3' services: php-apache: image: php:7.2.1-apache ports: - 80:80 volumes: - ./DocumentRoot:/var/www/html:z links: - 'mariadb' mariadb: image: mariadb:10.1 volumes: - mariadb:/var/lib/mysql environment: TZ: "Europe/Rome" MYSQL_ALLOW_EMPTY_PASSWORD: "no" MYSQL_ROOT_PASSWORD: "rootpwd" MYSQL_USER: 'testuser' MYSQL_PASSWORD: 'testpassword' MYSQL_DATABASE: 'testdb' volumes: mariadb:
It’s really important to respect indentation for the file to be interpreted correctly.
Let’s build our environment
Once we specified all instructions for our services, we can use the
docker-compose up command to build them. The command must be executed inside the same directory where the
docker-compose.yml file is located:
# docker-compose up
Few minutes and we will be ready to go. At the end if everything went well, by navigating to
localhost on our host, we shall see the output of the php script we placed inside
Our lamp environment is now ready to be used.
We have seen how to create a basic
LAMP environment, using docker and orchestrating containers and services with
docker-compose. The setup we used it’s focused on development, and can be further expanded and tweaked to match different needs: Docker documentation it’s a very well written source you can consult to expand your docker knowledge. Don’t hesitate to leave a comment for whatever doubts or questions you have.