Docker基础

安装与配置

安装

步骤

  1. 下载&安装
1
2
3
# https://download.docker.com/linux/static/stable/
curl https://download.docker.com/linux/static/stable/x86_64/docker-19.03.5.tgz -o docker-19.03.5.tgz && mkdir docker && tar xzf docker-19.03.5.tgz -C docker --strip-components=1
sudo mv docker /usr/local/bin
  1. 启动
1
2
3
4
5
6
7
8
# 1. Start the Docker daemon
sudo dockerd &
# 2. Start the daemon with additional options
# modify the above command accordingly or create and edit the file /etc/docker/daemon.json to add the custom configuration options
touch /usr/local/bin/docker/daemon.json 
sudo dockerd --config-file=/usr/local/bin/docker/daemon.json

docker run -d -v <host-directory>:/certs -p 2375:2375 swarm manage --tlsverify --tlscacert=/certs/ca.pem --tlscert=/certs/cert.pem --tlskey=/certs/key.pem token://<token>
  1. 测试
1
2
3
docker run hello-world
# Or
docker -H <hostname> run hello-world
  1. 升级
1
2
3
# 1. 停止dockerd
Ctrl+C
# 2. 安装新版本

命令概括

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
## List Docker CLI commands
docker
docker container --help

## Display Docker version and info
docker --version
docker version
docker info

## Execute Docker image
docker run hello-world

## List Docker images
docker image ls

## List Docker containers (running, all, all in quiet mode)
docker container ls
docker container ls --all
docker container ls -aq

使用非root用户

1
2
3
4
5
6
7
8
# 创建Docker用户组
sudo groupadd docker
# 将自己添加到用户组
sudo usermod -aG docker $USER #sudo gpasswd -a $USER docker
# 退出重新登录使更改生效,可以重启虚拟机,Linux上可使用如下命令:
newgrp docker
# 测试
docker run hello-world

如果在将自己添加到docker用户组之前使用sudo运行了Docker CLI命令,可能看到如下错误,因为sudo命令导致之前创建的~/.docker/目录权限不足。

WARNING: Error loading config file: /home/user/.docker/config.json -
stat /home/user/.docker/config.json: permission denied

要解决此问题,要么移除~/.docker/(稍后会自动重建,但是所有个性化设置都会丢失。),要么使用如下命令更改权限

1
2
sudo chown "$USER":"$USER" /home/"$USER"/.docker -R
sudo chmod g+rwx "$HOME/.docker" -R

配置

有两种方式配置daemon,一种是使用JSON配置文件(推荐),一种是在启动dockerd时添加标志。如果同时使用两种方式,而相同选项配置不同,daemon不会启动,而是输出错误信息。

比如以下命令行和JSON方式配置daemon的方式是等效的:

1
2
3
4
5
6
7
{
  "debug": true,
  "tls": true,
  "tlscert": "/var/docker/server.pem",
  "tlskey": "/var/docker/serverkey.pem",
  "hosts": ["tcp://192.168.59.3:2376"]
}
1
2
3
4
5
dockerd --debug \
  --tls=true \
  --tlscert=/var/docker/server.pem \
  --tlskey=/var/docker/serverkey.pem \
  --host tcp://192.168.59.3:2376

在Linux上使用JSON配置,创建/etc/docker/daemon.json文件,在Windows上则创建C:\ProgramData\docker\config\daemon.json,在macOS上选择Preferences > Daemon > Advanced

daemon.json示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
{
	"authorization-plugins": [],
	"data-root": "/usr/local/bin/docker/data",
	"dns": [],
	"dns-opts": [],
	"dns-search": [],
	"exec-opts": [],
	"exec-root": "/usr/local/bin/docker/run",
	"experimental": false,
	"features": {},
	"storage-driver": "overlay2",
	"storage-opts": [],
	"labels": [],
	"live-restore": true,
	"log-driver": "json-file",
	"log-opts": {
		"max-size": "10m",
		"max-file":"5",
		"labels": "production_status",
		"env": "os,customer"
	},
	"mtu": 0,
	"pidfile": "/usr/local/bin/docker/run/docker.pid",
	"cluster-store": "",
	"cluster-store-opts": {},
	"cluster-advertise": "",
	"max-concurrent-downloads": 3,
	"max-concurrent-uploads": 5,
	"default-shm-size": "64M",
	"shutdown-timeout": 15,
	"debug": true,
	"hosts": ["unix:///var/run/docker.sock", "tcp://127.0.0.1:2375"],
	"log-level": "info",
	"tls": true,
	"tlsverify": true,
	"tlscacert": "",
	"tlscert": "",
	"tlskey": "",
	"swarm-default-advertise-addr": "",
	"api-cors-header": "",
	"selinux-enabled": false,
	"userns-remap": "",
	"group": "docker",
	"cgroup-parent": "",
	"default-ulimits": {
		"nofile": {
			"Name": "nofile",
			"Hard": 64000,
			"Soft": 64000
		}
	},
	"init": false,
	"init-path": "/usr/local/bin/docker/docker-init",
	"ipv6": false,
	"iptables": true,
	"ip-forward": false,
	"ip-masq": false,
	"userland-proxy": false,
	"userland-proxy-path": "/usr/local/bin/docker/docker-proxy",
	"ip": "0.0.0.0",
	"bridge": "",
	"bip": "",
	"fixed-cidr": "",
	"fixed-cidr-v6": "",
	"default-gateway": "",
	"default-gateway-v6": "",
	"icc": false,
	"raw-logs": false,
	"allow-nondistributable-artifacts": [],
	"registry-mirrors": ["https://registry.docker-cn.com","https://yourid.mirror.aliyuncs.com"],
	"seccomp-profile": "",
	"insecure-registries": [],
	"no-new-privileges": false,
	"default-runtime": "runc",
	"oom-score-adjust": -500,
	"node-generic-resources": ["NVIDIA-GPU=UUID1", "NVIDIA-GPU=UUID2"],
	"runtimes": {
		"cc-runtime": {
			"path": "/usr/bin/cc-runtime"
		},
		"custom": {
			"path": "/usr/local/bin/my-runc-replacement",
			"runtimeArgs": [
				"--debug"
			]
		}
	},
	"default-address-pools":[{"base":"172.80.0.0/16","size":24},
	{"base":"172.90.0.0/16","size":24}]
}

