Docker makes use of Linux Kernel features like control groups and namespaces to create isolated execution spaces (containers) on the top of the host operating system.

It allows to isolate deployed applications by assigning the container holding the application with its own resources (network, filesystem, etc).

Let’s think how we ship cargo through the seas. We put the cargo in the container, typically in layers from the back to the front, then we close the container and ship it through the seas over a boat. At the destination, our cargo looks pretty much the same as when it was during departure.

Build once, run everywhere

But How does this differs from a virtual machine? Their biggest goal is pretty much the same: isolation. The most basic difference is that containers share the underlying operating system whilst virtual machines run over hypervisors that are all about emulating virtual hardware. This makes containers a lot more reliable in terms of stability and performance.

What we are going to do?

We are going to cover how to deploy Cassandra DB and a Spring Boot application through Docker containers and how to make them communicate with each other. The full code for the Spring Boot using Cassandra project can be accessed in this GitHub repository.

Docker

This post will not cover the Docker installation process as there are already plenty of information around. Please follow the official documentation and let’s just assume you have it installed.

Warming up

Let’s just play around with Docker for a while. Docker provides the ability of building images of the container with our software on it. The image build is instructed as stated in a file named Dockerfile.

The beauty of Docker images is that they are not only a way of making your application portable, but they can be also used as building blocks for more complex application stacks.

So let’s see an example:

# install CentOS 7 user space software
FROM centos:7

# attempt to update every package installed on the system
RUN yum -y upgrade

# install ruby package
RUN yum -y install ruby

# add a local file called hello.rb to the container on the specified directory
ADD hello.rb /opt/hello.rb

# tell how the application will run when docker run command is executed
CMD ["ruby", "/opt/hello.rb"]

That Dockerfile is counting on a local Ruby source file named hello.rb. We can create it with the following contents:

print "hello,world!\n"

Now we just need to build our image and run our application.

$ docker build -t myspace/test .
$ docker run -t myspace/test
hello,world!

For details on all Dockerfile commands please check the official documentation.

Containerizing Cassandra

To be able to deploy Cassandra we will need to understand the steps it requires to deploy a Cassandra DB on any machine, and translate them into Dockerfile commands:

  • Install Java (Cassandra requires Java 8 to work)
  • Add the necessary groups and users
  • Download and install Cassandra
  • Apply the necessary post-installation configurations
  • Expose cassandra ports
  • Define how it will startup

The Dockerfile

Our Dockerfile will look like:

# Let's base ourselves on the already existing image for Java 8 (OpenJDK)
FROM java:8

# groups and users
RUN groupadd -r cassandra --gid=999 && useradd -r -g cassandra --uid=999 cassandra

# install
ENV MIRROR http://apache.mirrors.pair.com/
ENV VERSION 3.7

RUN curl $MIRROR/cassandra/$VERSION/apache-cassandra-$VERSION-bin.tar.gz | tar -xzf - -C /opt \
    && mv /opt/apache-cassandra-$VERSION /opt/cassandra \
    && mkdir -p /tmp/cassandra

# post installation config
ADD config_cassandra.sh /opt/cassandra
RUN chmod +x /opt/cassandra/config_cassandra.sh
ENTRYPOINT ["/opt/cassandra/config_cassandra.sh"]

RUN chown -R cassandra:cassandra /opt/cassandra

# start
USER cassandra
WORKDIR /opt/cassandra

# 7000: ipc; 7001: tls ipc; 7199: jmx; 9042: cql; 9160: thrift
EXPOSE 7000 7001 7199 9042 9160

CMD ["/opt/cassandra/bin/cassandra", "-f"]

To be able to deploy a Cassandra cluster remotely, we need to change some default configurations. For that, a file config_cassandra.sh reads the current container IP and replaces it in the necessary Cassandra configuration fields.

#!/bin/bash
CASSANDRA_HOME=/opt/cassandra
CASSANDRA_HOST="$(hostname --ip-address)"

sed -ri 's/^(# )?('"listen_address"':).*/\2 '"$CASSANDRA_HOST"'/' "$CASSANDRA_HOME/conf/cassandra.yaml"
sed -ri 's/^(# )?('"rpc_address"':).*/\2 '"$CASSANDRA_HOST"'/' "$CASSANDRA_HOME/conf/cassandra.yaml"
sed -ri 's/^(# )?('"broadcast_address"':).*/\2 '"$CASSANDRA_HOST"'/' "$CASSANDRA_HOME/conf/cassandra.yaml"
sed -ri 's/^(# )?('"broadcast_rpc_address"':).*/\2 '"$CASSANDRA_HOST"'/' "$CASSANDRA_HOME/conf/cassandra.yaml"
sed -ri 's/(- seeds:).*/\1 "'"$CASSANDRA_HOST"'"/' "$CASSANDRA_HOME/conf/cassandra.yaml"

