loading...
Cover image for Debugging Docker Containers

Debugging Docker Containers

adamkdean profile image Adam K Dean ・8 min read

Since my post about docker and SSL made the top 7 last week, I've been thinking about things which I may possibly know but take for granted, things which may be useful to other people. There are several things I could discuss like the inner workings of containers, the more practical side of using docker, how docker elbows its way into your iptables etc, but I think for this week's post I'm going to stick to something simple, though powerful and super easy: debugging docker containers.

One thing I've noticed while observing new users of docker over the years is that for all intents and purposes, containers appear to be these little black boxes connected together via all manner of tubes and pipes, with nothing to indicate what's going on other than a couple of lights and a lack of fire.

I imagine it looks something a little bit like this.

My artistic rendering of containers through the lens of a newbie who doesn't really understand what is really going on

Now, the first you can do to find out what's going on is to check the logs of a container. Most folks know this, but you can do that using the docker logs command, like this.

$ docker logs website

[WEBSITE] Connecting to API...
[WEBSITE] Metal Tube connected to API
[WEBSITE] Loading blog posts via metal tube from API
[WEBSITE] Blog posts loaded
[WEBSITE] Listening on port 80

Sometimes, things take a bit longer. You kinda just have to wait for it, but that's fine, we can watch and wait using the -f or follow flag.

$ docker logs -f api

[API] Connecting to database...
[API] Connection established via glass pipe
[API] Request received from website, loading data... 
[API] Data loaded [ 4%]
[API] Data loaded [11%]
[API] Data loaded [19%]
[API] Data loaded [24%]

This is great for logs that are piped to the standard output (stdout) but you know, sometimes people containerise the strangest of things. I've seen all sorts, including containers within containers.

One time someone took a container and they put the container into a container and then they did it again, and again, and eventually they had containers full of containers and used these containers to build a container system, called Docker

Sometimes an application will write to a log file, so docker logs won't really help you. Maybe you have a database that isn't really doing anything. Perhaps it's running really slowly. The container is running...

$ docker ps

CONTAINER ID    IMAGE         STATUS          NAMES
1e98dd08aee8    project/web   Up 10 minutes   website
b76207f35778    project/api   Up 10 minutes   api
35f9ba3ed4c8    project/db    Up 10 minutes   database

...but for some reason when we check the container logs, there is nothing apparent going on, and we have to delve deeper.

$ docker logs database

[DATABASE] Loading...
[DATABASE] Listening on port 21000

We need to find the hidden logs. To find them, we must enter the haunted labyrinth running container, which we can do easily by using the docker exec command. The way it works is that you specify a command and a container to run it in, and docker executes that command within the container for you. If we run an interactive shell then we can have a poke around the container. Psst, here's a link so you can read more about docker exec.

$ docker exec -ti database bash

root@35f9ba3ed4c8:/#

We're in. You'll notice that we used the -ti flags. According to the docker docs, -t or --tty allocates a pseudo-TTY and -i or --interactive keeps stdin open even if not attached. All we need to know is that it hooks us up to the container so we can have ourselves a little look around, and find those pesky logs.

This is a really powerful trick, and I've used it so many times over the years. Sometimes you need to look at an application that is running, or find some logs, or perhaps you need to *gulp* edit some code while an application is running without going through the standard build process.

Once you've attached to the container and spawned a shell, you're free to look around, and that's so useful. You can easily read logs.

$ docker exec -ti database bash

root@35f9ba3ed4c8:/# tail -f /var/log/database.log

[MyDB][0.0001] started spooling database contraption
[MyDB][0.0004] reading tables from an ancient .dat file
[MyDB][0.0012] unable to read data, tables.dat corrupted...

Bingo.

Breakfast: juice, lunch: juice, dinner: juice

Sometimes, you might not be able to spawn a bash shell, especially if the container is based on a slimmed down image that's been on a juice detox, but you tend to always be able to rely on sh being available.

$ docker exec -ti database sh

# ps -p $$
  PID TTY          TIME CMD
   56 pts/0    00:00:00 sh

There are a few other useful tricks as well. You might not use these often but every now and then they'll come in handy. First up, docker top. This is like the top we all know well, except it runs within a container. Containers usually only have one process running, but that's not always the case.

$ docker top database

PID                 USER                TIME                COMMAND
2157                999                 0:35                mydb --bind_ip_all

Then you have the docker stats command. You can either run it against a specific container or view all running containers. It's useful when you want to monitor containers under load, or if you think you may have a memory leak somewhere. I've had to shorten the output a little bit, but you get the idea.

$ docker stats api

ID      NAME  CPU %   MEM USAGE / LIMIT     MEM %   NET I/O       BLOCK I/O
b76207  api   0.31%   44.52MiB / 15.64GiB   0.28%   1.75kB / 0B   1.64MB / 14.2MB

Now, when we run docker ps we see some information about a container: the image it's running, its status, when it was created etc, however containers actually have a lot more information behind the scenes such as where their filesystems are stored, what IP address they have assigned, and we can view all of this via the docker inspect command.

