Disclaimer: This post has been translated to English using a machine translation model. Please, let me know if you find any mistakes.
📚 **This entry is part of the _Docker Guide_ series**, divided into two chapters that are read in order:
> * 👉 **Part 1: Containers, images, and applications**
* Part 2: Docker Compose and advanced topics
Containers
Hello world
Run the first Hello world type container with the command docker run hello-world
InputPython!docker run hello-worldCopied
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world85e32844: Pull complete 457kB/2.457kBBDigest: sha256:dcba6daec718f547568c562956fa47e1b03673dd010fe6ee58ca806767031d1cStatus: Downloaded newer image for hello-world:latestHello 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.(amd64)3. The Docker daemon created a new container from that image which runs theexecutable that produces the output you are currently reading.4. The Docker daemon streamed that output to the Docker client, which sent itto your terminal.To try something more ambitious, you can run an Ubuntu container with:$ docker run -it ubuntu bashShare images, automate workflows, and more with a free Docker ID:https://hub.docker.com/For more examples and ideas, visit:https://docs.docker.com/get-started/
Since we do not have the container saved locally, Docker downloads it from Docker Hub. If we run the container again now, the first message will no longer appear, the one indicating that it is being downloaded.
InputPython!docker run hello-worldCopied
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.(amd64)3. The Docker daemon created a new container from that image which runs theexecutable that produces the output you are currently reading.4. The Docker daemon streamed that output to the Docker client, which sent itto your terminal.To try something more ambitious, you can run an Ubuntu container with:$ docker run -it ubuntu bashShare images, automate workflows, and more with a free Docker ID:https://hub.docker.com/For more examples and ideas, visit:https://docs.docker.com/get-started/
To see the containers that are running, run docker ps
InputPython!docker psCopied
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
As we can see, there is no container running. However, if we run docker ps -a (all), we see that they do appear
InputPython!docker ps -aCopied
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES1efb51bbbf38 hello-world "/hello" 10 seconds ago Exited (0) 9 seconds ago strange_thompson5f5705e7603e hello-world "/hello" 15 seconds ago Exited (0) 14 seconds ago laughing_jang
We see that two containers called hello-world appear, which are the two we ran before. Therefore, each time we run the run command, Docker creates a new container; it does not execute one that already exists
If we want to have more information about one of the two containers, we can run docker inspect <id>, where <id> corresponds to the container ID shown in the previous list
InputPython!docker inspect 1efb51bbbf38Copied
[{"Id": "1efb51bbbf38917affd1b5871db8e658ebfe0b2efa5ead17545680b7866f682e","Created": "2023-09-04T03:59:17.795499354Z","Path": "/hello","Args": [],"State": {"Status": "exited","Running": false,"Paused": false,"Restarting": false,"OOMKilled": false,"Dead": false,"Pid": 0,"ExitCode": 0,"Error": "","StartedAt": "2023-09-04T03:59:18.406663026Z","FinishedAt": "2023-09-04T03:59:18.406181184Z"},"Image": "sha256:9c7a54a9a43cca047013b82af109fe963fde787f63f9e016fdc3384500c2823d","ResolvConfPath": "/var/lib/docker/containers/1efb51bbbf38917affd1b5871db8e658ebfe0b2efa5ead17545680b7866f682e/resolv.conf","HostnamePath": "/var/lib/docker/containers/1efb51bbbf38917affd1b5871db8e658ebfe0b2efa5ead17545680b7866f682e/hostname","HostsPath": "/var/lib/docker/containers/1efb51bbbf38917affd1b5871db8e658ebfe0b2efa5ead17545680b7866f682e/hosts","LogPath": "/var/lib/docker/containers/1efb51bbbf38917affd1b5871db8e658ebfe0b2efa5ead17545680b7866f682e/1efb51bbbf38917affd1b5871db8e658ebfe0b2efa5ead17545680b7866f682e-json.log","Name": "/strange_thompson",...}}}]
Since remembering IDs is complicated for us, Docker assigns names to containers to make our lives easier. Thus, in the previous list, the last column corresponds to the name that Docker has assigned to each container, so if we now run docker inspect <name> we will get the same information as with the ID
I run docker ps -a again to see the list once more
InputPython!docker ps -aCopied
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES1efb51bbbf38 hello-world "/hello" 2 minutes ago Exited (0) 2 minutes ago strange_thompson5f5705e7603e hello-world "/hello" 2 minutes ago Exited (0) 2 minutes ago laughing_jang
And now I run docker inspect <name> to see the container information
InputPython!docker inspect strange_thompsonCopied
[{"Id": "1efb51bbbf38917affd1b5871db8e658ebfe0b2efa5ead17545680b7866f682e","Created": "2023-09-04T03:59:17.795499354Z","Path": "/hello","Args": [],"State": {"Status": "exited","Running": false,"Paused": false,"Restarting": false,"OOMKilled": false,"Dead": false,"Pid": 0,"ExitCode": 0,"Error": "","StartedAt": "2023-09-04T03:59:18.406663026Z","FinishedAt": "2023-09-04T03:59:18.406181184Z"},"Image": "sha256:9c7a54a9a43cca047013b82af109fe963fde787f63f9e016fdc3384500c2823d","ResolvConfPath": "/var/lib/docker/containers/1efb51bbbf38917affd1b5871db8e658ebfe0b2efa5ead17545680b7866f682e/resolv.conf","HostnamePath": "/var/lib/docker/containers/1efb51bbbf38917affd1b5871db8e658ebfe0b2efa5ead17545680b7866f682e/hostname","HostsPath": "/var/lib/docker/containers/1efb51bbbf38917affd1b5871db8e658ebfe0b2efa5ead17545680b7866f682e/hosts","LogPath": "/var/lib/docker/containers/1efb51bbbf38917affd1b5871db8e658ebfe0b2efa5ead17545680b7866f682e/1efb51bbbf38917affd1b5871db8e658ebfe0b2efa5ead17545680b7866f682e-json.log","Name": "/strange_thompson",...}}}]
But why with docker ps do we not see any container and with docker ps -a do we? This is because docker ps only shows the containers that are running, while docker ps -a shows all containers, both those that are running and those that are stopped.
We can create a container by assigning it a name ourselves using the command docker run --name <name> hello-world
InputPython!docker run --name hello_world hello-worldCopied
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.(amd64)3. The Docker daemon created a new container from that image which runs theexecutable that produces the output you are currently reading.4. The Docker daemon streamed that output to the Docker client, which sent itto your terminal.To try something more ambitious, you can run an Ubuntu container with:$ docker run -it ubuntu bashShare images, automate workflows, and more with a free Docker ID:https://hub.docker.com/For more examples and ideas, visit:https://docs.docker.com/get-started/
This will be more convenient for us, since we will be able to control the container names ourselves
If we now want to create another container with the same name, we won't be able to, because Docker does not allow container names to be duplicated. So if we want to rename the container, we can use the command docker rename <old name> <new name>
InputPython!docker rename hello_world hello_world2Copied
We now have a lot of identical containers. So if we want to delete one, we have to use the command docker rm <id> or docker rm <name>
InputPython!docker ps -aCopied
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESf432c9c2ca21 hello-world "/hello" 9 seconds ago Exited (0) 8 seconds ago hello_world21efb51bbbf38 hello-world "/hello" 4 minutes ago Exited (0) 4 minutes ago strange_thompson5f5705e7603e hello-world "/hello" 4 minutes ago Exited (0) 4 minutes ago laughing_jang
InputPython!docker rm hello_world2Copied
hello_world2
If we look at the list of containers again, the hello_world2 container will no longer be there
InputPython!docker ps -aCopied
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES1efb51bbbf38 hello-world "/hello" 5 minutes ago Exited (0) 5 minutes ago strange_thompson5f5705e7603e hello-world "/hello" 5 minutes ago Exited (0) 5 minutes ago laughing_jang
If we want to delete all the containers, we can do it one by one, but since that is very cumbersome, we can delete them all using the docker container prune command. This command removes only the containers that are stopped
InputPython!docker container pruneCopied
WARNING! This will remove all stopped containers.Are you sure you want to continue? [y/N] y
Docker asks if you’re sure, and if you say yes, it deletes them all. If you list all the containers now, none appears
InputPython!docker ps -aCopied
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
The interactive mode
We are going to run Ubuntu using the command docker run ubuntu
InputPython!docker run ubuntuCopied
Unable to find image 'ubuntu:latest' locallylatest: Pulling from library/ubuntuDigest: sha256:20fa2d7bb4de7723f542be5923b06c4d704370f0390e4ae9e1c833c8785644c1[1AStatus: Downloaded newer image for ubuntu:latest
As we can see, it has now taken longer to download. If we list the containers using the docker ps command, we see that the container we just created does not appear, that is, it is not running.
InputPython!docker psCopied
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
We now list all containers
InputPython!docker ps -aCopied
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESda16b3a85178 ubuntu "bash" 4 seconds ago Exited (0) 3 seconds ago hardcore_kare
We see that the container status is Exited (0)
If we look at the container command, it shows bash and along with the Exited (0) status it indicates that Ubuntu has started, has executed its *bash*, has finished execution, and has returned a 0. This happens because Ubuntu's Bash has not been told to do anything. To solve this, now we are going to run the container using the command docker run -it ubuntu, with it what we are indicating is that we want to run it in interactive mode
InputPython!docker run -it ubuntuCopied
root@5b633e9d838f:/#
Now we can see that we are inside the Ubuntu bash shell. If we run the command cat /etc/lsb-release we can see the Ubuntu distribution
InputPython!root@5b633e9d838f:/# cat /etc/lsb-releaseCopied
DISTRIB_ID=UbuntuDISTRIB_RELEASE=22.04DISTRIB_CODENAME=jammyDISTRIB_DESCRIPTION="Ubuntu 22.04.1 LTS"
If we open another terminal and see the list of containers, the container running Ubuntu will now appear.
InputPython!docker psCopied
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES5b633e9d838f ubuntu "bash" 3 minutes ago Up 3 minutes funny_mirzakhani
We see the container with Ubuntu and in its status we can see UP
If we now look at the list of all containers, we will see that the two containers with Ubuntu appear, the first one stopped and the second one running
InputPython!docker ps -aCopied
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES5b633e9d838f ubuntu "bash" 3 minutes ago Up 3 minutes funny_mirzakhanida16b3a85178 ubuntu "bash" 3 minutes ago Exited (0) 3 minutes ago hardcore_kare
If we return to the terminal where we had Ubuntu running inside Docker, if we type exit we will exit Ubuntu.
InputPython!root@5b633e9d838f:/# exitCopied
exit
If we run docker ps, the container no longer appears
InputPython!docker psCopied
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
But if I run docker ps -a it does appear. This means that the container stopped
InputPython!docker ps -aCopied
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES5b633e9d838f ubuntu "bash" 4 minutes ago Exited (0) 27 seconds ago funny_mirzakhanida16b3a85178 ubuntu "bash" 4 minutes ago Exited (0) 4 minutes ago hardcore_kare
This happens because when we type exit, we are actually typing it in the Ubuntu bash console, which means we are terminating the Ubuntu bash process.
Container lifecycle
In Docker, when the main process of a container ends, the container stops. Several processes can run inside a container, but the container only stops when the main process ends.
Therefore, if we want to run a container that does not stop when a process finishes, we must ensure that its main process does not end. In this case, do not let bash finish.
If we want to run a container with Ubuntu, but have it not exit when the Bash process ends, we can do it as follows
InputPython!docker run --name alwaysup -d ubuntu tail -f /dev/nullCopied
ce4d60427dcd4b326d15aa832b816c209761d6b4e067a016bb75bf9366c37054
What we do is first give it the name alwaysup, secondly pass the -d option (detach) so that the container runs in the background and finally we tell it the main process we want to run in the container, which in this case is tail -f /dev/null which is equivalent to a nop command
This will return the container ID to us, but we will not be inside Ubuntu as before
If we now look at the list of running containers, the container we just created appears.
InputPython!docker psCopied
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESce4d60427dcd ubuntu "tail -f /dev/null" 18 seconds ago Up 17 seconds alwaysup
As we already have a container running all the time, we can connect to it using the exec command. We tell it the name or ID of the container and pass the process we want to be executed. We also pass the -it option to indicate that it should be interactive
InputPython!docker exec -it alwaysup bashCopied
root@ce4d60427dcd:/#
Now we are back inside Ubuntu. If we run the command ps -aux we can see a list of the processes that are running inside Ubuntu.
InputPython!ps -auxCopied
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMANDroot 1 0.0 0.0 2820 1048 ? Ss 13:04 0:00 tail -f /dev/nullroot 7 0.0 0.0 4628 3796 pts/0 Ss 13:04 0:00 bashroot 15 0.0 0.0 7060 1556 pts/0 R+ 13:05 0:00 ps -aux
We only see three processes, the ps -aux, the bash and the tail -f /dev/null
This container will remain on as long as the tail -f /dev/null process keeps running
If we exit the container with the command exit and run the command docker ps we can see that the container is still running
InputPython!exitCopied
exit
InputPython!docker psCopied
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESce4d60427dcd ubuntu "tail -f /dev/null" 2 minutes ago Up 2 minutes alwaysup
To be able to finish the process and shut down the container, we must use the command docker stop <name>
InputPython!docker stop alwaysupCopied
alwaysup
If we now list the running containers again, the container with Ubuntu no longer appears
InputPython!docker psCopied
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
And if we list all the containers, the container with Ubuntu appears, and its status Exited
InputPython!docker ps -aCopied
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESce4d60427dcd ubuntu "tail -f /dev/null" 14 minutes ago Exited (137) About a minute ago alwaysup5b633e9d838f ubuntu "bash" 19 minutes ago Exited (0) 15 minutes ago funny_mirzakhanida16b3a85178 ubuntu "bash" 20 minutes ago Exited (0) 20 minutes ago hardcore_kare
We can also pause a container using the command docker pause <name>
InputPython!docker run --name alwaysup -d ubuntu tail -f /dev/nullCopied
8282eaf9dc3604fa94df206b2062287409cc92cbcd203f1a018742b5c171c9e4
Now we pause it
InputPython!docker pause alwaysupCopied
alwaysup
If we look at all the containers again, we can see that the container with Ubuntu is paused
InputPython!docker ps -aCopied
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES8282eaf9dc36 ubuntu "tail -f /dev/null" 41 seconds ago Up 41 seconds (Paused) alwaysup5b633e9d838f ubuntu "bash" 19 minutes ago Exited (0) 15 minutes ago funny_mirzakhanida16b3a85178 ubuntu "bash" 20 minutes ago Exited (0) 20 minutes ago hardcore_kare
Single-use containers
If, when running a container, we use the --rm option, that container will be deleted when it finishes executing.
InputPython!docker run --rm --name autoremove ubuntu:latestCopied
If we now see which containers we have
InputPython!docker ps -aCopied
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
We see that the container we just created is not there
Expose containers to the outside world
We are going to create a new container with a server
InputPython!docker run -d --name proxy nginxCopied
Unable to find image 'nginx:latest' locallylatest: Pulling from library/nginxf1ad4ce1: Pulling fs layerb079d0f8: Pulling fs layer5fbbebc6: Pulling fs layerffdd25f4: Pulling fs layer32c8fba2: Pulling fs layer24b8ba39: Pull complete 393kB/1.393kBB[5ADigest: sha256:2888a97f7c7d498bbcc47ede1ad0f6ced07d72dfd181071dde051863f1f79d7bStatus: Downloaded newer image for nginx:latest1a530e04f14be082811b72ea8b6ea5a95dad3037301ee8a1351a0108ff8d3b30
This creates a server; let's list the containers that are running again.
InputPython!docker psCopied
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES1a530e04f14b nginx "/docker-entrypoint.…" 1 second ago Up Less than a second 80/tcp proxy
Now a new column appears with the port, and it tells us that the server we just created is on port 80 under the tcp protocol.
If we open a browser and try to connect to the server using http://localhost:80 we are unable to connect. This is because each container has its own network interface. In other words, the server is listening on port 80 of the container, but we are trying to connect to port 80 of the host
We stop the container to relaunch it in a different way
InputPython!docker stop proxyCopied
proxy
If we list the containers, it doesn’t appear to be running
InputPython!docker psCopied
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
Let's delete it to create it again
InputPython!docker rm proxyCopied
proxy
If we list all the containers, they are no longer there
InputPython!docker ps -aCopied
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESce4d60427dcd ubuntu "tail -f /dev/null" 19 minutes ago Exited (137) 5 minutes ago alwaysup5b633e9d838f ubuntu "bash" 24 minutes ago Exited (0) 20 minutes ago funny_mirzakhanida16b3a85178 ubuntu "bash" 24 minutes ago Exited (0) 24 minutes ago hardcore_kare
To recreate the container with the server and be able to view it from the host, we need to use the -p option (publish), indicating first the port on which we want to view it on the host and then the container port, that is, -p <host ip>:<container ip>
InputPython!docker run -d --name proxy -p 8080:80 nginxCopied
c199235e42f76a30266f6e1af972e0a59811806eb3d3a9afdd873f6fa1785eae
We list the containers
InputPython!docker psCopied
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESc199235e42f7 nginx "/docker-entrypoint.…" 22 seconds ago Up 21 seconds 0.0.0.0:8080->80/tcp, :::8080->80/tcp proxy
We see that the container port is 0.0.0.0:8080->80/tcp. If we now open a browser and enter 0.0.0.0:8080, we will be able to access the container’s server
When listing the containers, in the PORTS column it shows 0.0.0.0:8080->80/tcp, which helps us see the port mapping.
To view the container logs, using the command docker logs <name> I can see the container records
InputPython!docker logs proxyCopied
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d//docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh/docker-entrypoint.sh: Configuration complete; ready for start up2022/09/13 13:24:06 [notice] 1#1: using the "epoll" event method2022/09/13 13:24:06 [notice] 1#1: nginx/1.23.12022/09/13 13:24:06 [notice] 1#1: built by gcc 10.2.1 20210110 (Debian 10.2.1-6)2022/09/13 13:24:06 [notice] 1#1: OS: Linux 5.15.0-46-generic2022/09/13 13:24:06 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 1048576:10485762022/09/13 13:24:06 [notice] 1#1: start worker processes2022/09/13 13:24:06 [notice] 1#1: start worker process 312022/09/13 13:24:06 [notice] 1#1: start worker process 322022/09/13 13:24:06 [notice] 1#1: start worker process 332022/09/13 13:24:06 [notice] 1#1: start worker process 342022/09/13 13:24:06 [notice] 1#1: start worker process 352022/09/13 13:24:06 [notice] 1#1: start worker process 362022/09/13 13:24:06 [notice] 1#1: start worker process 372022/09/13 13:24:06 [notice] 1#1: start worker process 382022/09/13 13:24:06 [notice] 1#1: start worker process 392022/09/13 13:24:06 [notice] 1#1: start worker process 402022/09/13 13:24:06 [notice] 1#1: start worker process 41...172.17.0.1 - - [13/Sep/2022:13:24:40 +0000] "GET /favicon.ico HTTP/1.1" 404 555 "http://0.0.0.0:8080/" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36" "-"172.17.0.1 - - [13/Sep/2022:13:25:00 +0000] " üâV$Zqi'×ü[ïºåÇè÷&3nSëÉìÂØÑ¾ Ç?áúaÎuã/ØRfOHì+\»±¿Òm°9 úúÀ+À/À,À0̨̩ÀÀ / 5 localhost ÿ " 400 157 "-" "-" "-"172.17.0.1 - - [13/Sep/2022:13:25:00 +0000] " ü)bCÙmñëd"ÏÄE#~LÁµk«lî[0 ÐÒ` Æ Rêq{Pòûâ¨IôtH~Ê1-| êêÀ+À/À,À0̨̩ÀÀ / 5 " 400 157 "-" "-" "-"172.17.0.1 - - [13/Sep/2022:13:26:28 +0000] "GET / HTTP/1.1" 304 0 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36" "-"
Now I can see all the requests that have been made to the server. But if I want to see the logs in real time, using docker logs -f <name> I can do it
InputPython!docker logs -f proxyCopied
Now I can see the logs in real time. To exit, enter CTRL+C
How can there come a time when there are many logs, if you only want the latest logs, using the option --tail <num> you can view the last <num> logs. If I add the option -f we will always be seeing the last <num> logs
InputPython!docker logs --tail 10 proxyCopied
2022/09/13 13:24:06 [notice] 1#1: start worker process 412022/09/13 13:24:06 [notice] 1#1: start worker process 42172.17.0.1 - - [13/Sep/2022:13:24:16 +0000] " üE¶EgóÉÊì§y#3ÜQïê$¿# ÷-,s!rê|®ß¡LZª4y³t«ÀÎ_¸çÿ'Ï êêÀ+À/À,À0̨̩ÀÀ / 5 localhost ÿ " 400 157 "-" "-" "-"172.17.0.1 - - [13/Sep/2022:13:24:16 +0000] " ü}©Dr{;z¼zÂxßæl?§àDoK'g»µ %»ýÙ?Û³TöcJ÷åÂÒ¼¢£ë½=R¼ ÊÊÀ+À/À,À0̨̩ÀÀ / 5 localhost ÿ " 400 157 "-" "-" "-"172.17.0.1 - - [13/Sep/2022:13:24:39 +0000] "GET / HTTP/1.1" 200 615 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36" "-"2022/09/13 13:24:40 [error] 34#34: *3 open() "/usr/share/nginx/html/favicon.ico" failed (2: No such file or directory), client: 172.17.0.1, server: localhost, request: "GET /favicon.ico HTTP/1.1", host: "0.0.0.0:8080", referrer: "http://0.0.0.0:8080/"172.17.0.1 - - [13/Sep/2022:13:24:40 +0000] "GET /favicon.ico HTTP/1.1" 404 555 "http://0.0.0.0:8080/" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36" "-"172.17.0.1 - - [13/Sep/2022:13:25:00 +0000] " üâV$Zqi'×ü[ïºåÇè÷&3nSëÉìÂØÑ¾ Ç?áúaÎuã/ØRfOHì+\»±¿Òm°9 úúÀ+À/À,À0̨̩ÀÀ / 5 localhost ÿ " 400 157 "-" "-" "-"172.17.0.1 - - [13/Sep/2022:13:25:00 +0000] " ü)bCÙmñëd"ÏÄE#~LÁµk«lî[0 ÐÒ` Æ Rêq{Pòûâ¨IôtH~Ê1-| êêÀ+À/À,À0̨̩ÀÀ / 5 " 400 157 "-" "-" "-"172.17.0.1 - - [13/Sep/2022:13:26:28 +0000] "GET / HTTP/1.1" 304 0 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36" "-"
If we also add the -t option, we can see the date and time of each log, so that if we have had a problem, we can know at what moment it occurred
InputPython!docker logs --tail -t 10 proxyCopied
2022-09-13T13:24:06.573362728Z 2022/09/13 13:24:06 [notice] 1#1: start worker process 412022-09-13T13:24:06.651127107Z 2022/09/13 13:24:06 [notice] 1#1: start worker process 422022-09-13T13:24:16.651160189Z 172.17.0.1 - - [13/Sep/2022:13:24:16 +0000] " üE¶EgóÉÊì§y#3ÜQïê$¿# ÷-,s!rê|®ß¡LZª4y³t«ÀÎ_¸çÿ'Ï êêÀ+À/À,À0̨̩ÀÀ / 5 localhost ÿ " 400 157 "-" "-" "-"2022-09-13T13:24:16.116817914Z 172.17.0.1 - - [13/Sep/2022:13:24:16 +0000] " ü}©Dr{;z¼zÂxßæl?§àDoK'g»µ %»ýÙ?Û³TöcJ÷åÂÒ¼¢£ë½=R¼ ÊÊÀ+À/À,À0̨̩ÀÀ / 5 localhost ÿ " 400 157 "-" "-" "-"2022-09-13T13:24:39.117398081Z 172.17.0.1 - - [13/Sep/2022:13:24:39 +0000] "GET / HTTP/1.1" 200 615 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36" "-"2022-09-13T13:24:39.117412408Z 2022/09/13 13:24:40 [error] 34#34: *3 open() "/usr/share/nginx/html/favicon.ico" failed (2: No such file or directory), client: 172.17.0.1, server: localhost, request: "GET /favicon.ico HTTP/1.1", host: "0.0.0.0:8080", referrer: "http://0.0.0.0:8080/"2022-09-13T13:24:40.117419389Z 172.17.0.1 - - [13/Sep/2022:13:24:40 +0000] "GET /favicon.ico HTTP/1.1" 404 555 "http://0.0.0.0:8080/" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36" "-"2022-09-13T13:25:00.117434249Z 172.17.0.1 - - [13/Sep/2022:13:25:00 +0000] " üâV$Zqi'×ü[ïºåÇè÷&3nSëÉìÂØÑ¾ Ç?áúaÎuã/ØRfOHì+\»±¿Òm°9 úúÀ+À/À,À0̨̩ÀÀ / 5 localhost ÿ " 400 157 "-" "-" "-"2022-09-13T13:25:00.223560881Z 172.17.0.1 - - [13/Sep/2022:13:25:00 +0000] " ü)bCÙmñëd"ÏÄE#~LÁµk«lî[0 ÐÒ` Æ Rêq{Pòûâ¨IôtH~Ê1-| êêÀ+À/À,À0̨̩ÀÀ / 5 " 400 157 "-" "-" "-"2022-09-13T13:26:25.223596738Z 172.17.0.1 - - [13/Sep/2022:13:26:28 +0000] "GET / HTTP/1.1" 304 0 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36" "-"
We stop and delete the container
InputPython!docker rm -f proxyCopied
proxy
InputPython!docker ps -aCopied
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESce4d60427dcd ubuntu "tail -f /dev/null" 26 minutes ago Exited (137) 13 minutes ago alwaysup5b633e9d838f ubuntu "bash" 31 minutes ago Exited (0) 27 minutes ago funny_mirzakhanida16b3a85178 ubuntu "bash" 32 minutes ago Exited (0) 32 minutes ago hardcore_kare
Data in Docker
Bind mounts
Let's see the containers we have stopped
InputPython!docker ps -aCopied
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESce4d60427dcd ubuntu "tail -f /dev/null" 26 minutes ago Exited (137) 13 minutes ago alwaysup5b633e9d838f ubuntu "bash" 31 minutes ago Exited (0) 28 minutes ago funny_mirzakhanida16b3a85178 ubuntu "bash" 32 minutes ago Exited (0) 32 minutes ago hardcore_kare
Let's delete the two Ubuntu instances whose main command is Bash, and we'll keep the one we left as non-operational.
InputPython!docker rm funny_mirzakhaniCopied
funny_mirzakhani
InputPython!docker rm hardcore_kareCopied
hardcore_kare
InputPython!docker ps -aCopied
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESce4d60427dcd ubuntu "tail -f /dev/null" 27 minutes ago Exited (137) 14 minutes ago alwaysup
Let's run the Ubuntu container we left behind again; this is done using the start command
InputPython!docker start alwaysupCopied
alwaysup
We go back inside the
InputPython!docker exec -it alwaysup bashCopied
root@ce4d60427dcd:/#
Inside the container, I can create a new folder called dockerfolder
InputPython!mkdir dockerfolderCopied
If we list the files, the new folder will appear
InputPython!lsCopied
bin boot dev dockerfolder etc home lib lib32 lib64 libx32 media mnt opt proc root run sbin srv sys tmp usr var
If we leave the container
InputPython!exitCopied
exit
And we delete it
InputPython!docker rm -f alwaysupCopied
alwaysup
If we list all the containers, the last one we created no longer appears
InputPython!docker ps -aCopied
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
Let's do everything again, but first let's create a folder on the host in which we will share the data with the container
InputPython!mkdir dockerHostFolderCopied
We see that there is nothing inside the folder
InputPython!ls dockerHostFolderCopied
Now we obtain our absolute path
InputPython!pwdCopied
/home/wallabot/Documentos/web/portafolio/posts
We create the container again but adding the -v option (bind mount). Next, the absolute path of the host folder and the absolute path of the folder in the container are added, -v <host path>:<container path>
InputPython!docker run -d --name alwaysup -v ~/Documentos/web/portafolio/posts/dockerHostFolder:/dockerContainerFolder ubuntu tail -f /dev/nullCopied
4ede4512c293bdcc155e9c8e874dfb4a28e5163f4d5c7ddda24ad2863f28921b
We enter the container, list the files, and the folder we had created already appears
InputPython!docker exec -it alwaysup bashCopied
root@4ede4512c293:/#
InputPythonroot@4ede4512c293:/# lsCopied
bin dev etc lib lib64 media opt root sbin sys usrboot dockerContainerFolder home lib32 libx32 mnt proc run srv tmp var
Let's go to the shared container directory, create a file, and exit the container
InputPythonroot@4ede4512c293:/# cd dockerContainerFolderCopied
InputPythonroot@4ede4512c293:/dockerContainerFolder# touch bindFile.txtCopied
InputPythonroot@4ede4512c293:/dockerContainerFolder# exitCopied
exit
Let's see what’s inside the shared folder
InputPython!ls dockerHostFolderCopied
bindFile.txt
But even more, if we delete the container, the file is still there
InputPython!docker rm -f alwaysupCopied
alwaysup
InputPython!ls dockerHostFolderCopied
bindFile.txt
If I create a container again sharing the folders, all the files will be in the container
InputPython!docker run -d --name alwaysup -v ~/Documentos/web/portafolio/posts/dockerHostFolder:/dockerContainerFolder ubuntu tail -f /dev/nullCopied
6c021d37ea29d8b23fe5cd4968baa446085ae1756682f65340288b4c851c362d
InputPython!docker exec -it alwaysup bashCopied
root@6c021d37ea29:/#
InputPython!root@6c021d37ea29:/# ls dockerContainerFolder/Copied
bindFile.txt:/#
We remove the container
InputPython!docker rm -f alwaysupCopied
alwaysup
InputPython!docker ps -aCopied
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
Volumes
Volumes were created as an evolution of bind mounts to provide more security. We can list all Docker volumes using docker volume ls
InputPython!docker volume lsCopied
DRIVER VOLUME NAME
Let's create a new volume for the Ubuntu container; to do this, we use the command docker volume create <volume name>
InputPython!docker volume create ubuntuVolumeCopied
ubuntuVolume
If we list the volumes again, the one we just created will appear
InputPython!docker volume lsCopied
DRIVER VOLUME NAMElocal ubuntuVolume
However, it does not appear as a folder in the host file system. With ls -d */ we list all folders
InputPython!ls -d */Copied
dockerHostFolder/ __pycache__/
We are going to create a container again, but now we will create it with the volume we just created using the --mount option, indicating the source volume with src=<volume name> (if the volume did not exist, Docker would create it), then the destination separated by a ,, dst=<container path>, that is, --mount src=<volume name>,dst=<container path>
InputPython!docker run -d --name alwaysup --mount src=ubuntuVolume,dst=/dockerVolumeFolder ubuntu tail -f /dev/nullCopied
42cdcddf4e46dc298a87b0570115e0b2fc900cb4c6db5eea22a61409b8cb271d
Once created, we can see the container volumes using the inspect command and filtering by '{{.Mounts}}'
$ docker inspect --format '{{.Mounts}}' alwaysup
[
{
volume ubuntuVolume /var/lib/docker/volumes/ubuntuVolume/_data /dockerVolumeFolder local z true
}
]We see that the volume is called ubuntuVolume and we can also see the path where it is stored, in this case in /var/lib/docker/volumes/ubuntuVolume/_data. We do the same as before, we enter the container, create a file in the volume path, exit, and check on the host whether it has been created
$ docker exec -it alwaysup bash
root@42cdcddf4e46:/# touch dockerVolumeFolder/volumeFile.txt
root@42cdcddf4e46:/# exit$ sudo ls /var/lib/docker/volumes/ubuntuVolume/_data
volumeFile.txtThe file has been created
Insert and extract files from a container
First, let’s create a file that we want to copy into a container
InputPython!touch dockerHostFolder/text.txtCopied
We entered the container
$ docker exec -it alwaysup bash
root@42cdcddf4e46:/#We create a new folder where we are going to copy the file and exit
root@42cdcddf4e46:/# mkdir folderToCopy
root@42cdcddf4e46:/# ls
bin boot dev dockerVolumeFolder etc folderToCopy home lib lib32 lib64 libx32 media mnt opt proc root run sbin srv sys tmp usr varroot@42cdcddf4e46:/# exit
exitWe copy the file inside the container using the cp command, indicating the file that **I want** to copy, the container where we want to copy it, and the path inside the container, docker cp <file> <container>:<container path>
InputPython!docker cp dockerHostFolder/text.txt alwaysup:/folderToCopyCopied
We re-enter the container and check that the file is there
$ docker exec -it alwaysup bash
root@42cdcddf4e46:/# ls folderToCopy/text.txtWe exit the container
/# exit
exitNow we are going to extract the file from the container and save it on the host with another name. To do this, we use the cp command again, but now indicating the container, the file path in the container, and the path and name we want the file to have on the host, docker cp <container>:<docker file path> <host file path>
InputPython!docker cp alwaysup:/folderToCopy/text.txt dockerHostFolder/fileExtract.txtCopied
We see that it is on the host
InputPython!ls dockerHostFolderCopied
bindFile.txt fileExtract.txt text.txt
Although the container is stopped, files can also be copied.
Finally, we remove the container
InputPython!docker rm -f alwaysupCopied
alwaysup
Images
Fundamental concepts
Images are the files ("templates") with all the configuration needed to create a container. Every time we create a container, it is created from an image. When we created new containers, the first time a message appeared saying that we did not have the image and that it was going to download it. On Docker Hub there are many images with all kinds of machines, but for a very specific development environment we can create our own template to pass it to someone so they can work in a container with the same configuration as ours
We can see all the images that we have saved on our computer using the command docker image ls
InputPython!docker image lsCopied
REPOSITORY TAG IMAGE ID CREATED SIZEnginx latest 2d389e545974 8 hours ago 142MBubuntu latest 2dc39ba059dc 11 days ago 77.8MBhello-world latest feb5d9fea6a5 11 months ago 13.3kB
We can see the sizes, and we can see how the nginx one takes up a lot of space, which is why it took longer to download than the rest
Another column we can see is the TAG column, this indicates the version of the image. They all show latest, which means it is the latest one. In other words, at the moment we downloaded it, we have downloaded the latest version available on Docker Hub. This is not optimal in a development environment, because we can download an Ubuntu image, and if we do not specify a version, the latest one is pulled, for example 20.04. But after some time someone may want to develop with you and download that image, but by not specifying the version, they will download the latest one again, which in their case could be 22.04. This can lead to problems and to things working for one person but not for the other
We can view all the images on Docker Hub by going to https://hub.docker.com/. There you can search for the image that best fits the project you want to build. If we navigate to the Ubuntu image, for example, we can see the versions (tags) of the images.
Let's download, **but not run**, an image. To do this we use the command docker pull <hub> <image name>:<tag>. If we do not specify the hub, it will download it from Docker Hub by default, but we can specify another one, for example a private one from our organization. Also, if we do not specify the tag, it will download the latest version by default
InputPython!docker pull ubuntu:20.04Copied
20.04: Pulling from library/ubuntuDigest: sha256:35ab2bf57814e9ff49e365efd5a5935b6915eede5c7f8581e9e1b85e0eecbe16[1AStatus: Downloaded newer image for ubuntu:20.04docker.io/library/ubuntu:20.04
If we list the images again, we see that we now have two Ubuntu images, one with the tag 20.04 and another with the tag latest
InputPython!docker image lsCopied
REPOSITORY TAG IMAGE ID CREATED SIZEnginx latest 2d389e545974 8 hours ago 142MBubuntu latest 2dc39ba059dc 11 days ago 77.8MBubuntu 20.04 a0ce5a295b63 11 days ago 72.8MBhello-world latest feb5d9fea6a5 11 months ago 13.3kB
Create images using Dockerfile
We create a directory on the host called dockerImages to work in it
InputPython!mkdir dockerImagesCopied
We create a Dockerfile with which we will create an image
InputPython!touch dockerImages/DockerfileCopied
We open the file created with our preferred editor and write the following:
FROM ubuntu:latestThis tells Docker to create the image based on the latest Ubuntu image.
Next, we write a command that will be executed at compile time
RUN touch /test.txtThis means that when the Dockerfile is built, that command will be executed, but not when the image container is run
In the end, the Dockerfile looks like this:
FROM ubuntu:latest
RUN touch /test.txtWe build the Dockerfile using the build command; with the -t option we can give it a tag. Finally, we must indicate the path of the build context; we will explain this later
InputPython!docker build -t ubuntu:test ./dockerImagesCopied
Sending build context to Docker daemon 2.048kBStep 1/2 : FROM ubuntu:latest---> 2dc39ba059dcStep 2/2 : RUN touch /test.txt---> Using cache---> a78cf3ea16d8Successfully built a78cf3ea16d8Successfully tagged ubuntu:testUse 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them
As we can see, it is compiled in 2 steps, each one has an id, each of those ids are layers of the image, we will also see this later
We look at the images we have saved on our computer again, and the one we just created appears.
InputPython!docker image lsCopied
REPOSITORY TAG IMAGE ID CREATED SIZEubuntu test a78cf3ea16d8 8 minutes ago 77.8MBnginx latest 2d389e545974 8 hours ago 142MBubuntu latest 2dc39ba059dc 11 days ago 77.8MBubuntu 20.04 a0ce5a295b63 11 days ago 72.8MBhello-world latest feb5d9fea6a5 11 months ago 13.3kB
We run the container from the image we just created
$ docker run -it ubuntu:test
root@b57b9d4eedeb:/#We enter the container's bash. As we said, the RUN command is executed at image build time, so the file we asked to be created should be in our container
root@b57b9d4eedeb:/# ls
bin boot dev etc home lib lib32 lib64 libx32 media mnt opt proc root run sbin srv sys test.txt tmp usr varIt is important to understand that that file was created when the image was built, that is, the container image already has that file. It is not created when the container is launched
We exit the container
root@b57b9d4eedeb:/# exit
exitSince we already have an image, we could upload it to Docker Hub, but let's list the images again before doing that.
InputPython!docker image lsCopied
REPOSITORY TAG IMAGE ID CREATED SIZEubuntu test a78cf3ea16d8 20 minutes ago 77.8MBnginx latest 2d389e545974 8 hours ago 142MBubuntu latest 2dc39ba059dc 11 days ago 77.8MBubuntu 20.04 a0ce5a295b63 11 days ago 72.8MBhello-world latest feb5d9fea6a5 11 months ago 13.3kB
If we look, it’s telling us that the image we just created belongs to the Ubuntu repository, but we do not have access to the Ubuntu repository, so on Docker Hub we need to create an account in order to upload the image to our repository. In my case, my repository is called maximofn, so I change the image repository using the tag command, telling it the image whose repository we want to change and the new repository. In the new repository, it is usually specified as the repository name followed by the image type and the tag; in my case maximofn/ubuntu:test
InputPython!docker tag ubuntu:test maximofn/ubuntu:testCopied
If we now list the images again
InputPython!docker image lsCopied
REPOSITORY TAG IMAGE ID CREATED SIZEubuntu test a78cf3ea16d8 24 minutes ago 77.8MBmaximofn/ubuntu test a78cf3ea16d8 24 minutes ago 77.8MBnginx latest 2d389e545974 8 hours ago 142MBubuntu latest 2dc39ba059dc 11 days ago 77.8MBubuntu 20.04 a0ce5a295b63 11 days ago 72.8MBhello-world latest feb5d9fea6a5 11 months ago 13.3kB
Now we need to log in to Docker Hub so we can push the image; to do this, we use the login command
$ docker login
Login with your Docker ID to push and pull images from Docker Hub. If you do not have a Docker ID, head over to https://hub.docker.com to create one.
Username: maximofn
Password:
Login SucceededNow we can upload the image using the push command
InputPython!docker push maximofn/ubuntu:testCopied
The push refers to repository [docker.io/maximofn/ubuntu]06994357: Preparing06994357: Pushed from library/ubuntu test: digest: sha256:318d83fc3c35ff930d695b0dc1c5ad1b0ea54e1ec6e3478b8ca85c05fd793c4e size: 735
Only the first layer has been uploaded; the second one, since I used it based on the Ubuntu image, what it does is place a pointer to that image so that I don’t have layers uploaded more than once
It should be noted that this repository is public, so you should not upload images with sensitive data. Also, if an image is not used for 6 months, it will be deleted
The layer system
Using the history command, we can see the layers of an image. If we look at the layers of the image we just created, we use docker history ubuntu:test
InputPython!docker history ubuntu:testCopied
IMAGE CREATED CREATED BY SIZE COMMENTa78cf3ea16d8 3 minutes ago /bin/sh -c touch /test.txt 0B2dc39ba059dc 12 days ago /bin/sh -c #(nop) CMD ["bash"] 0B<missing> 12 days ago /bin/sh -c #(nop) ADD file:a7268f82a86219801… 77.8MB
We see that the first layer has the command that we introduced in the Dockerfile; it also says that it was created 3 minutes ago. However, the rest of the layers were created 12 days ago, and they are the layers of the Ubuntu image we based ourselves on.
To the Dockerfile we created earlier, we add the line
RUN rm /test.txtIn the end, the Dockerfile looks like this:
FROM ubuntu:latest
RUN touch /test.txtRUN rm /test.txtIf we recompile, we’ll see what happens
InputPython!docker build -t ubuntu:test ./dockerImagesCopied
Sending build context to Docker daemon 2.048kBStep 1/3 : FROM ubuntu:latest---> 2dc39ba059dcStep 2/3 : RUN touch /test.txt---> Using cache---> a78cf3ea16d8Step 3/3 : RUN rm /test.txt---> Running in c2e6887f2025Removing intermediate container c2e6887f2025---> 313243a9b573Successfully built 313243a9b573Successfully tagged ubuntu:testUse 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them
As we can see, there is one more layer with the new line that we have added. If we look again at the image layers with history
InputPython!docker history ubuntu:testCopied
IMAGE CREATED CREATED BY SIZE COMMENT313243a9b573 About a minute ago /bin/sh -c rm /test.txt 0Ba78cf3ea16d8 3 minutes ago /bin/sh -c touch /test.txt 0B2dc39ba059dc 12 days ago /bin/sh -c #(nop) CMD ["bash"] 0B<missing> 12 days ago /bin/sh -c #(nop) ADD file:a7268f82a86219801… 77.8MB
We see that the first layers are the same as before and a new layer has been added with the new command
Docker Hub Search
There is no need to go to the Docker Hub page to look for images; it can be done from the terminal. To do this, we use the command docker search <image name>
InputPython!docker search ubuntuCopied
NAME DESCRIPTION STARS OFFICIAL AUTOMATEDubuntu Ubuntu is a Debian-based Linux operating sys… 16425 [OK]websphere-liberty WebSphere Liberty multi-architecture images … 297 [OK]open-liberty Open Liberty multi-architecture images based… 62 [OK]neurodebian NeuroDebian provides neuroscience research s… 104 [OK]ubuntu-debootstrap DEPRECATED; use "ubuntu" instead 52 [OK]ubuntu-upstart DEPRECATED, as is Upstart (find other proces… 115 [OK]ubuntu/nginx Nginx, a high-performance reverse proxy & we… 98ubuntu/squid Squid is a caching proxy for the Web. Long-t… 66ubuntu/cortex Cortex provides storage for Prometheus. Long… 4ubuntu/apache2 Apache, a secure & extensible open-source HT… 60ubuntu/kafka Apache Kafka, a distributed event streaming … 35ubuntu/mysql MySQL open source fast, stable, multi-thread… 53ubuntu/bind9 BIND 9 is a very flexible, full-featured DNS… 62ubuntu/prometheus Prometheus is a systems and service monitori… 51ubuntu/zookeeper ZooKeeper maintains configuration informatio… 12ubuntu/postgres PostgreSQL is an open source object-relation… 31ubuntu/redis Redis, an open source key-value store. Long-… 19ubuntu/grafana Grafana, a feature rich metrics dashboard & … 9ubuntu/memcached Memcached, in-memory keyvalue store for smal… 5ubuntu/dotnet-aspnet Chiselled Ubuntu runtime image for ASP.NET a… 11ubuntu/dotnet-deps Chiselled Ubuntu for self-contained .NET & A… 11ubuntu/prometheus-alertmanager Alertmanager handles client alerts from Prom… 9ubuntu/dotnet-runtime Chiselled Ubuntu runtime image for .NET apps… 10ubuntu/cassandra Cassandra, an open source NoSQL distributed … 2ubuntu/telegraf Telegraf collects, processes, aggregates & w… 4
Using Docker to create applications
Port exposure
Before, we saw how we could bind a port from a container to a port on the computer (-p 8080:80). But for that to be possible, when creating the image you have to expose the port; this is done by adding the line EXPOSE <port> to the Dockerfile, in the previous case
EXPOSE 80Or use images as a base that already have exposed ports
Reusing layer cache when compiling
When we build an image, if any of the layers we have defined have already been built before, Docker detects them and uses them; it does not rebuild them. If we build again the image we defined in the Dockerfile, it will now take very little time, because all the layers are already built and Docker does not rebuild them.
InputPython!docker build -t ubuntu:test ./dockerImagesCopied
Sending build context to Docker daemon 2.048kBStep 1/3 : FROM ubuntu:latest---> 2dc39ba059dcStep 2/3 : RUN touch /test.txt---> Using cache---> a78cf3ea16d8Step 3/3 : RUN rm /test.txt---> Using cache---> 313243a9b573Successfully built 313243a9b573Successfully tagged ubuntu:testUse 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them
In the second and third layers, the text Using cache appears
Since this is a Jupyter notebook, when running the cells it gives you information about how long they take to execute. The previous time I built the image it took 1.4 seconds, whereas now it has taken 0.5 seconds
But if I now change the Dockerfile, and in the first line, where it said that we were based on the latest version of Ubuntu, and we change it to version 20.04
FROM ubuntu:20.04In the end, the Dockerfile looks like this:
FROM ubuntu:20.04
RUN touch /test.txtRUN rm /test.txtIf we recompile it, it will take much longer
InputPython!docker build -t ubuntu:test ./dockerImagesCopied
Sending build context to Docker daemon 2.048kBStep 1/3 : FROM ubuntu:20.04---> a0ce5a295b63Step 2/3 : RUN touch /test.txt---> Running in a40fe8df2c0dRemoving intermediate container a40fe8df2c0d---> 0bb9b452c11fStep 3/3 : RUN rm /test.txt---> Running in 2e14919f3685Removing intermediate container 2e14919f3685---> fdc248fa833bSuccessfully built fdc248fa833bSuccessfully tagged ubuntu:testUse 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them
It took 1.9 seconds and the text Using cache no longer appears
When changing the first layer, Docker rebuilds all the layers. This can be a problem because, when developing code, the following situation may occur
- We develop the code on our computer
- When building the image, we copy all the code from our computer into the container
- Then we ask the image to install the necessary libraries
This can cause, when changing any part of the code, that when the image has to be rebuilt, the layer in which the libraries are installed has to be rebuilt as well, since a previous layer has changed
To solve this, the idea would be that, when creating the image, we first ask for the libraries to be installed and then for the code to be copied from our computer into the container. That way, each time we change the code and rebuild the image, only the layer where the code is copied will be rebuilt, so the build will be faster
You may think that it is better to share a folder between the host and the container (bind mount) where we will keep the code, so there is no need to rebuild the image every time we change the code. And the answer is that this is true; I only used this example because it is very easy to understand, but it is meant to illustrate that when creating images, you need to think carefully so that if it does need to be rebuilt, you rebuild the minimum number of layers.
Write a Dockerfile correctly
As we have seen, Docker does not rebuild layers of a Dockerfile if it has already built them before, so it loads them from cache. Let’s see what the correct way to write a Dockerfile is in order to take advantage of this.
We are going to start from this Dockerfile to discuss possible corrections.
FROM ubuntu
COPY ./sourceCode /sourceCode
RUN apt-get update
RUN apt-get install -y python3 sshCMD ["python3", "/sourceCode/sourceApp/app.py"]As can be seen, we start from an Ubuntu image, copy the folder with the code, update the repositories, install Python, install ssh as well, and run the application
Copy the code before execution
As we said before, if we first copy the code and then install Python, every time we make a change in the code and build the image it will rebuild everything, but if we copy the code after installing Python, every time we change the code and build the image, it will only build from the code copy and won’t reinstall Python, so the Dockerfile should now look like this
FROM ubuntu
RUN apt-get update
RUN apt-get install -y python3 sshCOPY ./sourceCode /sourceCode
CMD ["python3", "/sourceCode/sourceApp/app.py"]Copy only the necessary code
We are copying the folder with all the code, but maybe inside we have code that we do not need, so we need to copy only the code that we really need for the application. In this way, the image will take up less memory. So the Dockerfile would look like this:
FROM ubuntu
RUN apt-get update
RUN apt-get install -y python3 sshCOPY ./sourceCode/sourceApp /sourceCode/sourceApp
CMD ["python3", "/sourceCode/sourceApp/app.py"]Update repositories and install Python on the same line
We are updating the repositories in one line and installing python3 in another.
FROM ubuntu
RUN apt-get update && apt-get install -y python3 ssh
COPY ./sourceCode/sourceApp /sourceCode/sourceApp
CMD ["python3", "/sourceCode/sourceApp/app.py"]Do not install ssh
We had installed ssh in the image so we could debug it if needed, but that makes the image use more memory. If debugging is needed, we should enter the container, install ssh, and then debug. So we removed the ssh installation
FROM ubuntu
RUN apt-get update && apt-get install -y python3
COPY ./sourceCode/sourceApp /sourceCode/sourceApp
CMD ["python3", "/sourceCode/sourceApp/app.py"]Use --no-install-recommends
When we install something on Ubuntu, it installs recommended packages, but we do not need them, so the image takes up more space. So, to avoid that, we add --no-install-recommends to the installation.
FROM ubuntu
RUN apt-get update && apt-get install -y python3 --no-install-recommends
COPY ./sourceCode/sourceApp /sourceCode/sourceApp
CMD ["python3", "/sourceCode/sourceApp/app.py"]Delete updated repositories list
We have updated the repository list and installed Python, but once that is done we no longer need the updated repository list, because all it will do is make the image larger, so we remove them after installing Python and on the same line.
FROM ubuntu
RUN apt-get update && apt-get install -y python3 --no-install-recommends && rm -rf /var/lib/apt/lists/*
COPY ./sourceCode/sourceApp /sourceCode/sourceApp
CMD ["python3", "/sourceCode/sourceApp/app.py"]Use a Python image
Everything we have done to update the package list and install Python is unnecessary, since there are already Python images based on Ubuntu, which have likely also followed best practices, may have done it even better than us, and have been scanned for vulnerabilities by Docker Hub. So we remove all that and start from a Python image
FROM python
COPY ./sourceCode/sourceApp /sourceCode/sourceAppCMD ["python3", "/sourceCode/sourceApp/app.py"]Specify the Python image
Since the Python image is not specified, the latest one is being downloaded, but depending on when you build the container, one or another version may be pulled, so you need to add the tag with the Python version you want.
FROM python:3.9.18
COPY ./sourceCode/sourceApp /sourceCode/sourceAppCMD ["python3", "/sourceCode/sourceApp/app.py"]Choose a small tag
We have chosen the tag 3.9.18, but that Python version has a lot of libraries that we may not need, so we can use the 3.9.18-slim versions, which have many fewer libraries installed, or the 3.9.18-alpine version, which is a Python version based on Alpine rather than Ubuntu. Alpine is a very lightweight Linux distribution that has very few packages installed and is commonly used a lot in Docker containers so they take up very little space
The Python 3.9.18 image takes up 997 MB, the 3.9.18-slim takes up 126 MB, and the 3.9.18-alpine takes up 47.8 MB
FROM python:3.9.18-alpine
COPY ./sourceCode/sourceApp /sourceCode/sourceApp
CMD ["python3", "/sourceCode/sourceApp/app.py"]Indicate the workspace
Instead of specifying the image path /sourceCode/sourceApp, we set that path to be the image workspace. This way, when we copy the code or run the application, there is no need to specify the path.
FROM python:3.9.18-alpine
WORKDIR /sourceCode/sourceApp
COPY ./sourceCode/sourceApp .
CMD ["python3", "app.py"]Indicate the workspace
Instead of specifying the image path /sourceCode/sourceApp, we set that path to be the image workspace. That way, when we copy the code or run the application, there is no need to specify the path.
FROM python:3.9.18-alpine
WORKDIR /sourceCode/sourceApp
COPY ./sourceCode/sourceApp .
CMD ["python3", "app.py"]Code shared in a bind mount folder
We had created a folder called dockerHostFolder in which we had shared files between the host and a container. Inside, there should also be three files
InputPython!ls dockerHostFolderCopied
bindFile.txt fileExtract.txt text.txt
Let's use the text.txt file to see that. Let's see what's inside text.txt
InputPython!cat dockerHostFolder/text.txtCopied
There is no output, the file is empty. Let's create a new Ubuntu container sharing the dockerHostFolder directory
InputPython!docker run --name alwaysup -d -v ~/Documentos/web/portafolio/posts/dockerHostFolder:/dockerContainerFolder ubuntu tail -f /dev/nullCopied
24adbded61f507cdf7f192eb5e246e43ee3ffafc9944b7c57918eb2d547dff19
We can see that the container is running
InputPython!docker psCopied
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES24adbded61f5 ubuntu "tail -f /dev/null" 16 seconds ago Up 15 seconds alwaysup
We enter the container, see that there is a text.txt file, and that it is empty
$ docker exec -it alwaysup bash
root@24adbded61f5:/# ls dockerContainerFolder/
bindFile.txt fileExtract.txt text.txt
root@24adbded61f5:/# cat dockerContainerFolder/text.txt
root@24adbded61f5:/#Now we open the text.txt file on the host with the text editor we want, write Hola mundo, and save it. If we now look at what is inside the file in the container, we will see the same text
root@24adbded61f5:/# cat dockerContainerFolder/text.txt
Hello worldNow we edit the file in the container and exit the container
root@24adbded61f5:/# echo hola contenedor > dockerContainerFolder/text.txt
root@24adbded61f5:/# cat dockerContainerFolder/text.txt
hello container
root@24adbded61f5:/# exit
exitIf we look at the file on the host, we will see the text we wrote in the container
InputPython!cat dockerHostFolder/text.txtCopied
hola contenedor
We delete the container
InputPython!docker rm -f alwaysupCopied
alwaysup
Connect containers through network
In case we want to have several containers running and want them to communicate, we can make them communicate over a network. Docker gives us the possibility to do this through its virtual networks
Let's see what networks Docker has using the command docker network ls
InputPython!docker network lsCopied
NETWORK ID NAME DRIVER SCOPEde6e8b7b737e bridge bridge localda1f5f6fccc0 host host locald3b0d93993c0 none null local
We see that by default Docker has three networks
- bridge: It is there for backward compatibility with earlier versions, but we should no longer use it
- host: It is the host network
- none: This is the option we should use if we want a container to have no Internet access
We can create new networks to which other containers can connect. To do this, we use the command docker network create <name>. For other containers to be able to connect, we must also add the --attachable option.
InputPython!docker network create --attachable myNetworkCopied
2f6f3ddbfa8642e9f6819aa0965c16339e9e910be7bcf56ebb718fcac324cc27
We can inspect it using the command docker network inspect <name>
InputPython!docker network inspect myNetworkCopied
[{"Name": "myNetwork","Id": "2f6f3ddbfa8642e9f6819aa0965c16339e9e910be7bcf56ebb718fcac324cc27","Created": "2022-09-14T15:20:08.539830161+02:00","Scope": "local","Driver": "bridge","EnableIPv6": false,"IPAM": {"Driver": "default","Options": {},"Config": [{"Subnet": "172.18.0.0/16","Gateway": "172.18.0.1"}]},"Internal": false,"Attachable": true,"Ingress": false,"ConfigFrom": {"Network": ""},"ConfigOnly": false,"Containers": {},"Options": {},"Labels": {}}]
Now we need to create two containers so that they can communicate.
Let's create a new container, which we'll call container1, with a shared folder that will be called folder1 inside it
InputPython!docker run --name container1 -d -v ~/Documentos/web/portafolio/posts/dockerHostFolder:/folder1 ubuntu tail -f /dev/nullCopied
a5fca8ba1e4ff0a67002f8f1b8cc3cd43185373c2a7e295546f774059ad8dd1a
Now we create another container, called container2, with another shared folder, but named folder2
InputPython!docker run --name container2 -d -v ~/Documentos/web/portafolio/posts/dockerHostFolder:/folder2 ubuntu tail -f /dev/nullCopied
6c8dc18315488ef686f7548516c19b3d716728dd8a173cdb889ec0dd082232f9
We see the containers running and we see that both are there
InputPython!docker psCopied
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES6c8dc1831548 ubuntu "tail -f /dev/null" 3 seconds ago Up 2 seconds container2a5fca8ba1e4f ubuntu "tail -f /dev/null" 4 seconds ago Up 3 seconds container1
Now we have to connect the containers to the network; to do this, we use the command docker network connect <network name> <container name>
InputPython!docker network connect myNetwork container1Copied
InputPython!docker network connect myNetwork container2Copied
To verify that they have connected correctly, we can inspect the network, but filtering by the connected containers.
$ docker network inspect --format '{{.Containers}}' myNetwork
map
[
6c8dc18315488ef686f7548516c19b3d716728dd8a173cdb889ec0dd082232f9:
{
container2
f828d211e894f7a5a992ce41a2a0def8e2424e9737fb4e1485fc09cc2d607b69
02:42:ac:12:00:03
172.18.0.3/16
}
a5fca8ba1e4ff0a67002f8f1b8cc3cd43185373c2a7e295546f774059ad8dd1a:
{
container1
cff762e6286ebc169804b2a675bbff904102de796751d367c18d4b490c994c45
02:42:ac:12:00:02
172.18.0.2/16
}
]As we can see, the container1 container has the IP 172.18.0.2 and the container2 container has the IP 172.18.0.3
We enter the container1 container and install ping
$ docker exec -it container1 bash
root@a5fca8ba1e4f:/# apt update
...
root@a5fca8ba1e4f:/# apt install iputils-ping
...
root@a5fca8ba1e4f:/#We enter the container2 container and install ping
$ docker exec -it container2 bash
root@a5fca8ba1e4f:/# apt update
...
root@a5fca8ba1e4f:/# apt install iputils-ping
...
root@a5fca8ba1e4f:/#Now from the container1 container we make a ping to the IP 172.18.0.3, which belongs to the container2 container
root@a5fca8ba1e4f:/# ping 172.18.0.3
PING 172.18.0.3 (172.18.0.3) 56(84) bytes of data.
64 bytes from 172.18.0.3: icmp_seq=1 ttl=64 time=0.115 ms
64 bytes from 172.18.0.3: icmp_seq=2 ttl=64 time=0.049 ms
64 bytes from 172.18.0.3: icmp_seq=3 ttl=64 time=0.056 ms
64 bytes from 172.18.0.3: icmp_seq=4 ttl=64 time=0.060 ms
^C
--- 172.18.0.3 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3068ms
rtt min/avg/max/mdev = 0.049/0.070/0.115/0.026 msAnd from the container2 container, we perform a ping to the IP 172.18.0.2, which belongs to the container1 container
root@6c8dc1831548:/# ping 172.18.0.2
PING 172.18.0.2 (172.18.0.2) 56(84) bytes of data.
64 bytes from 172.18.0.2: icmp_seq=1 ttl=64 time=0.076 ms
64 bytes from 172.18.0.2: icmp_seq=2 ttl=64 time=0.045 ms
64 bytes from 172.18.0.2: icmp_seq=3 ttl=64 time=0.049 ms
64 bytes from 172.18.0.2: icmp_seq=4 ttl=64 time=0.051 ms
^C
--- 172.18.0.2 ping statistics ---
4 paquetes enviados, 4 recibidos, 0% de pérdida de paquetes, tiempo 3074ms
rtt min/avg/max/mdev = 0.045/0.055/0.076/0.012 msBut there is one better thing that Docker allows us to do: if I don’t know the IP of the container I want to connect to, instead of writing its IP I can write its name
Now from the container1 container we ping the IP of container2
root@a5fca8ba1e4f:/# ping container2
PING container2 (172.18.0.3) 56(84) bytes of data.
64 bytes from container2.myNetwork (172.18.0.3): icmp_seq=1 ttl=64 time=0.048 ms
64 bytes from container2.myNetwork (172.18.0.3): icmp_seq=2 ttl=64 time=0.050 ms
64 bytes from container2.myNetwork (172.18.0.3): icmp_seq=3 ttl=64 time=0.052 ms
64 bytes from container2.myNetwork (172.18.0.3): icmp_seq=4 ttl=64 time=0.053 ms
^C
--- container2 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3071ms
rtt min/avg/max/mdev = 0.048/0.050/0.053/0.002 msAs we can see, Docker knows that the IP of the container2 container is 172.18.0.3
And from the container2 container we ping the IP of container1
root@6c8dc1831548:/# ping container1
PING container1 (172.18.0.2) 56(84) bytes of data.
64 bytes from container1.myNetwork (172.18.0.2): icmp_seq=1 ttl=64 time=0.051 ms
64 bytes from container1.myNetwork (172.18.0.2): icmp_seq=2 ttl=64 time=0.058 ms
64 bytes from container1.myNetwork (172.18.0.2): icmp_seq=3 ttl=64 time=0.052 ms
64 bytes from container1.myNetwork (172.18.0.2): icmp_seq=4 ttl=64 time=0.056 ms
^C
--- container1 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3057ms
rtt min/avg/max/mdev = 0.051/0.054/0.058/0.003 msAs we can see, Docker knows that the IP of the container1 container is 172.18.0.2
We exit the containers and delete them
InputPython!docker rm -f container1 container2Copied
container1container2
We also delete the network we created
InputPython!docker network rm myNetworkCopied
myNetwork
GPU Usage
To be able to use the host GPUs inside Docker containers, it is necessary to follow the steps described on the Nvidia container toolkit installation page
Set up the repository and the GPG key
We need to configure the nvidia container toolkit repository and the GPG key, for this we run the following command in the console
distribution=$(. /etc/os-release;echo $ID$VERSION_ID) \
&& curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg \
&& curl -s -L https://nvidia.github.io/libnvidia-container/$distribution/libnvidia-container.list | \ sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.listInstallation of nvidia container toolkit
Once we have updated the repository and the key, we update the repositories using the command
sudo apt updateAnd we installed nvidia container toolkit
sudo apt install -y nvidia-docker2Docker restart
Once we have finished, we need to restart the Docker daemon by
sudo systemctl restart dockerGPU Usage
Now that we have configured Docker to be able to use the host GPUs inside the containers, we can test it using the --gpus all option. If you have more than one GPU and only want to use one, you would need to specify it, but for now here we only explain how to use all of them
We create a container that will not run in the background, but instead will execute the nvidia-smi command so we can see whether it has access to the GPUs
InputPython!docker run --name container_gpus --gpus all ubuntu nvidia-smiCopied
Unable to find image 'ubuntu:latest' locallylatest: Pulling from library/ubuntu6a12be2b: Pull complete .54MB/29.54MBBDigest: sha256:aabed3296a3d45cede1dc866a24476c4d7e093aa806263c27ddaadbdce3c1054Status: Downloaded newer image for ubuntu:latestMon Sep 4 07:10:36 2023+-----------------------------------------------------------------------------+| NVIDIA-SMI 510.39.01 Driver Version: 510.39.01 CUDA Version: 11.6 ||-------------------------------+----------------------+----------------------+| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC || Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. || | | MIG M. ||===============================+======================+======================|| 0 Quadro T1000 On | 00000000:01:00.0 Off | N/A || N/A 44C P0 15W / N/A | 9MiB / 4096MiB | 0% Default || | | N/A |+-------------------------------+----------------------+----------------------++-----------------------------------------------------------------------------+| Processes: || GPU GI CI PID Type Process name GPU Memory || ID ID Usage ||=============================================================================|| 0 N/A N/A 2545 G 4MiB || 0 N/A N/A 3421 G 4MiB |+-----------------------------------------------------------------------------+
We delete the container
InputPython!doker rm container_gpusCopied
---
➡️ **Continue in Part 2: Docker Compose and advanced topics**, where you will orchestrate multiple containers at once and go deeper into Docker.