2. Docker Concepts

In this lab, we will install Docker and use it to run containers.

Chapter Details
Chapter Goal Install Docker and use it to run containers
Chapter Sections

2.1. Install Docker

Step 1 To simplify the installation process, we have provided a set convenience scripts for installing Docker. Take a look at the scripts in the install directory:

$ ls ~/k8s-examples/install/

Later on, we will utilize this directory to install Kubernetes components as well.

Step 2 Run the following script to install Docker, the script will also print the version of Docker installed:

$ sudo ~/k8s-examples/install/install-docker.sh

Step 3 Let’s verify that Docker is installed correctly. The following command downloads a test image and runs it in a container. When the container runs, it prints an informational message and exits:

$ sudo docker run hello-world
Unable to find image 'hello-world:latest' locally
...
Status: Downloaded newer image for hello-world:latest

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

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker Hub account:
 https://hub.docker.com

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

Step 4 By default, the docker daemon binds to a Unix socket, which is owned by the user root and other users can access it with sudo. To make it possible for regular users to use the docker client without sudo there is a group called docker. When the docker daemon starts, it makes the ownership of the socket read/writable by the docker group.

Let’s add the current user stack to the docker group:

$ sudo usermod -aG docker stack

Step 5 Usually, to make the changes to take affect, you need to logout and login again, but in our case, we will create a new login session:

$ sudo su - stack

Step 6 Check that the user stack is in the docker group:

$ id
uid=1000(stack) gid=1000(stack) groups=1000(stack),109(docker)

Step 7 Now you can use docker without sudo:

$ docker run hello-world

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

2.2. Show running containers

Step 1 Run docker ps to show running containers:

$ docker ps
CONTAINER ID IMAGE       COMMAND  CREATED        STATUS                     PORTS NAMES

Step 2 The output shows that there are no running containers at the moment. Use the command docker ps --help to list all options available for ps subcommand:

$ docker ps --help

Usage:  docker ps [OPTIONS]

List containers

Options:
  -a, --all             Show all containers (default shows just running)
  -f, --filter filter   Filter output based on conditions provided
      --format string   Pretty-print containers using a Go template
      --help            Print usage
  -n, --last int        Show n last created containers (includes all states) (default -1)
  -l, --latest          Show the latest created container (includes all states)
      --no-trunc        Don't truncate output
  -q, --quiet           Only display numeric IDs
  -s, --size            Display total file sizes

Step 3 The output shows that there are no running containers at the moment. Use the command docker ps -a to list all containers:

$ docker ps -a
CONTAINER ID IMAGE       COMMAND  CREATED        STATUS                     PORTS  NAMES
6e6db2a24a8e hello-world "/hello" 15 minutes ago Exited (0) 15 minutes ago         dreamy_nobel
77609b91727a hello-world "/hello" 10 minutes ago Exited (0) 10 minutes ago         grave_pike

In the previous section we started two containers and the command docker ps -a shows that both of them exited. Note that Docker has generated random names for the containers (the last column). In your case, these names can be different.

Step 4 Let’s remove the stopped containers which permanently discards the Read/Write image layer and the container (note: your container names will be different):

$ docker rm dreamy_nobel grave_pike
dreamy_nobel
grave_pike

2.3. Specify a container main process

Step 1 Use the command docker image --help to list all options available for image subcommand:

$ docker image --help

Usage:  docker image COMMAND

Manage images

Commands:
  build       Build an image from a Dockerfile
  history     Show the history of an image
  import      Import the contents from a tarball to create a filesystem image
  inspect     Display detailed information on one or more images
  load        Load an image from a tar archive or STDIN
  ls          List images
  prune       Remove unused images
  pull        Pull an image or a repository from a registry
  push        Push an image or a repository to a registry
  rm          Remove one or more images
  save        Save one or more images to a tar archive (streamed to STDOUT by default)
  tag         Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE

Step 2 Let’s use the pull subcommand to download the existing Ubuntu image to our local server:

$ docker image pull ubuntu
Using default tag: latest
latest: Pulling from library/ubuntu
5667fdb72017: Pull complete
d83811f270d5: Pull complete
ee671aafb583: Pull complete
7fc152dfb3a6: Pull complete
Digest: sha256:b88f8848e9a1a4e4558ba7cfc4acc5879e1d0e7ac06401409062ad2627e6fb58
Status: Downloaded newer image for ubuntu:latest

