Troubleshooting Containers

Introduction

I find my self rather often than not in a situation where I have to trouble shoot a containerized application or a coworker needs some help doing so. In this post I want to show some practices how you can resolve your problems quickly.

On a side note: I have been nominated to be a Docker Captain. This is a big honor to me and I’m feeling really excited. For me containers in general and Docker containers in specific are some of the most disruptive and innovative features in IT.

Prerequisites

If you have not done so yet then please install Docker for Mac or Docker for Windows.

Why is this not working?

Let’s assume we are containerizing a RESTful API written in Python using Flask. The language and framework used here are not really relevant, the application could as well be a Node JS, a Java or a .NET application. What matters here is the techniques shown in an around containers.

Create a new project folder. Add a file app.py to this folder. The content of this file defines our simple API and looks like this

from flask import Flask, jsonify

app = Flask(__name__)

tasks = [
    {
        'id': 1,
        'title': u'Buy groceries',
        'description': u'Milk, Cheese, Pizza, Fruit, Tylenol',
        'done': False
    },
    {
        'id': 2,
        'title': u'Learn Python',
        'description': u'Need to find a good Python tutorial on the web',
        'done': False
    }
]

@app.route('/api/tasks', methods=['GET'])
def get_tasks():
    return jsonify({'tasks': tasks})

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

If we have Python and Flask installed on our developer machine we can run this application as follows

python app.py

Then we open a Browser and navigate to http://localhost:5000/api/tasks and should see this

So far so good. But the goal is to run this application in a container. Let’s add a Dockerfile to our project folder

FROM python:2.7
RUN mkdir -p /app
WORKDIR /app
COPY . /app
EXPOSE 5000
ENTRYPOINT python ./app.py

Now we want to build a Docker image using the above Dockerfile. Open a terminal and navigate to the project folder. Execute the following command to build the image

docker build -t sample-api .

you should see the following output in you terminal

Step 1 : FROM python:2.7
2.7: Pulling from library/python

357ea8c3d80b: Pull complete
52befadefd24: Pull complete
3c0732d5313c: Pull complete
ceb711c7e301: Pull complete
4211bb537697: Pull complete
c48f01f3aad7: Pull complete
72d54f32484a: Pull complete
Digest: sha256:675765c16c09a0f1b5031cd8e568b1c33eebd65fd4c31b658033e3793e738d9f
Status: Downloaded newer image for python:2.7
 ---> 1c6554d85da7
Step 2 : RUN mkdir -p /app
 ---> Using cache
 ---> 6379072d224e
Step 3 : WORKDIR /app
 ---> Using cache
 ---> 7808a3ff37c1
Step 4 : COPY . /app
 ---> b3d93e6f67ae
Removing intermediate container 91c24ce21395
Step 5 : EXPOSE 5000
 ---> Running in 5cf4a42cf55a
 ---> 804134cc8977
Removing intermediate container 5cf4a42cf55a
Step 6 : ENTRYPOINT python ./app.py
 ---> Running in 8aa0fbae2f0b
 ---> ef9a88292c76
Removing intermediate container 8aa0fbae2f0b
Successfully built ef9a88292c76

Well, that was easy. Now let’s try to run a container using the image we just built.

docker run --name my-api -d -p 5000:5000 sample-api

OK, in the above command we told Docker to run a container with the name my-api as a service (or daemon), mapping the container port 5000 to the host port 5000 using the image sample-api.

We can now double check whether we were successful by using this command

docker ps -a

We should see this

Wait a second… Let’s look at the status of the container. It says Exited. What the heck…? Apparently there is a problem. Help…!?! What can we do?

Analyzing the Log

Although the container doesn’t run and is in the Exited status it’s log is still available to us. We can access the log of any container by using the command

docker logs [Container ID]

or

docker logs [Container Name]

Let’s use the container name, my-api in our case

docker logs my-api

$ docker logs my-api
Traceback (most recent call last):
  File "./app.py", line 1, in <module>
    from flask import Flask, jsonify
ImportError: No module named flask

OK, the log informs us that Python encountered an error. The module flask could not be found. Oh yeah, my bad, I forgot to install this module. The Docker base image python:2.7 from which we inherit our image does not have any 3rd party modules installed. Let’s add a file requirements.txt to our project and add flask as a dependency. The content of the file looks like this

flask

