Docker 基础操作 

Last Update: 2023-10-07

目录

安装

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

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

docker.io

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

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

ssh 重连后生效。

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 公钥:

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

  1. Debian 系统:

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

增加拉取 docker 镜像的加速

使用国内镜像源

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

给 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

让配置生效。

容器日志设置

在 /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 程序环境

环境配置文件:

x86 设备报错

WARNING: No swap limit support

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 重启机器。

arm (树莓派) 设备报错

容易解决的报错

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

不支持 cfs 特性

如果报错出现:

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

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

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

早期没办法处理。

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

不支持 blkio 特性

如果报错出现:

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

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

docker 命令

基础操作

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> 容器不会自启动。

与容器交互 (-it)

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

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

-i 选项

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

所以可以有这样的操作:

echo some input | docker run -i debian cat

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

-t 选项

-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 使用物理机的网络,不做映射。

容器端口映射:

-p (小写) 参数可以使用多次来暴露多个端口。

-P (大写) 或者 --publish-all 参数可以映射 Dockerfile 中 EXPOSE 命令指定的每个端口到主机的随机端口上,写法是 docker run -P ... 。也可以在后面加 --expose 参数来追加需要映射的端口,写法是 docker run ... -P --expose 2222 --expose 3333

主机上随即被映射的端口可以在防火墙规则中查询,也可以使用 docker port <container_name> 查询。

容器存储卷

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

执行 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 是 dockerfile 的默认文件名,首字母必须大写,而且要有一个专用的工作目录。

docker build -f <docker-file> 可以指定 dockerfile。

.dockerignore 类似 .gitignore ,该文本文件中指明的文件不会被打包进镜像。

FROM 命令指定基础镜像

COPY 命令从宿主机复制文件到镜像中

ADD 指令类似于 COPY 命令

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

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

EXPOSE 指令指定容器中需要暴露的端口

ENV 指令为镜像定义所需的环境变量,可以被 Dockerfile 文件中位于其后的其他指令调用

RUN 指令定义 docker build 过程中要用的 shell 命令

CMD 指令定义 docker run 时要用的 shell 命令

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

MAINTAINER "<your_name> <your@email.com>" 指明作者。

LABEL 命令生成键值对,也可以代替 MAINTAINER 命令,比如 LABEL maintainer="<your_name> <your@email.com>"

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

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

常见问题

设置容器内的时区

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

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

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

卸载

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