As you see, Docker downloaded the image ubuntu:latest because it was not on the local machine.

Step 3 Let’s run the command docker image ls to show all the images on your local host:

$ docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu              latest              42118e3df429        11 days ago         124.8 MB
hello-world         latest              c54a2cc56cbb        4 weeks ago         1.848 kB

Step 4 Let’s run our own “hello world” container using the downloaded Ubuntu image. Docker will use the local copy of the image:

$ docker run ubuntu /bin/echo 'Hello world'
Hello world

2.4. Specify an image version

Step 1 As you see, Docker has downloaded the ubuntu:latest image. You can see Ubuntu version by running the following command:

$ docker run ubuntu /bin/cat /etc/issue.net
Ubuntu 18.04 LTS

Step 2 Let’s say you need an older Ubuntu release. In this case, you can specify the version you need. Docker detects that the image does not exist locally and automatically downloads it if available:

$ docker run ubuntu:14.04 /bin/cat /etc/issue.net
Unable to find image 'ubuntu:14.04' locally
14.04: Pulling from library/ubuntu
...
Status: Downloaded newer image for ubuntu:14.04
Ubuntu 14.04.5 LTS

Step 3 The docker image ls command should show that we have two Ubuntu images downloaded locally:

$ docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu              latest              42118e3df429        11 days ago         124.8 MB
ubuntu              14.04               0ccb13bf1954        11 days ago         188 MB
hello-world         latest              c54a2cc56cbb        4 weeks ago         1.848 kB

2.5. Run an interactive container

Step 1 Let’s use the ubuntu image to run an interactive bash session. We will use the -t command line argument to assigns a pseudo-tty or terminal inside the new container, and the -i to make an interactive connection by grabbing the standard input of the container. Use the exit command or press Ctrl-D to exit the interactive bash session:

$ docker run -t -i ubuntu /bin/bash
root@17d8bdeda98e:/# uname -a
Linux 69467ad115bf 4.15.0-1032-aws #34-Ubuntu ...
root@17d8bdeda98e:/# exit

Step 2 Let’s check that when the shell process has finished, the container stops:

$ docker ps
CONTAINER ID IMAGE       COMMAND  CREATED        STATUS                     PORTS NAMES

2.6. Run a container in a background

Step 1 To run a container in a background use the -d command line argument:

$ docker run -d ubuntu /bin/sh -c "while true; do date; echo hello world; sleep 1; done"
ac231579e57faf1afcb76e4f7ff22415d39af73eeeb4476eab3ea6bb8991f556

The command returned the container ID (it can be different in your case).

Step 2 Let’s use the docker ps command to see running containers:

$ docker ps
CONTAINER ID IMAGE  COMMAND                  CREATED        STATUS        PORTS NAMES
ac231579e57f ubuntu "/bin/sh -c 'while tr"   1 minute ago   Up 11 minute        evil_golick

ac231579e57f is the shortened container ID (it can be different in your case).

Step 3 Let’s use this short container ID to show the container standard output:

$ docker logs <short-id>
Thu Jan 26 00:23:45 UTC 2019
hello world
Thu Jan 26 00:23:46 UTC 2019
hello world
Thu Jan 26 00:23:47 UTC 2019
hello world
...

As we see, in the docker ps command output, the auto generated container name is evil_golick (your container can have a different name).

Step 4 Use your container name to to show the container standard output. Add options to follow the output:

$ docker logs -f <name>
Thu Jan 26 00:23:51 UTC 2019
hello world
Thu Jan 26 00:23:52 UTC 2019
hello world
Thu Jan 26 00:23:53 UTC 2019
hello world
...
^C

Use ^C to exit the command.

Step 5 Finally, let’s stop our container, adding the option to wait zero seconds before terminating the container:

$ docker stop -t 0 <name>
<name>

Step 6 Check, that there are no running containers:

$ docker ps
CONTAINER ID IMAGE       COMMAND  CREATED        STATUS                     PORTS NAMES

2.7. Expose containers ports

Step 1 Let’s run a simple web application. We will use the existing image training/webapp, which contains a Python Flask application. We also want to specify the container name (--name argument):

$ docker run -d -P --name hello-world-app training/webapp python app.py
...
Status: Downloaded newer image for training/webapp:latest
6e88f42d3d853762edcbfe1fe73fdc5c48865275bc6df759b83b0939d5bd2456

