李守中
该站已迁往根域名 https://lishouzhong.com
需要注意,迁移后的文章的 url 可能会发生变化。
域名 https://note.lishouzhong.com 下的内容将不再更新,但已有内容会永久保留。

Docker 基础操作

Table of Contents

1. 安装

docker 按照维护者不同,常用的有两个版本: Debian 打包的 docker.io 以及 Docker 官方维护的 docker-ce

虽然 docker-ce 版本较高,但它把所有的依赖都糊在一个二进制文件里,要打补丁只能升级 docker 版本,所以还是推荐安装 Debian 打包 ( 外部依赖分别安装 ) 的 docker.io。

1.1. docker.io

docker.io 可以直接用命令 sudo apt install docker.io 安装。

为了让 lsz 用户在使用 docker 时不用在命令前加 sudo,执行 sudo usermod -aG docker lszlsz 用户加入 docker 用户组。

ssh 重连后生效。

1.2. docker-ce

安装 docker-ce 可以使用官方的一键安装脚本:

  1. curl -fsSL https://get.docker.com -o get-docker.sh
  2. 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 公钥:

  • Debian 系统: curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key add -
  • Ubuntu 系统: curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

对于 amd64 架构的机器,添加软件仓库:

  1. Debian 系统:
sudo add-apt-repository \
   "deb [arch=amd64] https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/debian \
   $(lsb_release -cs) \
   stable"
  1. 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

2. 配置

基本的 /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"
  }
}

2.1. 增加拉取 docker 镜像的加速

2.1.1. 使用国内镜像源

在 /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。

2.1.2. 给 dockerd 进程添加代理

经测试,在 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

让配置生效。

2.2. 容器日志设置

在 /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 使配置生效。

2.3. docker 程序环境

环境配置文件:

  • /etc/sysconfig/docker-network
  • /etc/sysconfig/docker-storage
  • /etc/sysconfig/docker
  • Unit File:
    • /usr/lib/systemd/system/docker.service
  • Docker Registry 配置文件:
    • /etc/containers/registries.conf
  • docker daemon 配置文件: /etc/docker/daemon.json

2.4. x86 设备报错

2.4.1. WARNING: No swap limit support

docker run ... -m 64M --memory-swap=128M ... 可以用来限制容器的最大内存使用情况:

  • -m 64M 限制了容器运行可以使用的最大物理内存为 64M;
  • --memory-swap=128M 限制容器可以使用的最 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 重启机器。

2.5. arm ( 树莓派 ) 设备报错

2.5.1. 容易解决的报错

树莓派第一次安装完 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 两个参数,重启系统就解决了。

2.5.2. 不支持 cfs 特性

如果报错出现:

WARNING: No cpu cfs quota support
WARNING: No cpu cfs period support

一般树莓派系统才会报这个错。

原因是早期的 Raspbian 系统在编译时就没支持 cfs。

早期没办法处理。

但是 2020 年末支持了这几个特性。

2.5.3. 不支持 blkio 特性

如果报错出现:

WARNING: No blkio weight support
WARNING: No blkio weight_device support

使用 SATA3 硬盘时候没有这个问题,在换成 SATA2 的硬盘以后问题出现。解决方案未知。

3. docker 命令

3.1. 基础操作

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>

3.1.1. 打包镜像

将镜像文件导出为 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

3.1.2. 容器自启动

docker run --restart=always --name <container_name> 容器总是自启动。

docker run --restart=unless-stopped 除非手动停止容器,否则容器总是自启动。

docker update --restart=no <container_name> 容器不会自启动。

3.1.3. 与容器交互 ( -it )

docker exec -it <container_name> /bin/sh 可以以新建一个交互式 shell 终端的方式连接到容器。

docker attach <container_name> 可以直接连接到容器。

3.1.3.1. -i 选项

-i 的含义是为容器开启一个 stdin 以读取输入。

所以可以有这样的操作:

echo some input | docker run -i debian cat

这个例子中,echo 命令的输出将通过管道传输到容器的 stdin 中 (由 -i 选项开启),容器将从 stdin 读到的数据传输给容器内的,需要从 stdin 读取输入的程序 (cat 程序),最终得到执行结果。即,输出 some input

3.1.3.2. -t 选项

-t 的含义是为容器分配一个虚拟的 tty 终端。

tty 是一个软件或者硬件,它连接到 linux 系统用于读取用户输入,并把输入处理成标准输入流发送到 stdin,然后程序从 stdin 中读取输入的数据并做出响应。

如果不使用 -i 给容器开启一个 stdin 的话,用户的输入不被任何程序处理就会出现用户无法退出这个 tty 的情况。

