DEV Community

Samuel James
Samuel James

Posted on

Why Docker? Creating a multi-container application with docker

Multi Container Application

If you are just starting to learn docker, one of the common questions that come to mind is what docker is and why one would need it.
In this post, I provide basic answers to some of these questions and finally walk you through composing your first multi-container application with docker-compose.

Table of Contents

  1. Docker and why
  2. Docker Image
  3. Docker compose
  4. Building multi-container application with docker-compose
  5. Further Reading

Docker and why?

The definition I love personally about docker is the one provided by the docker team, who defines docker as an open platform for developers and sysadmins to build, ship, and run distributed applications, whether on laptops, data center virtual machines, or the cloud.

Maintaining consistent environments is hard

Without virtualization technologies, applications are not guaranteed to work exactly the same way as they work in one environment. This is mostly due to inconsistent environments and disparity in system libraries.

You could develop your app using python3 only to find out that production environment is running python2. It gets a bit complicated when you cannot upgrade to python3 because other applications on the host need python2 to run.

Developers work their ass off for hours finding solutions to inconsistent environments and yet at most times, their shining code won't still run because one library file is outdated.

The struggle is endless. I have experienced this agony that comes with troubleshooting a feature that works fine in one environment and not the other. Yeah!, It always works fine on our machines but production - never! :(

Installing application dependencies on local machine takes time

Setting up development environment can be hard without virtualization, we often find ourselves installing different versions of software to support various services we are working on. Maintaining different versions of PHP or MySQL database can take a considerable amount of your time that could be spent writing code.

Containers and virtual machines solve this problem and save you the stress by isolating your application and its dependencies into a single self-contained unit that can reliably run anywhere.

While virtual machines and containers have the same goal, containers are preferred to virtual machines because of their lightweight, speed, and low resource utilization.

A container in this context is similar in function to the one you see on highways: They are both enclosures for holding products. The difference is the content they hold. Ours hold software products.

One of the few popular container providers is Docker and in the next few paragraphs, we'll be exploring in details how you can dockerize your development environment.

Docker Image

Docker containers are built from docker images. An image is inert and immutable file that is essentially a snapshot of a container. I like to see a docker container as a running instance of docker image.

An image is built from Dockerfile which is basically a text document that contains instructions on how docker should build an image.

Docker compose

Docker-compose is a tool that helps you run complex applications.
A working software is never an island, it will most likely depend on a bunch of other things like database, cache and even other services. It's best practice to have each of these in their respective container. With docker-compose, you can define a multi-container application in a single file which can be spun up in a single command.

Building a multi-container application with docker-compose

Before this post gets unbearably long, let gets our hands dirty.
If you don't have docker and docker-compose installed, go here for install instructions.
We'll compose multiple services for our project. It's a laravel application and uses MySQL database and Redis for caching.

### Start a new Laravel project

Start a Laravel project by running composer create-project --prefer-dist laravel/laravel laravelApp
If everything works fine, your directories should look like this.
Project-directory

### Create a Dockerfile for the Webserver (Nginx)
We need a web server (nginx) to serve static contents. We create a Dockerfile which is basically a text file that contains instructions of how docker should build our image.
At the project root, we create a web.docker file with the content below.

 # /project/root/web.docker

 # start building image from nginx 1.13
 FROM nginx:1.13

 # copy site.conf from local directory to /etc/nginx/conf.d/default.conf
 ADD ./site.conf /etc/nginx/conf.d/default.conf

 WORKDIR /var/www
Enter fullscreen mode Exit fullscreen mode

site.conf contains vritual host configuration for nginx
At the project root, create site.conf with this content

# /project/root/site.conf

server {
    listen 80;
    index index.php index.html;

    access_log /var/www/storage/logs/access.log;
    error_log /var/www/storage/logs/error.log;
    root /var/www/public;

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

    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass app:9000;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
        add_header 'Access-Control-Allow-Origin' '*' always;
        add_header 'Access-Control-Allow-Headers' 'Authorization,DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,api_key' always;
        add_header 'Access-Control-Allow-Methods' 'POST, GET, PUT, DELETE' always;
    }
}
Enter fullscreen mode Exit fullscreen mode

Create a Dockerfile for PHP 7.1

Laravel needs PHP to run as well as some specific PHP extensions. We create a file with the name app.docker which contains the version of PHP and extensions we want.
Create this at the project root as well.