In the command above we specified the main process (python app.py), the -d command line argument, which tells Docker to run the container in the background. The -P command line argument tells Docker to map any required network ports inside our container to our host. This allows us to access the web application in the container.

Step 2 Use the docker ps command to list running containers:

$ docker ps
CONTAINER ID IMAGE           COMMAND         CREATED       STATUS       PORTS                   NAMES
6e88f42d3d85 training/webapp "python app.py" 3 minutes ago Up 3 minutes 0.0.0.0:32768->5000/tcp hello-world-app

The PORTS column contains the mapped ports. In our case, Docker has exposed port 5000 (the default Python Flask port) on port 32768 (can be different in your case).

Step 3 The docker port command shows the exposed port. We will use the container name (hello-world-app in the example above, it can be different in your case):

$ docker port hello-world-app 5000
0.0.0.0:32768

Step 4 Let’s check that we can access the web application exposed port:

$ curl http://localhost:<port>/
Hello world!

Step 5 Docker achieves this by adding a Destination NAT (DNAT) rule to the host iptables rules. You can see the rule:

$ sudo iptables -t nat -L -n | grep DNAT
DNAT       tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:32768 to:172.17.0.2:5000

Step 6 Let’s stop our web application for now:

$ docker stop -t 0 hello-world-app

Step 7 We want to manually specify the local port to expose (-p argument). Let’s use the standard HTTP port 80:

$ docker run -d -p 80:5000 --name webapp training/webapp python app.py
249476631f7d445c6a61a6067c4c24461b0e51ea0667c86cb1bce301981ecd22

Step 8 Let’s check that the port 80 is exposed:

$ sudo iptables -t nat -L -n | grep DNAT
DNAT       tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:80 to:172.17.0.2:5000

Step 9 Access your application from a browser using your host public ip address:

$ echo $PublicIP
<public ip address of your host>

Copy and paste to your web-browser http://<public ip address of your host>

2.8. Inspect a container

Step 1 You can use the docker inspect command to see the configuration and status information for the specified container in json output format:

