Dockerizing LEMP Stack with Docker-Compose on Ubuntu

Docker-Compose is a command line tool for defining and managing multi-container docker applications. Compose is a python script, it can be installed with the pip command easily (pip is the command to install Python software from the python package repository). With compose, we can run multiple docker containers with a single command. It allows you to create a container as a service, great for your development, testing and staging environment.

In this tutorial, I will guide you step-by-step to use docker-compose to create a LEMP Stack environment (LEMP = Linux - Nginx - MySQL - PHP). We will run all components in different Docker containers, we set up a Nginx container, PHP container, PHPMyAdmin container, and a MySQL/MariaDB container.

Prerequisites

  • Ubuntu server 16.04 -64bit
  • Root privileges

Step 1 - Install Docker

In this step, we will install Docker. Docker is available in the Ubuntu repository, just update your repository and then install it.

Update ubuntu repository and upgrade:

sudo apt-get update
sudo apt-get upgrade

Install latest Docker from ubuntu repository.

sudo apt-get install -y docker.io

Start docker and enable it to start at boot time:

systemctl start docker
systemctl enable docker

The Docker services are running.

Next, you can try using docker with the command below to test it:

docker run hello-world

Hello world from docker.

Hello Docker

Step 2 - Install Docker-Compose

In the first step, we've already installed Docker. Now we will install docker-compose.

We need python-pip for the compose installation, install python and python-pip with apt:

sudo apt-get install -y python python-pip

When the installation is finished, install docker-compose with the pip command.

pip install docker-compose

Now check the docker-compose version:

docker-compose --version

Docker-compose has been installed.

Install Docker Compose on Ubuntu

Step 3 - Create and Configure the Docker Environment

In this step, we will build our docker-compose environment. We will use a non-root user, so we need to create that user now.

Add a new user named 'hakase' (choose your own user name here if you like):

useradd -m -s /bin/bash hakase
passwd hakase

Next, add the new user to the 'docker' group and restart docker.

usermod -a -G docker hakase
sudo systemctl restart docker

Now the user 'hakase' can use docker without sudo.

Next, from the root user, log into the 'hakase' user with su.

su - hakase

Create a new directory for the compose environment.

mkdir lemp-compose/
cd lemp-compose/

This is our docker-compose environment, all files that shall be in the Docker container must be in this directory. When we are using docker-compose, we need a .yml file named 'docker-compose.yml'.

In the 'lemp-compose' directory, create some new directories and a docker-compose.yml file:

touch docker-compose.yml
mkdir -p {logs,nginx,public,db-data}

  • logs: Directory for Nginx log files.
  • nginx: contains Nginx configuration like virtual host etc.
  • public: directory for web files, index.html, and PHP info file.
  • db-data: MariaDB data directory volume.

Create the log files error.log and access.log in the 'logs' directory.

touch logs/{error,access}.log

Create a new nginx virtual host configuration file in the 'nginx' directory:

vim nginx/app.conf

Paste configuration below:

upstream php {
        server phpfpm:9000;
}

server {

        server_name 193.70.36.50;

        error_log "/opt/bitnami/nginx/logs/myapp-error.log";
        access_log  "/opt/bitnami/nginx/logs/myapp-access.log";

        root /myapps;
        index index.php index.html;

        location / {

                try_files $uri $uri/ /index.php?$args;
        }

        location ~ \.php$ {

                include fastcgi.conf;
                fastcgi_intercept_errors on;
                fastcgi_pass php;
        }

        location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ {
                expires max;
                log_not_found off;
        }
}

Save the file and exit vim.

Create a new index.html file and PHP info file in the 'public' directory.

echo '<h1>LEMP Docker-Compose</h1><p><b>hakase-labs</b>' > public/index.html
echo '<?php phpinfo(); ?>' > public/info.php

Now you can see the environment directory as shown below:

tree

Docker compose project environment

Step 4 - Configuration of the docker-compose.yml file

In the previous step, we've created the directories and files needed for our containers. In this step, we want to edit the file 'docker-compose.yml'. In the docker-compose.yml file, we will define our services for the LEMP stack, the base images for each container, and the docker volumes.

