docker 按照维护者不同,常用的有两个版本: Debian 打包的 docker.io 以及 Docker 官方维护的 docker-ce 。
虽然 docker-ce 版本较高,但它把所有的依赖都糊在一个二进制文件里,要打补丁只能升级 docker 版本,所以还是推荐安装 Debian 打包 (外部依赖分别安装) 的 docker.io。
docker.io 可以直接用命令 sudo apt install docker.io
安装。
为了让 lsz 用户在使用 docker 时不用在命令前加 sudo,执行 sudo usermod -aG docker lsz
把 lsz 用户加入 docker 用户组。
ssh 重连后生效。
安装 docker-ce 可以使用官方的一键安装脚本:
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
也可以手动安装:
如果过去安装过 docker,先删掉 sudo apt-get remove docker docker-engine docker.io
。
接着安装依赖 sudo apt-get install apt-transport-https ca-certificates curl gnupg2 software-properties-common
。
添加 Docker 的 GPG 公钥:
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key add -
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
对于 amd64 架构的机器,添加软件仓库:
Debian 系统:
sudo add-apt-repository \
"deb [arch=amd64] https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/debian \
$(lsb_release -cs) \
stable"
Ubuntu 系统:
sudo add-apt-repository \
"deb [arch=amd64] https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/ubuntu \
$(lsb_release -cs) \
stable"
如果用树莓派或其它 ARM 架构机器,运行:
echo "deb [arch=armhf] https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/debian \
$(lsb_release -cs) stable" | \
sudo tee /etc/apt/sources.list.d/docker.list
最后更新源 sudo apt-get update
安装 sudo apt-get install docker-ce
。
基本的 /etc/docker/daemon.json 配置为:
{
"registry-mirrors": [
"http://registry.docker-cn.com",
"http://docker.mirrors.ustc.edu.cn",
"http://hub-mirror.c.163.com"
],
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "5"
}
}
在 /etc/docker/daemon.json 文件里加入以下内容:
{
//...
"registry-mirrors": [
"http://registry.docker-cn.com",
"http://docker.mirrors.ustc.edu.cn",
"http://hub-mirror.c.163.com"
]
//...
}
sudo systemctl daemon-reload
重新加载配置文件, sudo systemctl restart docker
重启 docker。
经测试,在 x86 设备可行。
docker pull
由守护进程 dockerd 执行,所以代理要配给这个进程。而这个进程受 systemd 管控,所以要写一个 systemd 配置。
sudo mkdir -p /etc/systemd/system/docker.service.d
sudo vim /etc/systemd/system/docker.service.d/proxy.conf
在这个文件中添加以下内容 (代理服务器配置自行替代):
[Service]
Environment="HTTP_PROXY=http://192.168.1.11:10809/"
Environment="HTTPS_PROXY=http://192.168.1.11:10809/"
Environment="NO_PROXY=localhost,127.0.0.1"
最后执行:
sudo systemctl daemon-reload
sudo systemctl restart docker
让配置生效。
在 /etc/docker/daemon.json 文件里加入以下内容:
{
//...
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "5"
}
//...
}
这表示容器日志的格式为 json 文件,每个容器会产生最多 5 个日志文件,每个文件最大 10m,即最多 50M 的日志。
sudo systemctl daemon-reload
重新加载配置文件, sudo systemctl restart docker
重启 docker 使配置生效。
环境配置文件:
docker run ... -m 64M --memory-swap=128M ...
可以用来限制容器的最大内存使用情况:
在出现 WARNING: No swap limit support 警告时这两个参数是无效的。
原因是内核的 swap 功能没开,开了就行了。
编辑配置文件 /etc/default/grub,文件内部大概长这样:
# If you change this file, run 'update-grub' afterwards to update
# /boot/grub/grub.cfg.
# For full documentation of the options in this file, see:
# info -f grub -n 'Simple configuration'
GRUB_DEFAULT=0
GRUB_TIMEOUT=5
GRUB_DISTRIBUTOR=`lsb_release -i -s 2> /dev/null || echo Debian`
GRUB_CMDLINE_LINUX_DEFAULT="quiet"
GRUB_CMDLINE_LINUX=""
......
在 GRUB_CMDLINE_LINUX=""
的双引号中添加内容成 GRUB_CMDLINE_LINUX="cgroup_enable=memory swapaccount=1"
。注意,如果双引号内有内容则原内容不能改变,新内容和原内容也要用空格隔开。
然后 sudo update-grub
更新配置, sudo reboot
重启机器。
树莓派第一次安装完 docker 之后, docker info
报错基本都是这样的:
WARNING: No memory limit support
WARNING: No swap limit support
WARNING: No kernel memory limit support
WARNING: No kernel memory TCP limit support
WARNING: No oom kill disable support
编辑 /boot/cmdline.txt 文件,在其后加入 cgroup_enable=memory swapaccount=1 两个参数,重启系统就解决了。
如果报错出现:
WARNING: No cpu cfs quota support
WARNING: No cpu cfs period support
一般树莓派系统才会报这个错。
原因是早期的 Raspbian 系统在编译时就没支持 cfs。
早期没办法处理。
但是 2020 年末支持了这几个特性。
如果报错出现:
WARNING: No blkio weight support
WARNING: No blkio weight_device support
使用 SATA3 硬盘时候没有这个问题,在换成 SATA2 的硬盘以后问题出现。解决方案未知。
docker <command>
这种格式的命令与 docker image <command>
或者 docker container <command>
的功能有重叠的部分。 docker <command>
格式出现比较早,没有指明操作对象,所以推荐用指明操作对象的后一种格式的命令。
-d 参数表示在后台运行容器。
-e 选项用于设置容器内的环境变量。格式为 -e var=value 。
查看针对 image 或 container 的命令 docker (image | container) help
。
容器运行时指定容器内主机的主机名 docker run --hostname <name> <other_command>
。
将镜像文件导出为 tar 文件: docker save -o <file-name>.tar.gz repo/<image-name>:<tag> [repo/<image-name-2>:<tag>]
(可以把多个镜像打包在一个 tar 文件中)
从 tar 文件导入镜像 docker load -i <image_name>.tar.gz
。
docker run --restart=always --name <container_name>
容器总是自启动。
docker run --restart=unless-stopped
除非手动停止容器,否则容器总是自启动。
docker update --restart=no <container_name>
容器不会自启动。
docker exec -it <container_name> /bin/sh
可以以新建一个交互式 shell 终端的方式连接到容器。
docker attach <container_name>
可以直接连接到容器。
-i 的含义是为容器开启一个 stdin 以读取输入。
所以可以有这样的操作:
echo some input | docker run -i debian cat
这个例子中,echo 命令的输出将通过管道传输到容器的 stdin 中 (由 -i 选项开启),容器将从 stdin 读到的数据传输给容器内的,需要从 stdin 读取输入的程序 (cat 程序),最终得到执行结果。即,输出 some input 。
-t 的含义是为容器分配一个虚拟的 tty 终端。
tty 是一个软件或者硬件,它连接到 linux 系统用于读取用户输入,并把输入处理成标准输入流发送到 stdin,然后程序从 stdin 中读取输入的数据并做出响应。
如果不使用 -i 给容器开启一个 stdin 的话,用户的输入不被任何程序处理就会出现用户无法退出这个 tty 的情况。
用户连接到容器的情景一般有两个:
对于后一种连接到容器的情景来说,断开与容器的连接的最简单的方案是找到 docker attach 进程的 pid 再用 kill 杀掉这个进程。
而对于前一种情景来说,则需要分别讨论容器的启动参数 (见后文的例子)。
在指定 -i 不指定 -t 的情况下,默认有一个 --sig-proxy=true 的选项生效,这个选项的作用是把从 stdin 读到的除 SIGCHLD, SIGKILL 和 SIGSTOP 之外的信号转发到容器内 pid 为 1 的进程。
注: --sig-proxy 参数只在 non-tty (即,不使用 -t 选项) 的模式下起作用
所以用户输入 Ctrl-c 发出的信号会被 docker 转发到容器内 pid 为 1 的进程,使得进程退出,进而导致容器退出。
注: 在 foreground mode (即不使用 -d 选项) 时,当前终端会被连接到容器的 stdin 上
第一个例子: docker run -i --rm nginx
起一个 nginx 容器,然后使用 Ctrl-c 发送退出信号之后,容器里的 nginx 退出了,接着容器也退出了。在这个例子中,无法在不结束容器的情况下退出与容器的交互模式。
第二个例子: docker run -i --sig-proxy=false --rm nginx
启动容器再用 Ctrl-c 发送退出信号,那么这个退出信号会由 docker attach 处理,不会被转发到容器内,所以可以使用 Ctrl-c 退出交互模式而不影响容器内进程。(这个例子中 Ctrl-c 的效果是取消与 stdin 的连接)
第三个例子: docker run -i -d --rm --name nginx_tt nginx
起一个 nginx 容器。再用 docker attach nginx_tt
连接到容器。需要注意,用户此时通过 docker attach 这个进程连接到容器,要退出与容器的交互模式,需要另起一个终端连接到宿主机,用 kill 杀掉 docker attach 进程才能保证容器不退出。(当前终端通过 docker attach 这个进程连接到容器的 stdin, docker attach 进程退出后与 stdin 的连接也就中断了)
第四个例子: docker run -i -d --rm --name nginx_tt nginx
起一个 nginx 容器。再用 docker attach --sig-proxy=false nginx_tt
连接到容器。此时用户输入的 Ctrl-c 会被 docker attach 进程截获并处理,效果就是用户只退出了 docker attach 进程,而容器不受影响。(这个例子中的 Ctrl-c 使 docker attach 进程退出,从而断开与容器的 stdin 的连接)
在单指定 -t 的情况下,想要退出交互只能另起终端再用 kill 命令,因为容器启动后,用户终端连接到分配给容器的虚拟 tty上,但容器没有 stdin,用户发出的信号没有程序能读到,也就退不出这个 tty,只能从外部杀掉才能退出。
比如 docker run -t -d --name nginx_t nginx && docker attach nginx_t
(可以尝试) 执行后,用户根本退不出去,只能通过结束当前终端重新连接,或者起一个新终端再用 kill 杀掉 docker attach 进程才能使连接到容器的终端退出。
运行无网络的容器 docker run --network none <other-command>
。
docker run --network=host
使用物理机的网络,不做映射。
容器端口映射:
docker run ... --network=bridge -p <container_port>
容器端口映射至主机地址的一个随机端口。docker run ... --network=bridge -p <host_port>:<container_port>
容器端口映射到指定的主机端口。docker run ... --network=bridge -p <ip>::<container_port>
容器端口映射到 ip 指定的主机的随机端口。docker run ... --network=bridge -p <ip>:<host_port>:<container_port>
容器端口映射到 ip 指定的主机的端口。-p (小写) 参数可以使用多次来暴露多个端口。
-P (大写) 或者 --publish-all 参数可以映射 Dockerfile 中 EXPOSE 命令指定的每个端口到主机的随机端口上,写法是 docker run -P ...
。也可以在后面加 --expose 参数来追加需要映射的端口,写法是 docker run ... -P --expose 2222 --expose 3333
。
主机上随即被映射的端口可以在防火墙规则中查询,也可以使用 docker port <container_name>
查询。
在容器中挂载目录,如果要挂在的目录不存在则自动创建:
docker run --name <container_name> -v /data <image_name> <other_command>
把容器的 /data 目录交给 docker 管理并映射到本机的 /data 目录。docker run --name <container_name> -v <hostDir>:<volumeDir>
把宿主机上的 hostDir 映射到容器内的 volumeDir。执行 docker inspect -f {{.Mounts}} <container_name>
查看挂载关系。
多个容器可共享一个存储卷。
新启动的容器可以复制已启动容器的存储卷的映射关系: docker --volume-from <container_name>
。
列出所有不被挂载的容器卷 docker volume ls -qf dangling=true
。
删除所有不被挂载的容器卷 docker volume rm $(docker volume ls -qf dangling=true)
。
每一条 Dockerfile 指令都会生成一个单独的层,所以,尽量合并指令。
Dockerfile 是 dockerfile 的默认文件名,首字母必须大写,而且要有一个专用的工作目录。
docker build -f <docker-file>
可以指定 dockerfile。
.dockerignore 类似 .gitignore ,该文本文件中指明的文件不会被打包进镜像。
FROM
命令指定基础镜像
FROM <repository>[:<tag>]
: <repository>
用于指定基础镜像的名称, <tag>
缺省时默认为 latest 。FROM <repository>@<digest>
: <digest>
用于指定镜像的哈希码。COPY
命令从宿主机复制文件到镜像中
COPY <file> <dest-path>
: 把宿主机上的 <file>
文件复制到镜像的 <dest-path>
目录。COPY ["file-1", "file-2"..."file-n", "dest"]
: 一次性复制多个文件到镜像中。文件名支持通配符。<file>
是目录,其内部子文件会被递归复制,但目录本身不会被复制。<file>
或者使用了通配符,则 <dest>
必须是一个目录且必须以 / 结尾。<dest>
事先不存在则会被自动创建。ADD
指令类似于 COPY 命令
WORKDIR
指令用于为 dockerfile 中所有的 RUN, CMD, ENTERYPOINT, COPY 和 ADD 指定工作目录
WORKDIR <path>
指令可出现多次, <path>
可以是相对路径,但是,相对路径是相对于前一个 <path>
的相对路径。<path>
也可以是环境变量中指定的路径,类似 $STATEPATH 。VOLUME
指令用于在镜像随创建一个挂载点目录。
VOLUME <mountPoint>
或者 VOLUME ["<mountPoint>", "<mountPoint>"..]
可挂载多个卷。EXPOSE
指令指定容器中需要暴露的端口
EXPOSE <port>[/<protocol>] [<port>[/<protocol>]]
: <port>
指定端口, <protocol>
指定协议,tcp, udb 二选一,默认 tcp。docker rum -P
手动操作一下才会暴露命令中指定的端口。ENV
指令为镜像定义所需的环境变量,可以被 Dockerfile 文件中位于其后的其他指令调用
ENV <key> <value>
或者 ENV <key>=<value> [<key>=<value> ...]
。<value>
如果包含空格,可以使用 \ 进行转义或者使用 "value" 添加引号进行标识。$variable_name
或者 $<variable_name>
。RUN
指令定义 docker build
过程中要用的 shell 命令
FROM
指定的基础镜像中应该包含被执行的命令。RUN <command>
或 RUN ["<executable>", "<param1>", "<param2>"...]
:
<command>
被 /bin/sh -c
运行,意味着此进程在容器中的 PID 不为 1。docker stop <containerName>
停止容器时,被 /bin/sh -c
产生的进程接收不到 SIGTERM 信号。RUN ["/bin/bash", "-c", "<executable>", "<param1>", "<param2>"...]
。&&
进行并列,可用 \ 续行。CMD
指令定义 docker run 时要用的 shell 命令
CMD <COMMAND>
与 CMD ["<executable>", "<param1>", "<param2>"...]
意义同 RUN 命令。CMD ["/bin/bash", "-c", "<executable>", "<param1>", "<param2>"...]
。CMD ["<param1>", "<param2>"...]
用于为 ENTRYPOINT 指令提供默认参数。ENTRYPOINT
指令类似 CMD,为容器指定默认运行的程序
ENTRYPOINT <command>
或 ENTRYPOINT ["<executable>", "<param1>", "<param2>"...]
。MAINTAINER "<your_name> <your@email.com>"
指明作者。
LABEL 命令生成键值对,也可以代替 MAINTAINER 命令,比如 LABEL maintainer="<your_name> <your@email.com>"
。
进入专用目录执行 docker build ./
开始构建镜像, docker build -t <RepoName>:<tagName> ./
可以指定镜像的所属仓库与标签。
docker 被安装之后会自动创建一个叫 docker0 的 NAT 网桥。所有 docker 容器通过这个网桥与外界通信。
可以通过 sudo dnf install bridge-utils
或者 sudo apt install bridge-utils
获取工具来管理虚拟网桥。
每个使用 bridge 网络模式的容器被创建以后会生成 2 块虚拟网卡,一块连到 docker0 网桥上,一块连到容器里。
brctl show
(命令来源于 bridge-utils 包) 可以看到 docker0 网桥上绑定的所有虚拟网卡。
执行 ip a
也能看见绑定在 docker0 上的虚拟网卡。
可以使用环境变量 TZ 设置时区。有些容器没做时区功能,这个环境变量对这些容器没有意义。
如果容器不支持用 TZ 来设置时区,那么把本地的时区文件挂载在容器里然后设置为只读 docker run ... -v /etc/localtime:/etc/localtime:ro ...
可能是一个解决方案。
有命令: docker run ... -v source_volume:/data/path ...
如果容器的 /data/path 目录下有文件或目录,那么 docker 会把 /data/path 中的数据拷贝到 source_volume 中,遇到同名文件时,保留 source_volume 中的数据。 以 root 的身份操作。
然而,有时,source_volume 是个 NFS volume,而 NFS 通常会用 root_squash 参数使得 NFS client 使用 root 身份读写数据时,把 NFS client 的身份变成 nobody。
这就意味着,在使用 NFS volume 的情况下 docker 会以 nobody 的身份来将 /data/path 中的数据拷贝到 source_volume 中,而通常 nobody 用户并没有这样的权限,docker 拷贝文件失败,这个报错就出现了。
要解决这个问题,需要使用: docker run ... -v source_volume:/data/path:nocopy ...
注意里面的 nocopy 参数。这个参数的含义是,在容器启动时,docker 不会将 /data/path 目录中的数据复制到 source_volume 中,而是直接进行覆盖式的挂载。这意味着,原本在 /data/path 目录下的内容不会出现在 source_volume 中。
这会产生一些影响: 如果容器内的程序需要原本存在于 /data/path 中的数据,那么这个参数不适合于这种情形。此时,需要将 source_volume 挂载到容器的其他位置,然后启动容器,最后在容器中手动拷贝数据。
由于是手动操作,所以用户可以自行在容器中选择一个有权限的身份。最后,再使用 nocopy 参数将 source_volume 覆盖式地挂载在 /data/path 上。
当然,还有一种情况: 容器中的程序检测到 /data/path 中的数据消失了,就会再生成一份数据放在 /data/path 上。此时,由于容器已经运行,重新生成的数据实际上被存储在 source_volume 中。
在这种情况下, nocopy 的覆盖式挂载不会有什么影响。
最后给一个例子: linuxserver/transmission 这个镜像的行为就是第二种情况所描述的。
用户不带 nocopy 参数地将 NFS volume 挂载到容器的 /config 目录,而容器的 /config 目录本身有数据,NFS 的配置参数里也有 root_squash。此时 docker 就会报挂载失败,因为 nobody 用户并没有拷贝数据到 NFS volume 的权限。
用户使用 nocopy 参数将 NFS volume 挂载到容器的 /config 目录,容器中的 transmission 检测到 /config 目录下没有配置文件就会重新生成一份配置文件放在 /config 目录下。transmission 重新生成配置文件并保存到 NFS volume 的流程中并不涉及 root 权限,所以配置文件可以成功被保存下来,容器也就可以正常启动。
当然,如果 NFS volume 中本来就有容器内程序需要的数据,那就不需要以上操作,直接挂载即可。
首先卸载 Docker Engine, CLI 以及 Containerd packages: sudo apt-get purge docker-ce docker-ce-cli containerd.io
最后删除所有 images, containers 和 volumes: sudo rm -rf /var/lib/docker
apt-key list
找到 docker 的密钥手动删除。
sudo rm /etc/apt/sources.list.d/docker.list
删除 docker 的 apt 源。