$ docker inspect webapp
[
    {
        "Id": "249476631f7d...",
        "Created": "2019-06-11T23:42:56.932135327Z",
        "Path": "python",
        "Args": [
            "app.py"
        ],
        "State": {
            "Status": "running",
            "Running": true,
            "Paused": false,
            "Restarting": false,
            "OOMKilled": false,
            "Dead": false,
            "Pid": 16055,
            "ExitCode": 0,
            "Error": "",
            ...

Step 2 You can specify a filter (-f command line argument) to show only specific elements values. For example:

$ docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' webapp
172.17.0.2

This command returned the IP address of the container.

Step 3 To filter and display composite subsets in json format use the json verb. Use jq to pretty-print the json output:

$ docker inspect -f '{{json .NetworkSettings.Networks}}' webapp | jq
{
  "bridge": {
    "IPAMConfig": null,
    "Links": null,
    "Aliases": null,
    "NetworkID": "8b6af9d3a573b7e3e1ac63d6171cb76575fc9c744c4ee1985e68b7a58adccc2b",
    "EndpointID": "56348f7c1e14b55b9ebf4c24660827282c34bf8eb9cdfaea12c67d1c84dc5575",
    "Gateway": "172.17.0.1",
    "IPAddress": "172.17.0.2",
    "IPPrefixLen": 16,
    "IPv6Gateway": "",
    "GlobalIPv6Address": "",
    "GlobalIPv6PrefixLen": 0,
    "MacAddress": "02:42:ac:11:00:02",
    "DriverOpts": null
  }
}

2.9. Execute a process in a container

Step 1 Use the docker exec command to execute a command in the running container. For example:

$ docker exec webapp ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.2  0.0  52320 17384 ?        Ss   00:11   0:00 python app.py
root        26  0.0  0.0  15572  2104 ?        Rs   00:12   0:00 ps aux

The same command with the -it command line argument can be used to run an interactive session in the container:

$ docker exec -it webapp bash
root@249476631f7d:/opt/webapp# ps auxw
ps auxw
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.0  52320 17384 ?        Ss   00:11   0:00 python app.py
root        32  0.0  0.0  18144  3064 ?        Ss   00:14   0:00 bash
root        47  0.0  0.0  15572  2076 ?        R+   00:16   0:00 ps auxw

Step 2 Use the exit command or press Ctrl-D to exit the interactive bash session:

root@249476631f7d:/opt/webapp# exit

2.10. Restart a container

Step 1 Let’s stop the container with web application:

$ docker stop webapp

The main process inside the container will receive SIGTERM, and after a grace period, SIGKILL.

Step 2 You can start the container later using the docker start command:

$ docker start webapp

Step 3 Check that the web application works:

$ curl http://localhost/
Hello world!

2.11. Copy files to/from container

The docker cp command allows you to copy files from the container to the local machine or from the local file system to the container. This command works for a running or stopped container.

Step 1 Let’s copy the container’s app.py file to the local machine:

$ docker cp webapp:/opt/webapp/app.py .

Step 2 Edit the local app.py file. For example, change the line return 'Hello '+provider+'!' to return 'Hello '+provider+'!!!\n'. Copy the modified file back and restart the container:

$ docker cp app.py webapp:/opt/webapp/

Step 3 You also can restart the running container using the docker restart command. The -t command line argument specifies a number of seconds to wait for stop before killing the container, overriding the default value of 10:

$ docker restart -t 0 webapp

Step 4 Check that the modified web application works::

$ curl http://localhost/
Hello world!!!

2.12. Connect a container to a network

Step 1 By default, Docker runs containers in the legacy bridge network, which does not support service discovery. However, a container can be manually linked to other containers to enable discovery by name. Start a database container and link it to the webapp container:

$ docker run -d --name db --link webapp training/postgres

Step 2 Check that the webapp and db containers are running:

$ docker ps
CONTAINER ID IMAGE             COMMAND                CREATED        STATUS        PORTS                NAMES
...
c3afff20019a training/postgres "su postgres -c '/usr" 4 minutes ago  Up 4 minutes  5432/tcp             db
62ed4a627356 training/webapp   "python app.py"        30 minutes ago Up 30 minutes 0.0.0.0:80->5000/tcp webapp

Step 3 The docker network ls command shows Docker networks:

$ docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
d428e49e4869        bridge              bridge              local
0d1f78528cc5        host                host                local
4a07cef84617        none                null                local

Step 4 You can inspect your containers to see the networks they are connected to:

$ docker inspect -f '{{json .NetworkSettings.Networks}}' webapp | jq
{
  "bridge": {
    "IPAMConfig": null,
    "Links": null,
    "Aliases": null,
    "NetworkID": "8b6af9d3a573b7e3e1ac63d6171cb76575fc9c744c4ee1985e68b7a58adccc2b",
    "EndpointID": "ddd88e3ac5164e02b716231c2b29a930d6bedcf355e3002e9aec359d26e40861",
    "Gateway": "172.17.0.1",
    "IPAddress": "172.17.0.2",
    "IPPrefixLen": 16,
    "IPv6Gateway": "",
    "GlobalIPv6Address": "",
    "GlobalIPv6PrefixLen": 0,
    "MacAddress": "02:42:ac:11:00:02",
    "DriverOpts": null
  }
}

$ docker inspect -f '{{json .NetworkSettings.Networks}}' db | jq
{
  "bridge": {
    "IPAMConfig": null,
    "Links": null,
    "Aliases": null,
    "NetworkID": "8b6af9d3a573b7e3e1ac63d6171cb76575fc9c744c4ee1985e68b7a58adccc2b",
    "EndpointID": "ce842a346f4c682fb95aec42b2f2bbea157c6466c92240c68b9cd5cdaf1f60ec",
    "Gateway": "172.17.0.1",
    "IPAddress": "172.17.0.3",
    "IPPrefixLen": 16,
    "IPv6Gateway": "",
    "GlobalIPv6Address": "",
    "GlobalIPv6PrefixLen": 0,
    "MacAddress": "02:42:ac:11:00:03",
    "DriverOpts": null
  }
}

Step 5 Get IP addresses of the webapp and db containers by inspecting the legacy bridge network containers:

$ docker network inspect -f '{{json .Containers}}'  bridge | jq
{
  "0b423ca8e4a6920493b62f8227f75b1bde9e7183ce36e1b9232594fa01ae1701": {
    "Name": "webapp",
    "EndpointID": "56348f7c1e14b55b9ebf4c24660827282c34bf8eb9cdfaea12c67d1c84dc5575",
    "MacAddress": "02:42:ac:11:00:02",
    "IPv4Address": "172.17.0.2/16",
    "IPv6Address": ""
  },
  "49dfb55e294793fa6a7b6737d9f1771637fb2947c76b1d9b277f5f84301b4fc8": {
    "Name": "db",
    "EndpointID": "c5dfc42497d8fc09676e58e9e1611daf77c6ae7758a98edb2e5fc5551121d075",
    "MacAddress": "02:42:ac:11:00:03",
    "IPv4Address": "172.17.0.3/16",
    "IPv6Address": ""
  }
}

Step 6 In your case, the IP addresses can be different. Let’s ping webapp from the db container using the container name:

$ docker exec db ping -c 1 webapp
PING webapp (172.17.0.2) 56(84) bytes of data.
64 bytes from webapp (172.17.0.2): icmp_seq=1 ttl=64 time=0.055 ms

--- webapp ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.055/0.055/0.055/0.000 ms

This is possible becase we --link webapp when we started the db container in step 1. Check how Docker makes discovery by name work:

$ docker exec db cat /etc/hosts
127.0.0.1       localhost
::1     localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.17.0.2      webapp 0b423ca8e4a6
172.17.0.3      cf2e5d7595c8

Note that you can ping the db container from the webapp continer by IP address only. You can check what other containers a container is linked with using:

$ docker inspect -f "{{ .HostConfig.Links }}" db
[/webapp:/db/webapp]

In addition to modifying the /etc/hosts file Docker also injects several environment variables for each container that is being linked to:

$ docker exec db env | grep WEBAPP
WEBAPP_PORT=tcp://172.17.0.2:5000
WEBAPP_PORT_5000_TCP=tcp://172.17.0.2:5000
WEBAPP_PORT_5000_TCP_ADDR=172.17.0.2
WEBAPP_PORT_5000_TCP_PORT=5000
WEBAPP_PORT_5000_TCP_PROTO=tcp
WEBAPP_NAME=/db/webapp

Step 7 Docker relies on Linux Bridge virtual switch devices to implement container connectivity:

_images/docker-legacy-bridge-network.jpg

You can show the bridge status using the Linux Bridge CLI brctl:

$ brctl show
bridge name     bridge id               STP enabled     interfaces
docker0         8000.0242539faa49       no              veth5c072f5
                                                        vethaf6bbca

Trace the two ends of one of the veth-pairs to see how the container is connected to the bridge:

$ ip a show dev veth5c072f5
21: veth5c072f5@if20: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
    link/ether 82:cc:d2:66:33:7f brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::80cc:d2ff:fe66:337f/64 scope link
       valid_lft forever preferred_lft forever

$ docker exec webapp ip a show dev eth0
20: eth0@if21: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever

You can see that interface 21 on the host and interface 20 in the webapp container are the two ends of the same veth-pair.

2.13. Create a user defined networks

Step 1 By default, Docker runs containers in the bridge network. You may want to isolate one or more containers in a separate network. Let’s create a new network:

$ docker network create my-network -d bridge --subnet 172.19.0.0/16

The -d bridge command line argument specifies the bridge network driver and the --subnet command line argument specifies the network segment in CIDR format. If you do not specify a subnet when creating a network, then Docker assigns a subnet automatically, so it is a good idea to specify a subnet to avoid potential conflicts with any existing networks.

Step 2 To check that the new network is created, execute docker network ls:

$ docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
d428e49e4869        bridge              bridge              local
0d1f78528cc5        host                host                local
56ef0481820d        my-network          bridge              local
4a07cef84617        none                null                local

Step 3 Let’s inspect the new network:

$ docker network inspect my-network
[
    {
        "Name": "my-network",
        "Id": "56ef0481820d...",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.19.0.0/16"
                }
            ]
        },
        "Internal": false,
        "Containers": {},
        "Options": {},
        "Labels": {}
    }
]

