Introduction to Multi Docker Containers


Bhaskar S 04/07/2017


Overview

In the article Introduction to Dockerfile, we demonstrated how one could build custom Docker images using Dockerfile.

Enterprise applications are typically multi-tier. For example, a web application consists of a business tier (hosted on an application server like Tomcat) and a backend data tier (hosted on a database server like MySQL).

To emulate the production application stack in development, a developer could run the business tier in one Docker container and the data tier in another Docker container. But, how would one "tie" these multiple containers together so as to emulate what is in production ???

In this article, we will demonstrate how to link 2 containers to emulate a simple web application using the docker run command.

Setup

For this multi-container demonstration, we will use the base Docker image mysql:5.7.17 for the data tier and the base Docker image tomcat:8.5.13-jre8 for the business tier.

Data Tier (MySQL)

For the MySQL based data tier, we expect to have a database called hellodb with the user-id admin and with credential s3cret.

In the database hellodb, we expect to have a single database table called user_count with 2 columns - a name column and a count column.

Let us assume the current directory is /home/alice.

Create a directory called mysql under the current directory and change to that directory. The current directory now would be /home/alice/mysql.

Create the following SQL script called mysql-setup.sql in the current directory. This SQL script will be used to setup and initialize the desired database table:

mysql-setup.sql
USE hellodb;

CREATE TABLE user_count (name VARCHAR(10) NOT NULL, count INT NOT NULL, PRIMARY KEY (name));

INSERT INTO user_count (name, count) VALUES ('admin', 0);

Next, create the following Dockerfile in the current directory. It will be used to build our MySQL based Docker container for the data tier:

Dockerfile for MySQL
FROM mysql:5.7.17

ENV MYSQL_ROOT_PASSWORD="r00t"
ENV MYSQL_USER="admin"
ENV MYSQL_PASSWORD="s3cret"
ENV MYSQL_DATABASE="hellodb"

LABEL Version="1.0" \
      Author="Bhaskar.S" \
      Email="bswamina@polarsparc.com"

ADD mysql-setup.sql /docker-entrypoint-initdb.d

Now, let us pull the base Docker image mysql:5.7.17 by executing the following command:

$ docker pull mysql:5.7.17

The following would be a typical output:

Output.1

5.7.17: Pulling from library/mysql
6d827a3ef358: Pull complete
ed0929eb7dfe: Pull complete
03f348dc3b9d: Pull complete
fd337761ca76: Pull complete
7e6cc16d464a: Pull complete
ca3d380bc018: Pull complete
3fe11378d5c0: Pull complete
2b5dfd325645: Pull complete
b54281d17fbe: Pull complete
7eae4db8eea5: Pull complete
76cf68e17b09: Pull complete
Digest: sha256:49b7d6d8d45f8c3300cba056e8cdf36c714d99e0b40f7005b9e6e75e64ecdf7c
Status: Downloaded newer image for mysql:5.7.17

Finally, execute the following command to build the data tier Docker image using the Dockerfile we just created:

$ docker build -t mysql_hellodb .

The following would be a typical output:

Output.2

Sending build context to Docker daemon 3.072 kB
Step 1 : FROM mysql:5.7.17
 ---> 9546ca122d3a
Step 2 : ENV MYSQL_ROOT_PASSWORD "r00t"
 ---> Running in f5a6ae953c1c
 ---> a86039294085
Removing intermediate container f5a6ae953c1c
Step 3 : ENV MYSQL_USER "admin"
 ---> Running in 548a10861cce
 ---> 8bbf1ea32ae9
Removing intermediate container 548a10861cce
Step 4 : ENV MYSQL_PASSWORD "s3cret"
 ---> Running in 31ffb4126389
 ---> 4a82db9e201e
Removing intermediate container 31ffb4126389
Step 5 : ENV MYSQL_DATABASE "hellodb"
 ---> Running in 10eec68ec099
 ---> 10cf7415e7b4
Removing intermediate container 10eec68ec099
Step 6 : LABEL Version "1.0" Author "Bhaskar.S" Email "bswamina@polarsparc.com"
 ---> Running in cbff3bb6a6c2
 ---> 3db1dbc3f4fe
