The word Docker is a British expression meaning dock worker that referes to a person who loads and unloads cargo from ships.
There're two major parts to the Docker platform:
There're several standards and governance bodies influencing the development of containers and its ecosystem. e.g.:
A typical Docker installation installs the client and the engine on the same machine, and configures them to talk to each other. Run the docker version
command to ensure both are installed and running:
The images are objects that contain everything an application needs to run. It includes an OS filesystem, the application and all dependencies. They're similar to VM templates or classes in development.
Use the docker run
command to start a new container, and attach the shell to the container's terminal; We can also use the docker attach
command to attach the shell to the container's main process for executing command inside the container.
We can use the docker attach
command to attach the shell to the container's main process for executing command inside the container.
The Dockerfile is a plain-text document that tells Docker how to build the application and dependencies into an image. For example:
We can run docker build
command to create a new image based on the instructions in the Dockerfile. Also, that's called "containerized the application" in jargon.
Docker Engine is jargon for the server-side components of Docker that run and manage containers. It's similar to ESXi in VMware.
The Docker Engine had two major components:
But now, Docker replaced LXC with its own tool libcontainer. And the Docker Dameon are break apart the features.
runc (r)
and containerd (c)
Docker and Kubernetes both use runc
as their default low-level runtime, and both pair it with the containerd
high-level runtime:
containerd
operates as the high-level runtime managing lifecycle eventsrunc
operates as the low-level runtime executing lifecycle events by interfacingMost of the time, containerd is paired with runc as its lowlevel runtime. However, it uses shims
that make it possible to replace runc
with other low-level runtimes.
docker run
docker run
When we run commands like docker run
, the Docker client converts them into API requests and sends them to the API exposed by the Docker daemon.
/var/run/docker.sock
on Linux or \pipe\docker_engine
on Windows.Sometimes, we call this daemonless containers.
shim
ComponentThe Docker Engine uses shims in between containerd and the OCI layer. It brings the following benefits:
The containerd forks a shim and a runc process for every new container. Each runc process exits as soon as the container starts running, leaving the shim process as the container's parent process.
An image is a read-only package containing everything we need to run an application. This means they include application code, dependencies, a minimal set of OS constructs, and metadata.
The image registries contain one or more image repositories, and image repositories contain one or more images:
Most of the popular applications and operating systems have official repositories on Docker Hub. Their URLs that exist at the top level of the Docker Hub namespace.
A fully qualified image name include the registry name, user name, organization name, repository name, and tag. (Docker will automatically populates the registry and tag values if we don't specify them).
Images are made by stacking independent layers and representing them as a single
unified object. Note that images are build-time constructs, whereas containers are run-time constructs.
We can use the docker inspect
or docker history
command to inspect layer information. Also, when we pull the images, each line ending with Pull complete
represents a layer.
Note that
docker history
command shows the build history of an image and is not a strict list of layers in the final image. For example, some Dockerfile instructions (ENV
,EXPOSE
,CMD
, andENTRYPOINT
) only add metadata and don’t create layers.
Under the hood, Docker uses storage drivers to stack layers and present them as a unified filesystem and image. Almost all Docker setups use the overlay2 driver, but zfs, btrfs and vfs are alternative options.
docker buildx imagetools inspect alpine:latest
, we can see the different architectures supported behind the given image tag.The Containers are run-time instances of images, it's designed to be stateless, ephemeral and immutable. The containers should only run a single process and we use them to build microservices applications.
The VM models virtualized hardware. When the hypervisor boots, it claims all hardware resources such as CPU, RAM, Storage and Network Adapters. Once we have the VM, we install an OS and then an application.
The Container models virtualize operating systems. To deploy an application, we ask Docker to create a container by carving up OS resources into virtual versions.
There are three ways you can tell Docker how to start an app in a container:
ENTRYPOINT
instruction in the image.CMD
instruction in the image.The ENTRYPOINT
and CMD
instructions are optional image metadata that store the command Docker uses to start the default app.
ENTRYPOINT
instructions cannot be overridden on the CLI, and anything you pass in via the CLI will be appended to the ENTRYPOINT
instruction as an argument.CMD
instructions can be overridden by CLI arguments.The Container restart policies are a simple form of self-healing that allows the local Docker Engine to automatically restart failed containers. Docker supports the following four policies:
Restart Policy | non-zero exit code | Zero exit code | docker stop | daemon restarts |
---|---|---|---|---|
no |
N | N | N | N |
on-failure |
Y | N | N | Y |
always |
Y | Y | N | Y |
unless-stopped |
Y | Y | N | N |
The basic flow of containering applications is shown below:
Let's start with a simple Node.js web application that serves a web page on port 8080
. Newer versions of Docker support the docker init
command that analyses applications and automatically creates Dockerfile
that implement good practice:
The process created a new Dockerfile
and placed it in the current directory:
Then use the docker build
command to build the application into a container image. Don't forget the trailing period .
as this tells Docker to use the current working directory as the build context.
In the Dockerfile, all non-comment lines are called instructions or steps and take the format <INSTRUCTION> <arguments>
. Some instructions (e.g. FROM
, RUN
, COPY
and WORKDIR
) create new layers, whereas others (e.g. EXPOSE
, ENV
, CMD
and ENTRYPOINT
) add metadata.
The container images should only contain the stuff needed to run the applications in production.
That's why we need multi-stage builds —— Multi-stage builds use a single Dockerfile with multiple FROM
instructions. And each FROM
instruction represents a new build stage.
There're 4 FROM
instructions, and each of these is a distinct build stage. Docker will number them starting from 0
. By using the multi-stage builds, each stage ouputs an intermediate image that later stages can use. However, Docker deletes them when the final stage completes.
We can also build multiple images from a single Dockerfile. Docker makes it easy to create a separate image for each by splitting the final prod
stage into two stages as follows:
With a Dockerfile like this, we can use the docker build
command and give that which of the two final stages to target for the build.
Behind the scenes, Docker's build system has a client and server:
We can configure Buildx to talk to multiple Buildkit instances. Each instance called "builder", and the builders can be the local machine, cloud computing engine instance or Docker's Build Cloud.
We can use the following command to see the builders we have configured on the system.
We can use the docker build
command to build images from multiple architectures, including ones different from the local machine. We can use the docker buildx create
command to create a new builder which uses the docker-container
driver:
apt
with --no-install-recommends
flag.The modern cloud-native applications are combining lots of small services. That's the microservices applications. We can use the docker compose
command to deploy and manage the application with the compose file in YAML format.
There is also a Compose Specification driving Compose as an open standard for defining multi-container microservices applications. The specification is community-led and kept separate from the Docker implementation to maintain better governance and clearer demarcation.
The directory ddd-book/multi-container
is the build context and contains all the application code, configuration files needed to deploy and manage the application:
app
folder contains the application code, views, and templates.Dockerfile
describes how to build the image for the web-fe service.requirements.txt
file lists the application dependencies.compose.yaml
file is the Compose file that describes how the app works.The application with two services web-fe
and redis
, a network counter-net
and a volume counter-vol
.
Docker Compose use the compose files in YAML file to define microservices applications. It should be named as compose.yaml
or compose.yml
in convention:
Connecting both services to the counter-net
network means they can resolve each
other by name and communicate. This is important, as the following extract from the
app.py file shows the web app communicating with the redis service by name:
Run the following command to shut down the application. Docker removes both the containers and the networks. However, the volume still exists, including the data stored in it:
To check the current state and the network ports of the application, just use the docker compose ps
command; Moreover, run the docker compose top
command to list the processes inside each container:
The PID
numbers returned are the PID
numbers as seen from the Docker Host instead of the containers.
Both the docker compose down
command and the docker compose stop
command halt Docker Compose applications, but in different ways:
docker compose down
will stop all containers and deletes associated networks, volumes (when using -v
), and optionally images (when using --rmi
). This fully removes the application and its resources.docker compose stop
only stops the containers, leaving networks, volumes, and the containers themselves intact. The application can be quickly restarted later using docker compose start.Running the following command to stop and delete the application. The --volumes
flag will delete all of the app's volumes, and the --rmi all
will delete all of its images.
Docker Swarm is not only an enterprise-grade cluster of Docker nodes, but also an orchestrator of microservices applications.
A swarm is one or more Docker nodes that can be physical servers, VMs, cloud instances or even Raspberry Pi. The only requirement is that they all run Docker and can communicate over reliable networks.
Every node in a swarm is either a manager or a worker:
Swarm uses TLS to encrypt communications, authenticate nodes, and authorize roles (managers and workers). It also configures and performs automatic key rotation.
Run the following commands to create 5 VMs running Docker. Then named 3 of the nodes as mgr1
, mgr2
and mgr3
, and named the other 2 wrk1
and wrk2
:
Before a Docker node joins a swarm, it runs in single-engine mode and can only run regular containers. After joining a swarm, it switches into swarm mode and can run advanced containers called swarm services. To initialize a swarm:
mgr1
.wrk1
and wrk2
as worker nodes.mgr2
and mgr3
as additional managers.Step 01. Log on to mgr1
and initialize a new swarm.
--advertise-addr
flag is optional and tells Docker which of the node's--listen-addr
flag tells Docker which of the node's interfaces to accept--advertise-addr
if you--advertise-addr
is a load balancer, you must--listen-addr
to specify a local IP.Step 02. List the nodes in the swarm. See the tokens needed to add new workers and managers.
Step 03. Log on wrk1
and wrk2
then join them as worker nodes.
Step 04. Log on mgr2
and mgr3
then join them as managers.
Step 05. List the nodes in the swarm.
2377/tcp
, 7946/tcp,udp
, 4789/udp
MANAGE STATUS
column are work nodes.Swarm Clusters are highly available (HA) —— one or more managers can fail and the swarm will keep running. It implements active/passive multi-manager HA:
Leader and follower is Raft terminology. Swarm implements the The Raft Consensus Algorithm to maintain a consistent cluster state across multiple highly-available managers.
Here are the good practices apply when it comes to manager HA:
Consider the following situations:
Swarms automatically configures a lot of security features, but restarting an old manager or restoring an old backup can potentially compromise the cluster. Hence, we should use Swarms's autolock feature to force restarted managers to present a key before being admitted back into the swarm:
Support for Wasm is a beta feature in Docker Desktop. To configure Docker Desktop for Wasm:
Once installed Rust, run the following command to install the wasm32-wasi
target so that Rust can compile to Wasm:
Spin is a Wasm framekwork and runtime that makes building and running Wasm applications easy. Just search the web for "How to install fermyon spin" and follow the instructions for our system.
Wasm is a new type of application that is smaller, faster, and more portable than traditional Linux containers. It's a new virtual machine architecture that programming languages compile to.
Docker Desktop already ships with several Wasm runtimes:
We can use spin to create a simple web server:
After edited the src/librs
file, we can run the spin build
command to compile the application as a Wasm binary. Then the application could be run on any system with the spin runtime.
As always, we need a Dockerfile that tells Docker how to package the application as an image. Just create a new file called Dockerfile
in current directory:
Then run the following commands to containerize the Wasm application:
Docker networking is based on libnetwork, which is the reference implementation of an open-source architecture called the Container Network Model (CNM):
The single-host bridge network is the simplest type of Docker Network. The bridge network only spans a single Docker Host, and it's an implementation of an 802.1d
bridge (layer 2 switch).
bridge
that Docker connects new containers to, unless we override it with the --network
flag.bridge
network on all Linux-based Docker Hosts is called bridge
and maps to an underlying Linux bridge in the host's kernerl called docker0
.bridge
driver. If you run Windows containers, you'll need to use the nat
driver.Run the following command to create a new single-host bridge network:
If we add more containers to the localnet
network, they'll all be able to communicate using names. That's because Docker automatically registers container names with an internal DNS service. The exception to this rule is the built-in bridge
network that doesn't support DNS resolution.
Containers on bridge networks can only communicate with other containers on the same network. By mapping containers to ports on the Docker host, we can access the container external.
macvlan
Networkmacvlan
DriverThe build-in macvlan
driver (transparent
if using Windows containers) gives every container its own IP and MAC address on the external physical network. As it doesn't require port mappings or additional bridges, the performance is good.
macvlan
driver makes containers visible on external networks.macvlan
NetworkAssume we have the network with two VLANs, and then add the Docker Host connected to the network. To attach a container to VLAN 100, we need to create a new Docker network with macvlan
driver and configure: Subnet Info, Gateway, Range of IPs can be assigned, the Host's Interfaces to use:
Note that the Docker macvlan
driver also supports VLAN trunking. It means we can create multiple macvlan
networks that connect to different VLANs.
~\AppData\Local\Docker
.init
system been used. If systemd
been used, Docker will post logs to journald
(check with journalctl -u docker.service
command).We can also tell Docker how verbose we want daemon logging to be. By editing the daemon config file at /etc/docker/daemon.json
. Be sure to restart Docker after making any changes.
We can normally view container logs with the docker logs
command. For Docker Swarm, we should use the docker service logs
command.
json-file
and journald
are the easiest to configure. Both of them work with docker logs
and docker service logs
commands.--log-driver
and --log-opts
flags to override the settings in daemon.json
.The libnetwork
framework also provides service discovery that allows all containers and Swarm services to locate each other by name. The only requirement is that the containers be on the same network.
Assume the container c1
pinging another container c2
by name:
c1
container issues a ping c2
command. The container's local DNS resolver checks its cache to see if it has an IP address for c2
. Note that All Docker containers have a local DNS resolver!c2
, so it initiates a recursive query to the embedded Docker DNS server. Note that All Docker containers are pre-configured to know how to send queries to the embedded DNS server.--name
or --net-alias
flags.c2
container to the local resolver in the c1
container.c1
container sends the ping request (ICMP echo request) to the IP address of c2
.Besides, we can use the --dns
flag to start containers and services with a customized list of DNS servers, and the --dns-search
flag to add custom search domains for queries against unqualified names. Both of these useful if the applications query names outside the Docker environment. It will add entries to the container's /etc/resolv.conf
file.
Docker Swarm supports two ways of publishing services to external clients:
Docker creates containers by stacking read-only image layers and placing a thin layer of local storage on the top. This allows multiple containers to share the same read-only image layers:
The local storage layer (the thin writeable layer, ephemeral storage, read-write storage or graphdriver storage) is coupled to the container's lifecycle, it gets created when the container created, and deleted when the container deleted. It's not a good place for data persistant.
Docker keeps the local storage layer on the Docker Host's filesystem:
/var/lib/docker/<storage-driver>/...
C:\ProgramData\Docker\windowsfilter\...
We should treat containers as immutable objects, and never change them once deployed. If we need to fix or change the configuration of a live container, we should create and test a new container with the changes and then replace the live container with new one.
We can create volumes and mount them into the containers. When we mount the volumes, they could be a directory in the containers' filesystem, and anything we write to the directory gets stored in the volume:
The volumes are first-class objects in Docker. There's a docker volume
sub-command and a volume resource in the API. Let's create a new volume by following commands:
local
driver. We can also use the -d
or --driver
flag to specify a different driver.Mountpoint
tells us where the volume exists in the Docker Host's filesystem.docker volume prune
command deletes all volumes not mounted into a containerWe can also deploy volumes via Dockerfiles by using the VOLUME
instruction with the format VOLUME <container-mount-point>
. But we can't specifiy a host directory when define volumes in Dockerfiles.
We can specified the --mount
flag when using the docker run
command, to tell Docker for mounting a volume. If we sepcify a volume that already exists, Docker will use it; while the volume doesn't exist, Docker will create it.
Furthermore, integrating Docker with external storage system lets us present shared storage to multiple nodes.