exec "[email protected]"

It will only run when the container starts, as it is declared under a ENTRYPOINT command.

Building and Running it

To run it we just need to run the following command:

$ docker build -t sample/cassandra .
$ docker run --name cassandra -p 9042:9042 sample/cassandra

We just defined the name and the port mapping followed by the name of the image that we just created. You can use the flag -d with the run command to run in the background.

Containerizing Spring Boot application

Let’s say we have a simple spring boot application that provides the means to retrieve a list of users from a Cassandra table named “person” that belongs to the “sampleks” keyspace. Please check the implementation details here as the focus of this post is how we add it to Docker and allow it to communicate with the containerized Cassandra instance.

For adding this spring boot application into Docker, we need first to create the Dockerfile in the root path of our application, as below.

The Dockerfile

FROM java:8
VOLUME /tmp
ADD target/boots-1.0-SNAPSHOT.jar boots.jar
RUN sh -c 'touch /boots.jar'
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/boots.jar"]

Building and Running it

If you are using Maven as in the example, it’s easier to build the image if we include the following plugin in our application’s POM. It allows to build the docker image out of a maven project.

<plugin>
    <groupId>com.spotify</groupId>
    <artifactId>docker-maven-plugin</artifactId>
    <version>0.2.3</version>
    <configuration>
        <imageName>sample/${project.artifactId}</imageName>
        <dockerDirectory>${project.basedir}</dockerDirectory>
        <resources>
            <resource>
                <targetPath>/</targetPath>
                <directory>${project.build.directory}</directory>
                <include>${project.build.finalName}.jar</include>
            </resource>
        </resources>
    </configuration>
</plugin>

Then we just need to use it integrated with Maven.

mvn package docker:build

Linking Containers

As we need our application to access our Cassandra DB, we need to make Spring Boot know about the existance of Cassandra DB. For that we need to link the containers.

docker run --link cassandra:db --name boots -p 8080:8080 sample/boots

The link has the form container-name:alias. The alias is used as a prefix to create environment variables associated with the target container. In this case a lot of variables will be created in Boot container with a reference to information about the remote container.

One of those variables is the Cassandra host address. So to short the example, let’s trick our spring boot application to use it when it’s defined. For that, our application.properties file shall be defined as follows:

DB_PORT_9042_TCP_ADDR=localhost
cassandra.host=${DB_PORT_9042_TCP_ADDR}

Because of the way Spring Boot loads properties, if a system variable with the name DB_PORT_9042_TCP_ADDR is defined, it will be used instead of localhost.

Testing

Assuming everything went fine, you can just open your browser on port docker-ip:8080 and you will see a list of users. By this point you will just see an empty page because there’s no data in the database.

This was intentional for the example. We will be connecting to our cassandra instance and running CQL to insert data directly. To connect to our container and navigate inside it we will need to execute the following command:

$ docker exec -it cassandra bash
[email protected]:/opt/cassandra$ ./bin/cqlsh
cqlsh> CREATE KEYSPACE IF NOT EXISTS sampleks WITH REPLICATION = { 'class': 'SimpleStrategy', 'replication_factor': '2' }
cqlsh> USE sampleks;
cqlsh:samplks> CREATE TABLE IF NOT EXISTS person(id text, name text, PRIMARY KEY(id))
cqlsh:sampleks> INSERT INTO person (id, name) values ('XYZ123', 'John');
cqlsh:sampleks> INSERT INTO person (id, name) values ('ZYX567', 'Anna');

Now you can refresh your browser’s page and then you should be able to see John and Anna there.

Conclusion

Docker is a powerful tool for isolating your application in the hosting environment. You can build it once, and ship it to everywhere and it is expected to have a consistent behaviour.

It’s also very useful for testing in isolation. You can build entire clusters and have them dedicated just for you without the need to request more machines or environments.

This post meant to be an introduction on how to deploy two containers that communicate with each other.

We covered several topics that can make us achieve container deployment with custom configuration, linking containers, and accessing them as if they were any other remote machine.

You can access the full example with the corresponding instructions in this GitHub repository.

Happy Docking!