Removing intermediate container cbff3bb6a6c2
Step 7 : ADD mysql-setup.sql /docker-entrypoint-initdb.d
 ---> fae9253e0f4d
Removing intermediate container 795ca26199d4
Successfully built fae9253e0f4d

Business Tier (Tomcat)

For the Tomcat based business tier, we will deploy a simple servlet that will read and update a value from the user_count table in the hellodb database and display a single line result.

Change the current directory back to /home/alice.

Create a directory called tomcat under the current directory and change to that directory. The current directory now would be /home/alice/tomcat.

We will be leveraging Apache Maven for build and package management of our simple servlet project. Make sure Apache Maven is properly installed and setup on the host.

Execute the following command to setup the basic web application Maven project directory structure:

$ mvn archetype:generate -DgroupId="com.polarsparc" -DartifactId="hello" -Dversion="1.0" -DarchetypeArtifactId=maven-archetype-webapp -DinteractiveMode=false

The following is how the hello project directory structure would look after the above command completes:

Maven Webapp 1
Figure-1

Change to the directory hello/src/main and create an additional directory strucures as shown below:

$ cd hello/src/main

$ mkdir -p java/com/polarsparc

$ mkdir -p webapp/META-INF

$ cd ../../..

The following is how the hello project directory structure should look after executing the above commands:

Maven Webapp 2
Figure-2

First, create the following pom.xml file inside the hello project directory:

pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.polarsparc</groupId>
    <artifactId>helloworld</artifactId>
    <packaging>war</packaging>
    <version>1.0</version>

    <name>helloworld Maven Webapp</name>
    <url>http://maven.apache.org</url>

    <properties>
        <slf4j.version>1.7.25</slf4j.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>${slf4j.version}</version>
        </dependency>

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>${slf4j.version}</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.41</version>
        </dependency>

        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>3.8.1</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <finalName>helloworld3</finalName>
    </build>
</project>

Second, create the following context.xml file in the hello/src/main/webapp/META-INF directory:

context.xml
<Context>
    <Resource
        name="jdbc/hellodb"
        auth="Container"
        type="javax.sql.DataSource"
        maxTotal="5"
        maxIdle="30"
        maxWaitMillis="10000"
        driverClassName="com.mysql.jdbc.Driver"
        url="jdbc:mysql://db:3306/hellodb?useSSL=false"
        username="admin"
        password="s3cret"
        defaultAutoCommit="false"
        removeAbandoned="true"
    />
</Context>

Third, update the web.xml file located in the hello/src/main/webapp/WEB-INF directory to look as follows:

web.xml
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                             http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">
    <resource-ref>
        <description>Hello DB</description>
        <res-ref-name>jdbc/hellodb</res-ref-name>
        <res-type>javax.sql.DataSource</res-type>
        <res-auth>Container</res-auth>
    </resource-ref>
</web-app>

Fourth, create the following log4j.properties file in the hello/src/main/resources directory:

log4j.properties
###
### log4j Properties
###

log4j.rootLogger=INFO, STDOUT

log4j.appender.STDOUT=org.apache.log4j.ConsoleAppender
log4j.appender.STDOUT.layout=org.apache.log4j.PatternLayout
log4j.appender.STDOUT.layout.ConversionPattern=%-5p %d [%t] %c %x - %m%n

Finally, create the following HelloServlet3.java file in the hello/src/main/java/com/polarsparc directory:

HelloServlet3.java
package com.polarsparc;

import java.io.IOException;
import java.sql.Connection;
import java.sql.Statement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import javax.annotation.Resource;
import javax.servlet.annotation.WebServlet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.sql.DataSource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@WebServlet(name = "helloworld3",
            urlPatterns = {"/message"},
            loadOnStartup = 1)
public class HelloServlet3 extends HttpServlet {
    private static final long serialVersionUID = 1L;

    @Resource(name = "jdbc/hellodb")
    private DataSource ds;

    Logger log = LoggerFactory.getLogger(HelloServlet3.class);

    @Override
    public void init()
        throws ServletException {
        log.info(String.format("Servlet <%s> has been initialized\n", this.getServletName()));
    }

    @Override
    public void destroy() {
        log.info(String.format("Servlet <%s> has been destroyed\n", this.getServletName()));
    }