配置Docker守护进程监听

默认情况下,Docker守护程序在UNIX套接字上侦听连接以接受来自本地客户端的请求。通过将Docker配置为侦听IP地址和端口以及UNIX套接字,可以允许Docker接受来自远程主机的请求。

可以使用docker.service systemd单元文件来配置Docker来接受远程连接,该文件用于使用systemd的Linux发行版,例如RedHat,CentOS,Ubuntu和SLES的最新版本,或者通过daemon.json文件,推荐用于没有systemd的Linux发行版。

systemd vs daemon.json

将Docker配置为同时使用systemd单元文件和daemon.json文件监听连接会导致冲突,从而阻止Docker启动。

使用systemd单元文件配置远程访问

使用sudo systemctl edit docker.service命令在文本编辑器打开docker.service覆写文件。

添加如下行,或替换为自己的内容

1
2
3
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd -H fd:// -H tcp://127.0.0.1:2375

保存文件,重载配置并重启Docker

1
2
sudo systemctl daemon-reload
sudo systemctl restart docker.service

通过netstat命令查看输出以确认dockerd正在监听配置的端口

1
2
$ sudo netstat -lntp | grep dockerd
tcp        0      0 127.0.0.1:2375          0.0.0.0:*               LISTEN      3758/dockerd
使用daemon.json配置远程访问

设置/etc/docker/daemon.json中的hosts数组以连接到一个UNIX套接字和一个IP地址

1
2
3
{
"hosts": ["unix:///var/run/docker.sock", "tcp://127.0.0.1:2375"]
}

重启Docker并使用netstat命令确认dockerd正在监听配置的端口

1
2
$ sudo netstat -lntp | grep dockerd
tcp        0      0 127.0.0.1:2375          0.0.0.0:*               LISTEN      3758/dockerd

如果启动失败,显示配置冲突,那么新建/etc/systemd/system/docker.service.d/docker.conf文件,移除默认-H参数

1
2
3
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd

故障排除

Kernel兼容性

1
2
3
$ curl https://raw.githubusercontent.com/docker/docker/master/contrib/check-config.sh > check-config.sh

$ bash ./check-config.sh

检查日志

系统 位置
RHEL, Oracle Linux /var/log/messages
Debian /var/log/daemon.log
Ubuntu 16.04+, CentOS Use the command journalctl -u docker.service
macOS (Docker 18.01+) ~/Library/Containers/com.docker.docker/Data/vms/0/console-ring
Windows AppData\Local

使用

起步

启用debug

推荐创建或编辑/etc/docker/下的daemon.json文件,在Windows或macOS上不要直接编辑,而是选择Preferences / Daemon / Advanced

1
2
3
{
  "debug": true
}

确认log-level键也设置了值,使用info(默认)或debug,可用的值有debug, info, warn, error, fatal

最后发送HUP信号给daemon以让它重新加载配置,在Windows上重启Docker,在Linux上使用如下命令。

1
$ sudo kill -SIGHUP $(pidof dockerd)

如果守护程序无响应,则可以通过向守护程序发送SIGUSR1信号来强制记录完整的堆栈跟踪。

1
$ sudo kill -SIGUSR1 $(pidof dockerd)

测试安装

  1. 运行docker –version
1
2
docker --version
Docker version 18.09.4, build d14af54
  1. 运行docker info (或者docker version)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 需先启动Docker
docker info

Containers: 0
 Running: 0
 Paused: 0
 Stopped: 0
Images: 0
Server Version: 17.12.0-ce
Storage Driver: overlay2
...
  1. 也可以使用系统工具检查
1
2
3
4
5
sudo systemctl is-active docker
# Or
sudo status docker
# Or
sudo service docker status

使用systemd管理docker

手动启动

1
2
3
4
# systemctl:
$ sudo systemctl start docker
# service:
$ sudo service docker start

开机启动

1
2
3
4
# Enable boot start
sudo systemctl enable docker
# Disable boot start
sudo systemctl disable docker

HTTP/HTTPS proxy

Docker守护程序在其启动环境中使用HTTP_PROXYHTTPS_PROXYNO_PROXY环境变量来配置HTTP或HTTPS代理行为。您不能使用daemon.json文件配置这些环境变量。

1
$ sudo mkdir -p /etc/systemd/system/docker.service.d

创建文件/etc/systemd/system/docker.service.d/http-proxy.conf,添加HTTP_PROXY环境变量

1
2
3
4
5
[Service]
Environment="HTTP_PROXY=http://proxy.example.com:80/"
# HTTPS
[Service]
Environment="HTTPS_PROXY=https://proxy.example.com:443/"

如果您需要在不使用代理的情况下联系内部Docker注册表,则可以通过NO_PROXY环境变量指定它们。

NO_PROXY变量指定一个字符串,该字符串包含用逗号分隔的主机值,该值应排除在代理之外。您可以指定以下选项来排除主机:

  • IP地址前缀(1.2.3.4)
  • 域名或特殊的DNS标签(*)
  • 域名与该名称和所有子域匹配。 以“.”开头的域名仅与子域匹配。例如,给定域foo.example.comexample.com
    • example.com匹配example.comfoo.example.com
    • .example.com仅匹配foo.example.com
  • 单个星号(*)表示不应进行代理
  • IP地址前缀(1.2.3.4:80)和域名(foo.example.com:80)接受端口号。

