ObjectiveFollowing 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 TutorialUbuntu 20.04 (Focal Fossa)
IntroductionDocker 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 containerThere 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
Preliminary stepsBefore proceeding we need to install
docker-composeon our system:
# apt-get install docker docker-composeThe packages will be installed in few seconds, and the
dockerservice 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
Here our code consists simply in the
$ mkdir -p dockerized-lamp/DocumentRoot $ echo "<?php phpinfo(); ?>" > dockerized-lamp/DocumentRoot/index.php
phpinfofunction: 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.ymlfile for our project.
Php-apacheWe can now start providing instruction about building and connecting our containers into the docker-compose file. This is a file which uses the
yamlsyntax. All definitions must be provided into the
Let's take a look at what we just done here. The first line we inserted into the file,
version: '3' services: php-apache: image: php:7.2.1-apache ports: - 80:80 volumes: - ./DocumentRoot:/var/www/html links: - 'mariadb'
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
servicessection, 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.
imagekeyword lets docker know what image we want to use to build our container: in this case I used
7.2.1-apachewhich 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
80on our host, to the port
80on the container: this way will appear as we were running the web server directly on our system.
We then used the
volumesinstruction 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
/var/www/htmlinside the container. This directory represents the main apache
VirtualHostdocument root, therefore the code we put inside it, will be immediately available.
Finally we used the
mariadbas 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
mariadbservice would be reachable from inside the container built for the
apache-phpservice, by using its name as an hostname. The keyword does two things: first let us optionally specify an
aliaswe 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
linkdoes, is specify a dependency: in this case the
php-apacheservice will be considered as dependent from the
mariadbone, so the latter will be started before the former when building or starting the environment.
Install php extensionsThe 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:
As you can see, with the
FROM php:7.2.1-apache MAINTAINER egidio docile RUN docker-php-ext-install pdo pdo_mysql mysqli
FROMinstruction, we specified that this dockerfile should be based on the default one. Then we included a
RUNinstruction: 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:
What has changed? Instead of directly specifying the remote image to use, we provided the
version: '3' services: php-apache: build: context: ./php-apache ports: - 80:80 volumes: - ./DocumentRoot:/var/www/html links: - 'mariadb'
contextinstruction, inside the
buildsection, 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 serviceA database in an essential part of a LAMP environment, it is used for persistence. In this case we are going to use
We already know what the
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'
imagekeyword is for. The same goes for the
volumesinstruction, 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 mountis 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
<host_path>:<mountpoint_inside_the_container>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 volumeis something different: it is a proper
docker volumeused 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 volumeinside a service definition is:
named volumelife cycle is independent from that of a container which uses it, and must be declared in the
volumessection 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
TZto 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 sectionIn this section we must declare the
named volumewe referenced from the
At the end, this is how our file will look in its entirety:
It's really important to respect indentation for the file to be interpreted correctly.
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:
Let's build our environmentOnce we specified all instructions for our services, we can use the
docker-compose upcommand to build them. The command must be executed inside the same directory where the
docker-compose.ymlfile is located:
# docker-compose upFew minutes and we will be ready to go. At the end if everything went well, by navigating to
localhoston our host, we shall see the output of the php script we placed inside
Our lamp environment is now ready to be used.
Closing thoughtsWe have seen how to create a basic
LAMPenvironment, 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.