How to create a docker-based LAMP stack using docker-compose on Ubuntu 18.04 Bionic Beaver Linux

Objective

Following this tutorial you will be able to create a LAMP environment using the Docker technology.

Requirements

  • Root permissions
  • Basic knowledge of Docker

Conventions

  • # – requires given linux commands to be executed with root privileges either
    directly as a root user or by use of sudo command
  • $ – requires given linux commands to be executed as a regular non-privileged user

Other Versions of this Tutorial

Ubuntu 20.04 (Focal Fossa)

Introduction

docker_logo
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 docker-compose.

Preliminary steps

Before proceeding we need to install docker and 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 index.php:

$ 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.



Php-apache

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 services section.

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.

The 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:

link:
    mariadb:database-service

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:

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:

<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.

A 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:

<volume_name>:<mountpoint_inside_container>

A 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:

volumes:
    mariadb:

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 DocumentRoot:

phpinfo-output

Our lamp environment is now ready to be used.

Closing thoughts

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.