例子

1
2
3
4
5
[Service]    
Environment="HTTP_PROXY=http://proxy.example.com:80/" "NO_PROXY=localhost,127.0.0.1,docker-registry.example.com,.corp"
# HTTPS
[Service]    
Environment="HTTPS_PROXY=https://proxy.example.com:443/" "NO_PROXY=localhost,127.0.0.1,docker-registry.example.com,.corp"

后续步骤:

1
2
3
4
5
6
7
8
9
# 刷新变化
sudo systemctl daemon-reload
# 重启docker
sudo systemctl restart docker
# 确认生效
$ systemctl show --property=Environment docker
Environment=HTTP_PROXY=http://proxy.example.com:80/
$ systemctl show --property=Environment docker
Environment=HTTPS_PROXY=https://proxy.example.com:443/

配置容器

自启动容器

Docker提供了重启策略来控制您的容器在退出时还是在Docker重新启动时自动启动。重新启动策略确保链接的容器以正确的顺序启动。Docker建议您使用重启策略,并避免使用进程管理器来启动容器。

重新启动策略与dockerd命令的--live-restore标志不同。 使用--live-restore可以使您的容器在Docker升级期间保持运行,尽管网络和用户输入都会中断。

要为容器配置重启策略,请在使用docker run命令时使用--restart标志。--restart标志的值可以是以下任意值: |标志|描述| |—-|—| |no |不自动重启容器。(默认)| |on-failure |如果容器由于错误而退出,重新启动容器,该错误表示为非零退出代码。| |always |只要停止总是重启容器。如果手动停止,则仅在Docker守护程序重启或手动重启容器本身时才重启。| |unless-stopped |与always相似,除了在容器停止(手动或其他方式)时,即使重新启动Docker守护程序也不会重新启动容器。|

以下示例启动Redis容器并将其配置为始终重新启动,除非明确将其停止或重新启动Docker。

1
$ docker run -dit --restart unless-stopped redis

使用重启策略时,以下几点需铭记在心

  • 重新启动策略仅在容器成功启动后才生效。在这种情况下,成功启动意味着该容器已启动至少10秒钟,并且Docker已开始对其进行监视。这样可以防止根本无法启动的容器进入重启循环。
  • 如果手动停止容器,则其重新启动策略将被忽略,直到Docker守护进程重新启动或手动重新启动容器为止。这是防止重新启动循环的另一种尝试。
  • 重新启动策略仅适用于容器。swarm服务的重启策略配置不同。

在daemon停机期间使容器保持活动状态

实时还原(live restore)选项有助于减少由于守护程序崩溃,计划内的停机或升级而导致的容器停机时间。

/etc/docker/daemon.json文件中添加如下内容

1
2
3
{
  "live-restore": true
}

然后重启Docker Daemon。在Linux上,您可以通过重新加载Docker守护程序来避免重启(并避免容器出现任何停机)。如果使用systemd,请使用命令systemctl reload docker。 否则,将SIGHUP信号发送到dockerd进程。

不建议在dockerd命令中使用--live-restore(如果用,不得与上述json配置同时使用),因为它没有设置启动Docker进程时systemd或其他进程管理器将使用的环境。这可能会导致意外的行为。

在容器中运行多个服务

容器的主要运行进程是Dockerfile末尾的ENTRYPOINT和/或CMD。通常建议您通过每个容器使用一项服务来分离关注区域。该服务可以派生到多个进程中(例如,Apache Web服务器启动多个工作进程)。可以有多个进程,但是要从Docker中获得最大收益,请避免由一个容器来负责整个应用程序的多个方面。您可以使用用户定义的网络和共享卷连接多个容器。

容器的主要进程负责管理其启动的所有进程。在某些情况下,主进程设计不当,并且在容器退出时无法优雅地处理“reaping”(停止)子进程。如果您的进程属于此类别,则在运行容器时可以使用--init选项。 --init标志将一个很小的init进程作为主进程插入到容器中,并在容器退出时处理所有进程的收割。以这种方式处理此类进程优于使用成熟的init进程(例如sysvinitupstartsystemd)来处理容器中的进程生命周期。

如果您需要在一个容器中运行多个服务,则可以通过几种不同的方式来完成此操作。

  • 将所有命令放入包装脚本中,其中包含测试和调试信息。将包装程序脚本作为CMD运行。首先,包装脚本:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#!/bin/bash

# Start the first process
./my_first_process -D
status=$?
if [ $status -ne 0 ]; then
  echo "Failed to start my_first_process: $status"
  exit $status
fi

# Start the second process
./my_second_process -D
status=$?
if [ $status -ne 0 ]; then
  echo "Failed to start my_second_process: $status"
  exit $status
fi

# Naive check runs checks once a minute to see if either of the processes exited.
# This illustrates part of the heavy lifting you need to do if you want to run
# more than one service in a container. The container exits with an error
# if it detects that either of the processes has exited.
# Otherwise it loops forever, waking up every 60 seconds

while sleep 60; do
  ps aux |grep my_first_process |grep -q -v grep
  PROCESS_1_STATUS=$?
  ps aux |grep my_second_process |grep -q -v grep
  PROCESS_2_STATUS=$?
  # If the greps above find anything, they exit with 0 status
  # If they are not both 0, then something is wrong
  if [ $PROCESS_1_STATUS -ne 0 -o $PROCESS_2_STATUS -ne 0 ]; then
    echo "One of the processes has already exited."
    exit 1
  fi
done
1
2
3
4
5
FROM ubuntu:latest
COPY my_first_process my_first_process
COPY my_second_process my_second_process
COPY my_wrapper_script.sh my_wrapper_script.sh
CMD ./my_wrapper_script.sh
  • 如果您有一个需要首先启动并保持运行的主要进程,但暂时需要运行其他一些进程(可能与该主要进程进行交互),则可以使用bash的作业控制来简化这一过程。 首先,包装脚本:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#!/bin/bash

