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.


  • 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

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 {


        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:


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:

    image: 'bitnami/nginx'
        - '80:80'
        - phpfpm
        - ./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:

    image: 'bitnami/php-fpm'
        - '9000:9000'
        - ./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:

    image: 'mariadb'
        - '3306:3306'
        - ./db-data:/var/lib/mysql
        - 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:

    image: 'phpmyadmin/phpmyadmin'
    restart: always
       - '8080:80'
        - mysql:mysql
        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.


nginx docker container is work

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


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;

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.


Share this page:

Suggested articles

23 Comment(s)

Add comment


By: Jetchko Jekov

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?

By: Joshua

@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.

By: poggdogg

Agreed, it could be better - bitnami runs Apache while this stack runs NGINX too BUT the tutorial was about quick.

Ideally the solution would have 1 web server/proxy to reduce overhead and the proxy would handle pgpMyAdmin too.

By: Chris Yeun

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

By: Steve

Slightly related - container security/deployment/management best practice presentations from SouthEast LinuxFest 2016: 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?

By: Angelo

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


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.

By: orbond

how do I get the server IP Address ?

By: Elproducto

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.

By: Elproducto

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.

By: Data Pulley

Thanks for publishing instruction to setup a LEMP stack in docker.

In addition to the compose command line readers might find useful our GUI for Docker on Ubuntu.

It is named Data Pulley.

Data Pulley is a lightweight management UI which allows you to easily manage Docker containers on your local machine.

And, we have just added option to run using Docker Compose as well (File -> Open Docker Compose)

By: Michael

after issuing up command:

nginx_1       | nami    INFO  Initializing nginx

nginx_1       | nginx   INFO  ==> nginx.conf not found. Applying bitnami configuration...

nginx_1       | nami    INFO  nginx successfully initialized

nginx_1       | INFO  ==> Starting nginx... 




but inside the container:


I have no [email protected]:/opt/bitnami/nginx/html$ nginx -t

nginx: the configuration file /opt/bitnami/nginx/conf/nginx.conf syntax is ok


nginx: configuration file /opt/bitnami/nginx/conf/nginx.conf test is successful



What is the issue?

By: BobBobBob

I'm going to assume that you are maybe running into the same problem that I was getting. Going to 127.0.01:80 in my browser was giving me a message saying "the connection was reset." 

I logged onto the nginx box to see what was going on by using ...

docker exec -it --user root lempcompose_nginx_1 bash

... and then looked at the contents of the config file via ... 

cat /opt/bitnami/nginx/conf/nginx.conf

... and found that by default, nginx is serving on port 8080 rather than port 80, which is what this tutorial was mapping. So in my docker-compose.yml file, (under nginx:ports:) I added this line:

- '21212:8080'

... and then I was able to go to and get a nice message welcoming me to nginx. 


I'll still end up having to do a bit more reconfiguration for my needs, but after wasting half a weekend, I am happy that I finally got this thing "working" to some extent, and I hope that my experience can at least set somebody on the right track. Cheers!

By: nitima

when i do compose up -d and see the logsi got this :

nginx_1       | INFO  ==> Starting nginx... 

nginx_1       | 2017/12/10 20:00:19 [emerg] 19#0: open() "/opt/bitnami/nginx/logs/myapp-error.log" failed (13: Permission denied)

nginx_1       | nginx: [emerg] open() "/opt/bitnami/nginx/logs/myapp-error.log" failed (13: Permission denied)


lempcompose_nginx_1 exited with code 1

any idea guys?

By: kinnaridalvi

I got similar error as nitima, Can you please share how was this resolved?

By: Abdullah Al Farooq

chmod 777 logs/error.log logs/access.log

It would solve your issue.

By: Lucas

Nice tutorial, but needs some update in the nginx blocks of docker-compose file.

The volumes should be:

- ./nginx/app.conf:/bitnami/nginx/conf/vhosts/myapp.conf

- ./public:/app

and not

- ./nginx/app.conf:/bitnami/nginx/conf/vhosts/app.conf

- ./public:/myappss

By: Efendi

nice tutorial, can you explain @Joshua why we should add IP server_name;

in nginx config?

By: Don

I have this same question. Any help would be appreciated.

By: poggdogg

It's a space-delimited list of addresses to allow NGINX to resolve to:

server_name localhost legion-1 legion-1.kilbourne.local;

By: Michael Cooper

Hey Guys,

     Sorry for the stupid question but is the upstream server the webserver?




By: Michael Cooper

Hey Guys,

       Sorry another stupid question, How would I configure it to host multiple websites? The same as the regular nginx would be configured? Any help would be greatly appreciated. If you have a sight or tutorial send me there i don't mind reading so I can get a thorough understanding.


Thank you again,



By: David Henzler

I had no problems except for an error message when I ran the test on the yml file.  So downloaded the one from Github.  It worked.  I am not able to see info.php nor is the (purposely different) index.html showing up.  Have looked for a document root, but alas none to be found.  I also loaded a WordPress docker file, and it seems to have run as it shows up in the TREE display.

I'm totally new to Docker, so go easy on me.  I follow directions flawlessly and yet NO JOY !


By: Inopsek

why expose port that are only used in the docker network and don't need to be open on the host?