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.