Login to the 'hakase' user and edit the docker-compose.yml file with vim:

su - hakase
cd lemp-compose/
vim docker-compose.yml

- Define Nginx services

Paste the nginx configuration below:

nginx:
    image: 'bitnami/nginx'
    ports:
        - '80:80'
    links:
        - phpfpm
    volumes:
        - ./logs/access.log:/opt/bitnami/nginx/logs/myapp-access.log
        - ./logs/error.log:/opt/bitnami/nginx/logs/myapp-error.log
        - ./nginx/app.conf:/bitnami/nginx/conf/vhosts/app.conf
        - ./public:/myapps

In that configuration, we've already defined:

  • nginx: services name
  • image: we're using 'bitnami/nginx' docker images
  • ports: expose container port 80 to the host port 80
  • links: links 'nginx' service container to 'phpfpm' container
  • volumes: mount local directories to the container. Mount the logs file directory, mount the Nginx virtual host configuration and mount th web root directory.

- Define PHP-fpm services

Paste the configuration below the Nginx block:

phpfpm:
    image: 'bitnami/php-fpm'
    ports:
        - '9000:9000'
    volumes:
        - ./public:/myapps

We defined here:

  • phpfpm: define the service name.
  • image: define base image for the phpfpm service with 'bitnami/php-fpm' image.
  • ports: We are running PHP-fpm with TCP port 9000 and exposing the port 9000 to the host.
  • volumes: mount the web root directory 'public' to 'myapps' on the container.

- Define the MySQL service

In the third block, paste the configuration below for the MariaDB service container:

mysql:
    image: 'mariadb'
    ports:
        - '3306:3306'
    volumes:
        - ./db-data:/var/lib/mysql
    environment:
        - MYSQL_ROOT_PASSWORD=hakase-labs123

Here you can see that we are using:

  • mysql: as the service name.
  • image: the container is based on 'mariadb' docker images.
  • ports: service container using port 3306 for MySQL connection, and expose it to the host on port 3306 .
  • volumes: db-data directory mysql
  • environment: set the environment variable 'MYSQL_ROOT_PASSWORD' for the mysql root password to the docker images, executed when building the container.

- PHPMyAdmin services configuration

The last block, paste the configuration below:

phpmyadmin:
    image: 'phpmyadmin/phpmyadmin'
    restart: always
    ports:
       - '8080:80'
    links:
        - mysql:mysql
    environment:
        MYSQL_USERNAME: root
        MYSQL_ROOT_PASSWORD: hakase-labs123
        PMA_HOST: mysql

We are using a 'phpmyadmin' docker image, mapping container port 80 to 8080 on the host, link the container to the mariadb container, set restart always and set some environment variables of the docker image, including set 'PMA_HOST'.

Save the file and exit vim.

You can see full example on github.

Step 5 - Run Docker-Compose

Now we're ready to run docker-compose. Note: when you want to run docker-compose, you must be in the docker-compose project directory and make sure there is the yml file with the compose configuration.

Run the command below to spin up the LEMP stack:

docker-compose up -d

-d: running as daemon or background

You will see the result that the new containers have been created, check it with the command below:

docker-compose ps

running docker-compose

Now we've four containers running Nginx, PHP-fpm, MariaDB and PHPMyAdmin.

Step 6 - Testing

Checking ports that are used by the docker-proxy on the host.

netstat -plntu

all docker port mapped to host

We can see port 80 for the Nginx container, port 3306 for the MariaDB container, port 9000 for the php-fpm container, and port 8080 for the PHPMyAdmin container.

Access port 80 from the web browser, and you will see our index.html file.

http://serverip-address/

nginx docker container is work

Make sure PHP-fpm is running, access it from the web browser.

http://serverip-address/info.php

php-fpm docker container is working

Access the MySQL container in the MySQL shell.

docker-compose exec mysql bash
mysql -u root -p
TYPE MYSQL PASSWORD: hakase-labs123

Now create a new database:

