The LAMP stack is one of the most common software stacks for building web applications. Each letter in the acronym stands for one of its four open-source building blocks:
- Linux for the operating system.
- Apache HTTP Server
- MySQL for the relational database management system
- PHP.
Nowadays LAMP refers to a generic software stack model where its components are largely interchangeable. For example, MariaDB instead of MySQL, and Python (or even Perl) instead of PHP.
I chose Docker container to avoid issues at deployment (actually evaluation). In other words, to avoid incidents when we move our Web app from our development machine to the server.
Let's assume Docker is already installed in our development machine.
Docker Compose is a tool for defining and running multi-container Docker applications. In our case, our web-app is gonna need the following containers:
With Compose, we just have to write a YAML file to configure your application’s services. Then, with a single command, you create and start all the services from our configuration.
The Compose file is a YAML file named compose.yaml
located at the root of our project. In such a file we'll define things such as:
- Services, which are the containers that our application needs to run (Apache, MySQL, PHP).
- Networks, through which services communicate with each other.
- Volumes, where services store and share persistent data.
- Configs, for our services.
- Secrets, for our services.
For example, let's create a basic file to serve PHP files:
services:
web:
image: php:8.1-rc-apache-buster
volumes:
- './app:/var/www/html'
ports:
- 8080:80
- 4430:443
-
The
services
keyword is the only REQUIRED element in the file. Under it, we're creating a service namedweb
(arbitrary name). This service is gonna be based on a PHP container, whose image we'll pull from Docker Hub (find the PHP official image, and once there click on Available Tags to choose the one we prefer). -
We'll also gonna need a
volume
, which is a mapping of a location in our filesystem to a location within the container. In this case we're connecting the./app
folder in our development machine to the directory from which Apache serves the PHP stuff (/var/www/html
).
Remember, the location we just mentioned is the default document root in Apache.
- Finally, we'll map the port
8080
in our physical machine, to the port80
within the container.
For testing purposes, we'll create a basic index.php
file in our ./app
folder, with the classic call to the phpinfo() function:
<?php phpinfo(); ?>
In order to test out our small set up, we have two choices:
- Use the original
docker-compose up
command. - Or make use of the version 2
docker compose up
, which integrates the compose features into thedocker
platform.
Don't forget to make sure the
docker
service is running:sudo systemctl start docker
If you face some /var/run/docker.sock: connect: permission denied.
issue (I was working on Linux, not likely to find that in macOS) try adding your user to the docker
group:
sudo usermod -a -G docker $USER
After adding your user to the
docker
group, you may have to reboot your system before continuing on.
That should start pulling the images that don't exist in our system. After the process finishes, we should be able of pointing our browser to http://localhost:8080/ and see the image below:
Once we've checked that our configuration file is working, we'll hit Ctrl + c
to stop the server and:
docker compose down
This last command will remove any containers that were running from Compose.
For this container we'll use the official MySQL image, along with the latest
tag. This page contains information about some environment variables that we'll have to set up, namely:
- MYSQL_DATABASE: example
- MYSQL_USER: bob
- MYSQL_PASSWORD: 1234
- MYSQL_ROOT_PASSWORD: 1234
So this is what we'll add:
services:
# more stuff
db:
image: mysql:latest
restart: always
environment:
MYSQL_DATABASE: example
MYSQL_USER: bob
MYSQL_PASSWORD: 1234
MYSQL_ROOT_PASSWORD: 1234
Let's create a service for phpMyAdmin; so under services
we'll add:
services:
# more stuff
phpmyadmin:
image: phpmyadmin:latest
ports:
- 8081:80
environment:
PMA_HOST: db
PMA_PORT: 3306
Once you've finished the configuration don't forget to Ctrl + c
and:
docker compose down
docker compose up
Or even better, use
docker compose restart
❗.
After this we should be able of pointing our browser to http://localhost:8081/ and start manipulating our database.
A good idea after creating some records to test our database, would be to export our data to our local filesystem.
If you have available some data, or we want to use the dump.sql
file you generated at the end of the last section, we could do so by mounting a volume
under our db
service:
services:
# more stuff
db:
image: mysql:latest
restart: always
environment:
MYSQL_DATABASE: example
MYSQL_USER: root
MYSQL_PASSWORD: 1234
MYSQL_ROOT_PASSWORD: 1234
volumes:
- 'db_data:/var/lib/mysql'
In the last line above, we're mapping the db_data
folder (in our local filesystem) to the standard /var/lib/mysql
folder in the container.
Don't forget to place the
dump.sql
file into thedb_data
folder.
Once you've finished the configuration don't forget to Ctrl + c
and:
docker compose down
docker compose up
Let's put everything together and try to read data from our database using our index.php
:
<?php
// Set up database
$options = [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION];
$dsn = "mysql:host=db";
$dbh = new PDO($dsn,
'root',
'1234',
$options);
$dbh->exec('CREATE DATABASE `camagru`');
die('DB has been set the fuck up!');
?>
Even though it seems we've done everything right, if we point our browser to, we'd be punished with the following error:
Fatal error: Uncaught Error: Call to undefined function ...
The reason for that is that PDO is a PHP extension that is not installed in the image we're pulling in our compose.yml
file.
To solve the issue related above, we have to refactor our php
service in the following manner:
services:
web:
build:
context: ./dockerfiles
dockerfile: Dockerfile
volumes:
- './app:/var/www/html'
ports:
- 8080:80
- 4430:443
# more stuff
And in the ./php_extensions
directory, we'll add a Dockerfile
with the following content:
FROM php:8.1-rc-apache-buster
RUN docker-php-ext-install pdo && docker-php-ext-enable pdo
One of the things I noticed while developing the app in Docker, was the need of editing configuration files for the server, PHP interpreter and so on. A pattern that proved useful to me was:
- Copy the original configuration file to your host:
$ docker cp a22fac:./your_proj/apache2.conf
- Mount it as a Docker volume:
volumes:
- ./docker/php_apache/apache2.conf:/etc/apache2/apache2.conf
After that, you'll have a gate to that configuration file on your running container (Don't forget that some services must be restarted after configuration changes).
What if, during the building image stage you need to make some change (e.g. change permissions) to a configuration file that does not exist yet? Well, you can create the file, let's say one for configuring msmtp, which must have certain permissions. Then, in your Dockerfile you have to:
- Copy the file to the image:
COPY msmtprc /etc/msmtprc
- Change the permissions in the same
Dockerfile
:
RUN chmod 600 /etc/msmtprc
What if you have to make configurations to that file while the container is running? Easy, mount the file as a volume in your compose.yml
:
volumes:
- ./docker/php_apache/msmtprc:/etc/msmtprc