Docker
Intro
-
Add user to docker group for root access on docker host:
sudo usermod -aG docker USER_NAME
-
Install Docker Machine and Docker Compose separately on Linux.
-
docker container run --publish 8080:80 --detach --name webhost nginx:1.11 nginx -T
8080
is the host listening port.--detach
runs the container in the backgroud.1.11
is the image version.nginx -T
is the CMD run on start.- Use
-e
to pass environment variables. - Use
--rm
to automatically remove the container after it exists.
What happens in “docker container run”
- Looks for that image locally in image cache, if it doesn’t find anything:
- Then looks in remote image repository (defaults to Docker Hub)
- Downloads the latest version of that image (or specific version if specified)
- Creates new container based on that image and prepares to start
- Gives it a virtual IP on a private network inside docker engine
docker container logs
: show logs of a container.docker container top
: process list.docker container inspect
: config detail.docker container stats
: performance stats for all containers.
Run bash inside centos-7 image: docker container run --rm -it centos:7 bash
.
docker container run -it
: start new container interactively.docker container run exec -it
: run additional command in existing container.
Networks
docker container port CONTAINER_ID
lists ports on each container.- Docker containers use a different IP address from host.
docker container inspect --format '{{ .NetworkSettings.IPAddress }}' CONTAINER_ID
shows the IP address of specified container. (the--format
option acts like agrep
command, but more consistent for docker config inspection.)- Docker containers can talk to each other on the virtual network without explicitly opening up their ports to host.
Virtual Network
Docker comes with the “bridge” virtual network, and by default when a Docker container is created it is added to the “bridge” network. Containers on the same virtual network can communicate with each other without restriction, container names are also served as the DNS name.
By default no port on the container is exposed to the host machine, to change that, use docker run -p [IP_EXPOSING_TO:]HOST_PORT:CONTAINER_PORT
to expose port to host through the virtual network this container is on.
DNS
Instead of using the container name as DNS name, we can give our container a specific DNS name by using --network-alias DNS_NAME
when starting a container. This way we can assign multiple containers to the same DNS name, and by default all network requests sent to the same DNS name would be routed to different containers using (random) Round Robin method.
# Create a network for elastic search containers to attach to.
docker network create searchers
# Create two elasticsearch 2 containers, and them to "searchers" network, and give them the same DNS name "search".
docker container run --net searchers --network-alias search -d --name els1 elasticsearch:2
docker container run --net searchers --network-alias search -d --name els2 elasticsearch:2
# Use alpine to lookup the DNS of "search".
docker container run --rm --net searchers alpine nslookup search
# Run following command multiple times to recieve different results.
docker container run --rm --net searchers centos curl -s search:9200
Container Images
A version of image can have multiple tags, latest
is a special tag. Since multiple tags can point to the same image (same content hash), running containers with different tags that points to the same version uses the same image instead of downloading it multiple times.
Image Layers
A Docker image is made up of layers of file system change history and metadata. We can use docker history IMAGE
to inspect different layers of the image. Not all layer causes a file size change. Each layer has its own unique hash, so if two images uses a same layer, instead of downloading it again, Docker is able to reuse the same layer for both images.
docker history nginx:latest
# Output
# IMAGE CREATED CREATED BY SIZE COMMENT
# 5ad3bd0e67a9 10 days ago /bin/sh -c #(nop) CMD ["nginx" "-g" "daemon… 0B
# <missing> 10 days ago /bin/sh -c #(nop) STOPSIGNAL SIGTERM 0B
# <missing> 10 days ago /bin/sh -c #(nop) EXPOSE 80 0B
# <missing> 10 days ago /bin/sh -c ln -sf /dev/stdout /var/log/nginx… 22B
# <missing> 10 days ago /bin/sh -c set -x && addgroup --system -… 57.5MB
# <missing> 10 days ago /bin/sh -c #(nop) ENV PKG_RELEASE=1~buster 0B
# <missing> 10 days ago /bin/sh -c #(nop) ENV NJS_VERSION=0.3.8 0B
# <missing> 10 days ago /bin/sh -c #(nop) ENV NGINX_VERSION=1.17.8 0B
# <missing> 5 weeks ago /bin/sh -c #(nop) LABEL maintainer=NGINX Do… 0B
# <missing> 5 weeks ago /bin/sh -c #(nop) CMD ["bash"] 0B
# <missing> 5 weeks ago /bin/sh -c #(nop) ADD file:04caaf303199c81ff… 69.2MB
Relationship between Container and Image
The file system inside a container seems to be just a normal file system, but it is actually layers of file system changes. When we run multiple containers on top of the same image, each container only record the differences between the container itself and the base image (which is read-only). However, if there is a container that changes the file in the base image, a copy-on-write action is triggered and this container would have a copy of the base image file, along with the changes it made.
Image Tag
Images do not have a name, a specific image can be identified with repository, tag.
Use docker image tag SOURCE_IMAGE[:TAG] TARGET_IMAGE[:TAG]
to give an existing image a new tag, and use docker image push [OPTIONS] NAME[:TAG]
to publish the image.
Dockerfile
Dockerfile is the recipe for images. Dockerfile
is the default name for a Docker file, however, we can use -f
option to specify any Docker file (e.g., docker build -f DOCKER_FILE_PATH -t NAME:TAG
).
Commands
Each Dockerfile command is a layer of change, so the order does matter. Since each command is a layer of change, it’s best practice to keep things that change the least at top of the Docker file, so Docker can use the cached result and only rerun commands at later layers.
Command | Description |
---|---|
FROM |
Base image layer. |
ENV |
Set environment variables. |
RUN |
Execute bash command inside the container itself. |
WORKDIR |
Change working directory, preferred to using RUN cd /some/path . |
COPY |
Copy file from local machine to current container. |
EXPOSE |
Expose specified ports on Docker virtual network. |
CMD |
Required command that is ran every time a container is launched. |
When a Docker file inherit from another one, it also inherits the CMD
command, which will be ran after all file changes, including the changes in the new image.
Logging
The best practice for logging inside Docker is not to log into log files, instead log everything to stdout
and stderr
.
# e.g., for the nginx container, log everything to stdout and stderr.
RUN ln -sf /dev/stdout /var/log/nginx/access.log && ln -sf /dev/stderr /var/log/nginx/error.log
Cleaning Up
Command | Description |
---|---|
docker image prune [-a] |
Clean up “dangling” images, -a remove all not being used. |
docker system prune |
Clean up everything. |
Container Lifetime & Persistent Data
Containers are usually immutable and ephemeral. Container’s UFS file system does not go away if the container is stopped, it goes away when the container is removed. This problem is known as “persistent data”.
There are two ways to persistent data in Docker:
- Data Volumes: make special location outside of container UFS, can be attached to any container later.
- Bind Mounts: link container path to host path.
Volumes
Use VOLUME PATH-TO-VOLUME
in Docker file to create volume inside of container. Volume has to be manually removed, it does not go away after the container is removed. Use docker container run -v VOLUME_NAME:VOLUME_PATH IMAGE_NAME
to specify volume at time of starting container.
Bind Mount
Use docker container run -v LOCAL_PATH:BIND_MOUNT_PATH IMAGE_NAME
to specify bind mount.
Docker Swarm
Swarm Mode
A single Swarm service can have multiple tasks, and each task will launch a container.
docker service
in a Swarm replaces docker run
.
docker service create alpine ping 8.8.8.8
creates an alpine service that pings Google’s DNS server.docker service ls
shows a list of services.docker service ps SERVICE_NAME
shows the containers running that service.docker service update SERVICE_NAME --replicas NUM_REPLICAS
scales the services up into multiple containers.- When a container goes down in a service, Swarm automatically launches a new one.
Creating Service on a Swarm
Create local Docker machines with docker-machine create NAME
to create a machine, and then use docker-machine ssh NAME
to connect to the machine.
Use docker swarm init --advertise-addr IP_ADDRESS
to initialize the Swarm. (Running docker swarm init
only will give an error with suggested IP addresses.)
After initializing the first Swarm node (as leader), use docker swarm join --token TOKEN IP_ADDRESS
on other nodes to join the Swarm.
Workers in a Swarm do not have privilege to run Docker Swarm commands, use docker node update --role manager NODE_NAME
in a leader node to promote other nodes to leaders.
Below is an example of creating a service on a Swarm.
# Creating service on node1
$ docker service create --replicas 3 alpine ping 8.8.8.8
# pfq0lfntgwtox2dv5hakjs4jn
# overall progress: 3 out of 3 tasks
# 1/3: running [==================================================>]
# 2/3: running [==================================================>]
# 3/3: running [==================================================>]
# verify: Service converged
$ docker node ps node1
# ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
# hyhx9nvq9wt1 loving_yonath.1 alpine:latest node1 Running Running 13 seconds ago
$ docker node ps node2
# ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
# x8cooqtfe7fi loving_yonath.2 alpine:latest node2 Running Running 23 seconds ago
$ docker node ps node3
# ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
# zz1o6v63vg20 loving_yonath.3 alpine:latest node3 Running Running 44 seconds ago
$ docker service ps loving_yonath
# ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
# hyhx9nvq9wt1 loving_yonath.1 alpine:latest node1 Running Running 59 seconds ago
# x8cooqtfe7fi loving_yonath.2 alpine:latest node2 Running Running 58 seconds ago
# zz1o6v63vg20 loving_yonath.3 alpine:latest node3 Running Running 59 seconds ago
Overlay Network
Use docker network create --driver overlay NETWORK_NAME
to create an overlay network. All services added to this network can talk to each other just like they are running on the same subnet, even if they are running on different nodes.
Routing Mesh
Accessing a service through the IP address of any node will result in the correct behavior, that’s due to routing mesh. Routing mesh routes ingress (incoming) packets for a Service to proper Task, it spans all nodes in Swarm and load balances Swarm Services across their Tasks. (It achieves this through IPVS from Linux Kernel.)
Secrets
A secret is:
- Usernames and passwords
- TLS certificates and keys
- SSH keys
- Any sensitive data.
Secrets are only stored on disk on Manager nodes, they are first stored in Swarm, then assigned to Services. Only containers in assigned Services can see them. They look like files in container but are actually in-memory fs.
Local docker-compose can use file-based secrets, but not secure.
Use docker secret create SECRET_NAME PATH_TO_SECRET_FILE
or echo SECRET_CONTENT | docker secret create SECRETE_NAME
to create a secret in Docker Swarm (but they are not safe methods for production!) After creating the Secret, use docker secret inspect SECRET_NAME
to inspect the secret. Swarm stores all secrets in /run/secrets/
(in-memory) directory. When using a secret, -e ENV_NAME_FILE=/run/secrets/SECRET_NAME
will actually store the content of the file into ENV_NAME
environment variable.
To create and use a Secret in a service:
# Create Secrets
$ docker secret create psql_pass /path/to/psql/pass/file
$ echo "mypsql_user" | docker secret create psql_user
# Inspect Secrets
$ docker secret inspect psql_pass
# Use Secret in a Service
$ docker service create \
--name psql \
--secret psql_user \
--secret psql_pass \
-e POSTGRES_PASSWORD_FILE=/run/secrets/psql_pass \
-e POSTGRES_USER_FILE=/run/secrets/psql_user \
postgres
Secrets and Stack
version: 3.7
services:
service_name:
image: image_name
secrets:
- db_user_name
- db_password
environment:
DB_USER_NAME_FILE: /run/secrets/db_user_name
DB_PASSWORD_FILE: /run/secrets/db_password
secrets:
db_user_name:
file: ./db_user.txt
db_password:
file: ./db_pass.txt
$ docker stack deploy --compose-file docker-compose.yml
Swarm App Lifecycle
docker-compose
automatically picks up docker-compose.yml
and docker-compose.override.yml
, use these two files for local development.
A good practice is to have docker-compose.test.yml
file, and use docker-compose -f docker-compose.yml -f docker-compose.test.yml up -d
# Local development:
$ docker-compose up # Look for docker-compose.yml and docker-compose.override.yml
# CI testing:
$ docker-compose -f docker-compose.yml -f docker-compose.test.yml up -d
# Generate Swarm production config file:
$ docker-compose -f docker-compose.yml -f docker-compose.prod.yml config > prod-compose.yml
Service Updates
Swarm service updates provides rolling replacement of tasks/constainers in a service, it limits downtime.
# Update the image to use a new version
$ docker service update --image IMAGE_NAME SERVICE_NAME
# Adding an environment variable and remove a port
$ docker service update --env-add NODE_ENV=production --publish-rm 8080
# Change number of replicas of two services
$ docker service scale web=8 api=6
Docker Health Checks
Docker engine will exec
's the command in the container (e.g. curl localhost
), it expects exit 0
(OK) or exit 1
(Error).
$ docker run \
--health-cmd="curl -f localhost:9200/_cluster/health || false" \
--health-interval=5s \
--health-retries=3 \
--health-timeout=2s \
--health-start-period=15s \
elasticsearch:2