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

Let's test this out . 
$ docker-compose up -d 

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.

To stop the container, simply run:
$ docker-compose stop 

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:

php8

Let's inspect the PHP container:

$ docker-compose exec php bash 

root@23ba40064293:/var/www/php# ls 
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


Go back to your terminal and run the following command:
$ docker compose logs -f 

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:

    image: mysql/mysql-server:8.0      
       ports:
          - 3306:3306
       stdin_open: true  # docker run -i
       tty: true              # docker run -t 
      environment: 
      MYSQL_ROOT_PASSWORD:root
      MYSQL_ROOT_HOST:localhost
      MYSQL_USER: rose
      MYSQL_PASSWORD: password 
      MYSQL_DATABASE: demo
    volumes:
      - ./.docker/mysql/my.cnf:/etc/mysql/conf.d/my.cnf
      - mysqldata:/var/lib/mysql
    healthcheck:
      test: mysqladmin ping -h 127.0.0.1 -u root --password=$$MYSQL_ROOT_PASSWORD
      interval: 5s
      retries: 10


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:

FROM php:8.0-fpm
 
RUN docker-php-ext-install pdo_mysql

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:

[mysqld]
collation-server  = utf8mb4_unicode_ci
character-set-server = utf8mb4

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 :

volumes:
    
    mysqldata:

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.

depends_on:
    mysql:
        condition: service_healthy

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:

  phpmyadmin:
      image: phpmyadmin/phpmyadmin:5
      ports:
          - 8080:80
      environment:
            PMA_HOST: mysql
      depends_on:
          mysql:
              condition: service_healthy

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

Popular posts from this blog

BdsDex: failed to load Boot0001 "UEFI BHYVE SATA DISK BHYVE-OABE-20A5-E582" from PciRoot(0x0)/Pci (0x2, 0x0)/Stat(0x0,0xFFFF,0x0) : Not Found

How To Install Nginx, MySQL and PHP (FEMP) Stack on FreeBSD 13.0

Install samba on FreeBSD(on VMware Workstation) to share files with Window.