Introduction
Docker image management involves creating, managing, and distributing Docker images. Docker images are the building blocks of Docker containers, which are lightweight and portable virtualized environments that can run anywhere. It uses a layered architecture to build and manage Docker images. Each layer in the Docker image represents a specific set of changes or additions to the previous layer.
Docker Image Archive
Docker provides two different methods to save and load Docker images:
- docker save/docker load
- docker export/docker import
👉 docker save
and docker load
are used to save and load entire Docker images along with all of their layers and metadata.
👉 docker export
and docker import
are used to export and import a container as a tar file. Docker export and docker import do not include metadata or information about the image's layers.
Short hands on
Save/Load
Creating an empty directory
mkdir /image_backup
Checking current images detail
docker images;
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx latest 904b8cb13b93 4 days ago 142MB
ubuntu bionic b89fba62bc15 4 days ago 63.1MB
mysql latest 4f06b49211c0 10 days ago 530MB
mysql 5.7 be16cf2d832a 4 weeks ago 455MB
To save a backup for a specific image
docker save -o /image_backup/ubuntu.tar ubuntu:bionic
So now if we delete the image
docker rmi ubuntu:bionic
# Checking for the image
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx latest 904b8cb13b93 4 days ago 142MB
mysql latest 4f06b49211c0 10 days ago 530MB
mysql 5.7 be16cf2d832a 4 weeks ago 455MB
We can load the ubuntu:bionic
image from the backup directory
docker load -i /image_backup/ubuntu.tar
Checking the results
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx latest 904b8cb13b93 4 days ago 142MB
ubuntu bionic b89fba62bc15 4 days ago 63.1MB
mysql latest 4f06b49211c0 10 days ago 530MB
mysql 5.7 be16cf2d832a 4 weeks ago 455MB
Export/Import
Creating a new container using the ubuntu image
docker run -d --name image_con ubuntu:bionic
Exporting this container
docker export image_con -o /image_backup/image_con.tar
ls -l /image_backup/
total 127984
-rw------- 1 root root 65521664 Mar 6 09:34 image_con.tar
-rw------- 1 root root 65529856 Mar 6 09:30 ubuntu.tar
Now we can import this image with a different tag
docker import /image_backup/image_con.tar myubuntu:v1
Checking the results
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
myubuntu v1 3e3a5217f941 3 seconds ago 63.1MB
nginx latest 904b8cb13b93 4 days ago 142MB
ubuntu bionic b89fba62bc15 4 days ago 63.1MB
Docker Image Commit & Build
docker commit
and docker build
are both Docker commands used to create Docker images
docker commit
command is used to create a new Docker image from an existing container. This command is useful when you have made changes to a running container, and you want to save those changes as a new image.
docker build
command is used to create a new Docker image from a Dockerfile. A Dockerfile is a script that contains instructions on how to build a Docker image
Some of the commonly used Dockerfile instructions:
- FROM: This instruction is used to specify the base image that the new image will be built on top of
- LABEL: This instruction is used to add metadata to the image
- RUN: This instruction is used to execute commands within the container
- ADD: This instruction is used to copy files from the host system into the container
- WORKDIR: This instruction is used to set the working directory for any subsequent commands in the Dockerfile
- EXPOSE: This instruction is used to specify which port(s) the container will listen on at runtime
- CMD: This instruction is used to specify the default command to be executed when the container starts
- ENTRYPOINT: This instruction is used to specify the command that will be executed when the container starts, and it cannot be overridden when the container is run
Short hands on
Docker Commit
To check how many layers are there in the ubuntu image
docker inspect ubuntu:bionic
"Layers": [
"sha256:52c5ca3e9f3bf4c13613fb3269982734b189e1e09563b65b670fc8be0e223e03"
]
Creating the container using the -it
option
docker run -it --name commit_con ubuntu:bionic
From inside the container,
cat > TestFile
TestFile
ls
TestFile boot etc lib media opt root sbin sys usr
bin dev home lib64 mnt proc run srv tmp var
Now from the host machine
docker commit -a "MyName" -m "Image Comment" commit_con myubuntu:v1.0.0
We can confirm this commit using
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
myubuntu v1.0.0 2e214faf96f7 32 seconds ago 63.1MB
Inspecting our v1.0.0 ubuntu image
"Layers": [
"sha256:52c5ca3e9f3bf4c13613fb3269982734b189e1e09563b65b670fc8be0e223e03",
"sha256:cea6ad35f448cdba9f2bb5c32b245c497e90cafa36c8f856706c8257bb666e34"
]
👉 We can see that a new image was created with an extra layer when we committed the changes for our ubuntu container
Docker Build
Creating an empty directory for our dockerfile
mkdir /DockerFile_root
cd /DockerFile_root
Creating an empty dockerfile
vi Dockerfile # Using this default name is more efficient
Adding the following inside this file
FROM ubuntu:bionic
LABEL maintainer "Author <Author@localhost.com>" # (Key : Value) Format
RUN apt-get update && apt-get install apache2 -y
ADD index.html /var/www/html
WORKDIR /var/www/html # works just like 'cd'
RUN ["/bin/bash", "-c", "echo RunTest > Test.html"]
EXPOSE 80
CMD ["apachectl", "-DFOREGROUND"] # Either have to use 'CMD' or 'ENTRYPOINT' to run the service
Creating an index.html
echo Test > index.html
Our working directory is currently /DockerFile_root
which is also known as Build Context Directory
This is where the docker build should happen
docker build -t my:v1.0.0 ./
Successfully built e5581d044d20
Successfully tagged my:v1.0.0
Checking our new image
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
my v1.0.0 e5581d044d20 28 seconds ago 204MB
Creating and starting the container
docker run -d --name apache2_con -p 80:80 my:v1.0.0
891473bcd83ed3f94830e5595aa698f3e40652183183a205bc3aa60fe8eef843
If we check inside the container
docker exec -it apache2_con /bin/bash
root@891473bcd83e:/var/www/html# cat ./Test.html
RunTest
Returning to the host system and checking port 80
curl localhost:80
Test
curl localhost:80/Test.html
RunTest
Docker Build Cache & Image Sizes
Finally, I want to discuss regarding Docker build sizes and how to manage them
Creating a new Dockerfile
vi Dockerfile_L
FROM ubuntu:bionic
LABEL maintainer "Author <Author@localhost.com>"
RUN mkdir /dummy
RUN fallocate -l 100m /dummy/A
RUN rm -rf /dummy/A
To build this image
docker build -t dummy:v1.0.0 ./ -f Dockerfile_L
👉 The -f
option in the docker build command specifies the Dockerfile name to use during the build process, when the Dockerfile is not named "Dockerfile"
This will build the image as follows
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
dummy v1.0.0 4ef94c4001fe 2 minutes ago 168MB
👉 As we can see that it is 168MBs big. We know that ubuntu:bionic is only about 63MBs. In the Dockerfile we just created dummy directory and deleted a file inside the directory that cost us over 100MBs
To decrease the size we can try to edit our Dockerfile
FROM ubuntu:bionic
LABEL maintainer "Author <Author@localhost.com>"
RUN mkdir /dummy && \
fallocate -l 100m /dummy/A && \
rm -rf /dummy/A
Now if we check the image size after building this new image
docker build -t dummy:v1.0.1 ./ -f Dockerfile_L
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
dummy v1.0.1 6ced40e23a77 11 seconds ago 63.1MB
dummy v1.0.0 4ef94c4001fe 8 minutes ago 168MB
✨ Another important thing is Docker Cache
💡 Docker caches build layers to speed up subsequent builds of a Dockerfile. However, when using GitHub for building, changes to source code may not be properly reflected due to cached layers. To force a rebuild of all layers and ensure changes are properly incorporated, use the
--no-cache
option with the docker build command
We can try to compile a C
code as well
vi ./app.c
#include <stdio.h>
void main()
{
printf("Hello World\n");
}
To compile our C
code, we need the gcc
compiler
vi Dockerfile_M
FROM gcc:latest
LABEL maintainer "Author <Author@localhost.com>"
ADD app.c /root
WORKDIR /root
RUN gcc -o ./app ./app.c
CMD ["./app"]
Now building this Dockerfile
docker build -t multi:v1.0.0 ./ -f Dockerfile_M
👉 This will first pull the gcc:latest image from the docker hub website
If we check the image size
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
multi v1.0.0 8b00f89eb22c 23 seconds ago 1.27GB
gcc latest c6aa7ca27d67 4 days ago 1.27GB
Testing the image by running it in a container
docker run -it --rm --name pritn_c multi:v1.0.0
Hello World
Well this works but the size of images are obnoxiously large for such a simple task 😥
To solve this, we can edit the Dockerfile as follows
# GCC Compile Block
FROM gcc:latest as compile_base
LABEL maintainer "Author <Author@localhost.com>"
ADD app.c /root
WORKDIR /root
RUN gcc –o ./app ./app.c
# APP Running Block
FROM alpine:latest
RUN apk add --no-cache gcompat
WORKDIR /root
COPY --from=compile_base /root/app ./ # we set an alias in the compile block as "compile_base" which is being used here
CMD ["./app"]
This is called multi-stage build. Multi-stage builds in Docker allow you to use multiple FROM statements in a single Dockerfile to create multiple intermediate images, each with its own set of instructions and layers.
✨ The advantage of using multi-stage builds is that it allows you to create smaller and more efficient Docker images.
Now we will build this image
docker build -t multi:v1.0.1 ./ -f Dockerfile_M
Checking this image size
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
multi v1.0.1 1cc2b8149d7d 6 seconds ago 7.23MB
Testing the image by running it in a container
docker run -it --rm --name print_c multi:v1.0.1
Hello World
We can even reduce this image size further by dividing the Package install block as well
vi Dockerfile_M2
# GCC Compile Block
FROM gcc:latest as compile_base
LABEL maintainer "Author <Author@localhost.com>"
ADD app.c /root
WORKDIR /root
RUN gcc –o ./app ./app.c
# Package Install Block
FROM alpine:latest as package_install
RUN apk add --no-cache gcompat
WORKDIR /root
COPY --from=compile_base /root/app ./
# App running Block
FROM package_install as run
WORKDIR /root
CMD ["./app"]
Building the image
docker build -t multi:v1.0.2 ./ -f Dockerfile_M2
Checking the size
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
multi v1.0.2 60500955b71d 4 seconds ago 7.23MB
Testing the image by running it in a container
docker run -it --rm --name print_c multi:v1.0.2
Hello World
💡 The size different can't be seen in the above case as the actual compile file is really small as compared to an actual program
Conclusion
In conclusion, managing Docker images is an important part of working with Docker containers. By understanding the various Docker image management commands and best practices I discussed above, users can effectively manage their Docker images to optimize their container environment and workflow ✔
Top comments (7)
Super nice article great work!
Thank you Markus :)
Hi,
Great article. I still do not understand why moving from a 2 stage build to a 3 stage build saves memory. Could explain it a bit more ?
The first stage builds the app.
The second stage installs the app's dependency.
The third stage copies only the app and dependencies from the second stage.
The saving will be the package manager itself. Say we need to install Node's Yarn or Python's poetry.
The second saving will be the package Manager's cache and temp files. We used to disable caching when using two stage builds.
Hey,
In my example above, I divided the App running block into 2 parts which should technically save more memory. However, as I was testing a simple C language code that gives "Hello World" as an output, the memory decrement wasn't visible.
Normally, multi-staging our Dockerfiles should reduce the size of Docker images leading to faster deployment times.
Awesome article, great usage of images to visually explain the Docker concepts. Thanks for sharing!
Thank you!