Generic Top

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

1. Overview

In this tutorial, we're going to see how to access Docker container information from inside the container using the Docker Engine API.

2. Setup

We can connect to the Docker engine in multiple ways. We'll cover the most useful ones under Linux, but they also work on other operating systems.

However, we should be very careful, because enabling remote access represents a security risk. When a container can access the engine, it breaks the isolation from the host operating system.

For the setup part, we will consider that we have full control of the host.

2.1. Forwarding the Default Unix Socket

By default, the Docker engine uses a Unix socket mounted under /var/run/docker.sock on the host OS:

$ ss -xan | grep var

u_str LISTEN 0      4096              /var/run/docker/libnetwork/dd677ae5f81a.sock 56352            * 0           
u_dgr UNCONN 0      0                                 /var/run/chrony/chronyd.sock 24398            * 0           
u_str LISTEN 0      4096                                      /var/run/nscd/socket 23131            * 0           
u_str LISTEN 0      4096                              /var/run/docker/metrics.sock 42876            * 0           
u_str LISTEN 0      4096                                      /var/run/docker.sock 53704            * 0    
...       

With this approach, we can strictly control which container gets access to the API. This is how the Docker CLI works behind the scenes.

Let's start the alpine Docker container and mount this path using the -v flag:

$ docker run -it -v /var/run/docker.sock:/var/run/docker.sock alpine

(alpine) $

Next, let's install some utilities in the container:

(alpine) $ apk add curl && apk add jq

fetch http://dl-cdn.alpinelinux.org/alpine/v3.11/community/x86_64/APKINDEX.tar.gz
(1/4) Installing ca-certificates (20191127-r2)
(2/4) Installing nghttp2-libs (1.40.0-r1)
...

Now let's use curl with the –unix-socket flag and Jq to fetch and filter some container data:

(alpine) $ curl -s --unix-socket /var/run/docker.sock http://dummy/containers/json | jq '.'