    @Override
    protected void doGet(HttpServletRequest reqest, HttpServletResponse response)
        throws ServletException, IOException {
        int count = getUserCount();

        count++;

        updateUserCount(count);

        response.setContentType("text/html");
        response.getWriter().println(String.format("<h3><center>[%d] Hello (Servlet 3.x + Slf4j + JNDI) World !!!</center></h3>", count));
    }

    // ----- Private Method(s) -----

    private int getUserCount() {
        int count = 0;

        String sql = "SELECT count FROM user_count WHERE name = 'admin'";

        Connection con = null;
        Statement st = null;
        ResultSet rs = null;

        try {
            con = ds.getConnection();
            st = con.createStatement();
            rs = st.executeQuery(sql);
            if (rs.next()) {
                count = rs.getInt(1);
            }
        }
        catch (SQLException sqlex) {
            log.error(sqlex.getMessage());
        }
        finally {
            if (rs != null) {
                try {
                    rs.close();
                }
                catch (SQLException sqlex) {
                    log.error(sqlex.getMessage());
                }
            }
            if (st != null) {
                try {
                    st.close();
                }
                catch (SQLException sqlex) {
                    log.error(sqlex.getMessage());
                }
            }
            if (con != null) {
                try {
                    con.close();
                }
                catch (SQLException sqlex) {
                    log.error(sqlex.getMessage());
                }
            }
        }

        return (count);
    }

    private void updateUserCount(int count) {
        String sql = "UPDATE user_count SET count = ? WHERE name = 'admin'";

        Connection con = null;
        PreparedStatement st = null;

        try {
            con = ds.getConnection();
            st = con.prepareStatement(sql);
            st.setInt(1, count);
            st.executeUpdate();
            con.commit();
        }
        catch (SQLException sqlex) {
            log.error(sqlex.getMessage());
        }
        finally {
            if (st != null) {
                try {
                    st.close();
                }
                catch (SQLException sqlex) {
                    log.error(sqlex.getMessage());
                }
            }
            if (con != null) {
                try {
                    con.close();
                }
                catch (SQLException sqlex) {
                    log.error(sqlex.getMessage());
                }
            }
        }
    }
}

In the end, the following is how the hello project directory structure should look:

Maven Webapp 3
Figure-3

To build and package the Maven project, execute the following command in the hello directory:

$ mvn clean package

Once the above command finishes, one should find the helloworld3.jar file in the hello/target directory.

Next, create the following Dockerfile in the current directory. It will be used to build our Tomcat based Docker container for the business tier:

Dockerfile for Tomcat
FROM tomcat:8.5.13-jre8

LABEL Version="1.0" \
      Author="Bhaskar.S" \
      Email="bswamina@polarsparc.com"

ADD target/helloworld3.war /usr/local/tomcat/webapps/

Now, let us pull the base Docker image tomcat:8.5.13-jre8 by executing the following command:

$ docker pull tomcat:8.5.13-jre8

The following would be a typical output:

Output.3

8.5.13-jre8: Pulling from library/tomcat
6d827a3ef358: Already exists
2726297beaf1: Pull complete
d6e483851652: Pull complete
ef624abeb7b8: Pull complete
0e108ce2208d: Pull complete
6a77bcb48281: Pull complete
016fd08a71c8: Pull complete
97c72b1a17df: Pull complete
b9f2ccd12a13: Pull complete
0e54b0c11e81: Pull complete
37da287a8961: Pull complete
6cef72a72237: Pull complete
a675c4562932: Pull complete
Digest: sha256:d6bc16eec9f85c5efdac8d3918dfd240ddf6151cd288ac45ae97458864c81dd9
Status: Downloaded newer image for tomcat:8.5.13-jre8

Finally, execute the following command to build the business tier Docker image using the Dockerfile we just created:

$ docker build -t tomcat_hellodb .

The following would be a typical output:

Output.4

Sending build context to Docker daemon 4.467 MB
Step 1 : FROM tomcat:8.5.13-jre8
 ---> 40ab38c1ce33
Step 2 : LABEL Version "1.0" Author "Bhaskar.S" Email "bswamina@polarsparc.com"
 ---> Using cache
 ---> dc595ef8eb96
