The LAMP stack
LAMP is the software stack on which probably the majority of websites run. Linux represents the foundation of the stack, and the traditional implementation includes Apache as the web server, the MySQL database, and PHP as the server-side programming language. There are, however, many possible variations: MariaDB, for example, is often used in place of MySQL, of which it is a fork, and other programming languages, as Python or Perl can be used instead of PHP. In this article we will see how to implement a basic LAMP stack using docker and the docker-compose utility.In this tutorial you will learn:
- How to install docker and docker-compose on Ubuntu 20.04
- How to define services and volumes using docker-compose
- How to map host ports to container ports in the docker-compose configuration file
- How to use bind mounts and named volumes
- How to build a project with docker-compose
Software Requirements and Conventions Used
|Category||Requirements, Conventions or Software Version Used|
|System||Installed Ubuntu 20.04 or Upgraded Ubuntu to 20.04 Focal Fossa|
|Other||Root permissions to build docker containers and launch the docker service|
|Conventions|| # - requires given linux commands to be executed with root privileges either directly as a root user or by use of |
Installing packages and starting the docker service
In order to create a Docker-based LAMP stack on Ubuntu 20.04 Focal Fossa, the first thing we have to do is to install the software we need:
docker-compose, which is an utility that let us easily organize multi-container applications using
yamlconfiguration files. Both packages are available in the Ubuntu official repositories. We can install them via
$ sudo apt install docker docker-compose
After the installation is performed we must start the
dockerservice and enable it at boot. We can perform both operations with one single command:
$ systemctl enable --now docker
The first step in our journey consists into the creation of the directory we will use as the root of our project. For the sake of this article we will call it
linuxconfig. Inside this directory we will create another one,
DocumentRoot, which will host our website files. We can create both directories at once using the
-poption of the
$ mkdir -p linuxconfig/DocumentRoot
linuxconfigdirectory, we define the docker-compose configuration for our project inside a yaml file, which by default should be called
docker-compose.yml. There are three main stanzas we can use in the configuration file: services, volumes and networks.
Each section is used to configure the corresponding aspect of a project. In this tutorial we will use only the first two. We will implement the components of the LAMP stack as services inside their own separate containers.
The containers created with docker-compose will be members of the same network and therefore will be able to talk to each other by default. In the network, each container will be able to reference the others by an hostname identical to their name, or by the name used to define the service implemented by the container.
By default containers will be named using the name of the directory containing the configuration file as prefix. In this case, for example, the container used for a service called php-httpd, will be named linuxconfig_php-httpd_1.
Defining the php + httpd service
The first service we will define in the configuration file will include PHP as the Apache web server module. We will use one of the official PHP images available on dockerhub as the base for our container, specifically the one with the
-apachesuffix, which provides the setup we mentioned above. Let’s start writing our configuration:
version: '3.7' services: php-httpd: image: php:7.3-apache ports: - 80:80 volumes: - "./DocumentRoot:/var/www/html"
The first thing we specified in the configuration file is
version. With this instruction we declare what specific version of the compose file we are going to use. At the moment of writing, version
3.7is the latest and recommended one.
After declaring the compose file version, we started writing the service stanza; inside of it we define the services which will compose our LAMP stack. We called the first service
php-httpd. The service name is completely arbitrary, but is always a good habit to use one that is meaningful in the context of the project.
imageinstruction is used to specify on which image the container should be based, in this case
portsinstruction is used to expose ports on the container, and to create a map between host ports and container ports. Such map is defined by separating the ports with a
:. On the left side we specify the host port, and on the right, the port inside the container it should be mapped to. In this case we mapped port
80on the host to port
80on the container, since it is the default port used by the Apache web server.
The last instruction we used is
volumes: with it we can specify a mapping between a named volume or a path (relative or absolute) on the host system to a path on the container, on which it will be mounted.
In our setup, the
./DocumentRootdirectory will host the site files: it will be mounted on the
/var/www/htmldirectory inside the container, because the latter is the document root used by the default Apache VirtualHost. Such setup is called a bind mount and is especially useful during development because the changes we make on the project files, are immediately reflected inside the container. The downside of this configuration is that it establishes a dependency between the container and the host machine file structure, diminishing one of the main advantage of using Docker: portability.
The directory to be mounted inside the container will be created automatically if it doesn’t exist when the
docker-compose upcommand is launched: in that case it will be owned by root if not otherwise specified.
DocumentRootdirectory we can now create an index file, and try to build our project to verify the setup is working:
$ echo "<?php phpinfo();" > DocumentRoot/index.php $ sudo docker-compose up -d
After executing the command, the needed docker images will be downloaded from dockerhub and the containers we will be created with the settings we provided and run in background (they will not block the terminal), because of the
-doption we provided to the
docker-composecommand. With the project up and running, if we navigate to
localhostwith our browser, we should see the following page:
To stop the project, from the directory hosting the
docker-compose.ymlfile, we can run:
$ sudo docker-compose stop
Defining the MariaDB service
An essential part of the LAMP stack is the database layer. In our configuration we will use MariaDB and its official docker image available on dockerhub:
version: '3.7' services: php-httpd: image: php:7.3-apache ports: - 80:80 volumes: - "./DocumentRoot:/var/www/html" mariadb: image: mariadb:10.5.2 volumes: - mariadb-volume:/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-volume:
Inside the services stanza, we defined another service and call it
mariadband with the
imageinstruction we specified we want to use the
10.5.2version of the official image.
In the previous service definition we used a bind mount. This time, instead, we used a proper docker named volume, to be mounted on
/var/lib/mysqlinside the container (it is the default data directory used by MariaDB). Unlike a bind mount, named volumes don’t create dependencies of the container on the host filesystem structure. Completely managed by Docker, they are the recommended method of persisting data which otherwise would be lost when containers are destroyed.
Named volumes can be defined in the main
volumesstanza of the configuration file and can be referenced from the
volumessubsection of each defined services. In this case we called our volume
As a next step we defined the value of some environment variables used to influence the container behavior. Environment variables are defined in the
environmentsection of a service definition. The variables we defined in this case have the following effect:
Variable Effect TZ Set the timezone used by the MariaDB server MYSQL_ALLOW_EMPTY_PASSWORD Enable or disable the use of blank password for the db root user MYSQL_ROOT_PASSWORD This is a mandatory variable and is used to set the db root user password MYSQL_DATABASE Optionally used to specify the name of database to be created on image startup MYSQL_USER Optionally used to specify the name of a user that will be created with superuser permissions for the database specified with MYSQL_DATABASE MYSQL_PASSWORD Used to specify the password for the user created with the name provided by MYSQL_USER
At this point we should have a working web server able to work with PHP, and a database to store our data.
Bonus - phpMyAdmin
Our basic LAMP stack should now be complete. As a bonus, we may want to add phpMyAdmin to it, in order to easily control our MariaDB database from a user-friendly web interface. Let’s add the related service definition to our docker-compose configuration:
version: '3.7' services: php-httpd: image: php:7.3-apache ports: - 80:80 volumes: - "./DocumentRoot:/var/www/html" mariadb: image: mariadb:10.5.2 volumes: - mariadb-volume:/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' phpmyadmin: image: phpmyadmin/phpmyadmin links: - 'mariadb:db' ports: - 8081:80 volumes: mariadb-volume:
We named our service
phpmyadmin and configured it to use the phpmyadmin/phpmyadmin image from dockerhub. We also used the
links keyword for the first time; what is this for? As we already know, by default, and with no special configurations needed, all the containers created in the same docker-compose configuration are able to talk to each other. The phpMyAdmin image is configured to reference a running database container by the
db name, therefore we need to create an alias with the same name for our mariadb service. This is exactly what
links is used for: to define extra aliases to reach a service from another one.
Inside the service definition we also mapped port
8081 of our host machine, to port
80 inside the container (port 80 is already mapped to the same port inside the php-httpd container). The phpMyAdmin interface will be therefore reachable at the localhost:8081 address. Let’s rebuild our project and verify it:
$ sudo docker-compose up -d --build
We can login with the credentials we defined for our database service, and verify that the
testdb database has been created:
Using a custom image for a service
In the examples above we always used vanilla images in our services definition. There are cases in which we may want to use custom docker images based on them. For example, say we want to build the php-httpd service, but include an additional php extension: how can we do it? On the root of the project, we define a new directory, and for convenience name it after the service:
$ mkdir php-httpd
Inside this directory we create a Dockerfile, used to extend the base image, with the following content:
FROM php:7.3-apache LABEL maintainer="firstname.lastname@example.org" RUN apt-get update && apt-get install -y libmcrypt-dev \ && pecl install mcrypt-1.0.2 \ && docker-php-ext-enable mcrypt
Back in our
docker-compose.yml file, we modify the definition of the
php-httpd service. We cannot reference the image directly as we did before. Instead, we specify the directory containing our custom Dockerfile as the build context:
version: '3.7' services: php-httpd: build: context: ./php-httpd ports: - 80:80 volumes: - "./DocumentRoot:/var/www/html" [...]
build section we define configurations that are applied at build time. In this case, we used
context to reference the directory containing the Dockerfile: said directory is used as build context, and its content is sent to the Docker daemon when the container is built. To apply the modification we must re-build the project.
By the way, to know more about additional extensions in the php docker image, you can take a look at the official documentation, and specifically the PECL extensions section.
In this tutorial we saw how to build a basic LAMP stack using the container technology with Docker and docker-compose. We saw how to define the various services inside the docker-compose.yml configuration file, and how to configure bind mounts, named volumes and host-container ports mapping. We also saw how to use custom images. You can take a look at the docker-compose reference for the detailed list of instructions that can be used inside the docker-compose configuration file.