# turn on bash's job control
set -m

# Start the primary process and put it in the background
./my_main_process &

# Start the helper process
./my_helper_process

# the my_helper_process might need to know how to wait on the
# primary process to start before it does its work and returns


# now we bring the primary process back into the foreground
# and leave it there
fg %1
1
2
3
4
5
FROM ubuntu:latest
COPY my_main_process my_main_process
COPY my_helper_process my_helper_process
COPY my_wrapper_script.sh my_wrapper_script.sh
CMD ./my_wrapper_script.sh
  • 使用进程管理器,例如supervisord。这是一种中等重量的方法,要求您将管理程序及其配置打包在镜像中(或将镜像基于包含管理程序的镜像打包)以及它管理的不同应用程序。 然后,您将启动supervisord,该supervisord将为您管理进程。这是使用此方法的示例Dockerfile,假定预先编写的supervisord.confmy_first_processmy_second_process文件都与Dockerfile位于同一目录中。
1
2
3
4
5
6
7
FROM ubuntu:latest
RUN apt-get update && apt-get install -y supervisor
RUN mkdir -p /var/log/supervisor
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY my_first_process my_first_process
COPY my_second_process my_second_process
CMD ["/usr/bin/supervisord"]

开发

快速开始

设置您的Docker环境

镜像和容器 从根本上说,一个容器不过是一个正在运行的进程,并对其应用了一些附加的封装功能,以使其与主机和其他容器隔离。容器隔离的最重要方面之一是每个容器都与自己的私有文件系统进行交互。该文件系统由Docker镜像提供。镜像包括运行应用程序所需的所有内容–代码或二进制文件,运行时,依赖项以及所需的任何其他文件系统对象。

容器和虚拟机 容器在Linux上本地运行,并与其他容器共享主机的内核。它运行一个离散进程,不占用任何其他可执行文件更多的内存,从而使其轻巧。

相比之下,虚拟机(VM)运行成熟的“guest”操作系统,并通过虚拟机管理程序对主机资源进行虚拟访问。 通常,VM会产生大量开销,超出了应用程序逻辑所消耗的开销。

安装Docker桌面

通过如下链接安装对应版本的Docker桌面

启用Kubernetes

打开Docker,在Preferences… -> Kubernetes,勾选Enable Kubernetes,点击Apply。Docker桌面将会自动设置Kubernetes。

为了确认Kubernetes已经启动并运行,创建pod.yaml,并添加如下内容:

1
2
3
4
5
6
7
8
9
 apiVersion: v1
 kind: Pod
 metadata:
   name: demo
 spec:
   containers:
   - name: testpod
     image: alpine:3.5
     command: ["ping", "8.8.8.8"]

找到你创建的pod.yaml,在命令行运行

1
kubectl apply -f pod.yaml

检查你的pod已经启动并运行

1
kubectl get pods

你会看到如下类似内容

NAME      READY     STATUS    RESTARTS   AGE
demo      1/1       Running   0          4s

检查日志

1
kubectl logs demo

会看到如下内容

PING 8.8.8.8 (8.8.8.8): 56 data bytes
64 bytes from 8.8.8.8: seq=0 ttl=37 time=21.393 ms
64 bytes from 8.8.8.8: seq=1 ttl=37 time=15.320 ms
64 bytes from 8.8.8.8: seq=2 ttl=37 time=11.111 ms
...

最后,销毁你的测试pod

1
kubectl delete -f pod.yaml
启用Docker Swarm

在命令行初始化Docker Swarm模式

1
 docker swarm init

如果一切正常,会看到如下内容

Swarm initialized: current node (tjjggogqpnpj2phbfbz8jd5oq) is now a manager.

To add a worker to this swarm, run the following command:

    docker swarm join --token SWMTKN-1-3e0hh0jd5t4yjg209f4g5qpowbsczfahv2dea9a1ay2l8787cf-2h4ly330d0j917ocvzw30j5x9 192.168.65.3:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

运行一个简单的Docker服务,该服务使用基于alpine的文件系统,并将ping隔离到8.8.8.8:

1
 docker service create --name demo alpine:3.5 ping 8.8.8.8

检查你的服务创建了一个运行的容器

1
 docker service ps demo

你应该看到如下内容:

ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE           ERROR               PORTS
463j2s3y4b5o        demo.1              alpine:3.5          docker-desktop      Running             Running 8 seconds ago

最后查看ping进程的日志

1
 docker service logs demo

如果一切正常会看到如下内容

demo.1.463j2s3y4b5o@docker-desktop    | PING 8.8.8.8 (8.8.8.8): 56 data bytes
demo.1.463j2s3y4b5o@docker-desktop    | 64 bytes from 8.8.8.8: seq=0 ttl=37 time=13.005 ms
demo.1.463j2s3y4b5o@docker-desktop    | 64 bytes from 8.8.8.8: seq=1 ttl=37 time=13.847 ms
demo.1.463j2s3y4b5o@docker-desktop    | 64 bytes from 8.8.8.8: seq=2 ttl=37 time=41.296 ms
...

最后销毁你的测试服务

1
 docker service rm demo

容器化一个应用

配置

从GitHub上克隆一个示例项目

1
2
git clone -b v1 https://github.com/docker-training/node-bulletin-board
cd node-bulletin-board/bulletin-board-app

这是一个简单的公告板应用程序,用node.js编写。在此示例中,假设您编写了此应用,现在正尝试将其容器化。

看一下名为Dockerfile的文件。Dockerfile描述了如何为容器组装私有文件系统,还可以包含一些元数据,这些元数据描述了如何基于该镜像运行容器。公告板应用程序Dockerfile如下所示:

1
2
3
4
5
6
7
8
FROM node:6.11.5    

WORKDIR /usr/src/app
COPY package.json .
RUN npm install    
COPY . .

CMD [ "npm", "start" ]    

编写Dockerfile是容器化一个应用的第一步。可以将其看作一步一步构建镜像的命令:

  1. 从(FROM)现有的node:6.11.5镜像开始
  2. 使用WORKDIR可以指定所有后续操作均应从镜像文件系统中的目录/usr/src/app中执行(而不是主机的文件系统中)。
  3. 将文件package.json从主机复制(COPY)到镜像中的当前位置(.)(在这种情况下,复制到/usr/src/app/package.json
  4. 在镜像文件系统中运行(RUN)命令npm install(它将读取package.json以确定应用程序的node依赖性,然后安装它们)
  5. 从主机将应用程序其余部分的源代码复制到镜像文件系统。

可以看到,这些步骤与在主机上设置和安装应用程序所采取的步骤几乎相同-但是将它们描述为Dockerfile可以使我们在可移植的隔离Docker镜像中执行相同的操作。

上面的步骤构建了镜像的文件系统,但是Dockerfile中还有一行。CMD指令是我们在镜像中指定一些元数据的第一个示例,该元数据描述了如何基于该镜像运行容器。在这种情况下,这就是说该镜像应支持的容器化过程是npm start

上面您看到的是组织一个简单的Dockerfile的好方法。始终以FROM命令开头,然后按照步骤构建您的私有文件系统,并以任何元数据规范作为结束。Dockerfile指令比上面我们看到的要多。有关完整列表,请参阅Dockerfile参考

构建并测试镜像

确保您位于终端或Powershell的目录node-bulletin-board/bulletin-board-app中,构建公告板镜像:

1
docker image build -t bulletinboard:1.0 .

您会看到Docker逐步完成Dockerfile中的每条指令,并逐步构建镜像。如果成功,则构建过程应以如下消息结束。

Successfully tagged bulletinboard:1.0.

基于新镜像启动一个容器

1
docker container run --publish 8000:8080 --detach --name bb bulletinboard:1.0

我们在这里使用了几个常见的标志:

  • --publish要求Docker将主机端口8000上传入的流量转发到容器的端口8080(容器具有自己的私有端口集,因此如果我们要从网络访问一个端口,则必须在此将流量转发给它)方式;否则,防火墙规则将阻止所有网络流量到达您的容器,这是默认的安全状态)。
  • --detach要求Docker在后台运行此容器。
  • --name使我们可以指定一个名称,在后续命令中使用该名称可以引用我们的容器,在本例中为bb。

另请注意,我们没有指定我们要运行容器的进程。我们不必这样做,因为在构建Dockerfile时使用了CMD指令;因此,Docker知道在启动时会自动在容器内运行npm start进程。

在浏览器中的localhost:8000上访问您的应用程序。您应该看到公告板应用程序已启动并正在运行。在这一步,我们通常会竭尽所能,以确保我们的容器按预期的方式工作; 例如,现在是运行单元测试的时候了。

对公告板容器正常工作感到满意后,将其删除:

1
docker container rm --force bb

部署到Kubernetes

使用Kubernetes YAML描述应用

Kubernetes中的所有容器都计划为Pod,这是一组共享资源的共享容器。此外,在实际的应用程序中,我们几乎从不创建单个pod。取而代之的是,我们的大多数工作负载都按deployments计划,这些部署是Kubernetes自动维护的可扩展Pod组。最后,所有Kubernetes对象都可以并且应该在称为Kubernetes YAML文件的清单中进行描述。这些YAML文件描述了Kubernetes应用程序的所有组件和配置,可用于在任何Kubernetes环境中轻松创建和销毁您的应用程序。

您已经在本教程的第一部分中编写了一个非常基本的Kubernetes YAML文件。让我们现在写一个稍微复杂一些的脚本,以运行和管理公告栏。将以下内容放在一个名为bb.yaml的文件中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
apiVersion: apps/v1
kind: Deployment
metadata:
  name: bb-demo
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      bb: web
  template:
    metadata:
      labels:
        bb: web
    spec:
      containers:
      - name: bb-site
        image: bulletinboard:1.0
---
apiVersion: v1
kind: Service
metadata:
  name: bb-entrypoint
  namespace: default
spec:
  type: NodePort
  selector:
    bb: web
  ports:
  - port: 8080
    targetPort: 8080
	nodePort: 30001

在此Kubernetes YAML文件中,我们有两个对象,中间用---分隔:

  • Deployment,描述一组相同的可扩展Pod。在这种情况下,您将仅获得Pod的一个副本或副本,并且该Pod(在模板:键下进行描述)中仅包含一个容器,该容器基于上述的bulletinboard:1.0镜像。
  • NodePort服务,它将流量从主机上的端口30001路由到其路由到的Pod内的端口8080,从而使您可以从网络到达公告板。

还要注意,虽然Kubernetes YAML乍看起来可能很长很复杂,但几乎总是遵循相同的模式:

  • apiVersion,指示解析此对象的Kubernetes API
  • kind,指示这是哪种对象
  • 一些metadata,将名称等内容应用到对象
  • spec,指定对象的所有参数和配置。
部署并检查应用

在创建bb.yaml文件的地方打开终端

1
kubectl apply -f bb.yaml

如果创建成功会看到如下内容

deployment.apps/bb-demo created
service/bb-entrypoint created

列出部署内容确保工作正常

1
kubectl get deployments

如果一切正常,内容应该如下

NAME      DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
bb-demo   1         1         1            1           48s

同样地,检查服务

1
kubectl get services
NAME            TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
bb-entrypoint   NodePort    10.106.145.116   <none>        8080:30001/TCP   53s
kubernetes      ClusterIP   10.96.0.1        <none>        443/TCP          138d