now we can modify the Dockerfile as follows

FROM python:2.7
RUN mkdir -p /app
WORKDIR /app
COPY requirements.txt /app/
RUN pip install -r requirements.txt
COPY . /app
EXPOSE 5000
ENTRYPOINT python ./app.py

Note how I added two lines after the WORKDIR command. One to copy the requirements.txt file into the image and the other to run pip install. We can now rebuild the image

docker build -t sample-api .

Once the image is built it is now including Flask. We can try to run a container with the new image. But before we do that we need to remove the Exited container which is still around using

docker rm my-api

now we’re good to go

docker run --name my-api -d -p 5000:5000 sample-api

executing docker logs my-api will show the expected output

$ docker logs my-api
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)

The Flask (web-) server is listening at port 5000. If we open a browser and navigate to http://localhost:5000/api/tasks we see the same result as when we ran the Python application directly on our host.

Using an alternative Entrypoint

Let’s for a moment assume that we still have problems with our container and it doesn’t work as expected. What other means do we have to analyze the situation? The problem is that every time we run a container it exits with an error. In this situation it is helpful to have the opportunity to create a container from the very same image but this time we override the predefined entry point. The ENTRYPOINT command in a Dockerfile defines what action is executed upon start of the container. In our case the action is

python app.py

which means that we run the application whose starting point is defined in app.py. Now let’s execute this command instead

docker run --rm -it --entrypoint /bin/bash sample-api

If we do this then a container based on the sample-api image is started in interactive mode with an attached terminal (-it) and the command that is executed upon start of the container is /bin/bash, that is, instead of starting our Python application we start a bash shell.

Doing that we find ourselves inside the container and have all possibilities to analyze the situation and do all kinds of experiments. We can e.g. investigate whether all expected assets are available and in the right place, or we can use pip to install (missing) modules and even execute the default start command python app.py.

If we do the latter then the difference here is that if an error happens we find ourselves still inside the container and can do further investigations and/or experiments.

Once we’re done we can exit the container by just typing exit in the shell. The container will stop and will automatically be removed from the Docker workspace due to the command line argument --rm we used when running the container.

Exec into a running Container

A third possibility we have is to intrude into a running container using the docker exec command. This is a very useful method if the container is running but it’s behavior is somewhat unexpected. Let’s start a a container from our image sample-api as we did before

docker run --name my-api -d -p 5000:5000 sample-api

We can verify using docker logs my-api that the Flask web server is running and listening at port 5000. Now we can use the following command to break into the running container

docker exec -it my-api /bin/bash

with the above command we start a new process running bash inside the container my-api. We also run this process interactively with terminal (-it).

We can now use the ps aux command to show all running processes in the container

root@6e90b9351aeb:/app# ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.0   4336   736 ?        Ss   01:03   0:00 /bin/sh -c python ./app.py
root         7  0.0  0.9  99116 19600 ?        S    01:03   0:00 python ./app.py
root        10  0.0  0.1  21928  3636 ?        Ss   01:03   0:00 /bin/bash
root        25  0.0  0.1  19188  2396 ?        R+   01:10   0:00 ps aux

And indeed we see our Python application running as well as the bash shell that we just started.

Summary

In this post I have shown a few techniques that we can use to trouble shoot misbehaving containers. I have shown how we can retrieve the log generated by the application running inside the container using docker logs. I have also shown how we can override the entrypoint of a container upon start. Last I have discussed how one can hijack a running container using the docker exec command. These are only the most straight forward techniques. I’m sure that I will elaborate on more advanced techniques in a future post. Please stay tuned.

About Gabriel Schenker

Gabriel N. Schenker started his career as a physicist. Following his passion and interest in stars and the universe he chose to write his Ph.D. thesis in astrophysics. Soon after this he dedicated all his time to his second passion, writing and architecting software. Gabriel has since been working for over 25 years as a consultant, software architect, trainer, and mentor mainly on the .NET platform. He is currently working as senior software architect at Alien Vault in Austin, Texas. Gabriel is passionate about software development and tries to make the life of developers easier by providing guidelines and frameworks to reduce friction in the software development process. Gabriel is married and father of four children and during his spare time likes hiking in the mountains, cooking and reading.
This entry was posted in containers, docker, How To and tagged , , . Bookmark the permalink. Follow any comments here with the RSS feed for this post.

Comments are closed.