Concerning Containers' Connections: on Docker Networking

Designing Your Application: the World Database

Let's say you need an application that will let you search for cities that include a given text string in their names. (Figure 2 shows a sample run.) For this example, I used the geographical information at GeoNames (see Resources) to create an appropriate database. Basically, you work with countries (identified by their ISO 3166-1 two-letter codes, such as "UY" for "Uruguay") and cities (with a name, a pair of coordinates and the country to which they belong). Users will be able to enter part of the city name and get all the matching cities (not very complex).

Figure 2. This sample application finds these cities with DARWIN in their names.

How should you design your mini-system? Docker is meant to package single applications, so in order to take advantage of containers, you'll run separate containers for each required role. (This doesn't necessarily imply that only a single process may run on a container. A container should fulfill a single, definite role, and if that implies running two or more programs, that's fine. With this very simple example, you'll have a single process per container, but that need not be the general case.)

You'll need a Web server, which will run in a container, and a database server, in a separate container. The Web server will access the database server, and end users will need connections to the Web server, so you'll have to set up those network connections.

Start by creating the database container, and there's no need to start from scratch. You can work with the official MySQL Docker image (see Resources) and save a bit of time. The Dockerfile that produces the image can specify how to download the required geographical data. The RUN commands set up a loaddata.sh script that takes care of that. (For purists: a single longer RUN command would have sufficed, but I used three here for clarity.) See Listing 2 for the complete Dockerfile file; it should reside in an otherwise empty directory. Building the worlddb image itself can be done from that directory with the sudo docker build -t worlddb . command.

Listing 2. The Dockerfile to create the database server also pulls down the needed geographical data.


FROM mysql:latest
MAINTAINER Federico Kereki fkereki@gmail.com

RUN     apt-get update && \
        apt-get -q -y install wget unzip && \
        wget 'http://download.geonames.org/export/dump/countryInfo.txt' && \
        grep -v '^#' countryInfo.txt >countries.txt && \
        rm countryInfo.txt && \
        wget 'http://download.geonames.org/export/dump/cities1000.zip' && \
        unzip cities1000.zip && \
        rm cities1000.zip

RUN     echo "\
        CREATE DATABASE IF NOT EXISTS world;    \
        USE world;                              \
        DROP TABLE IF EXISTS countries;         \
        CREATE TABLE countries (                \
                id CHAR(2),                     \
                ignore1 CHAR(3),                \
                ignore2 CHAR(3),                \
                ignore3 CHAR(2),                \
                name VARCHAR(50),               \
                capital VARCHAR(50),            \
                PRIMARY KEY (id));              \
        LOAD DATA LOCAL INFILE 'countries.txt'  \
                INTO TABLE countries            \
                FIELDS TERMINATED BY '\t';      \
        DROP TABLE IF EXISTS cities;            \
        CREATE TABLE cities (                   \
                id NUMERIC(8),                  \
                name VARCHAR(200),              \
                asciiname VARCHAR(200),         \
                alternatenames TEXT,            \
                latitude NUMERIC(10,5),         \
                longitude NUMERIC(10,5),        \
                ignore1 CHAR(1),                \
                ignore2 VARCHAR(10),            \
                country CHAR(2));               \
        LOAD DATA LOCAL INFILE 'cities1000.txt' \
                INTO TABLE cities               \
                FIELDS TERMINATED BY '\t';      \
        " > mydbcommands.sql

RUN     echo "#!/bin/bash \n                    \
        mysql -h localhost -u root -p\$MYSQL_ROOT_PASSWORD <mydbcommands.sql \
        " >loaddata.sh && \
        chmod +x loaddata.sh

The sudo docker images command verifies that the image was created. After you create a container based on it, you'll be able to initialize the database with the ./loaddata.sh command.

Searching for Data: Your Web Site

Now let's work on the other part of the system. You can take advantage of the official PHP Docker image, which also includes Apache. All you need is to add the php5-mysql extension to be able to connect to the database server. The script should be in a new directory, along with search.php, the complete code for this "system". Building this image, which you'll name "worldweb", requires the sudo docker build -t worldweb . command (Listing 3).