除了默认的kubernetes服务,我们还看到了bb-entrypoint服务,该服务接受端口30001/TCP上的流量。

打开浏览器,访问位于localhost:30001的公告板;一旦满意,请删除您的应用程序:

1
kubectl delete -f bb.yaml

部署到Swarm

通过输入docker system info并查找消息Swarm: active,确保在Docker桌面上启用了Swarm。

如果Swarm没有运行,只需在shell提示符下键入docker swarm init进行设置。

使用Stack文件描述应用

Swarm从来不会像上一步那样创建单个容器。而是将所有Swarm工作负载安排为服务,这些服务是可伸缩的容器组,具有由Swarm自动维护的附加网络功能。此外,所有Swarm对象都可以并且应该在称为stack文件的清单中进行描述。 这些YAML文件描述了Swarm应用程序的所有组件和配置,可用于在任何Swarm环境中轻松创建和销毁您的应用程序。

让我们编写一个简单的stack文件来运行和管理公告栏。将以下内容放入名为bb-stack.yaml的文件中:

1
2
3
4
5
6
7
version: '3.7'    

services:
  bb-app:
    image: bulletinboard:1.0
    ports:
	  - "8000:8080"    

在这个Swarm YAML文件中,我们只有一个对象:service,描述一组可伸缩的相同容器。在这种情况下,您只会得到一个容器(默认容器),并且该容器基于上述bulletinboard:1.0镜像。我们还要求Swarm将所有到达开发计算机上8000端口的流量转发到公告板容器内的8080端口。

Kubernetes服务和Swarm服务有很大的不同!尽管名称相似,但两个协调器的‘service’一词含义却截然不同。在Swarm中,服务同时提供调度和联网功能,创建容器并提供用于将流量路由到它们的工具。在Kubernetes中,调度和联网分别处理:deployments(或其他控制器)将容器的调度作为Pod处理,而服务仅负责将网络功能添加到这些Pod。

部署并检查应用
1
docker stack deploy -c bb-stack.yaml demo

一切正常会显示如下内容

Creating network demo_default
Creating service demo_bb-app

请注意,除服务外,Swarm默认还会创建一个Docker网络,以隔离作为stack一部分部署的容器。

列出服务以检查

1
docker service ls

一切正常则显示如下内容

ID                  NAME                MODE                REPLICAS            IMAGE               PORTS
il7elwunymbs        demo_bb-app         replicated          1/1                 bulletinboard:1.0   *:8000->8080/tcp

访问localhost:8000,如果满意,可以删除应用

1
docker stack rm demo

在Docker Hub上共享您的容器化应用程序

https://hub.docker.com/signup创建一个帐号,然后新建一个仓库

在命令行登录你的帐号

1
docker login

镜像必须命名为:<Docker Hub ID>/<Repository Name>:<tag>

1
docker image tag bulletinboard:1.0 yourDockerID/bulletinboard:1.0

最后,推送镜像到Docker Hub

1
docker image push gordon/bulletinboard:1.0

最佳实践

保持镜像小巧

持久化应用数据

使用CI/CD测试并部署

配置网络

管理应用数据

Docker为容器提供了两个选项来将文件存储在主机中,这样即使容器停止后文件也可以持久保存:volumesbind mounts。如果您在Linux上运行Docker,则还可以使用tmpfs挂载。如果您在Windows上运行Docker,则还可以使用named pipe

选择正确的类型

Volumes存储在由Docker管理的主机文件系统的一部分中(在Linux上为/var/lib/docker/volumes/)。非Docker进程不应修改文件系统的这一部分。卷是在Docker中持久保存数据的最佳方法。

Bind mounts可以存储在主机系统上的任何位置。它们甚至可能是重要的系统文件或目录。Docker主机或Docker容器上的非Docker进程可以随时对其进行修改。

tmpfs mounts仅存储在主机系统的内存中,并且永远不会写入主机系统的文件系统中。

Volumes

卷是用于持久化由Docker容器生成和使用的数据的首选机制。绑定挂载取决于主机的目录结构,但是卷完全由Docker管理。

选择-v或–mount

-v--volume:由三个字段组成,以冒号(:)分隔。这些字段必须以正确的顺序排列,并且每个字段的含义不是立即显而易见的。

  • 对于命名卷,第一个字段是卷的名称,在给定的主机上是唯一的。对于匿名卷,将省略第一个字段。
  • 第二个字段是文件或目录在容器中的安装路径。
  • 第三个字段是可选的,并且是选项的逗号分隔列表,例如ro。这些选项将在下面讨论。

--mount:由多个键/值对组成,用逗号分隔,每对均由一个<key>=<value>元组组成。 --mount语法比-v--volume更为冗长,但是键的顺序并不重要,并且标志的值更易于理解。

  • 挂载的type,可以是bindvolumetmpfs。本主题讨论卷,因此类型始终是卷。
  • 挂载的source。对于命名卷,这是卷的名称。对于匿名卷,将省略此字段。可以指定为源或src。
  • destination将文件或目录在容器中的安装路径作为其值。可以指定为destination,dst,或target
  • readonly选项(如果存在)使绑定安装以只读方式安装到容器中。
  • 可以多次指定的volume-opt选项,采用由选项名称及其值组成的键值对。