The actual output is much larger, so I've truncated it to give you a rough idea of what it looks like. This is perhaps 10% of the information actually available.

$ docker inspect website

[
    {
        "Id": "1e98dd08aee8b1c6e79eccf0551f4af88299a52fec567a8b27ad4711fe2ac287",
        "Created": "2020-02-19T15:25:13.582120171Z",
        "State": {
            "Status": "running",
            "Running": true,
            "Paused": false,
            "Restarting": false,
            "OOMKilled": false,
            "Dead": false
        },
        "Image": "sha256:6d5319f761c08cb3053c676a274ce6500f31c98fb1c1fcab8ab736e39968a2fe",
        "LogPath": "/var/lib/docker/containers/1e1933e26402e3b062051c63c3fbc6d327641059213192ab6b94d1afb36484ca/1e1933e26402e3b062051c63c3fbc6d327641059213192ab6b94d1afb36484ca-json.log",
        "Name": "/website",
        "RestartCount": 0,
        "Driver": "overlay2",
        "Platform": "linux"
    }
]

If you know what you're looking for, it's easier to extract that information than it is to sift through the entire document. You can do that using the --format argument. For example, getting the IP address of a container.

$ docker inspect --format '{{.NetworkSettings.IPAddress}}' api

172.17.0.3

The last trick I want to show you is less about docker, but it's still super useful. Sometimes you may have running services in containers but you're not able to connect to them. Is it a firewall issue? A networking issue? Is it an application issue? Well, whenever we have issues the best thing to do is a binary search of possible causes, that is to say, half things, and then half them again. If you can connect two processes over a network then you can rule out a network issue, for example.

We can do this using netcat, or nc. It is perhaps one of the most versatile and powerful tools that you can put in your digital toolbox. The concept is simple, on one end you create a listener, and on the other end you create a connector.

Can I hear you now?

Let's create a bridge network and two containers to demonstrate: one alpine, one ubuntu. Sometimes nc is installed (alpine) and sometimes we have to install it (ubuntu) so that's what we'll do.

$ docker network create metal-tube

7507bfb685510ae7e93f808cdc2392bedf25b2912cc77abc3801d6c575795d30

--------------------------------------------------------------------

$ docker run --rm -ti --network metal-tube --name jeff alpine

jeff#

--------------------------------------------------------------------

$ docker run --rm -ti --network metal-tube --name alan ubuntu

alan# apt update -qq && apt install netcat -qqy

alan#

Okay, we're ready. So we have two containers on the same network, which we've called metal-tube. We've given them the names jeff and alan. I've changed the terminal prompts to jeff# and alan# to make this easier to read.

I've previously talked about user defined bridge networks in my post Automatic SSL with Let's Encrypt & Nginx, but the main thing to know is that they have built in DNS servers that allow you to resolve containers by their names. Don't believe me? Let's ping Alan from Jeff.

jeff# ping alan

PING alan (172.21.0.3): 56 data bytes
64 bytes from 172.21.0.3: seq=0 ttl=64 time=0.247 ms

So now, let's see if we can connect from Alan to Jeff. For this, we first setup a listener on Alan with netcat using the -l listen flag, the -p local port flag, and, so that we know what's going on, the -vv very verbose flag.

alan# nc -vvlp 9000

listening on [any] 9000 ...

Now this will hang until it receives data. Over on Jeff, we can attempt to send some data via netcat. The way we'll do this is to echo some data into the netcat process via a | pipe. We'll be using the -vv very verbose flag again.

jeff# echo "hello" | nc -vv alan 9000

alan (172.21.0.3:9000) open
sent 6, rcvd 0

Great, it seems to have worked. If it didn't, we may have have seen something like nc: alan (172.21.0.3:9000): Connection refused. Run the command again and you'll see. Once a connection to a netcat listener is closed, the listener process exits. But let's look at Alan now.

alan# nc -vvlp 9000

listening on [any] 9000 ...
connect to [172.21.0.3] from jeff.metal-tube [172.21.0.2] 39993
hello
 sent 0, rcvd 6

It worked, therefore we know that TCP connections between these two containers are working fine. Now, locally, they're going to work fine, but sometimes you may have services on different physical servers, and this is often a great way to test connectivity between machines. You don't always need to setup a netcat listener either, you can use netcat on it's own to connect to a service to check that it's up and accepting connections. That's super useful too.

There are a number of other tricks you can use to debug containers, such as overriding entry points, mapping volumes to keep an eye on the filesystem, and more, but the above are the ones I use 95% of the time. If you have some cool tricks, why not share them in the comments below?

T..h..a..n..k....y..o..u....f..o..r..r..e..a..d..i..n..g

Posted on by:

adamkdean profile

Adam K Dean

@adamkdean

Principal Network Engineer / Master of Engineering (Mechanical) student

Discussion

pic
Editor guide