What is container image?
When running a container, is uses an isolated filesystem. This custom filesystem is provided by a container image
. Since the image contains the container's filesystem, it must contain everything needed to run an application - all dependencies, scripts, binaries, etc. The image also contains other configuration for the container, such as environment variables, a default command to run, and other metadata.
How to build image?
classic
: The classic method build the image via docker daemon.buildkit
: The buildkit is integrating from version 18.09. uses should see an improvement on performance, storage, management, feature functionality, and security. But buildkit only supported for building Linux containers.
What is Dockerfile?
A Dockerfile
is a text documnent that contains all the commands a user could call on the command line to assemble an image. Using docker build
users can create an automated build that executes several command-line instructions in succession.
The docker build
command builds an image from a Dockerfile
and a context
. The build's context is the set of files at a specified location PATH
(On the local filesystem) or URL
(A git repository location). The build context is processed recursively. So, a PATH
includes any subdirectories and the URL
includes the repository and its submodules.
The build is run by Docker daemon, not by the CLI. The first thing a build process does is send the entire context(recursively) to the daemon. In most cases, it's best to start with an empty directory as context and keep your Dockerfile in that directory. Add only the files needed for building the Dockerfile.
Warning
Do not use your root directory('/') as thePATH
for your build context, as it causes the build to transfer the entire contents of your hard drive to the Docker daemon.
These recommendations are designed to help you create an efficient and maintainable Dockerfile
. The instruction is not case-sensitive. However, convention is for them to be UPPERCASE
to distinguish them from arguments more easily. The format of Dockerfile
like below.
# Comment
INSTRUCTION arguments
This is a curated list of all the important commands and instructions that are extensively used in a dockerfile. I will explained them with examples below. Let's get started.
The FROM
instruction initializes a new build stage and sets the Base Image
for subsequent instructions. As such, a vaild Dockerfile
must start with a FROM
instruction.
FROM [--platform=<platform>] <image> [AS <name>]
FROM [--platform=<platform>] <image>[:<tag>] AS [<name>]
FROM [--platform=<platform>] <image>[@<digest>] AS [<name>]
# Example
FROM centos
FROM centos:centos8.4.2105
The RUN
instruction will execute any commands in a new layer on top of the current image and commit the results. The resulting commited image will be used for the next step in the Dockerfile
.
Each RUN
command creates a new cache layer or an intermediate image layer and hence chaining all of them into a single line, becomes efficient. However, chaining multiple RUN
instructions cloud lead to cache bursts as well.
Note
Theexec
form is parsed as a JSON array, which means that you must use double-quotes(") around words not single-quotes(').
RUN <command> (shell form)
RUN ["executable", "param1", "param2"] (exec form)
# Example
RUN echo "hello-world" > info_hello
RUN ["/bin/bash", "-c", "echo 'hello-world' > info_hello"]
The main purpose of a CMD
is to provide defaults for an executing container. If the user specifies arguments to docker run
then they will override the default specified in CMD
. These defaults can include an executable, or they can omit the executable, in which case you must specify an ENTRYPOINT
instruction as well. There can only be one CMD
instruction in a Dockerfile
. If you list more than one CMD
then only the last one will take effect.
CMD ["executable", "param1", "param2"] (exec form, this is the preferred form)
CMD ["param1", "param2"] (as default parameters to ENTRYPOINT)
CMD command param1 param2 (shell form)
# Dockerfile,
FROM centos
CMD ["/bin/ls"]
# CMD ls
# build and run container
# docker build -t new_image:v1.0 .
Sending build context to Docker daemon 3.072kB
Step 1/2 : FROM centos
---> 5d0da3dc9764
Step 2/2 : CMD ["/bin/ls"]
---> Using cache
---> 82efcb23e4f4
Successfully built 82efcb23e4f4
Successfully tagged new_image:v1.0
# When the container run, it will list all files and directorys in current path
# docker run --name nb_1 --rm new_image:v1.0
bin
dev
etc
...
var
# Override the default CMD, specifies arguments to "docker run" command, the arguments will override the default CMD
# docker run --name nb_1 --rm new_image:v1.0 uptime
01:05:57 up 5:51, 0 users, load average: 0.00, 0.01, 0.00
The ENTRYPOINT
allows you to configure a container that will run as executable. The difference between ENTRYPOINT
and CMD
is that, if you try to specify default arguments in the docker run command, it will not ignore the ENTRYPOINT arguments.
Note
Usingexec form ENTRYPOINT
, command line arguments ondocker run <image>
will be appended after all elements.
The shell form ENTRYPOINT
prevents any CMD
or docker run
command line arguments from being used. Another disadvantage is that shell form ENTRYPOINT
will be started as a subcommand of /bin/sh -c
, which does not pass signals. That means the executable will not be the container's PID 1, and will not receive Unix signals, so you can't send a SIGTERM from docker stop <container>
.
ENTRYPOINT ["executable", "param1", "param2"] (exec form)
ENTRYPOINT command param1 param2 (shell form)
# Dockerfile
FROM centos
ENTRYPOINT ["/bin/ls"]
CMD ["-l", "-h"]
# docker build -t new_image:v1.2 .
Sending build context to Docker daemon 3.072kB
Step 1/3 : FROM centos
---> 5d0da3dc9764
Step 2/3 : ENTRYPOINT ["/bin/ls"]
---> Using cache
---> 163abdd6ce9e
Step 3/3 : CMD ["-l", "-h"]
---> Running in 6deb4b3d3abc
Removing intermediate container 6deb4b3d3abc
---> e36804f35fff
Successfully built e36804f35fff
Successfully tagged new_image:v1.2
# docker run --name nb_1 --rm new_image:v1.2
total 48K
lrwxrwxrwx 1 root root 7 Nov 3 2020 bin -> usr/bin
drwxr-xr-x 5 root root 340 Feb 25 02:03 dev
...
drwxr-xr-x 20 root root 4.0K Sep 15 14:17 var
# Override the default CMD, specifies arguments to "docker run" command, the arguments will override the default CMD
# and the arguments append to ENTRYPOINT
# docker run --name nb_1 --rm new_image:v1.2 -d etc
etc
# Dockerfile
FROM centos
ENTRYPOINT ls
CMD ["-l", "-h"]
# docker build -t new_image:v1.3 .
Sending build context to Docker daemon 3.072kB
Step 1/3 : FROM centos
---> 5d0da3dc9764
Step 2/3 : ENTRYPOINT ls
---> Running in 3540d3766239
Removing intermediate container 3540d3766239
---> 996cbee7fbb4
Step 3/3 : CMD ["-l", "-h"]
---> Running in 5229fd7239af
Removing intermediate container 5229fd7239af
---> ab20183d22f9
Successfully built ab20183d22f9
Successfully tagged new_image:v1.3
# Because using the shell form ENTRYPOINT, the running command is /bin/sh -c ls
# It will skip the docker run command arguments and CMD instructions.
# So These two docker run command get the same output -- list all files & directory in /
# docker run --name nb_1 --rm new_image:v1.3
# docker run --name nb_1 --rm new_image:v1.3 -d etc
bin
dev
...
var
No ENTRYPOINT | ENTRYPOINT exec_entry p1_entry | ENRYPOINT ["exec_entry", "p1_entry"] | |
---|---|---|---|
No CMD | error, not allowed | /bin/sh -c exec_entry p1_entry | exec_entry p1_entry |
CMD [“p1_cmd”, “p2_cmd”] | p1_cmd p2_cmd | /bin/sh -c exec_entry p1_entry | exec_entry p1_entry p1_cmd p2_cmd |
CMD exec_cmd p1_cmd | /bin/sh -c exec_cmd p1_cmd | /bin/sh -c exec_entry p1_entry | exec_entry p1_entry /bin/sh -c exec_cmd p1_cmd |
The LABEL
instruction adds metadata to an image. A LABEL is a key-value pair. To include spaces within a LABEL value, use quotes and backslashes as you would in command-line parsing.
LABEL <key>=<value> <key>=<value> <key>=<value> ...
# Example
LABEL version="1.0"
LABEL description="This text illustrates \
that label-values can span multiple lines."
The ENV
instruction sets the environment variable <key>
to the value <value>
. This value will be in the environment for all subsequent instructions in the build stage and can be replaced inline in many as well. The environment variables set using ENV will persist when a container is run from the resulting image, you can change them using docker run -e <key>=<value>
. If an environment variable is only needed during build, and not in the final image, consider setting a value for a single command instead, or using ARG
instruction, which is not persisted in the final image.
ENV MY_ENV="MY_ENV"
ENV YOUR_ENV=this\ is\ your\ env
ENV ENV_WITHOUT_QUOTE=envwithoutquote
# Dockerfile
FROM centos
ENV MY_ENV=1.1.1
ENTRYPOINT ["/bin/sh", "-c", "echo ${MY_ENV}"]
# docker build -t new_image:v1.7 .
Sending build context to Docker daemon 3.072kB
Step 1/3 : FROM centos
---> 5d0da3dc9764
Step 2/3 : ENV MY_ENV=1.1.1
---> Running in 6d06a68f24b3
Removing intermediate container 6d06a68f24b3
---> f307d64aaf83
Step 3/3 : ENTRYPOINT ["/bin/sh", "-c", "echo ${MY_ENV}"]
---> Running in 14d1028405fc
Removing intermediate container 14d1028405fc
---> 3a86410cef4a
Successfully built 3a86410cef4a
Successfully tagged new_image:v1.7
# docker run --name nb_1 --rm new_image:v1.7
1.1.1
# docker run --name nb_1 --rm -e MY_ENV=2.2.2 new_image:v1.7
2.2.2
The ADD
instruction copies new files, directories or remote file URL from <src>
and adds them to filesystem of the image at the path <dest>
. Multiple <src>
resources may be specified but if they are files or directories, their paths are interpreted as relative to the source of the context of the build. Each <src>
may contain wildcards and matching will be done using Go’s filepath.Match rules.
ADD [--chown=<user>:<group>] <src>... <dest>
ADD [--chown=<user>:<group>] ["<src>",... "<dest>"]
# Dockerfile
FROM centos
ADD add_dir /data
ADD add_file /data
ADD add_tar.tar.gz /data
ADD http://mirror.fileplanet.com/centos/7.9.2009/isos/x86_64/sha256sum.txt /data
ENTRYPOINT ["/bin/ls", "/data"]
# docker build -t new_image:v1.8 .
Sending build context to Docker daemon 398.8kB
Step 1/6 : FROM centos
---> 5d0da3dc9764
Step 2/6 : ADD add_dir /data
---> 3ed1779cb343
Step 3/6 : ADD add_file /data
---> 9a2b17ca7ccd
Step 4/6 : ADD add_tar.tar.gz /data
---> 49f8a90bae3c
Step 5/6 : ADD http://mirror.fileplanet.com/centos/7.9.2009/isos/x86_64/sha256sum.txt /data
Downloading 398B/398B
---> 8eab0f80695b
Step 6/6 : ENTRYPOINT ["/bin/ls", "/data"]
---> Running in 21698c381d73
Removing intermediate container 21698c381d73
---> e3073dd5926d
Successfully built e3073dd5926d
Successfully tagged new_image:v1.8
# Run container and list the context in /data
# the directory add_dir didn't copied to image, but the entire contents will, including filesystem and metadata
# the add_file copied to the image
# the add_tar.tar.gz unpacked as a directory -- etc
# the URL downloaded into image
# docker run --name nb_1 --rm new_image:v1.8 -lhrt
total 20K
-rw------- 1 root root 398 Nov 4 2020 sha256sum.txt
drwxr-xr-x 73 root root 4.0K Feb 25 11:32 etc
-rw-r--r-- 1 root root 6 Feb 25 18:51 add_file
-rw-r--r-- 1 root root 2 Feb 25 18:58 1
-rw-r--r-- 1 root root 2 Feb 25 18:58 2
The COPY
is same as the ADD
instruction. But COPY
only allow copy local files and directory to the docker container.
COPY [--chown=<user>:<group>] <src>... <dest>
COPY [--chown=<user>:<group>] ["<src>",... "<dest>"]
# Dockerfile
FROM centos
COPY add_dir /data
COPY add_file /data
COPY add_tar.tar.gz /data
ENTRYPOINT ["/bin/ls", "/data"]
# docker build -t new_image:v1.9 .
Sending build context to Docker daemon 398.8kB
Step 1/5 : FROM centos
---> 5d0da3dc9764
Step 2/5 : COPY add_dir /data
---> ed45812a5afb
Step 3/5 : COPY add_file /data
---> db88f4282b1c
Step 4/5 : COPY add_tar.tar.gz /data
---> f981fbc21660
Step 5/5 : ENTRYPOINT ["/bin/ls", "/data"]
---> Running in 1de5a46cc5ae
Removing intermediate container 1de5a46cc5ae
---> 36d0c003930f
Successfully built 36d0c003930f
Successfully tagged new_image:v1.9
# docker run --name nb_1 --rm new_image:v1.9 -lhrt
total 396K
-rw-r--r-- 1 root root 6 Feb 25 18:51 add_file
-rw-r--r-- 1 root root 384K Feb 25 18:51 add_tar.tar.gz
-rw-r--r-- 1 root root 2 Feb 25 18:58 1
-rw-r--r-- 1 root root 2 Feb 25 18:58 2
The WORKDIR
instruction sets the working directory for any RUN
, CMD
, ENTRYPOINT
, COPY
and ADD
instructions that follow it in the Dockerfile. It will be created if the WORKDIR doesn't exist.
WORKDIR /path/to/workdir
# Example
WORKDIR /app
The EXPOSE
instruction informs Docker that the container listens on the specified network at runtime. You can specify whether the port listens on TCP or UDP, and the defaults is TCP if the protocol is not specified. The EXPOSE
instruction does not actually publish the port, it functions as a type of documentation that which ports are intended to be published. To actually publish the port when running the container, use the -p
flag on docker run to publish and map one or more ports, or the -P
flag to publish all exposed ports and map them to high-order ports.
EXPOSE <port> [<port>/<protocol>...]
# Example
EXPOSE 80
EXPOSE 80/tcp
EXPOSE 80/udp
The VOLUME
instruction creates a mount point with the specified name and marks it as holding externally mounted volumes from native host or other containers. The value can be a ==JSON array==, VOLUME ["/var/log/"], or a plain string with ==multiple arguments==, such as VOLUME /var/log or VOLUME /var/log /var/db. For more information/examples and mounting instructions via the Docker client, refer to Share Directories via Volumes documentation.
The docker run command initializes the newly created volume with any data that exists at the specified location within the base image. For example, consider the following Dockerfile snippet:
VOLUME ["/data"]
# Example
VOLUME ["/myvol1", "/myvol2"]
VOLUME /myvol1 /myvol2
Tips
For more information of other instructions, please check The offical Dockerfile reference
The Docker offical offer the best practices guidelines in Dockerfile
. Follow the suggestions below to enhance your dockerfile:
RUN
, COPY
, ADD
create layers. Use single-line to reduce layers. Use multi-stage builds, and only copy the artifacts you need into the final image.# Dockerfile (x)
FROM debian
RUN apt -y update
RUN apt install -y vim
# Dockerfile (✔)
FROM debian
RUN apt -y update \
&& apt install -y vim
Decouple applications: each container should have only one concern. Decoupling applications into multiple containers makes it easier to scale horizontally and reuse containers. Use your best judgment to keep containers as clean and modular as possible. The size of image can be less for separate containers.
Don't install unnecessary package: To reduce complexity, dependencies, file sizes, and build times, avoid installing extra or unnecessary packages just because they might be “nice to have.” For example, you don’t need to include a text editor in a database image. Delete all unnecessary file which generated on build stage, such as "/var/lib/apt/lists/*".
# Dockerfile (x)
FROM debian
RUN apt-get -y update \
&& apt-get -y install vim
CMD ["/bin/ls", "/"]
# Dockerfile (✔)
FROM debian
RUN apt-get -y update \
&& apt-get -y install vim \
&& rm -rf /var/lib/apt/lists/*
CMD ["/bin/ls", "/"]
# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
helloapp v3 b89255bd44e0 5 seconds ago 161MB
helloapp v2 647851d0fa73 8 minutes ago 179MB
build context
when you issue a docker build
command. All recursive contents of files and directories in the current directory are sent to the Docker daemon as the build context. Take out the unnecessary files and directorys to reduce the time to build the image.# Dockerfile
FROM busybox
COPY /hello /
RUN cat /hello
# ls -lhrt
total 8.0K
-rw-r--r-- 1 root root 42 Feb 25 14:32 Dockerfile
-rw-r--r-- 1 root root 6 Feb 25 14:33 hello
# docker build --no-cache -t helloapp:v1 .
Sending build context to Docker daemon 3.072kB
Step 1/3 : FROM busybox
---> ec3f0931a6e6
Step 2/3 : COPY /hello /
---> f26fb396db18
Step 3/3 : RUN cat /hello
---> Running in 0a6cf4c0e4e4
hello
Removing intermediate container 0a6cf4c0e4e4
---> 987ebcbe5e13
Successfully built 987ebcbe5e13
Successfully tagged helloapp:v1
real 0m1.028s
user 0m0.021s
sys 0m0.005s
# cp ~/unnecessary.pdf .
# ls -lhrt
total 68M
-rw-r--r-- 1 root root 42 Feb 25 14:32 Dockerfile
-rw-r--r-- 1 root root 6 Feb 25 14:33 hello
-rw-r--r-- 1 root root 68M Feb 25 15:13 unnecessary.pdf
# time docker build --no-cache -t helloapp:v2 .
Sending build context to Docker daemon 70.95MB
Step 1/3 : FROM busybox
---> ec3f0931a6e6
Step 2/3 : COPY /hello /
---> 47f1c49ffae3
Step 3/3 : RUN cat /hello
---> Running in 83ec6cc2fe98
hello
Removing intermediate container 83ec6cc2fe98
---> bc8a06d09e16
Successfully built bc8a06d09e16
Successfully tagged helloapp:v2
real 0m1.218s
user 0m0.031s
sys 0m0.034s
# Dockerfile (x)
FROM debian
COPY . .
RUN apt-get -y update
RUN apt-get -y install vim
CMD ["/bin/ls", "/"]
# Dockerfile (✔)
FROM debian
RUN apt-get -y update
RUN apt-get -y install vim
COPY . .
CMD ["/bin/ls", "/"]
This example demonstrates how to build a personal blog web image, we write the documents by markdown, generate to static site via Material for Mkdocs, and than publish the web site using nginx server.
The goal is that to build a docker image, which is include the static site files, and running by nginx server to publish the blog web site. These are two steps need to do to build the image.
python:3.9.10-slim-buster
image as a intermediate image to build the envirenment for generating markdown files to html files.The procedure to build our new image will below in details.
# ls -lhrt
total 20K
-rw-r--r-- 1 root root 86 Feb 26 15:34 README.md
drwxr-xr-x 6 root root 4.0K Feb 26 15:34 docs
-rw-r--r-- 1 root root 626 Feb 26 15:34 requirements.txt
-rw-r--r-- 1 root root 1.3K Feb 26 15:34 mkdocs.yml
-rw-r--r-- 1 root root 382 Feb 26 15:57 Dockerfile
# cat Dockerfile
FROM python:3.9.10-slim-buster AS builder
LABEL description="This image use for mkdocs personal blog application."
LABEL maintenance=cerek
COPY . /app
WORKDIR /app
RUN pip3 install -U --no-cache-dir pip \
&& pip3 install --no-cache-dir -r requirements.txt \
&& mkdocs build -d /personal_blog
FROM nginx
COPY --from=builder /personal_blog /usr/share/nginx/html
# docker build -t mkdocs_blog:v1 .
Sending build context to Docker daemon 3.722MB
Step 1/8 : FROM python:3.9.10-slim-buster AS builder
---> 63cd0ec269b6
Step 2/8 : LABEL description="This image use for mkdocs personal blog application."
---> Running in 3b1b072178ca
Removing intermediate container 3b1b072178ca
---> e06db5f3c46d
Step 3/8 : LABEL maintenance=cerek
---> Running in eb922111d0f8
Removing intermediate container eb922111d0f8
---> 56a1b24a9d49
Step 4/8 : COPY . /app
---> ac33c2cd7979
Step 5/8 : WORKDIR /app
---> Running in b13dee709dc2
Removing intermediate container b13dee709dc2
---> 65a6388eac2b
Step 6/8 : RUN pip3 install -U --no-cache-dir pip && pip3 install --no-cache-dir -r requirements.txt && mkdocs build -d /personal_blog
---> Running in ee5a3dc7a9d7
Requirement already satisfied: pip in /usr/local/lib/python3.9/site-packages (21.2.4)
Collecting pip
Downloading pip-22.0.3-py3-none-any.whl (2.1 MB)
Installing collected packages: pip
Attempting uninstall: pip
Found existing installation: pip 21.2.4
Uninstalling pip-21.2.4:
Successfully uninstalled pip-21.2.4
Successfully installed pip-22.0.3
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv
Collecting beautifulsoup4==4.10.0
Downloading beautifulsoup4-4.10.0-py3-none-any.whl (97 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 97.4/97.4 KB 6.9 MB/s eta 0:00:00
Collecting bracex==2.1.1
Downloading bracex-2.1.1-py3-none-any.whl (10 kB)
Collecting certifi==2021.5.30
Downloading certifi-2021.5.30-py2.py3-none-any.whl (145 kB)
...
INFO - Documentation built in 0.91 seconds
Removing intermediate container ee5a3dc7a9d7
---> efb1ad302670
Step 7/8 : FROM nginx
---> c316d5a335a5
Step 8/8 : COPY --from=builder /personal_blog /usr/share/nginx/html
---> a6079e0eb270
Successfully built a6079e0eb270
Successfully tagged mkdocs_blog:v1
# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
mkdocs_blog v1 a6079e0eb270 About a minute ago 146MB
# docker run -itd --name my_web --rm -p 8081:80 mkdocs_blog:v1
e39f97dcaa72f3d13e6bbde01d7aa7e97f3583a0474dc98337b53a073a8d4d6e
# curl -I http://192.168.93.146:8081
HTTP/1.1 200 OK
Server: nginx/1.21.6
Date: Sun, 27 Feb 2022 01:02:12 GMT
Content-Type: text/html
Content-Length: 19941
Last-Modified: Sun, 27 Feb 2022 00:42:06 GMT
Connection: keep-alive
ETag: "621ac8de-4de5"
Accept-Ranges: bytes
A Docker repository is where you can store 1 or more versions of a specific Docker image. An image can have 1 or more versions (tags). A repository can be Publish
for others, also can keep it Private
for internal usage.
After the images built via docker build -t <hub-user>/<repo-name>[:<tag>]
, docker images can be pushed to Docker repository through the docker push <hub-user>/<repo-name>[:<tag>]
command.
Docker Hub repositories allow you share container images with your team, customers, or the Docker community at large. Docker images are pushed to Docker Hub through the docker push command. A single Docker Hub repository can hold many Docker images (stored as tags).
For more information that how to use Docker Hub, please check here. Register an Docker Hub account, try to build a new image by yourself, and push it to your Docker Hub repository.
Private repositories let you keep container images private, either to your own account or within an organization or team. You get one private repository for free with your Docker Hub user account (not usable for organizations you're a member of).
Or you can host the private repository via Harbor. Harbor is an open source registry that secures artifacts with policies and role-based access control, ensures images are scanned and free from vulnerabilities, and signs images as trusted. You can try Harbor with Harbor Demo Server.
If you want to host the harbor platform by yourself, follow the install instruction.