FROM php:7.1.9-fpm

# Install any custom system requirements here
RUN apt-get update && apt-get install -y libmcrypt-dev zip unzip git mysql-client \
    && docker-php-ext-install mcrypt pdo_mysql

WORKDIR /var/www

Enter fullscreen mode Exit fullscreen mode

As for MySQL database and Redis, prebuilt images will suffice for this demonstration. We do not have to customize them.

Define services in Docker-compose file

Now, we create a docker-compose.yml. This should be at the root directory of your project.

version: '2'
services:
 web:
  build:
   context: ./
   dockerfile: web.docker
  container_name: web
  ports:
   - "8080:80"
  volumes:
   - ./:/var/www
  links:
   - app
 app:
  build:
   context: ./
   dockerfile: app.docker
  volumes:
   - ./:/var/www
  depends_on:
   - mysql
   - redis
  environment:
   - "APP_ENV=${APP_ENV}"
   - "APP_DEBUG=${APP_DEBUG}"
   - "APP_KEY=${APP_KEY}"
   - "DB_CONNECTION=${DB_CONNECTION}"
   - "DB_HOST=${DB_HOST}"
   - "DB_PORT=${DB_PORT}"
   - "DB_DATABASE=${DB_DATABASE}"
   - "DB_USERNAME=${DB_USERNAME}"
   - "DB_PASSWORD=${DB_PASSWORD}"
   - "CACHE_DRIVER=${CACHE_DRIVER}"
   - "QUEUE_DRIVER=${QUEUE_DRIVER}"
   - "REDIS_HOST=${REDIS_HOST}"
   - "REDIS_PASSWORD=${REDIS_PASSWORD}"
   - "REDIS_PORT=${REDIS_PORT}"
 mysql:
  image: mysql:5.7
  environment:
   MYSQL_DATABASE: ${DB_DATABASE}
   MYSQL_USER: ${DB_USERNAME}
   MYSQL_PASSWORD: ${DB_PASSWORD}
  volumes:
    - ./mysql:/var/lib/mysql
  ports:
   - "3306:3306"
 redis:
  image: redis:3.0
  ports:
    - "6379:6379"
  volumes:
   - ./redis:/data

Enter fullscreen mode Exit fullscreen mode

Let's take a moment to understand the content of docker-compose.yml above.

version 2: Version of docker-compose syntax we are using.

services: Services are different pieces of our application i.e (app, redis, mysql).

build: It defines a string which contains the path to the build context and dockerfile argument that references our Dockerfile

container_name: Used to specify the name of the container after built

environment: This is used to pass environment variables to containers.

Note: Docker compose will make variables defined in .env available in service configuration. This means you can automatically access laravel configuration in .env file in docker-compose.yml.

image: Used to specify a prebuilt image to start building the container from.

Ports: used to map container's port to host's port. In app service, we mapped port 8080 on the host machine to port 80 in the container.

volumes: used to mount container's directory to host's directory. In this example, docker will create /var/www directory in app service and point files in your local directory to it. This is useful during development, If you make changes to your source code, the changes are effected immediately.

depends_on: This ensures that mysql and redis are started before app service.

During execution, docker-compose builds a network between the containers such that each containers can refer to each other by using names defined in the docker-compose.yml.

Create a DockerIgnore file

A dockerignore file is similar to gitignore file. With dockerignore file, you specify files and folders that will be excluded from the build and these files will not be copied over to the image.

Let's create a dockerignore file at root folder with code below

app.docker
web.docker
docker-compose.yml
.env
Enter fullscreen mode Exit fullscreen mode

Build and run project with Compose

To build the project, we navigate to the project root and run:

docker-compose build 
Enter fullscreen mode Exit fullscreen mode

You should see a console output similar to this:
Docker-compose build console output

Now, we can proceed to run our services with:

docker-compose up -d
Enter fullscreen mode Exit fullscreen mode

To verify that these services are actually running, we issue docker ps command. This will list all running containers.

 docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
7275af6e2f3f        laravelapp_web      "nginx -g 'daemon of…"   9 seconds ago       Up 6 seconds        0.0.0.0:8080->80/tcp     web
2ce0817bd07e        laravelapp_app      "docker-php-entrypoi…"   10 seconds ago      Up 8 seconds        9000/tcp                 laravelapp_app_1
5a85911e6ecd        mysql:5.7           "docker-entrypoint.s…"   58 seconds ago      Up 11 seconds       0.0.0.0:3306->3306/tcp   laravelapp_mysql_1
41739940ec82        redis:3.0           "docker-entrypoint.s…"   58 seconds ago      Up 10 seconds       0.0.0.0:6379->6379/tcp   laravelapp_redis_1
Enter fullscreen mode Exit fullscreen mode


`

Finally, point your web browser to http://localhost:8080/ and you should see a default laravel homepage as shown below.

Laravel-home-page
Since we have already mounted the project's directory to application container's directory, changes made to the source code are automatically applied.

Further Readings

Docker cheat sheet
Getting started with compose
Getting Started With Docker: Key Concepts for Beginners

Do you have a comment or noticed a bug? Please feel free to drop them here or connect with me on Linkedin

Top comments (15)

Collapse
 
suprnova32 profile image
Patricio Cano
Collapse
 
abiodunjames profile image
Samuel James

:) I love this.

Collapse
 
bacchusplateau profile image
Bret Williams

One of the clearest and most concise "Docker 101" articles I've come across. Thanks!

Collapse
 
nicosouv profile image
Nicooo

Very good post!

Since you build all the services in the same docker-compose (so, in the same network), all services should be able to talk to each other, so you might not need the "link" in your docker-compose.yml.
I think. I may have to check that though ;)

Collapse
 
azazqadir profile image
Muhammad Azaz Qadir

For deploying php apps on a docker multi container, the process is not really that difficult. Once you have setup the environment, you have to add docker compose yml file to your project root.

Source: dockerize php application

Collapse
 
r0bnet profile image
rob

Nice introduction. I missed two things that may not be clear for beginners.

  1. The image shows 5 containers (or container like objects) but there are only 4. The app is included via volumes. That confused me when i saw the pic but i got it when i read the article.
  2. I think you forgot to explain the "links" statement as part of the web container. With that it's possible to directly address the app container in the nginx config.

Keep going! :)

Collapse
 
abiodunjames profile image
Samuel James • Edited

Thank you for the comment. I'll update it shortly.

Collapse
 
lumayara profile image
Luana Mayara Fernandes da Silva Bezerra

Hi Samuel! Thank you so much for your article! It worked here the only little container that didnt run was Mysql.. I kept getting this error:

W: Failed to fetch deb.debian.org/debian/dists/jessie... Unable to find expected entry 'main/binary-amd64/Packages' in Release file (Wrong sources.list entry or malformed file)

E: Some index files failed to download. They have been ignored, or old ones used instead.

So it wasn't working and I saw someone on github said to put this code before RUN apt-get update: RUN sed -i '/jessie-updates/d' /etc/apt/sources.list

and I did and the rest of the stuff worked however I didnt get the MySQL instance running.. no idea why as I am super new to docker...
Could you help me?

Thanks!

Collapse
 
tylerlwsmith profile image
Tyler Smith

Thanks for writing this article!

Do you have any advice regarding when to use docker-compose vs when to use a Dockerfile and programmatically install the individual services into one container?

Collapse
 
abiodunjames profile image
Samuel James • Edited

Hi Tyler,
Thanks for reading.

A container is standardized unit of software which is lightweight, standalone while Docker Compose provides a way to orchestrate multiple containers (applications) that work together.
In a real-world, you may need more than one components (application) to run a software. For example: a software may need many components like database, webserver, cache server, and more to run. Each of these components is a unit.
When you have multiple components to deal with, you should use docker-compose to orchestrate them.

Don't package everything into a single container. A container is a unit of software.

Collapse
 
tylerlwsmith profile image
Tyler Smith

"A container is a standardized unit of software."

That's a killer way to describe containers, thank you for your insight! I've been spending the day wrangling with docker-compose and was wondering if I was crazy for not just using a single Dockerfile.

Collapse
 
bengreenberg profile image
Ben Greenberg

This is great! I've been interested in exploring Docker for a while and found what you wrote to be a really accessible entry into it, thanks for this.

Collapse
 
abiodunjames profile image
Samuel James

I'm happy you find it useful.

Collapse
 
falansari profile image
Ash

This really demystified docker for me. Thanks for that!

Collapse
 
abiodunjames profile image
Samuel James

Thanks for reading. I'm glad you find it helpful.