create database hakase_db;
exit

MariaDB mysql shell container is accessible, and we've created a new database 'hakase_db'.

access mysql shell docker container

Next, access PHPMyAdmin on port 8080: http://serverip-address:8080/.

You will see the PHPMyAdmin login page, just type user name 'root' and the password is 'hakase-labs123'.

phpmyadmin docker container

You will be automatically connected to the mysql container that has been defined in the PMA_HOST environment variable.

Click 'Go' and you will see the phpmyadmin dashboard that is connected to the 'mysql' container.

phpmyadmin and mysql docker container is working

Success! The LEMP Stack is running under a docker-compose setup, consisting of four containers.

Reference

https://hub.docker.com/r/bitnami/nginx/

https://hub.docker.com/r/bitnami/php-fpm/

https://hub.docker.com/_/mariadb/

https://hub.docker.com/r/phpmyadmin/phpmyadmin/

https://docs.docker.com/compose/

https://github.com/yuzukiseo/hakase-labs/tree/master/lemp-compose

Share this page:

Suggested articles

8 Comment(s)

Add comment

Comments

From: Jetchko Jekov at: 2017-03-02 10:18:17

Nice try but bad execution. Sorry.

The phpmyadmin Docker image already includes phpfpm and nginx. So you have 2 phpfpm and 2 nginx services for no reason. Also running random Docker images is never a good idea. Why not using official images?

From: Joshua at: 2017-03-08 14:08:07

@Jetchko I disagree with your assessment of bad execution. This is the idea of Docker. Just as an exercise, what if PHPMyAdmin required a different version of PHP than an app that was running in another container? What if for some reason Apache was needed? The power of Docker is in creating simple useful containers, not building smaller virtual machines. 

In fact, this setup is the most secure/flexible. The PHPMyAdmin container can be started and stopped when needed. 

Although I do agree with not running random images, but you can go through the entire  stack and determine if it is what you need.

From: Chris Yeun at: 2017-03-02 16:19:20

Are the hypens needed in the docker-compose.yml for the environment definitions for the phpmyadmin service?

From: Steve at: 2017-03-02 20:19:56

Slightly related - container security/deployment/management best practice presentations from SouthEast LinuxFest 2016: https://www.youtube.com/playlist?list=PLvG1nXgsl22FhL55suZtb7RIjEJtkYYC5 Learned a bunch here last year.  Mainly don't deploy container images from untrusted sources. Seems obvious, right?  What do each container repo require to post an image?

From: Angelo at: 2017-03-06 09:20:01

Great how-to, I wull give it a try as soon as I find some time.

@Jetchko

It would be nice if you gave some more information and/or (less generic) suggestions

1. phpmyadmin Docker image already includes phpfpm and nginx, maybe the official one, the bitnami/php-fpm does not seem to contain them...

2.  Also running random Docker images is never a good idea, not true the bitnami images are not just "random images", last instead of downloading the image you can always build them yourself.

From: orbond at: 2017-04-09 03:49:11

how do I get the server IP Address ?

From: Elproducto at: 2017-04-09 19:07:09

If you are not looking for the IP given the container.  Your servers IP should be the same as the Host you are running docker on.

From: Elproducto at: 2017-04-09 19:31:29

Thanks for publishing instruction to setup a LEMP stack in docker.  I ran into a bit of any issue, but maybe it was expected with this setup.  

The first PHP dependent application I tested post install of LEMP stack was GRAV.  However GRAV was not working properly as pages where not loading. When I checked the Nginx logs I saw an error "FastCGI sent in stderr: "Primary script unknown" while reading response header from upstream.  I made changes to the php location variable in app.conf, as this was a common answer with the error, but changing the php location information did not fix my issue. 

The issue is related to the document root set in the app.conf file. Folder containing GRAV is nested one folder under myapps folder.  Once I changed the root variable to something like /myapps/GRAVFOLDER, everything worked as expected.

I wanted to inquire if it is possible to have subfolders with PHP application within, served correctly with this setup.  I would prefer not to have to specify document root if testing multiple PHP applications.