Listing 3. The Dockerfile to create the Apache Web server is even simpler than the database one.


FROM php:5.6-apache
MAINTAINER Federico Kereki fkereki@gmail.com

COPY search.php /var/www/html/

RUN     apt-get update && \
        apt-get -q -y install php5-mysql && \
        docker-php-ext-install mysqli

The search application search.php is simple (Listing 4). It draws a basic form with a single text box at the top, plus a "Go!" button to run a search. The results of the search are shown just below that in a table. The process is easy too—you access the database server to run a search and output a table with a row for each found city.

Listing 4. The whole system consists of only a single search.php file.


<html>
<head>
<h3>Cities Search</h3>
</head>
<body>
<form action="search.php">
Search for: <input type="text" name="searchFor" 
 ↪value="<?php echo $_REQUEST["searchFor"]; ?>">
<input type="submit" value="Go!">
<br><br>
<?php
if ($_REQUEST["searchFor"]) {
  try {
    $conn = mysqli_connect("MYDB", "root", "ljdocker", "world");
    $query = "SELECT countries.name, cities.name, 
     ↪cities.latitude, cities.longitude ".
      "FROM cities JOIN countries ON cities.country=countries.id ".
      "WHERE cities.name LIKE ? ORDER BY 1,2";
    $stmt = $conn->prepare($query);

    $searchFor = "%".$_REQUEST["searchFor"]."%";
    $stmt->bind_param("s", $searchFor);
    $stmt->execute();
    $result = $stmt->get_result();

    echo "<table><tr><td>Country</td><td>City</td><td>Lat</td>
↪<td>Long</td></tr>";
    foreach ($result->fetch_all(MYSQLI_NUM) as $row) {
      echo "<tr>";
      foreach($row as $data) {
        echo "<td>".$data."</td>";
      }
      echo "</tr>";
    }
    echo "</table>";

  } catch (Exception $e) {
    echo "Exception " . $e->getMessage();
  }
}
?>
</form>
</body>
</html>

Both images are ready, so let's get your complete "system" running.

Linking Containers

Given the images that you built for this example, creating both containers is simple, but you want the Web server to be able to reach the database server. The easiest way is by linking the containers together. First, you start and initialize the database container (Listing 5).

Listing 5. The database container must be started first and then initialized.


# su -
# docker run -it -d -e MYSQL_ROOT_PASSWORD=ljdocker 
 ↪--name MYDB worlddb
fbd930169f26fce189a9d6020861eb136643fdc9ee73a4e1f114e0bfd0fe6a5c
# docker exec -it MYDB bash
root@fbd930169f26:/# dir
bin   cities1000.txt  dev    etc   lib    
 ↪loaddata.sh  mnt   opt   root  sbin     
 ↪srv  tmp  var
boot  countries.txt   entrypoint.sh  home  lib64  media 
 ↪mydbcommands.sql  proc  run   selinux  sys  usr
root@fbd930169f26:/# ./loaddata.sh
Warning: Using a password on the command line interface 
 ↪can be insecure.
root@fbd930169f26:/# exit

Now, start the Web container, with docker run -it -d -p 80:80 --link MYDB:MYDB --name MYWEB worldweb. This command has a couple interesting options:

  • -p 80:80 — This means that port 80 (the standard HTTP port) from the container will be published as port 80 on the host machine itself.

  • --link MYDB:MYDB — This means that the MYDB container (which you started earlier) will be accessible from the MYWEB container, also under the alias MYDB. (Using the database container name as the alias is logical, but not mandatory.) The MYDB container won't be visible from the network, just from MYWEB.

In the MYWEB container, /etc/hosts includes an entry for each linked container (Listing 6). Now you can see how search.php connects to the database. It refers to it by the name given when linking containers (see the mysqli_connect call in Listing 4). In this example, MYDB is running at IP 172.17.0.2, and MYWEB is at 172.17.0.3.

______________________