如果您的卷驱动程序接受逗号分隔的列表作为选项,则必须从外部CSV解析器中转义该值。要转义volume-opt,请用双引号(")括起来,并用单引号(')括住整个安装参数。例如:

1
2
3
4
$ docker service create \
     --mount 'type=volume,src=<VOLUME-NAME>,dst=<CONTAINER-PATH>,volume-driver=local,volume-opt=type=nfs,volume-opt=device=<nfs-server>:<nfs-path>,"volume-opt=o=addr=<nfs-address>,vers=4,soft,timeo=180,bg,tcp,rw"'
    --name myservice \
    <IMAGE>

创建和管理volume

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# 创建
docker volume create my-vol
# 列出
docker volume ls
# 检查
docker volume inspect my-vol
[
    {
        "Driver": "local",
        "Labels": {},
        "Mountpoint": "/var/lib/docker/volumes/my-vol/_data",
        "Name": "my-vol",
        "Options": {},
        "Scope": "local"
    }
]
# 移除
docker volume rm my-vol

用Volume启动容器

如果您使用尚不存在的卷启动容器,则Docker将为您创建该卷。

以下案例用-v和–mount两种方式演示

–mount

1
2
3
4
docker run -d \
--name devtest \
--mount source=myvol2,target=/app \
nginx:latest

-v

1
2
3
4
docker run -d \
--name devtest \
-v myvol2:/app \
nginx:latest

docker inspect devtest命令查看其Mounts部分以确认卷被创建并正确加载

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
"Mounts": [
    {
        "Type": "volume",
        "Name": "myvol2",
        "Source": "/var/lib/docker/volumes/myvol2/_data",
        "Destination": "/app",
        "Driver": "local",
        "Mode": "",
        "RW": true,
        "Propagation": ""
    }
],

停止容器并删除卷

1
2
3
$ docker container stop devtest
$ docker container rm devtest
$ docker volume rm myvol2

使用volume启动服务

创建服务不支持-v--volume,只能使用--mount

1
2
3
4
5
docker service create -d \
--replicas=4 \
--name devtest-service \
--mount source=myvol2,target=/app \
nginx:latest

使用docker service ps devtest-service确认服务运行

1
2
3
4
$ docker service ps devtest-service

ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE            ERROR               PORTS
4d7oz1j85wwn        devtest-service.1   nginx:latest        moby                Running             Running 14 seconds ago

移除服务

1
docker service rm devtest-service

使用只读volume

1
2
3
4
docker run -d \
--name=nginxtest \
--mount source=nginx-vol,destination=/usr/share/nginx/html,readonly \
nginx:latest

使用docker inspect nginxtest命令检查

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
"Mounts": [
    {
        "Type": "volume",
        "Name": "nginx-vol",
        "Source": "/var/lib/docker/volumes/nginx-vol/_data",
        "Destination": "/usr/share/nginx/html",
        "Driver": "local",
        "Mode": "",
        "RW": false,
        "Propagation": ""
    }
],

移除volume

容器被删除时,自动移除匿名volume,保留命名的volume

1
docker run --rm -v /foo -v awesome:/bar busybox top

移除全部volume

1
docker volume prune

生产环境运行

Docker对象标签

标签是一种将元数据应用于Docker对象的机制。

标签键和值

标签是键-值对,存储为字符串。您可以为一个对象指定多个标签,但是每个键值对在一个对象内必须唯一。如果给同一个键赋予多个值,则最近写入的值将覆盖所有先前的值。

键格式建议
标签键是键值对的左侧。键是字母数字字符串,可能包含句点(.)和连字符(-)。大多数Docker用户都使用其他组织创建的镜像,而且以下准则有助于防止跨对象的标签意外复制,尤其是当您计划将标签用作自动化机制时。

  • 第三方工具的作者应在每个标签键之前添加其拥有的域的反向DNS表示法,例如com.example.some-label
  • 未经域所有者的许可,请勿在标签键中使用域。
  • com.docker.*, io.docker.*org.dockerproject.*名称空间由Docker保留供内部使用。
  • 标签键应以小写字母开头和结尾,并且只能包含小写字母数字字符,句点字符(.)和连字符(-)。不允许连续的句号或连字符。
  • 句点字符(.)分隔名称空间“字段”。不带名称空间的标签键保留供CLI使用,从而使CLI的用户可以使用较短的键入友好字符串来交互式地标记Docker对象。

这些准则目前尚未执行,其他准则可能适用于特定用例。

值准则
标签值可以包含任何可以表示为字符串的数据类型,包括(但不限于)JSON,XML,CSV或YAML。唯一的要求是使用特定于结构类型的机制,首先将值序列化为字符串。例如,要将JSON序列化为字符串,可以使用JSON.stringify()JavaScript方法。

由于Docker不会反序列化该值,因此除非您将此功能内置到第三方工具中,否则无法在按标签值查询或过滤时将JSON或XML文档视为嵌套结构。

更多参考关于如何管理对象标签

镜像 & 容器

本地守护进程

Volumes

网络

Swarm nodes

Swarm services

清理未使用的Docker对象

Docker采用保守的方法来清理未使用的对象(通常称为“垃圾收集”),例如镜像,容器,卷和网络:除非明确要求Docker这样做,否则通常不会删除这些对象。这可能会导致Docker使用额外的磁盘空间。对于每种类型的对象,Docker提供一个prune命令。此外,您可以使用docker system prune一次清除多种类型的对象。

清理镜像

docker image prune命令可让您清理未使用的镜像。默认情况下,docker image prune仅清除悬空的镜像。悬空镜像是未标记且未被任何容器引用的镜像。要删除悬空的镜像:

1
2
3
4
$ docker image prune

WARNING! This will remove all dangling images.
Are you sure you want to continue? [y/N] y

要删除所有现有容器未使用的镜像,请使用-a标志:

1
2
3
4
$ docker image prune -a

WARNING! This will remove all images without at least one container associated to them.
Are you sure you want to continue? [y/N] y

默认情况下,系统会提示您继续。要绕过提示,请使用-f--force标志。

您可以使用带有--filter标志的过滤表达式来限制清理哪些镜像。 例如,仅考虑24小时前创建的镜像:

1
$ docker image prune -a --filter "until=24h"

清理容器

停止容器时,除非使用--rm标志将其启动,否则不会自动将其删除。要查看Docker主机上的所有容器(包括已停止的容器),请使用docker ps -a。您可能会惊讶地发现有多少个容器,尤其是在开发系统上!停止的容器的可写层仍会占用磁盘空间。要清理此问题,可以使用docker container prune命令。

1
2
3
4
$ docker container prune

WARNING! This will remove all stopped containers.
Are you sure you want to continue? [y/N] y

默认情况下,系统会提示您继续。要绕过提示,请使用-f--force标志。

默认情况下,所有停止的容器都将被删除。您可以使用--filter标志来限制范围。例如,以下命令仅删除24小时以上的已停止容器:

1
$ docker container prune --filter "until=24h"

清理卷

卷可由一个或多个容器使用,并占用Docker主机上的空间。卷不会自动删除,因为这样做可能会破坏数据。

1
2
3
4
$ docker volume prune

WARNING! This will remove all volumes not used by at least one container.
Are you sure you want to continue? [y/N] y

默认情况下,系统会提示您继续。要绕过提示,请使用-f--force标志。

默认情况下,将删除所有未使用的卷。您可以使用--filter标志来限制范围。例如,以下命令仅删除未使用keep标签标记的卷:

1
$ docker volume prune --filter "label!=keep"

清理网络

Docker网络不会占用太多磁盘空间,但是它们确实会创建iptables规则,桥接网络设备和路由表条目。为了清理这些问题,您可以使用docker network prune清理所有容器未使用的网络。

1
2
3
4
$ docker network prune

WARNING! This will remove all networks not used by at least one container.
Are you sure you want to continue? [y/N] y

默认情况下,系统会提示您继续。要绕过提示,请使用-f--force标志。

默认情况下,将删除所有未使用的网络。 您可以使用--filter标志来限制范围。例如,以下命令仅删除早于24小时的网络:

1
$ docker network prune --filter "until=24h"

清理一切

docker system prune命令是用于修剪镜像,容器和网络的快捷方式。在Docker 17.06.0及更早版本中,还修剪了卷。在Docker 17.06.1及更高版本中,必须为docker system prune指定--volumes标志以修剪卷。

1
2
3
4
5
6
7
8
$ docker system prune

WARNING! This will remove:
        - all stopped containers
        - all networks not used by at least one container
        - all dangling images
        - all build cache
Are you sure you want to continue? [y/N] y

如果您使用的是Docker 17.06.1或更高版本,并且还希望修剪卷,请添加--volumes标志:

1
2
3
4
5
6
7
8
9
$ docker system prune --volumes

WARNING! This will remove:
        - all stopped containers
        - all networks not used by at least one container
        - all volumes not used by at least one container
        - all dangling images
        - all build cache
Are you sure you want to continue? [y/N] y

默认情况下,系统会提示您继续。要绕过提示,请使用-f--force标志。

格式化命令并输出日志

安全

保护Docker守护进程套接字

默认情况下,Docker通过非网络UNIX套接字运行。它还可以选择使用HTTP套接字进行通信。

如果您需要以安全的方式通过网络访问Docker,则可以通过指定tlsverify标志并将Docker的tlscacert标志指向受信任的CA证书来启用TLS。

在守护程序模式下,它仅允许来自由该CA签名的证书验证的客户端的连接。 在客户端模式下,它仅连接到具有该CA签名的证书的服务器。

使用TLS和管理CA是一个高级主题。 在生产中使用OpenSSL,x509和TLS之前,请先熟悉一下它们。

使用OpenSSL创建CA,服务器和客户端密钥

注意:在以下示例中,将$HOST的所有实例替换为Docker守护程序主机的DNS名称。

首先,在Docker守护程序的主机上,生成CA私钥和公钥:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ touch ~/.rnd && mkdir .ssl && cd .ssl
$ openssl genrsa -aes256 -out ca-key.pem 4096
#Enter pass phrase for ca-key.pem:dockerwx
$ openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem
# Country Name (2 letter code) [AU]:
# State or Province Name (full name) [Some-State]:Queensland
# Locality Name (eg, city) []:Brisbane
# Organization Name (eg, company) [Internet Widgits Pty Ltd]:Docker Inc
# Organizational Unit Name (eg, section) []:Sales
# Common Name (e.g. server FQDN or YOUR name) []:$HOST
# Email Address []:Sven@home.org.au

现在您已经有了CA,您可以创建服务器密钥和证书签名请求(CSR)。 确保 “Common Name” 与您用于连接Docker的主机名匹配:

注意:在以下示例中,将$HOST的所有实例替换为Docker守护程序主机的DNS名称。

1
2
$ openssl genrsa -out server-key.pem 4096
$ openssl req -subj "/CN=$HOST" -sha256 -new -key server-key.pem -out server.csr

接下来,我们将用我们的CA签署公钥:

由于可以通过IP地址和DNS名称建立TLS连接,因此在创建证书时需要指定IP地址。 例如,要允许使用10.10.10.20和127.0.0.1的连接:

1
$ echo subjectAltName = DNS:$HOST,IP:10.10.10.20,IP:127.0.0.1 >> extfile.cnf

将Docker守护进程密钥的扩展使用属性设置为仅用于服务器身份验证:

1
$ echo ExtendedKeyUsage = serverAuth >> extfile.cnf

现在,生成签名证书:

1
2
3
4
5
6
$ openssl x509 -req -days 365 -sha256 -in server.csr -CA ca.pem -CAkey ca-key.pem \
  -CAcreateserial -out server-cert.pem -extfile extfile.cnf
Signature ok
subject=/CN=your.host.com
Getting CA Private Key
Enter pass phrase for ca-key.pem:

/etc/ssl/openssl.cnf添加如下内容

1
2
[req]
req_extensions = v3_req