Step 4 As expected, there are no containers connected to the my-network. Let’s recreate the db container in the my-network:

$ docker rm -f db

$ docker run -d --network=my-network --name db training/postgres

Step 5 Inspect the containers in my-network:

$ docker network inspect -f '{{json .Containers}}' my-network | jq
{
  "7aa573cf38e8d222965631a3fa91932e4f29373de50b42266c3aff701522cfc2": {
    "Name": "db",
    "EndpointID": "ce549fc3b95427997c9b14f91a09665e14c6a36d704e0a68ce015161473ecc96",
    "MacAddress": "02:42:ac:13:00:02",
    "IPv4Address": "172.19.0.2/16",
    "IPv6Address": ""
  }
}

As you see, the db container is connected to the my-network and has 172.19.0.2 address.

Let’s confirm that a bridge network is just another Linux Bridge virtual switch device:

$ brctl show
bridge name             bridge id               STP enabled     interfaces
br-ba62f7084206         8000.024237df3c6f       no              veth3582635
docker0                 8000.0242539faa49       no              veth5c072f5

Step 6 Let’s ping the IP address of the webapp again:

$ docker exec -it db bash
root@c3afff20019a:/# ping -c 1 172.17.0.2
PING 172.17.0.3 (172.17.0.3) 56(84) bytes of data.