Step 3 : ADD helloworld3.war /usr/local/tomcat/webapps/
 ---> 8c46aa7c76bd
Removing intermediate container 54853c073930
Successfully built 8c46aa7c76bd

Change the current directory back to /home/alice.

Hands-on with Multi Docker Containers

In this section, we will demonstrate how one could link the multiple containers (data and business tier containers in our example) using the docker run command.

To list all the Docker Images on the local host, execute the following command:

$ docker images

The following could be a typical output:

Output.5

REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
mysql_hellodb       latest              fae9253e0f4d        5 days ago          407 MB
tomcat_hellodb      latest              8c46aa7c76bd        5 days ago          368.2 MB
tomcat              8.5.13-jre8         40ab38c1ce33        7 days ago          366.7 MB
mysql               5.7.17              9546ca122d3a        8 days ago          407 MB
ubuntu_flask        latest              54f0fa012cd1        12 days ago         239.8 MB
mycentos            latest              91b1f32a62f3        13 days ago         273.3 MB
centos              latest              98d35105a391        3 weeks ago         192.5 MB
ubuntu              latest              0ef2e08ed3fa        5 weeks ago         130 MB
ubuntu              14.04               7c09e61e9035        5 weeks ago         188 MB

Create a directory called data under the current directory. The current directory should be /home/alice.

First start the data tier, which is the MySQL based Docker image with the name mysql_hellodb, using the following command:

$ docker run -v /home/alice/data:/var/lib/mysql -d --name mysql mysql_hellodb

Notice the use of the command line options --name and -v in the command above.

The --name option is used to reference a container for networking (like a host name). The specified name mysql will be used below when we launch the business tier container.

The -v option mounts the directory /home/alice/data on the local host into the container at /var/lib/mysql. This allows the database data to be preserved for future runs.

The following would be a typical output:

Output.6

7541f659e7a66914bdb04de1bccb9148707d12ded31a454a7adac34cbb3ade71

To list all the running Docker containers, execute the following command:

$ docker ps

The following would be a typical output:

Output.7

CONTAINER ID    IMAGE           COMMAND                  CREATED           STATUS          PORTS         NAMES
7541f659e7a6    mysql_hellodb   "docker-entrypoint.sh"   14 seconds ago    Up 12 seconds   3306/tcp      mysql

Next start the business tier, which is the Tomcat based Docker image with the name tomcat_hellodb, using the following command:

$ docker run -d -p 8080:8080 --link mysql:db tomcat_hellodb

Notice the use of the command line option --link in the command above.

The --link option is what "ties" the data tier with the name mysql to the business tier container via the alias db. Refer to the file context.xml above and see how this alias is used in the database url jdbc:mysql://db:3306/hellodb?useSSL=false.

The following would be a typical output:

Output.8

5c4c70f234cf66470f80720394be2e1c91c84fb9df5f06074ed76ab2156a0403

To list all the running Docker containers, execute the following command:

$ docker ps

The following would be a typical output:

Output.9

CONTAINER ID    IMAGE           COMMAND                  CREATED         STATUS          PORTS                    NAMES
5c4c70f234cf    tomcat_hellodb  "catalina.sh run"        5 seconds ago   Up 3 seconds    0.0.0.0:8080->8080/tcp   nauseous_khorana
7541f659e7a6    mysql_hellodb   "docker-entrypoint.sh"   9 minutes ago   Up 9 minutes    3306/tcp                 mysql

Open a browser on the local host and access the URL http://localhost:8080/helloworld3/message. The following would be a typical view:

Docker Tomcat 1
Figure-4

HOORAY !!! we have successfully demonstrated the use of multiple Docker containers.

To stop the business tier and data tier containers, execute the following command:

$ docker stop 5c4c70f234cf 7541f659e7a6

The following would be a typical output:

Output.10

5c4c70f234cf
7541f659e7a6

To list all the running Docker containers (none should be running at this point), execute the following command:

$ docker ps

The following would be a typical output:

Output.11

CONTAINER ID    IMAGE       COMMAND              CREATED         STATUS          PORTS                NAMES

References

Introduction to Docker

Introduction to Dockerfile

Official Docker Documentation