3.1.3.3. 和容器交互完怎么退出

用户连接到容器的情景一般有两个:

  • 容器启动时在 docker run 命令中指定 -i 但不指定 -d ,容器启动后当前终端直接连接到容器的 stdin
  • 使用 docker attach 命令连接到已经启动的容器的 stdin

对于后一种连接到容器的情景来说,断开与容器的连接的最简单的方案是找到 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 进程才能使连接到容器的终端退出。

3.2. 容器网络

运行无网络的容器 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> 查询。

3.3. 容器存储卷

在容器中挂载目录,如果要挂在的目录不存在则自动创建:

  • 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)

4. Dockerfile

每一条 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 命令。

  • ADD 格式与 COPY 相同。
  • ADD 支持 tar 文件与 url 路径。
  • 如果 ADD 了一个本地压缩文件,这个文件进入镜像之后会被自动解压缩到目标目录下。
  • 如果从 url 下载了一个压缩文件,则不会被自动解压。

WORKDIR 指令

用于为 dockerfile 中所有的 RUN, CMD, ENTERYPOINT, COPY 和 ADD 指定工作目录。

  • WORKDIR <path> 指令可出现多次, <path> 可以是相对路径,但是,相对路径是相对于前一个 <path> 的相对路径。
  • <path> 也可以是环境变量中指定的路径,类似 $STATEPATH

VOLUME 指令

用于在镜像随创建一个挂载点目录。

  • VOLUME <mountPoint> 或者 VOLUME ["<mountPoint>", "<mountPoint>"..] 可挂载多个卷。
  • 不可指定宿主机的映射点,docker 会在 /var/lib/docker 文件夹下创建映射目录。

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 信号。
    • 如果要执行的命令依赖 shell 特性 ( 比如要引用环境变量 ),可以指定运行 RUN ["/bin/bash", "-c", "<executable>", "<param1>", "<param2>"...]
  • 多个命令用 && 进行并列,可用 \ 续行。

CMD 指令

定义 docker run 时要用的 shell 命令。

  • CMD 指定的命令执行完毕后,容器终止。
  • CMD 命令可以被 docker run 的命令行选项覆盖。
  • Dockerfile 中可以有多个 CMD 命令,但只有最后一个会生效。
    • CMD <COMMAND>CMD ["<executable>", "<param1>", "<param2>"...] 意义同 RUN 命令。
  • 如果要执行的命令依赖 shell 特性 ( 比如要引用环境变量 ),可以指定运行 CMD ["/bin/bash", "-c", "<executable>", "<param1>", "<param2>"...]
  • CMD ["<param1>", "<param2>"...] 用于为 ENTRYPOINT 指令提供默认参数。

ENTRYPOINT 指令

类似 CMD,为容器指定默认运行的程序。

  • Dockerfile 中可以有多个 ENTRYPOINT 命令,但只有最后一个会生效。
  • 如果既有 CMD 又有 ENTRYPOINT,CMD 内容会被作为参数传给 ENTRYPOINT。
  • 格式为 ENTRYPOINT <command>ENTRYPOINT ["<executable>", "<param1>", "<param2>"...]
  • 不可被 docker run 指定的参数覆盖,而且这些参数会被传递给 ENTRYPOINT 指定运行的程序。
  • --entrypoint 选项的参数可以覆盖 Dockerfile 的 ENTRYPOINT 指定的程序。

MAINTAINER "<your_name> <[email protected]>" 指明作者。

LABEL 命令生成键值对,也可以代替 MAINTAINER 命令,比如 LABEL maintainer="<your_name> <[email protected]>"

进入专用目录执行 docker build ./ 开始构建镜像, docker build -t <RepoName>:<tagName> ./ 可以指定镜像的所属仓库与标签。

5. docker 容器虚拟化网络

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 上的虚拟网卡。

6. 常见问题

6.1. 设置容器内的时区

可以使用环境变量 TZ 设置时区。有些容器没做时区功能,这个环境变量对这些容器没有意义。

如果容器不支持用 TZ 来设置时区,那么把本地的时区文件挂载在容器里然后设置为只读 docker run ... -v /etc/localtime:/etc/localtime:ro ... 可能是一个解决方案。

6.2. 挂载 volume 时报错 failed to copy file info ...

有命令: 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 中本来就有容器内程序需要的数据,那就不需要以上操作,直接挂载即可。

7. 卸载

首先卸载 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 源。



Last Update: 2023-10-07 Sat 22:45

Generated by: Emacs 28.2 (Org mode 9.5.5)   Contact: [email protected]

若正文中无特殊说明,本站内容遵循: 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议