--- 172.17.0.3 ping statistics ---
1 packets transmitted, 0 received, 100% packet loss, time 0ms

As expected, the webapp container is no longer accessible from the db container, because they are connected to different networks.

Step 7 Let’s connect the webapp container to the my-network:

$ docker network connect my-network webapp

Check that the webapp container now is connected to the my-network:

$ docker network inspect -f '{{json .Containers}}' my-network | jq
{
  "0b423ca8e4a6920493b62f8227f75b1bde9e7183ce36e1b9232594fa01ae1701": {
    "Name": "webapp",
    "EndpointID": "a624841cc2dd4ab98b743039ba9355f63c5ab2bdd0b88412eb7f79fa58265e1f",
    "MacAddress": "02:42:ac:13:00:03",
    "IPv4Address": "172.19.0.3/16",
    "IPv6Address": ""
  },
  "7aa573cf38e8d222965631a3fa91932e4f29373de50b42266c3aff701522cfc2": {
    "Name": "db",
    "EndpointID": "ce549fc3b95427997c9b14f91a09665e14c6a36d704e0a68ce015161473ecc96",
    "MacAddress": "02:42:ac:13:00:02",
    "IPv4Address": "172.19.0.2/16",
    "IPv6Address": ""
  }
}

The output shows that two containers are connected to the my-network and the webapp container has 172.19.0.3 address in that network.

Step 8 Check that the webapp container is accessible from the db container using its new IP address:

$ docker exec -it db ping -c 1 webapp
PING webapp (172.19.0.3) 56(84) bytes of data.
64 bytes from webapp.my-network (172.19.0.3): icmp_seq=1 ttl=64 time=0.073 ms

--- webapp ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.073/0.073/0.073/0.000 ms

Note that we did not use --link webapp yet we can ping by container name. That’s because user defined network implement service discovery using Docker’s built-in DNS server. Check that webapp can also ping db by name:

$ docker exec -it webapp ping -c 1 db
PING db (172.19.0.2) 56(84) bytes of data.
64 bytes from db.my-network (172.19.0.2): icmp_seq=1 ttl=64 time=0.047 ms

--- db ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.047/0.047/0.047/0.000 ms

Step 9 Note that the webapp container is now connected to multiple networks:

$ docker inspect -f '{{json .NetworkSettings.Networks}}' webapp | jq
{
  "bridge": {
    "IPAMConfig": null,
    "Links": null,
    "Aliases": null,
    "NetworkID": "8b6af9d3a573b7e3e1ac63d6171cb76575fc9c744c4ee1985e68b7a58adccc2b",
    "EndpointID": "56348f7c1e14b55b9ebf4c24660827282c34bf8eb9cdfaea12c67d1c84dc5575",
    "Gateway": "172.17.0.1",
    "IPAddress": "172.17.0.2",
    "IPPrefixLen": 16,
    "IPv6Gateway": "",
    "GlobalIPv6Address": "",
    "GlobalIPv6PrefixLen": 0,
    "MacAddress": "02:42:ac:11:00:02",
    "DriverOpts": null
  },
  "my-network": {
    "IPAMConfig": {},
    "Links": null,
    "Aliases": [
      "0b423ca8e4a6"
    ],
    "NetworkID": "ba62f708420615f6d5e91d01d07b9021ed71fd20299134d843e589bdfead3b0b",
    "EndpointID": "a624841cc2dd4ab98b743039ba9355f63c5ab2bdd0b88412eb7f79fa58265e1f",
    "Gateway": "172.19.0.1",
    "IPAddress": "172.19.0.3",
    "IPPrefixLen": 16,
    "IPv6Gateway": "",
    "GlobalIPv6Address": "",
    "GlobalIPv6PrefixLen": 0,
    "MacAddress": "02:42:ac:13:00:03",
    "DriverOpts": null
  }
}

