
On this post we will present a basic introduction to Docker. By the end you should have an understanding of its main concepts and usage scenarios.
First, let’s establish some groundwork on virtualization.
Hypervisor virtualization
In this mode, one or more independent machines run virtually on physical hardware via an intermediation layer.
Each virtual machine has its own operating system. This provides the possibility of the guest operating system being completely different than the host’s operating system, at the cost of additional overhead since we are running one kernel on top of another.
Common tools that follow this approach are VirtualBox and VMWare.
Operating system-level virtualization (also called container virtualization)
In Operating system-level virtualization, several isolated user space instances (called containers) run on top of a host operating system’s kernel. Each user space instance consists of a set of processes which are isolated in such a way that from the point of view of the processes inside the container, they are the only processes running on the system.
Containers do not require an emulation layer or a hypervisor layer to run and instead use the operating system’s normal system call interface. In other words, the processes that run inside a container are just normal processes in the host kernel (you can see them in the host with ps). This reduces the overhead required to run containers and can allow a greater density of containers to run on a host, since there is only once kernel involved.
Since containers use the underlying kernel, they are limited to run applications which are compatible with said kernel.
Docker is an example of a tool based on this approach.
What is Docker?
Docker is an open-source virtualization engine based on operating system-level virtualization. It provides tools that allow to create containers by assembling applications (called packages in Docker terminology).
Relationship with Kubernetes
While both Docker and Kubernetes are tools for container automation, they operate at different levels.
Docker is a tool for creating a container by combining a set of packages.
Kubernetes is a tool for creating a muti-application system by combining containers. For this reason, it’s common for large software projects to use both tools in combination, since the latter is based on the former.
Installation
In order to play around and do tests with Docker on your local machine you will need to install it. For this we will refer you to the official documentation on the matter, which you can find here.
Also, if you are working on a Linux system, it is practical to set up Docker so that you can run docker commands without using sudo, by adding your user to the docker group. You can find the instructions on how to do that here. Keep in mind this makes it easier to play around in your development environment, but should be done with caution when in a production environment. Any user that belongs to the docker group can run Docker without sudo.
In the sections that follow, all command examples will be provided without sudo.
Once you complete the installation, try the following command which shows general information about the docker daemon running on your host:
$ docker info
If you see an empty table in the output, you are all set! Docker is successfully installed on your system. The output is empty because we haven’t created any containers yet. We will get to that shortly. Let’s go over some concepts first.
Getting help
The first docker command you should know is the help command, which is used to access the official documentation:
$ docker help <command>
For example, to read the documentation of the docker run command, use:
$ docker help run
or alternatively:
$ man docker-run
Components of Docker
The Docker installation is composed of a root-privileged daemon (dockerd) that takes care of operations that can’t be performed by normal users, and a client program (the docker binary) which takes commands from the user and executes them by sending them to the daemon.
Usually when we run docker commands we are working against the locally running daemon. This is the default and it’s what the docker client does when we don’t specify a daemon address explicitly. We can connect to a daemon running remotely by specifying its address to the docker client, like so:
$ docker -H tcp://<daemon_ip_address>:2375
We can also specify the address of the daemon using the DOCKER_HOST environment variable:
$ export DOCKER_HOST="tcp://<daemon_ip_address>:2375"
docker <arguments>
Containers, Images, Tags, Repositories and Registries
Containers in Docker have an id and a name. Both of these values are unique and can be used interchangeably in commands that take a container as argument.
A Docker image is a read-only template that contains a set of instructions and file system data necessary for creating a container that can run on the Docker platform. Every container is created from a given image.
It’s possible to create an image by starting from an existing image and adding things on top of it. When we do this, the image we start from is called the base, and the image we create is said to have two layers. Note any image can be used as a base to build another image.
Images are identified by an image id. An image can have any number of tags, which are labels that help to differentiate image versions. The docker images command lists the images that are stored on the local cache. Note that the output of this command may show the same image on several lines, one line per each tag. What the command is listing is really tags, not images.
Images live on repositories. A repository is like a folder containing several images. In docker commands that accept an image as parameter, it’s possible to specify them as <repository_id>:<image_tag> or simply <repository> where the tag is understood to be latest. Repositories don’t need to be created explicitly, instead they are “created” implicitly when the first image referencing them is created. It’s common for repository names to begin with the username of their owner, e.g. jamtur01/apache2, but this is not mandatory.
Repositories live on registries. A registry is a server containing many repositories. The default registry is the public registry managed by Docker, Inc., Docker Hub.
Docker and the Union file system
A Union File System is a mechanism that allows to create a file system by stacking several file systems on top of each other. When two file systems have a file in the same path, the topmost version (i.e. most recently mounted) is the one that is used, and the others are “hidden” as if they didn’t exist.
Each docker container has an isolated file system able to hold its own state. This is accomplished by giving each container a Union File System. The base is a boot file system, which contains the files necessary to boot. On top of that Docker adds a root file system, which contains the files that make up a given Linux distribution (e.g. a Debian or Ubuntu file system). The root file system is mounted read-only, and Docker mounts a read-write file system on top of the root.
Docker images are also based on a Union File System. When a container is created, the layers of the image it is created from become the layers of the container’s file system. The initial read-write layer of the container is empty. Note the data in the image’s files is not duplicated. You can have several containers created from the same image and all the common files will be stored only once. When a file is modified, it is copied from the read-only layer into the read-write layer on top of it. The read-only version of the file still exists but it is hidden underneath the copy.
This pattern is usually called “copy on write”, and it is one of the reasons Docker containers are more lightweight that full-fledged virtual machines.
Basic workflow
In this section we will go over a simple example that illustrates the life cycle of a container.
As we said above, in order to create a container we need to specify an image to create it from. We use the docker images command to list the images currently installed in our local cache:
$ docker images
Since we haven’t downloaded any images yet, the output will show an empty table.
We can search for images in the Docker Hub registry with the docker search command:
$ docker search ubuntu
We use the docker pull command to download images to our host:
$ docker pull <repository>:<tag>
The tag can be omitted, in which case docker will default to the tag latest. Let’s download the ubuntu image in its latest version:
$ docker pull ubuntu
Once the command finishes the download, you will be able to see the Ubuntu image listed in your local cache:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu latest 8e428cff54c8 4 months ago 72.9MB
You can use the docker inspect command to get details about the image:
$ docker inspect ubuntu
This displays a json document showing information such as the creation date of the image, its parent (if any), its author, etc.
We use the docker run command to create and start a container:
$ docker run -i -t --name <container_name> <image> <command>
The first thing to understand about the run command, is that it does several things at once. The above command will create a container, start it, attach to it, and run the given command inside the container. Attaching to a container allows you to view its ongoing output or to control it interactively, as though the commands were running directly in your terminal. You can attach to the same contained process multiple times simultaneously, from different sessions on the Docker host.
The -i option instructs docker to keep the standard input of the container open in our terminal, even if the container is detached. The -t option tells docker to allocate a pseudo-tty, that connects our terminal (running on the host) to the process running inside the container. For interactive processes (like a shell), you must use -i -t together in order to allocate a tty for the container process. -i -t is often written -it.
The --name option specifies the name for the container. This option can be omitted, and docker will assign a random name.
The image can be specified with an image id, or with a <repository>:<tag> combination.
The <command> argument can be omitted, which will make docker start the container with the image’s default command (more on how this default command is specified later). Note that when we specify the command, the path we are giving refers to the container’s internal file system.
For example, let’s start a container from the ubuntu image, run bash inside it, and attach to the bash process from our terminal:
$ docker run -it ubuntu /bin/bash
This will attach your terminal to the bash process running inside the container. You can enter some commands and see their output in your terminal.
Now in another terminal, enter the following command:
$ docker ps
You will see an output similar to this:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
707a2d53267a ubuntu "/bin/bash" 4 seconds ago Up 1 second trusting_hypatia
The docker ps command shows the containers that are running on the host. Note the output shows, among other things, the container id, which we will use as input to other commands that operate on containers.
You can also use docker container ls as an alternative to the docker ps command.
You can list the processes running inside the container with the docker top command:
$ docker top <container_id>
You will see an output similar to this:
UID PID PPID C STIME TTY TIME CMD
root 8064 8026 0 18:50 pts/0 00:00:00 /bin/bash
You can see the recent output of a container with the docker logs command:
$ docker logs <container_id>
You can add the -f option to continue monitoring the output in real time.
When a container is running, it can be paused with the docker pause command (get the container id from the output of docker ps):
$ docker pause <container_id>
If you go back to the terminal where you started the container, you will notice that it appears to be blocked, not reacting to text input. When a container is paused, its processes are frozen in memory and don’t receive the CPU. Let’s resume the container with the docker unpause command:
$ docker unpause <container_id>
This puts the process to run again, picking up at the point where it left off. Note that the bash process remained in memory even when it was paused, so it retains all its state.
Now, in a different terminal from the one where you started the container, enter the following command:
$ docker stop <container_id>
The docker stop command sends a SIGTERM signal to the main process running inside the container, and after a grace period, if the processes hasn’t finished, it sends a SIGKILL.
As an alternative, you can also stop a container with docker kill <container_name>. This is analogous to stop, but doesn’t send the SIGTERM and instead goes straight to SIGKILL. The kill command is also useful to send arbitrary signals to the container, e.g. we could send a SIGHUP like so:
$ docker kill -s=SIGHUP <container_id>
After stopping the container, if you try the docker ps again, you will see that the container no longer shows. The container still exists, but it’s not running anymore. When the bash command finished its execution, the container went into stopped state. We can still see it though, by adding the -a option in docker ps:
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
707a2d53267a ubuntu "/bin/bash" 6 minutes ago Exited (0) 2 minutes ago trusting_hypatia
You can restart the host and the container will remain there, in stopped state. If you made any changes to the container’s internal file system while it was running, these changes will remain too, persisted within docker’s local cache.
You can get detailed information about a container with the docker inspect command (note, the container doesn’t need to be running):
$ docker inspect <container_id>
This provides a json document showing details such as the container’s state, the image it was started from, etc.
The stopped container can be started again at any time with the start command:
$ docker start <container_id>
When you run this you will notice it looks like nothing has happened. However, a docker ps will show you that the container is again in running state. We don’t see anything because we are not attached to it, so we will need to attach again. We didn’t need to attach explictly before because when we started the container for the first time, we did it with the docker run command, which attaches automatically.
To attach to the container, use the docker attach command:
$ docker attach <container_id>
To definitely destroy the container, we go to another terminal, stop the container again an then use the docker rm command:
$ docker stop <container_id>
$ docker rm <container_id>
This will remove all persistent data belonging to the container. If you do a docker ps -a, the container will not show anymore. Note we must stop the container before destroying it. If we attempt to destroy a running container, we will get an error message.
You can also start a container without attaching to it. For this, we use the -d option in docker run:
$ docker run -d ubuntu /bin/sh -c "while true; do echo This is running inside the container; sleep 1; done"
This will create the container and start it, but without attaching to it. The container will continue running its command in the background. Note we have used a long running command in this example. If we had used bash as command, bash would have ended as soon as starting due to not having a terminal to read as input, and the container would have stopped after the termination of its command.
You can attach to a running container from any other terminal. Use docker ps to get the container id, then try the following:
$ docker attach <container_id>
You terminal will start showing the output of the loop that is running inside the container.
You can attach to the same container from any number of terminals. All of them will be able to provide input to the container, and its output will show in all of them.
In one of the attached terminals, you can use Ctrl-C to stop the loop, which will send the container to Stopped state. After this, you can destroy the container with docker rm as we did above.
Container states
The workflow above gives you an informal overview of the states a container can go through during its life cycle. Let’s go into a little more detail. Once a container is created, it can transition between the following states:
- Created
- The initial state. A container starts out in this state when it is created with
docker create, and can transition out of this state by being started withdocker start. Once a container has been started, it never goes back to created, instead it will alternate between Running, Stopped and Paused. It’s common to create containers usingdocker run, which is analogous to doing acreatefollowed by astart.
- The initial state. A container starts out in this state when it is created with
- Running
- The state a container goes to when it is started with
docker startordocker run. These operations launch a process inside the container. - A paused container also transitions into Running with
docker unpause. - A running container occupies spaces in memory due to its running processes, and also on the hard drive due to its file system state.
- The state a container goes to when it is started with
- Stopped:
- The state a running container goes to when it is stopped with
docker stop. When a container is stopped, its main process is terminated with SIGTERM and then after a grace period, SIGKILL. - When a container is stopped, its processes are terminated. The container’s file system is persisted across restarts of the host and it can be started again at any time or destroyed with
docker rm. - A stopped container does not occupy memory since it has no processes running, but it does occupy space on the hard drive due to its file system state.
- The state a running container goes to when it is stopped with
- Paused
- The state a running container goes to when it is paused with
docker pause. When a container is paused, its main process is suspended with SIGSTOP. - When a container is paused, its processes remain in memory, but the kernel will not give them CPU time until the container is unpaused. The container’s file system is persisted across restarts of the host and it can be started again at any time.
- A paused container occupies spaces in memory due to its processes, and also on the hard drive due to its file system state. In this way the Paused state is similar to Stopped, with the difference being that when the container is Paused, its processes are still loaded in memory ready to resume execution, whereas in Stopped state the container has no processes running.
- A Paused container can be unpaused with
docker unpause. This resumes execution of the processes at the point where they were at when the container was paused, and transitions the container into Running state.
- The state a running container goes to when it is paused with
Building docker images
The ability to automate the creation of container images is one of the main features of Docker. There are two approaches to creating docker images:
- Using the commit command: this starts from a running container and creates an image by taking a snapshot of its filesystem state.
- Using a Dockerfile: this starts from a metadata file that describes what the image is made of (its layers) and creates an image by processing it. This is the method that lends itself best to automation and reuse.
Next we will study both methods with more detail.
Building images using the commit command
Create an image by taking a snapshot of a container and save it in our local image cache:
$ docker commit <container_id> <repository_id>
The commit command creates an image by taking a snapshot of a container and saving it in our local image cache. This command will assign the image id automatically and will assign the new image the tag latest. It’s also possible to assign the image id and tags explicitely via arguments to the commit command.
By default, the commit command will pause the container right before taking the snapshot, to ensure the files are saved in a consistent state.
It’s worth noting that what this command is actually saving is not the entire filesystem of the container, but the differences between the current state of the container and its base image
Building images using a Dockerfile (recommended approach)
The other way to build an image in docker is by creating a Dockerfile. This is the recommended approach. A Dockerfile is a file written in a Docker-specific Domain Specific Language that contains instructions to build a container proceduraly, step by step, from a “context”. The context is a directory passed as an argument at build time that must contain the Dockerfile and other files that can be used in the build. The following is an example Dockerfile from The Docker Book:
# Version: 0.0.1
FROM ubuntu:14.04
MAINTAINER John Doe "jdoe@example.com"
RUN apt-get update
RUN apt-get install -y nginx
RUN echo 'Hi, I am in your container' \
>/usr/share/nginx/html/index.html
EXPOSE 80
First off, any line that begins with # is a comment.
As you can see, the Dockerfile starts from an existing base image (in this case ubuntu:14.04) creates a container from it, runs some commands inside it, and produces an image by commiting the final state of the container. A key element to understand here is that each line in the Dockerfile is executed in an entirely new container, and once the command is executed, the container is commited, thus creating an intermediate image for each line. Each one of these intermediate images becomes a layer of the final image. This means that state like environment variables or in memory information is not preserved between lines, only files are. If a command fails, the process stops, and the last image created can be used to troubleshoot the problem.
The EXPOSE instruction tells Docker to enable port 80 for listening (we will review this instruction as well as others in more detail later).
Once we have our Dockerfile defined, we can tell docker to execute it with the following command:
$ docker build <path_to_context>
The above command will:
- Look for a file named
Dockerfilein the given path. - Send the contents of said path to the docker daemon.
- The docker daemon will then execute the steps of the Dockerfile one by one, creating a new container for each, and commiting a new image in each step, as described above.
You can add the option -t="<output_repository>/<output_tag>" to the docker build command, to indicate the desired repository and tag of the output image.
The docker build command can also be run with a git repo as path. This causes docker to download the repo and send a copy of its contents to the daemon, as build context.
The output of the build command will look something like this:
$ docker build -t="jdoe/static_web" .
Sending build context to Docker daemon
2.56 kB
Sending build context to Docker daemon
Step 0 : FROM ubuntu:14.04
---> ba5877dc9bec
Step 1 : MAINTAINER John Doe "jdoe@example.com"
---> Running in b8ffa06f9274
---> 4c66c9dcee35
Removing intermediate container b8ffa06f9274
Step 2 : RUN apt-get update
---> Running in f331636c84f7
Step 2 : RUN apt-get update
---> Running in f331636c84f7
---> 9d938b9e0090
Removing intermediate container f331636c84f7
Step 3 : RUN apt-get install -y nginx
---> Running in 4b989d4730dd
---> 93fb180f3bc9
Removing intermediate container 4b989d4730dd
Step 4 : RUN echo 'Hi, I am in your container'
>/usr/share/ ↩
nginx/html/index.html
---> Running in b51bacc46eb9
---> b584f4ac1def
Removing intermediate container b51bacc46eb9
Step 5 : EXPOSE 80
---> Running in 7ff423bd1f4d
---> 22d47c8cb6e5
Successfully built 22d47c8cb6e5
The lines like Running in b8ffa06f9274 indicate the id of the temporary container that was created to execute that step. The lines like ---> 4c66c9dcee35 show the id of the image that was created after successfully executing the step. If the build fails, you can use these image ids to create a container for debugging and manually run the step that failed inside it.
Once the docker build command finishes, you can use the docker history command to trace back the list of intermediate images that were created during the execution of the Dockerfile:
$ docker history <image_id>
Image building and the image cache
When Docker builds a dockerfile, each instruction results in an image that gets stored in the image cache. If you run the build multiple times, Docker will reuse the intermediate images stored in the cache and not build them again, unless the instruction that originated the image has changed in the Dockerfile since the time when the image was built. In that case, Docker will rebuild that instruction and all instructions that follow it.
You can take advantage of this by putting an innocuous instruction at the beginning of the Dockerfile that you update every time you want to trigger a full rebuild. Take this example from “The Docker Book”:
FROM ubuntu:14.04
MAINTAINER John Doe "jdoe@example.com"
ENV REFRESHED_AT 2021-08-18
RUN apt-get -qq update
Every time you update the dockerfile, simply update the date in the ENV instruction and docker will trigger a full rebuild of all instructions that come after it.
Next we will go into detail about some of the most used Dockerfile instructions.
Declaring ports
To declare that the application listens on a given port number, use the EXPOSE instruction:
EXPOSE <port_number>
The EXPOSE instruction does not actually publish the port. It functions as a type of documentation between the person who builds the image and the person who runs the container, about which ports are intended to be published.
To actually publish the port when running the container, you must use the -p flag on docker run . When you run a container, Docker has two methods of assigning ports on the Docker host:
- Docker can randomly assign a high port from the range 49000 to 49900 on the Docker host that maps to port 80 on the container. Example:
docker run -p 80 <image_id>- You can see what host port was assigned with the
docker pscommand, or also withdocker port <container_id> <container_port>
- You can specify a specific port on the Docker host that maps to port 80 on the container. Example:
docker run -p 2000:80 <image_id>- This maps port 2000 of the host to port 80 of the container.
You can also use the shortcut -P in docker run which publishes all ports that are present in EXPOSE instructions in the dockerfile, mapping them to random port numbers on the host.
Networking in docker
Within a host, there can be several docker networks, which are created with the docker network create command. Each container can be part of several networks. A container is connected to a network with the docker network connect command.
The networks a container is connected to determine what other containers it can talk to. Containers within the same network can talk to each other via their container-level ports. The host can talk to the containers via their host-level ports. Containers in separate networks are isolated and cannot talk to each other.
Setting persisting and single-command environment variables
To set an environment variable in an image in such a way that all subsequent containers created from it in the dockerfile will have the variable defined, use the ENV instruction:
ENV <key> <value>
To set an environment variable for a single command, add it inline in the command. Example:
RUN DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -y ...
Specifying the command the container should run
Every docker container has a command that it runs when it starts. There are two approaches to specify this command:
- Using the
CMDinstruction in the Dockerfile. - Using the
ENTRYPOINTinstruction in the Dockerfile.
Note, the Dockerfile must contain either one of those instructions. It’s also possible to use both as we’ll see later.
To use the CMD instruction, include a line in the Dockerfile like the following:
CMD ["executable","param1","param2"]
Example:
CMD ["/bin/bash", "-l"]
When the image provides a CMD instruction, anything you provide in the docker run command after the image will be interpreted as <command> <arguments>. If you don’t provide any arguments after the image, the command and arguments will be taken from the CMD instruction.
In the example above, if you run the container with docker run <image_id> /usr/sbin/nginx -g "daemon off;" the container will the run the command /usr/sbin/nginx -g "daemon off;". If you run the container with docker run <image_id>, it will run /bin/bash", "-l. As you can see, the CMD instruction provides a default command with its arguments, and allows to override both the command and the arguments when calling docker run.
To use the ENTRYPOINT instruction, include a line in the Dockerfile like the following:
ENTRYPOINT ["/usr/sbin/nginx"]
When the image provides an ENTRYPOINT instruction, anything you provide in the docker run command after the image will be interpreted as arguments to the executable in the ENTRYPOINT. The executable is fixed to what was specified in the ENTRYPOINT instruction and cannot be overiden. If you don’t provide any arguments after the image, the executable in the ENTRYPOINT will be run without arguments.
Consider this example:
ENTRYPOINT ["/usr/sbin/nginx"]
Now if you run the container like so:
docker run -t -i <repo_name>:<image_tag> -g "daemon off;"
This will have the effect that the container will run /usr/sbin/nginx -g "daemon off;"
You can also use ENTRYPOINT with a CMD of the form CMD ["param1","param2"] to specify default arguments. When both are present and we run the container with arguments, the command defined in ENTRYPOINT is executed with those arguments, but if no arguments were given, the arguments in the CMD are used as arguments to the executable in the ENTRYPOINT. This can be used to define a default behavior of the container when no arguments were given.
Note: when specifying the command to run in the container, the command must be such that it runs in the foreground, because Docker is designed around this approach. This is usually not a problem, but it’s a concern in some cases like nginx, which be default, forks itself to start a daemon and the the original process stops. For cases like these, you’ll need to configure the process to run in the foreground via its configuration files.
Copying files from the build environment to the container
To copy files and directories from the build environment to the container’s file system, use the ADD instruction. Examples:
# Copy a file into a given destination directory (destination ends in a slash)
ADD nginx/global.conf /etc/nginx/conf.d/
# Copy a file to a given destination path (destination ends without a slash)
ADD nginx/nginx.conf /etc/nginx/nginx.conf
Note the different interpretation of the destination path depending on whether it ends in a slash or not.
The paths of source files in the ADD instruction are interpreted as relative to the context directory given at build time.
It’s also possible for the source to be a URL. See Dockerfile reference for more details.
Volumes
Volumes are specially designated directories within one or more containers that bypass the layered Union File System to provide persistent or shared data for Docker. Think of it like a “shared folder” from a virtualization engine like VirtualBox o VMWare. Volumes are not included when we commit or build an image. Volumes can also be shared between containers and can persist even when containers are stopped. Using volumes, we can create containers that persist their state in storage media that is external to the containers themselves. This is useful, for example, to containerize database engines.
A volume can be attached to a container when the container is created by using the option -v like so:
$ docker run -v <path_on_host_filesystem>:<mount_point_in_container>:ro <repository>:<image_tag> <command>
If the mount point doesn’t exist inside the container, it will be created. In this example we have specified the volume to be Read Only (ro). We could also specify it as Read Write (rw).
Logging into a container without SSH
In the container world, it’s common for a container to not run any ssh daemon inside it, thus we can’t ssh into it. However, the Linux kernel provides a tool we can use to “log in” a container without using SSH. The nsenter tool works with the kernel namespaces concept, and can start a shell inside a given container that our terminal attaches to. Example:
$ PID=$(sudo docker inspect --format {{.State.Pid}} <container>)
$ sudo nsenter --target $PID --mount --uts --ipc --net --pid
The above command requires having the nsenter tool installed. This tool can be installed via your Linux distribution’s package manager.
It’s worth noting that nsenter can be used to run any kind of process inside the container, by appending it a the end of the command above. The following example shows how to run ls inside the container:
PID=$(sudo docker inspect --format {{.State.Pid}} <container>)
sudo nsenter --target $PID --mount --uts --ipc --net --pid ls
Docker best practice: one concern per container
As a final note, we will talk about a best practice about creating containers:
Each container should have only one concern, one purpose (this usually translates to one process).
There are several advantages to this philosophy:
- It makes it possible to deploy a new version of the application by destroying the container and creating a new one, without having to shut down other services.
- It makes it easier to reuse containers.
- It makes it easier to scale applications horizontally.
- Docker’s logging mechanism is based on each container having one internal log. This lends itself better to a one concern per container approach.
Note that in practice, one concern will usually translate to one process, but this is not always the case. For example, some programs might spawn additional processes of their own accord. For instance, Celery can spawn multiple worker processes, and Apache can create one process per request.
Sources
- “The Docker Book” – James Turnbull.
- Wikipedia on Union file system: https://en.wikipedia.org/wiki/UnionFS
- Installation instructions:
- Post installation instructions:
- Wikipedia article on OS-level virtualization:
- JFrog’s beginner’s guide to Docker:
- Lifecycle of a Docker container:
- Difference between docker stop and docker pause:
- Official Dockerfile reference:
- About reasons to have a single concern per container: