Containerizing Applications Using Docker

erinborders
7 min readMar 2, 2021

The most consistent issue I experience, as a DevOps engineer, is how to containerize a new application using Docker. Every task that I get in this category is different — different technology, different execution. Sometimes I’m migrating existing applications and sometimes I’m developing cloud native solutions. What it boils down to is this: I need a consistent process I can follow to point me in the right direction.

That process is, essentially, build and explore.

It’s way too confusing to attempt to containerize an application using Docker if I don’t know the name of the app package being built or the contents of that package. I need to know this information so I can copy packages into the Docker container I’m eventually going to create, and it’s too tedious to have to talk to the developer every time I’m asked to containerize an application.

So I just build the package myself in my local terminal.

The Build Phase

On my project, we consistently use Gradle, so I looked into the documentation on how to use it. The following is a basic rundown for all my DevOps people that need to understand how to work with a build.gradle file created by their developer team.

How to use Gradle:

  1. Navigate to directory containing build.gradle file
  2. Read the file — it will contain the name of the tasks that you need to run in order to build the package. In my case, I have a task that builds the package and another that moves it to a specific folder. We’ll call those tasks ‘build’ and ‘move’ respectively.
  3. Once I know the tasks to run, all I have to do is run ‘gradle build move’ in my terminal in the same directory as build.gradle and Gradle will execute those tasks.

So, once I run those tasks and the Gradle build is successful, it’s time for the exploration phase. I navigate to the directory that the package was moved to during the Gradle scripts and now there’s a new, compressed TAR file there.

You can think of TAR files as an archive — they store multiple files in a single file. This file contains everything I need, but since it’s compressed I don’t know what specifically is inside of it yet — the file system structure, the names of the folders or files. So now it’s time to look around.

The Explore Phase

For this example, let’s say my starting file is called example.tar.gz. I extract the files from it by running tar -xvf example.tar.gz. Now I can see the contents of the file. The tar command also leaves the example.tar.gz file, but we don’t really need that anymore since we can see the contents so we can go ahead and remove that.

Now, this next part is dependent on the image that I’m using. Let’s say that I’m using an Apache httpd image from the Docker hub. For those unfamiliar, Apache is a popular web server software — your favorite websites might be using it to show you their content. In most cases, Docker is super helpful because they often include instructions on how to use an image, which is the case for the official Apache image. But even if they don’t contain that information, you can often find it in the documentation for that software. For example, if Docker didn’t specify what I need to get the Apache image up and running, then I can always go to the Apache documentation to see what they’re expecting and that will point me in the right direction.

Instructions for how to use the official Apache image

Luckily, Docker hub does have the instructions!

The above is the general instructions on how to get this particular Apache image running with a Dockerfile. I can tell from the instructions that, in order to run correctly, Apache needs its html files to be located at a specific path (in this case: /usr/local/apache2/htdocs/, which is the default setting). So that means, I need to figure out where the html files are located in the app package so that I can write instructions in the Dockerfile to copy those html files from my local file system, to /usr/local/apache2/htdocs in my soon to be container.

So let’s say my package contained two folders: html, which contains all the html files, and conf, which contains all the configuration files. So that means at least one of the instructions I need to write in my Dockerfile will be:

COPY html/* /usr/local/apache2/htdocs/

That line means that I will copy all the contents of the html folder on my local file system, to the /usr/local/apache2/htdocs/ path in the container.

Now, the fact that the package contains a conf directory tells me that there might be important configuration files that this Apache image will need. So the first step I’ll take is to go back to the Docker hub’s instructions on how to use the Apache image to see if they mention anything about configuration. And they do!

Instructions for how to add configuration files in image.

Bless Docker Hub.

We have our own configuration so we don’t need to worry about the first step of obtaining the upstream default configuration. So all we need to do is copy our configuration to the default path in the container. So first, I need to check what’s in my conf folder. If the only file inside it is httpd.conf, for example, all I need is:

COPY conf/httpd.conf /usr/local/apache2/conf/httpd.conf

But let’s say my configuration has multiple files besides httpd.conf. All of those files are necessary, so I can copy them with the below command to make sure they all get copied into the container’s conf directory.

COPY conf/* /usr/local/apache2/conf/

Creating the Dockerfile

Okay, now I’ve determined the steps on my local machine. From start to finish, I built the app package, un-tared the app package to see its contents, and copied the contents to specific locations in the image.

But the magic of Docker is that, when other people use my code repository, they should be able to make a successful container using only the code in the repository. That means, they’ll have access to the build.gradle and, once I create it, my Dockerfile, but they won’t have the untar step.

Without that step, they won’t have the html or conf folders to work with, just the example.tar.gz file, and they won’t be able to complete the ensuing steps of copying the html and conf contents to the right locations. So let’s write the Dockerfile to include the tar step.

DOCKERFILE

FROM httpd:2.4
COPY example.tar.gz .
RUN tar -xvf example.tar.gz && rm example.tar.gz
RUN cp -r html/* /usr/local/apache2/htdocs/
RUN cp -r conf/* /usr/local/apache2/conf/

So that’s it. Our FROM command indicates the image we’re going to use as our base (in this case, the Apache image from Docker hub). We’ll COPY the TAR file that gets created from the build into the current directory of our image. We’ll RUN the command to extract the files from the TAR package then remove the example.tar.gz that’s left behind. Then we’ll RUN the commands to copy the html and conf folder contents to the appropriate places.

Anyone who has access to our code repo, and therefore the build.gradle and Dockerfile, will just have to run the gradle command to build the package, then the following docker command, to create an image:

docker image build -t example-image .

This command will create an image named example-image, and from this image they can build a container with the following command:

docker container run -n example-container -p 8080:80 example-image

This command creates a container called example-container from example-image, and ensures that the container is listening on port 8080 and forwarding all traffic to port 80 inside the container.

You might be wondering where we got the 8080:80 from. The short answer is that that too is included in the Docker hub instructions for this image. The long answer is that Apache listens on port 80 by default. The container wrapping it, however, listens on whatever port we tell it to. So by saying -p 8080:80, we are telling the container: ‘Watch port 8080. If a message shows up there, send it to port 80 — the port that Apache is watching.”

There you have it: the basic steps to figuring out how to containerize an application. Will this need tweaking? Absolutely. There’s always the possibility that the dev team you’re working with changed the default paths or some other small detail that will break your container when you try to run it, so communication is key. But at least you have a place to start.

It might seem like a small deal, but having a process like this — of troubleshooting and figuring out exactly what you need — is so much less frustrating than trying to shoot in the dark. This is a crucial addition to your toolbox because just relying on other people’s examples to try to figure out how to containerize your application can be a long and arduous slog. You have to find multiple examples and try to reverse engineer their thought process, and sometime you end up doing steps you didn’t even need because you were following someone else’s lead without really knowing why they’re taking you in that direction.

It’s my hope that just providing my thought process upfront will help someone somewhere avoid that slog.

--

--