How to Dockerizing LEMP Stack with Docker-Compose
LEMP: Linux, Nginx, PHP, MySQL
Nginx
Create docker-compose.yml in a directory(lemp) is as follows:
version: '3.8'
services:
nginx:
image: nginx:1.20.2
ports:
- 80:80
The -d option indicates that run the containers in the background. It might take a little while as the Nginx image will first be downloaded from Docker Hub. When it is done, open localhost in your browser, which should display Nginx's welcome page:
Welcome to nginx!
If you see this page, the nginx web server is successfully installed and working. Further configuration is required.
For online documentation and support please refer to
nginx.org.
Commercial support is available at
nginx.com.
Thank you for using nginx.
PHP
Replace the content of docker-compose.yml with this one:
version: '3.8'
services:
nginx:
image: nginx:1.20.2
ports:
- 80:80
volumes:
- ./src:/var/www/php
- ./.docker/nginx/conf.d:/etc/nginx/conf.d
depends_on:
- php
php:
image: php:8.0-fpm
working_dir: /var/www/php
volumes:
- ./src:/var/www/php
The depens_on configuration ensures the PHP container will start before Nginx.
Create the src directory (at the same level as docker-compose.yml) and add index.php file to it. The content of index.php is <?php phpinfo();?> .
Create the .docker/nginx/conf.d folder and add the following php.conf file to it:
server {
listen 80 default_server;
listen [::]:80 default_server;
root /var/www/php;
index index.php;
location ~* \.php$ {
fastcgi_pass php:9000;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
}
}
Access localhost/index.php you will see the PHP version page as following:
Let's inspect the PHP container:
$ docker-compose exec php bash
index.php
root@23ba40064293:/var/www/php# cat index.php
<?php phpinfo();?>
root@23ba40064293:/var/www/php# pwd
/var/www/php
root@23ba40064293:/var/www/php# exit
exit
Wait for a few logs to display, and hit the return key a few times to add some empty lines. Refresh localhost/index.php again and take another look at your terminal, which should have printed some new lines.
Hit Ctrl+C to get your terminal back.
MySQL
Let's update docker-compose.yml again:
version: '3.8'
services:
nginx:
image: nginx:1.20.2
ports:
- 80:80
volumes:
- ./src:/var/www/php
- ./.docker/nginx/conf.d:/etc/nginx/conf.d
depends_on:
- php
php:
build: ./.docker/php
working_dir: /var/www/php
volumes:
- ./src:/var/www/php
depends_on:
mysql:
condition:service_healthy
mysql:
volumes:
mysqldata:
The new build section PHP service replaced the image one. Instead of using the official PHP image as is, we tell Docker Compose to use the Dockerfile from .docker/php to build a new image.
Create the .docker/php folder and add a file named Dockerfile to it, with the following content:
PHP needs the pdo_mysql extension in order to read from a MySQL database.
For the time being, let's update index.php to leverage the new extension:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Hello there</title> <style> body { font-family: "Arial", sans-serif; font-size: larger; } .center { display: block; margin-left: auto; margin-right: auto; width: 50%; } </style> </head> <body> <img src="https://www.freebsd.org/images/logo-red.png" alt="Hello there" class="center"> <?php $connection = new PDO('mysql:host=mysql;dbname=demo;charset=utf8', 'root', 'root'); $query = $connection->query("SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = 'demo'"); $tables = $query->fetchAll(PDO::FETCH_COLUMN); if (empty($tables)) { echo '<p class="center">There are no tables in database <code>demo</code>.</p>'; } else { echo '<p class="center">Database <code>demo</code> contains the following tables:</p>'; echo '<ul class="center">'; foreach ($tables as $table) { echo "<li>{$table}</li>"; } echo '</ul>'; } ?> </body> </html>
The image section points to MySQL Server's image for version 8.0, and it is followed by a section we haven't come across yet: environment . It contains three key - MYSQL_ROOT_PASSWORD , MYSQL_ROOT_HOST and MYSQL_DATABASE - which are environment variables that will be set on the container upon creation. They allow us to set the root password, authorize connections from any IP address, and create a default database respectively.
And a demo database will automatically be created for us when the container starts.
The first volume is a configuration file we will be using to set the chracter set to utf8mb4_unicode_ci by default, which is pretty standard nowdays.
Create the .docker/mysql folder and add the following my.cnf file to it:
The second volume looks a bit different than what we have seen so far: instead of pointing to a local folder, it refers to a named volume defined in a whole new volumes section which sits at the same level as services :
We need such a volume because without it, every time the mysql service container is destroyed the database is destroyed with it. To make it persistent, we basically tell the MySQL container to use the mysqldata volume to store the data locally, local being the default driver.
The last section is a new one: healthcheck . It allows us to specify on which condition a container is ready, as opposed to just started. In this case, it is not enough to start the MySQL container - we also want to create the database before the PHP container tries to access it. In other words, without this health check the PHP container might try to access the database even though it doesn't exist yet, causing connection errors.
Be default, depends_on will just wait for the referenced containers to be started, unless we specify otherwise. This health check might not work on the first attempt, however; that's why we set it up to retry every 5 seconds up to 10 times, using the interval and retries keys respectively.
The health check itself uses mysqladmin, a MySQL server adminstration program, to ping the server until it gets a response. It does so using the root user and the value set in the MYSQL_ROOT_PASSWORD environment variable as the password (which also happens to be root in our case).
Your directory and file structure should now look similar to this:
lemp
├── .docker
│ ├── mysql
│ │ └── my.cnf
│ ├── nginx
│ │ └── conf.d
│ │ └── php.conf
│ └── php
│ └── Dockerfile
├── docker-compose.yml
└── src
└── index.php
Go back to your terminal and run :
$ docker-compose up -d
You should see this:
We now have Nginx serving PHP files that can connect to a MySQL database, meaning our LEMP stack is pretty much complete. The next steps are about improving our setup, starting with seeing how we can interact with the database in a user-friendly way.
phpMyAdmin
When it comes to dealing with a MySQL database, phpMyAdmin remains a popular choice; conveniently, they provide a Docker image which is pretty straightforward to set up.
Open docker-compose.yml one last time and add the following service configuration after MySQL's:
We start from version 5 of the image and we map the local machine's port 8080 to the container's port 80. We indicate that the MySQL container should be started and ready first with depends_on , and set the host that phpMyAdmin should connect to using the PMA_HOST environment variable (remember that Docker Compose will automatically resolve mysql to the private IP address it assigned to the container).
Save the changes and run:
$ docker-compose up -d
The image will be download, then, once everything is up, visit localhost:8080
Enter root / root as username and password, create a couple of tables under the demo database and refresh localhost to confirm they are correctly listed.
And that's it! That one was easy, right?
Reference:
Docker for local web development, part 1: a basic LEMP stack
Comments
Post a Comment