And has multiple ip addresses:

$ docker exec webapp ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
20: eth0@if21: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever
29: eth1@if30: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:ac:13:00:03 brd ff:ff:ff:ff:ff:ff
    inet 172.19.0.3/16 brd 172.19.255.255 scope global eth1
       valid_lft forever preferred_lft forever

Step 10 You can remove the existing containers. You should stop the container before removing it. Alternatively you can use the -f command line argument:

$ docker rm -f webapp
$ docker rm -f db

2.14. Use a Data Volume

Step 1 Add a data volume to a container:

$ docker run -d -P --name webapp -v /webapp training/webapp python app.py

This command started a new container and created a new volume inside the container at /webapp.

Step 2 Locate the volume on the host using the docker inspect command:

$ docker inspect -f '{{json .Mounts}}' webapp | jq
[
  {
    "Type": "volume",
    "Name": "dce199cf635cc7be49b3e5444f2286687a460849bcceb05a65103e0abe10d53d",
    "Source": "/var/lib/docker/volumes/dce199cf635cc7be49b3e5444f2286687a460849bcceb05a65103e0abe10d53d/_data",
    "Destination": "/webapp",
    "Driver": "local",
    "Mode": "",
    "RW": true,
    "Propagation": ""
  }
]

Alternatively, you can specify a host directory you want to use as a data volume:

$ mkdir db
$ docker run -d --name db -v /home/stack/db:/db training/postgres

Step 3 Create a new file in the /db directory of the db container:

$ docker exec db touch /db/hello_from_db_container

Step 4 Check that the local db directory contains the new file:

$ ls -l db
total 0
-rw-r--r-- 1 root root 0 Nov 16 00:38 hello_from_db_container

Step 5 Check that the data volume is persistent. Remove the db container:

$ docker rm -f db

Step 6 Create the db container again:

$ docker run -d --name db -v /home/stack/db:/db training/postgres

Step 7 Check that its /db directory contains the hello_from_db_container file:

$ docker exec db ls /db
hello_from_db_container

2.15. Use a Data Volume Container

Step 1 Let’s create a new named container with a volume to share:

$ docker create -v /dbdata -v /appconfig --name dbstore training/postgres /bin/true

This container does not run an application and reuses the training/postgres image.

Step 2 Use the --volumes-from flag to mount the /dbdata volume in another containers:

$ docker run -d --volumes-from dbstore --name db1 training/postgres
$ docker run -d --volumes-from dbstore --name db2 training/postgres

Step 3 Check all three containers to verify the mounted directories are the same:

$ docker inspect dbstore db1 db2 | \
  jq '.[] | {Name: .Name, Volumes: [.Mounts[] | {Source: .Source, Destination: .Destination}]}'

Step 4 Create some content on db1 container mountpoint:

$ docker exec db1 touch /dbdata/hello_from_db1

Step 5 Observe the content on db2 container mountpoint:

$ docker exec db2 ls /dbdata

A common software design pattern is to utilize one container to write data to a volume and for another closely-working container to retrieve and process the data from the same shared volume.

Step 6 Clean up by removing all running and terminated containers before we move to the next lab:

$ docker rm -f $(docker ps -qa)

Step 7 Remove the extra network you created:

$ docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
6de50d42f1a4        bridge              bridge              local
9936d152eb18        host                host                local
324ebba7db04        my-network          bridge              local
02a5328082f2        none                null                local

$ docker network rm my-network
my-network

Checkpoint

  • Install Docker
  • List running containers
  • Specify a container main process
  • Run an interactive container
  • Run a container in a background
  • Use a container ID and container name to display container standard output
  • Expose container ports
  • Execute a process in a container
  • Copy files to/from container
  • Connect a container to a network
  • Use a data volume
  • Use a data volume container
  • Use shared volume between containers
  • Stop a container
  • Remove all containers