[
  {
    "Id": "483c5d4aa0280ca35f0dbca59b5d2381ad1aa455ebe0cf0ca604900b47210490",
    "Names": [
      "/wizardly_chatelet"
    ],
    "Image": "alpine",
    "ImageID": "sha256:e7d92cdc71feacf90708cb59182d0df1b911f8ae022d29e8e95d75ca6a99776a",
    "Command": "/bin/sh",
    "Created": 1595882408,
    "Ports": [],
...

Here, we issue a GET on the /containers/json endpoint and get the currently running containers. We then prettify the output using jq.

We'll cover the details of the engine API a bit later.

2.2. Enabling TCP Remote Access

We can also enable remote access using a TCP socket.

For Linux distributions that come with systemd we need to customize the Docker service unit. For other Linux distros, we need to customize the daemon.json usually located /etc/docker.

We'll cover just the first kind of setup since most of the steps are similar.

The default Docker setup includes a bridge network. This is where all containers are connected unless specified otherwise.

Since we want to allow just the containers to access the engine API let's first identify their network:

$ docker network ls

a3b64ea758e1        bridge              bridge              local
dfad5fbfc671        host                host                local
1ee855939a2a        none                null                local

Let's see its details:

$ docker network inspect a3b64ea758e1

[
    {
        "Name": "bridge",
        "Id": "a3b64ea758e1f02f4692fd5105d638c05c75d573301fd4c025f38d075ed2a158",
...
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16",
                    "Gateway": "172.17.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
...

Next, let's see where the Docker service unit is located:

$ systemctl status docker.service

docker.service - Docker Application Container Engine
     Loaded: loaded (/usr/lib/systemd/system/docker.service; disabled; vendor preset: disabled)
...
     CGroup: /system.slice/docker.service
             ├─6425 /usr/bin/dockerd --add-runtime oci=/usr/sbin/docker-runc
             └─6452 docker-containerd --config /var/run/docker/containerd/containerd.toml --log-level warn

Now let's take a look at the service unit definition:

$ cat /usr/lib/systemd/system/docker.service

[Unit]
Description=Docker Application Container Engine
Documentation=http://docs.docker.com
After=network.target lvm2-monitor.service SuSEfirewall2.service

[Service]
EnvironmentFile=/etc/sysconfig/docker
...
Type=notify
ExecStart=/usr/bin/dockerd --add-runtime oci=/usr/sbin/docker-runc $DOCKER_NETWORK_OPTIONS $DOCKER_OPTS
ExecReload=/bin/kill -s HUP $MAINPID
...

The ExecStart property defines what command is run by systemd (the dockerd executable). We pass the -H flag to it and specify the corresponding network and port to listen on.

We could modify this service unit directly (not recommended), but let's use the $DOCKER_OPTS variable (defined in the EnvironmentFile=/etc/sysconfig/docker):

$ cat /etc/sysconfig/docker 

## Path           : System/Management
## Description    : Extra cli switches for docker daemon
## Type           : string
## Default        : ""
## ServiceRestart : docker
#
DOCKER_OPTS="-H unix:///var/run/docker.sock -H tcp://172.17.0.1:2375"

Here, we use the gateway address of the bridge network as a bind address. This corresponds to the docker0 interface on the host:

$ ip address show dev docker0

3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:6c:7d:9c:8d brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:6cff:fe7d:9c8d/64 scope link 
       valid_lft forever preferred_lft forever

We also enable the local Unix socket so that the Docker CLI still works on the host.

There's one more step we need to do. Let's allow our container packets to reach the host:

$ iptables -I INPUT -i docker0 -j ACCEPT

Here, we set the Linux firewall to accept all packages that come through the docker0 interface.

Now, let's restart the Docker service:

$ systemctl restart docker.service
$ systemctl status docker.service
 docker.service - Docker Application Container Engine
     Loaded: loaded (/usr/lib/systemd/system/docker.service; disabled; vendor preset: disabled)
...
     CGroup: /system.slice/docker.service
             ├─8110 /usr/bin/dockerd --add-runtime oci=/usr/sbin/docker-runc -H unix:///var/run/docker.sock -H tcp://172.17.0.1:2375
             └─8137 docker-containerd --config /var/run/docker/containerd/containerd.toml --log-level wa

Let's run our alpine container again:

(alpine) $ curl -s http://172.17.0.1:2375/containers/json | jq '.'

[
  {
    "Id": "45f13902b710f7a5f324a7d4ec7f9b934057da4887650dc8fb4391c1d98f051c",
    "Names": [
      "/unruffled_cray"
    ],
    "Image": "alpine",
    "ImageID": "sha256:a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e",
    "Command": "/bin/sh",
    "Created": 1596046207,
    "Ports": [],
...

We should be aware that all containers connected to the bridge network can access the daemon API.

Furthermore, our TCP connection is not encrypted.

3. Docker Engine API

Now that we've set up our remote access let's take a look at the API.

We'll explore just a few interesting options but we can always check the complete documentation for more.

Let's get some info about our container:

(alpine) $ curl -s http://172.17.0.1:2375/containers/"$(hostname)"/json | jq '.'

{
  "Id": "45f13902b710f7a5f324a7d4ec7f9b934057da4887650dc8fb4391c1d98f051c",
  "Created": "2020-07-29T18:10:07.261589135Z",
  "Path": "/bin/sh",
  "Args": [],
  "State": {
    "Status": "running",
...

Here we use the /containers/{container-id}/json URL to obtain details about our container.

In this case, we run the hostname command to get the container-id.

Next, let's listen to events on the Docker daemon:

(alpine) $ curl -s http://172.17.0.1:2375/events | jq '.'

Now in a different terminal let's start the hello-world container:

$ docker run hello-world

Hello from Docker!
This message shows that your installation appears to be working correctly.
...

Back in our alpine container, we get a bunch of events:

{
  "status": "create",
  "id": "abf881cbecfc0b022a3c1a6908559bb27406d0338a917fc91a77200d52a2553c",
  "from": "hello-world",
  "Type": "container",
  "Action": "create",
...
}
{
  "status": "attach",
  "id": "abf881cbecfc0b022a3c1a6908559bb27406d0338a917fc91a77200d52a2553c",
  "from": "hello-world",
  "Type": "container",
  "Action": "attach",
...

So far, we've been doing non-intrusive things. Time to shake things a little.

Let's create and start a container. First, we define its manifest:

(alpine) $ cat > create.json << EOF
{
  "Image": "hello-world",
  "Cmd": ["/hello"]
}
EOF

Now let's call the /containers/create endpoint using the manifest:

(alpine) $ curl -X POST -H "Content-Type: application/json" -d @create.json http://172.17.0.1:2375/containers/create

{"Id":"f96a6360ad8e36271cc75a3cff05348761569cf2f089bbb30d826bd1e2d52f59","Warnings":[]}

Then, we use the id to start the container:

(alpine) $ curl -X POST http://172.17.0.1:2375/containers/f96a6360ad8e36271cc75a3cff05348761569cf2f089bbb30d826bd1e2d52f59/start

Finally, we can explore the logs:

(alpine) $ curl http://172.17.0.1:2375/containers/f96a6360ad8e36271cc75a3cff05348761569cf2f089bbb30d826bd1e2d52f59/logs?stdout=true --output -

Hello from Docker!
KThis message shows that your installation appears to be working correctly.

;To generate this message, Docker took the following steps:
3 1. The Docker client contacted the Docker daemon.
...

Notice we get some strange characters at the beginning of each line. This happens because the stream over which the logs are transmitted is multiplexed to distinguish between stderr and stdout.

As a result, the output needs further processing.

We can avoid this by simply enabling the TTY option when we create the container:

(alpine) $ cat create.json

{
  "Tty":true,	
  "Image": "hello-world",
  "Cmd": ["/hello"]
}

4. Conclusion

In this tutorial, we learned how to use the Docker Engine Remote API.

We started by setting up the remote access either from the UNIX socket or TCP and moved further showing how we can use the remote API.

Generic bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE
Comments are closed on this article!