Docker Images and Containers
Behavior is the mirror in which everyone shows their image. – Johann_Wolfgang_von_Goethe
<Reference Container Setup article that you did last time on ghost.io.>
The book referenced below is: Docker Deep Dive: Zero to Docker in a Single Book, Nigel Poulton.
https://learning.oreilly.com/library/view/docker-deep-dive/9781837028351/
Docker was very tricky for me to get my head around for a while but with the help of many tutorials and books I think I am starting to get the hang of it. Docker has a layered caching mechanism which means we only pull down images when we need them, as you will see.
We will be doing this for Ubuntu 24.04.
Prerequisites
Working Docker environment (see setup article referenced above).
Working network connection with firewall that does not block Docker-related traffic.
64-bit kernel version required for Ubuntu 24.04.
Ideally you have sudo privileges on the account.
Images vs Containers vs Container Registries
From the book Chapter 6 (my addition in bold):
An image is a (mostly) read-only package containing everything you need to run an application. This means they include application code, dependencies, a minimal set of OS constructs, and metadata. You can start multiple containers from a single image.
For a developer like me it is helpful to think of images like Classes and containers as Objects (instances of a class). An image is essentially a container that might or might not be running.
To talk about images we need to talk a bit about container registries because that is very likely where you will obtain your first images. A container registry is a repository or a collection of repositories that store images. Among the most famous is DockerHub at https://hub.docker.com/. That is where we will be obtaining images for this tutorial. You can read more about container registries at: https://www.redhat.com/en/topics/cloud-native-apps/what-is-a-container-registry
and
https://www.wiz.io/academy/container-registries
Ideally, the container runs only a single application or service. In practice, I have found this is rarely the case, but it is the ideal we strive towards.
Try the following command to see your docker images:
docker images
If you don’t see any that means you don’t have any images. If you do see some and you’d like to remove them you can type at a terminal: Warning the following is a destructive command so make sure you have all needed images and containers backed up before you proceed!
docker system prune -a
The -a flag tells docker to prune just about everything.
Your First Image
For more on Alpine Linux see: https://wiki.alpinelinux.org/wiki/Main_Page
To pull down the alpine image from Docker Hub:
docker pull alpine
That should result in something similar to the following output on the terminal:
Using default tag: latest
latest: Pulling from library/alpine
f18232174bc9: Pull complete
Digest: sha256:a8560b36e8b8210634f77d9f7f9efd7ffa463e380b75e2e74aff4511df3ef88c
Status: Downloaded newer image for alpine:latest
docker.io/library/alpine:latest
Now type:
docker images
and you should get back (the IMAGE ID will be different):
REPOSITORY TAG IMAGE ID CREATED SIZE
alpine latest aded1e1a5b37 3 months ago 7.83MB
That states that we have the latest version of the alpine repository (from docker hub) and the size is also given.
If we wanted to download a certain version of Alpine Linux then we do so:
docker pull alpine:3.21
Then docker images should show:
REPOSITORY TAG IMAGE ID CREATED SIZE
alpine 3.21 aded1e1a5b37 3 months ago 7.83MB
alpine latest aded1e1a5b37 3 months ago 7.83MB
As of this writing the 3.21 version of alpine happens to be the latest one. What about 3.20?
docker pull alpine 3.20
This should pull alpine version 3.20. After it downloads confirm:
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
alpine 3.21 aded1e1a5b37 3 months ago 7.83MB
alpine latest aded1e1a5b37 3 months ago 7.83MB
alpine 3.20 ff221270b9fb 3 months ago 7.8MB
With a different image id, we know that the version of alpine 3.21 we have is different from 3.20.
Some final book notes before we move on (Chapter 6):
A couple of things are worth noting.
As previously mentioned, if you don’t specify an image tag after the repository name, Docker assumes you want the image tagged as latest. The command will fail if the repository has no image tagged as latest.
Images tagged as latest are not guaranteed to be the most up-to-date in the repository.
You don’t have to use Docker Hub for all your images. The book states how to pull an image using its DNS name but that is not covered here.
Layers vs Images
Docker layers are fundamental building blocks used in the creation of Docker images. Each image is composed of a series of layers which stack on top of one another to form the complete image. Types of layers include:
- Base Layer: This is often a minimal operating system image (like Alpine, Ubuntu, or CentOS) from which other layers build.
- Intermediate Layers: These layers represent the series of commands or instructions specified in the Dockerfile (more on Dockerfiles a little later), such as installing packages, copying files, setting environment variables, etc. Each instruction in the Dockerfile generates a new layer.
- Read-only Nature: Once a layer is created, it is immutable, meaning it cannot be changed. If you need to change something in a layer, you would create a new layer with the desired changes.
- Reusability and Caching: Layers are reusable and can be shared between different Docker images. If multiple Docker images use the same base layer, Docker will pull the layer only once and reuse it, saving bandwidth and storage. Docker also caches layers during the build process, which speeds up subsequent builds if the layers haven't changed.
- Layer IDs: Each layer is identified by a unique SHA-256 hash.
Next we’ll look at an image with multiple layers: redis.
docker pull redis
Using default tag: latest
latest: Pulling from library/redis
254e724d7786: Pull complete
e09c770fca85: Pull complete
d6b0a8fda729: Pull complete
d76f79e53729: Pull complete
73134d65a174: Pull complete
4f4fb700ef54: Pull complete
6162363dcb24: Pull complete
Digest: sha256:1b7c17f650602d97a10724d796f45f0b5250d47ee5ba02f28de89f8a1531f3ce
Status: Downloaded newer image for redis:latest
docker.io/library/redis:latest
Something a bit different happened that time. Those SHAs (link) in the middle there mean we pulled down multiple layers. To see them enter:
docker inspect redis
I won’t paste all the text that is shown but the layers are at:
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:6c4c763d22d0c5f9b2c5901dfa667fbbc4713cee6869336b8fd5022185071f1c", "sha256:eff6ea600e98083b0a20ffd862375d656d804baef6d5164d5c4751d81ef7dc1a",
"sha256:f9fb29ec84deda8ac3502294047471cbb1f7674518032c88e609b8e194ba4fb4",
"sha256:9f310f38790dfb1107e3b011f98d2fbafe30a23c869d562ec84025c750daa181",
"sha256:364f298388701fc3b40e82fe78a490f6b56f229ff6af26d653b2330edb8ff54d",
"sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef",
"sha256:e44077c1d10f2ecf95e847c25d52698ae0266a36c7a6680a999ab1e08299890d"
]
},
Removing Images
You’ve already seen how to blow away all of your docker images and cache with the docker system prune command.
To delete only the alpine:latest image type:
docker rmi alpine:latest
To delete ALL images type:
docker rmi $(docker images -q) -f
Containers
To run a container from an image use the "docker run" command. If you’ve deleted your images pull down the alpine one again:
docker pull alpine
Then run:
docker run -it alpine:latest
It should bring up a root prompt at the / directory. You are now inside a Linux Alpine container. Inside said container enter the following to play around with it. These commands show the version, the installed packages, etc.
cat /etc/alpine-release
uname -a
apk list --installed
ip a
ping -c 4 google.com
df -h
free -m
Type:
exit
when you are done playing. This should drop you out of the container into your Ubuntu host. To see the container type:
docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
212aef22685a alpine:latest "/bin/sh" 10 minutes ago Exited (0) About a minute ago cool_tesla
Notice the cool_tesla nickname at the end of that list. Your name will definitely be different!
You will need to start the container before you can execute it as follows:
docker start <container_nickname>
docker exec -it <container_nickname> sh
That should drop you back into a shell inside the container. Before we exit the container let’s try editing a file. Type:
touch test.txt
ls
You should see the test.txt file. Now exit the container:
exit
Once you have started a container your file changes should persist. Type:
docker start <container_nickname>
docker exec -it <container_nickname> sh
This should once again drop you into your container and you can type ls to verify your file is indeed there. However, don’t get complacent! If you delete the container and restore it from the image your file changes will be lost!
Note that the book states in Chapter 7:
[Containers are] also designed to be stateless and ephemeral, whereas VMs are designed to be long-running and can be migrated with their state and data.
Containers are also designed to be immutable. This means you shouldn’t change them after you’ve deployed them — if a container fails, you replace it with a new one instead of connecting to it and making a live fix.
We may get fancy with volumes https://docs.docker.com/engine/storage/volumes/ and bind mounts https://docs.docker.com/engine/storage/bind-mounts/ in a later tutorial but for now just assume this is the ideal setup: containers are stateless and immutable. This means if we exit the container with:
exit
Then stop the container with the command:
docker stop <container_nickname>
And use the nickname to delete your container:
docker container rm <container_nickname>
Then do another docker ps -a to convince yourself the container is gone.
If we recreate the container, our file will be gone! Observe:
docker run -it alpine:latest
ls
The file is no more!!
This is because we have restored our container from the image which means our test.txt file no longer exits.
Note: you can use Ctrl+P+Q to detach from a container without killing the main process.
Containerizing an App
The book uses Docker Desktop with the docker init command. We are going to do it the old-fashioned way because:
- You will learn more about setting up Dockerfiles.
- I don’t have Docker Desktop.
We also aren’t going to build a web application. We will just build a simple C++ sorting class and benchmark it. It is up on my github at: https://github.com/mday299/keypuncher/tree/main/Docker/imagesConts. Please note that this code requires at least the C++17 standard https://en.wikipedia.org/wiki/C%2B%2B17.
The part I want to highlight is the Dockerfile:
# Use an Alpine-based image with C++ support
FROM alpine:3.21.3
# Install necessary build tools and dependencies
RUN apk add --no-cache g++
# Set the working directory
WORKDIR /usr/src/app
# Copy source files
COPY . .
# Compile the C++ application
RUN g++ -o app main.cpp
# Run the compiled application
CMD ["./app"]
This Dockerfile does the following operations in order:
- Pulls the Alpine Linux 3.21.3 base image.
- Installs the g++ compiler without caching Alpine Package Keeper (APK) index to keep the image small.
- Sets /usr/src/app as the working directory within the container.
- Copies all files from the current directory on the host to the container's working directory.
- Compiles the main.cpp file using g++ and produces an executable named app.
- Specifies that the Docker container should run the ./app executable when it starts.
With this Dockerfile, you can build a Docker image that compiles and runs a simple C++ application.
Note that order matters: keep the commands that are least likely to change near the top of your Dockerfile. This ensures that as many layers as possible get cached.
And Voila! You have a working C++ application managed by Docker!
Credits:
Docker Deep Dive: Zero to Docker in a Single Book, Nigel Poulton.
https://learning.oreilly.com/library/view/docker-deep-dive/9781837028351/
