Docker 以及部署 DevOps 工具时常见的问题

持续更新,记录 Docker 以及部署 DevOps 工具时常见的问题。

注意,以下各项内容均在 Linux 系统中实践过,但不保证能正确应用于 Windows 和 macOS。

常用链接

Docker Hub:https://hub.docker.com/
Docker Compose 文档:https://docs.docker.com/compose/compose-file/
Dockerfile 文档:https://docs.docker.com/reference/dockerfile/
Docker 配置文件 daemon.json 文档:https://docs.docker.com/reference/cli/dockerd/
将 Docker run 转化为 Docker Compose:https://www.composerize.com/ (建议外网访问)

Docker 引擎 daemon.json 配置

官网文档:https://docs.docker.com/reference/cli/dockerd/#daemon-configuration-file
可以看到在不同操作系统下,可以使用的字段有所不同。

Linux 编辑配置文件:

sudo vim /etc/docker/daemon.json

macOS 编辑配置文件:

sudo vim ~/.docker/daemon.json

Windows 配置文件:
使用 WSL 时优先在 %userprofile%\.docker\daemon.json
未使用 WSL 时优先在 %programdata%\docker\config\daemon.json

(Windows 和 macOS 如果安装了 Docker Desktop,可以直接在设置界面找到 “Docker Engine” 配置项修改,这个入口其实就是在编辑 daemon.json

修改配置文件后,需要重启 Docker 来使配置生效(Docker Desktop 选择 “Restart”):

sudo systemctl daemon-reload
sudo systemctl restart docker.service

使用 Docker 源镜像:

2024 年 6 月更新:国内因政策原因,所有 Docker 镜像站均已关闭,以下所有源镜像均不可用了。
建议 阅读此文 了解还有什么国外可用的源镜像,此外文中还有利用 Cloudflare Workers 自行搭建反代的介绍,可以尝试。

用于国内拉取镜像加速。网上的版本太旧,很多都不能用了,GitHub Gist 上有人持续更新一个列表,点击前往

{
  "registry-mirrors": [
    "https://docker.mirrors.sjtug.sjtu.edu.cn",
    "https://docker.nju.edu.cn",
    "https://dockerproxy.com",
    "https://mirror.baidubce.com",
    "https://hub.c.163.com"
  ]
}

上面的镜像源分别来自:
上海交通大学镜像服务,他们也有 k8s 的镜像加速;
南京大学镜像服务,他们有 k8s、ghcr(GitHub 的镜像仓库)、gcr(Google 的镜像仓库)等镜像加速,很赞;
Dockerproxy 免费镜像,他们也提供上面各种国外镜像仓库加速,具体可以看文档;
百度云;
网易数帆。

阿里云需要去注册一个 镜像加速器,会提供一个带用户 ID 的域名;
腾讯云的服务器可以使用 https://mirror.ccs.tencentyun.com,注意非腾讯云的服务器可能无法使用的。


配置 Docker 的日志存储方式和体积上限:

推荐参考 Docker 官方文档 来进行进一步了解。
简单地限制 Docker 日志的体积:

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "1024m",
    "max-file": "3"
  }
}

上述配置将 Docker 日志限制为 1024MB 大小,至多 3 个文件。注意不要把 "max-file": "3" 中值 "3" 的双引号去掉。

也可以针对某个容器单独设置 Compose:

logging:
  driver: "json-file"
  options:
    max-size: "1024m"
    max-file: "3"

k8s 配置 cgroups:

{
  "exec-opts": ["native.cgroupdriver=systemd"]
}

容器中服务的时区不正确

遇到此问题,可以尝试给服务的 Docker Compose 配置时区环境变量,或者把宿主机的时区和时间文件以只读方式映射到容器:

environment:
  TZ: Asia/Shanghai # 只配这一条,就能解决大多数问题
volumes:
  - /etc/timezone:/etc/timezone:ro
  - /etc/localtime:/etc/localtime:ro

必须 sudo 才能执行 docker 指令

需要将当前用户添加到 docker 组中。此处以 Ubuntu 举例:

# 创建名为 docker 的用户组,如果已有则跳过此步骤
sudo groupadd docker

# 将当前用户加入 docker 组(重新登录后生效)
sudo gpasswd -a $USER docker

提示 Docker Compose 文件存在错误

注意你的所有端口映射写法,参考:

ports:
  - 80:80   # 一切正常
  - 443:443 # 一切正常
  - 2222:22 # 这行会导致出问题!

这是因为一个很隐晦的 yaml 规则:数值如果带有冒号,可能会被当做时间来解析。
通常较大的数字带冒号,不会被当做时间,比如上面的 80:80443:443;但如果数字较小,即使很多位,也可能被当做时间来解析,导致报错。

推荐的做法是,任何情况都给 ports 里面的项加上引号,养成这个习惯:

ports:
  - '80:80'
  - '443:443'
  - '2222:22'

哪种写法声明 environment 更好

Docker Compose 有两种声明 environment 的写法,分别是对象写法和数组写法。

对象写法:

environment:
  TZ: Asia/Shanghai

数组写法:

environment:
	- TZ=Asia/Shanghai

两种写法没有区别,也谈不上谁好谁坏。
如果非得分个优劣,我觉得对象写法比较好,因为 IDE 可以给出文本高亮,便于区分键和值。

Docker 日志太占空间

此日志清理命令为转载,原文链接:https://blog.csdn.net/qq165285727/article/details/132557787 ,应该是网上最好的方法了。
指令如下:

sudo docker ps -aq | xargs docker inspect --format='{{.LogPath}}' | xargs truncate -s 0

此指令会执行的操作:

  • docker ps -aq:列出所有容器的ID;
  • docker inspect --format='{{.LogPath}}':获取每个容器的日志路径;
  • xargs truncate -s 0:使用 truncate -s 0 命令清空每个日志文件。

同时建议参考下文中全局配置日志文件上限的方式,这样可以避免日志经常占满磁盘空间。

查看 Docker 占用的磁盘空间

执行命令:

docker system df

可以输出 Docker 镜像、容器、卷、构建缓存等所占用的磁盘空间。
其中,容器、卷占用的空间肯定无法释放,除非清理日志,但是日志一般也清理不了多少空间出来。所以我们主要清理镜像、构建缓存所占用的空间。

被容器使用中镜像无法删除,未被任何容器使用的镜像可以删除:

# 列出所有镜像,可以查看它们的占用空间
docker images
# 删除指定 id 或 tag 的镜像,被任何容器使用到的镜像无法删除
docker rmi <镜像id>

# 快速删除所有无标签的镜像
docker image prune

构建缓存也可以清理:

# 清理未使用的构建缓存
docker builder prune

服务监听不到流量

有可能是因为服务绑定的地址不对,如果监听 127.0.0.1 无法成功,可以试试 0.0.0.0,反之亦然;
区别是:

  • 127.0.0.1 绑定来自本地计算机的连接;
  • 0.0.0.0 表示所有可用的 IPv4 地址,服务绑定后可以监听来自主机、局域网、互联网上所有的连接。

通常而言,Docker 会创建网关并为每个容器管理流量,此时,容器收到的流量都是被 Docker 网络引擎转发过来的,对于容器来说这些流量是来自 127.0.0.1 的;所以这种情况下,如果你将容器移出 Docker 运行,可能就需要改为监听 0.0.0.0

更常见的情况是使用 Nginx,来自服务器外部的流量,经过 Nginx 后,对于服务而言也是来自 127.0.0.1 的了;所以你将服务运行在没有 Nginx 的环境下,可能也需要改为监听 0.0.0.0

端口映射时,也可以指定 IP 地址,例如:

ports:
  - '127.0.0.1:8080:8080'
  - '9090:9090'

这两种方式是不同的,8080 端口映射仅允许来自本地网络的连接进入容器;而 9090 端口映射没有指定 IP,它默认就是 0.0.0.0,即表示允许所有连接进入容器。
如果你的服务直接对外暴露,允许宿主机的端口直通容器,那么需要按照 9090 的写法,前面不能加上 127.0.0.1

容器内如何访问宿主机

Docker 默认使用桥接模式管理容器的网络:为每个容器建立一个虚拟网络,容器之间互相不通,由 Docker 引擎管理通讯。
因此,容器中的 localhost 或者 127.0.0.1 实际上是容器在自己所在的虚拟网络中的回环地址,它并不指向宿主机。

但是,容器有可能需要访问宿主机自身暴露的端口,总会有这种需求。所以需要一个表示宿主机的主机名或者 IP 地址。
方法有很多:


直接获取容器中网关的地址,此地址即指向宿主机:

执行指令:

# 此指令列出了容器所有底层信息,也包括网络设置
docker inspect <容器名>

注意结果中的 NetworkSettings.Gateway 字段,如果有值,则它就是容器中用于访问宿主机的 IP 地址;
如果你使用 Docker Compose,上述值可能为空,改为寻找 NetworkSettings.Networks,它是一个对象,表示当前容器加入的所有 Docker 网络,你可以通过任一网络的 Gateway 字段来得到用于访问宿主机的 IP 地址。如果当前容器加入多个网络,选择其中任一个即可访问宿主机。

Docker 建立的网络,其网关地址不会自行发生改变,但是此方法不够通用,在不同机器上有可能网关地址不同,还需要想办法把这个 IP 传给容器里的服务,实际应用较少。


【推荐】将网关添加到 hosts 当做一个域名:

Docker 允许我们将网关地址添加到容器的 hosts 中,当做一个域名。编辑 Docker Compose 配置:

extra_hosts:
  - '<自定义域名>:host-gateway'

经过这样配置后,在此容器中便可以使用自定义的域名来访问宿主机。这里的 host-gateway 是关键字,不要动它。
你可以使用 docker exec 进入容器中,找到 /etc/hosts 文件,便可以看到 Docker 自动添加了 hosts 记录。

这种方法实际上是上一种方法的升级版,它让 Docker 自动把不固定的网关地址映射为了一个固定的域名,所以是最稳定和最推荐的做法。


使用魔法域名(仅 Docker Desktop 可用):

使用在 Windows 和 macOS 上安装的 Docker Desktop 时,容器中可以直接使用以下域名来访问宿主机:

host.docker.internal

注意,Linux 是不支持的,所以这个方法也不推荐。


使用宿主机的网络模式(仅 Linux 系统可用):

编辑 Docker Compose 配置(再说一遍,仅 Linux 可以用):

network_mode: host

这会使得容器共享宿主机的网络,此时便可以通过 localhost127.0.0.1 直接访问宿主机。
此模式通讯效率也是最高的,但是会存在安全性问题,也没有做到网络隔离。实际上这种方法牺牲了 Docker 的很多特性,仅用于特定场景,不是广泛的解决方法。

部署 Nginx 时如何初始化

Nginx 镜像在启动容器后,如果没有给它的配置目录 /etc/nginx/ 做目录映射,那么它会使用内部的一套默认配置来启动;
一旦将这个配置目录映射到宿主机的某个目录,那么 Nginx 镜像便不会创建任何默认配置了,这个目录就是空着的,导致第一次启动报错。

按照以下步骤来初始化:

# 提前新建目录,如果让 Docker 来创建的话,目录的拥有者会变成 root
sudo mkdir -p <配置文件目录>

# 临时启动一个 Nginx 容器
sudo docker run --name tmp-nginx-container -d nginx

# 复制出默认配置
sudo docker cp tmp-nginx-container:/etc/nginx/ <配置文件目录>

# 删除临时的 Nginx 容器
sudo docker rm -f tmp-nginx-container

这样便得到了一份默认的 Nginx 配置文件,正常启动 Nginx 时就可以把这个配置文件目录映射过去了。

部署 Gitea 并与宿主机的 22 端口直通

因为 Git 使用 SSH 通讯,默认走 22 端口,所以如果想让 Gitea(或者 Gitlab、Gogs 等)的容器也绑定到 22 端口,会与操作系统的 sshd 服务抢占 22 端口,导致无法成功。

SSH 的 Git 克隆地址通常是这样的:git@paperplane.cc:jia-niang/paperplane-blog.git,其中指定了用户名 git,因此我们可以实现一个程序,判断连接到主机的用户名,如果叫 git,便将它转发给 Git 服务。

如果直接将 Gitea 安装到宿主机,安装程序会自动帮我们部署转发程序;但在 Docker 中部署 Gitea 时,这个转发程序便需要由用户自行来完成了。这里有两份官方文档:gitea.com 的中文文档gitea.cn 的中文文档,可以提供第一手的支持,本文旨在写出自己的实践以避坑。

依次执行这些指令,这里以 Ubuntu 系统为例:

# 新建 git 用户,如果已有则跳过此步
sudo adduser git

# 为 git 用户创建一个 SSH 密钥对
sudo -u git ssh-keygen -t rsa -b 4096 -C "Gitea Host Key"

然后,创建一个可执行文件:

sudo vim /usr/local/bin/gitea

在这个文件中写入以下内容:

ssh -p 2222 -o StrictHostKeyChecking=no git@127.0.0.1 "SSH_ORIGINAL_COMMAND=\"$SSH_ORIGINAL_COMMAND\" $0 $@"

这段内容用于把原始的命令转发给本机的 127.0.0.1:2222,并使用 git 用户名。这里的 2222 端口你可以更换成别的。
完成写入后保存退出,继续执行:

# (重要)给刚才那个文件赋予可执行权限
sudo chmod a+x /usr/local/bin/gitea

# (重要)把刚才给 git 用户生成的公钥添加到 git 用户的已认证公钥列表中,允许自己连自己
sudo echo "$(cat /home/git/.ssh/id_rsa.pub)" >> /home/git/.ssh/authorized_keys

# 查看并记住 git 用户的用户 ID (左)、用户组 ID(右)
sudo vim /etc/passwd

# 此步骤非必须
# 如果你担心 git 用户密码泄露被人登录,可以删去它的密码
sudo vim /etc/shadow # 把密码的哈希字符串改为 “*”

完成以上步骤后,便可以准备启动 Gitea 容器。
编辑 Docker Compose 配置:

environment:
  - USER_UID=1002 # 填写刚才看到的 git 用户的 ID(左)
  - USER_GID=1002 # 填写刚才看到的 git 用户的用户组 ID(右)
volumes:
  - /home/git/.ssh/:/data/git/.ssh   # 必须
  - /etc/timezone:/etc/timezone:ro   # 推荐
  - /etc/localtime:/etc/localtime:ro # 推荐
  - <持久化存储目录>:/data
ports:
  - '127.0.0.1:2222:22' # 必须,左侧的端口号不一定是 2222,只要和上面的匹配即可

至此便完成了设置。

原理也很简单,未登录的用户只能使用 https 克隆,想使用 SSH 则必须在 Gitea 中登记自己的公钥。

而 Gitea 在登记公钥时,向 /home/git/.ssh/authorized_keys 中写入的不只是公钥本身,而是在开头加了一段 command="/usr/local/bin/gitea ... 的指令,这会使得使用这些公钥的用户连接时,自动执行上面所写的可执行文件,从而使指令被转发到 2222 (或者是你自己指定的)端口。

而我们在 Docker 中配置了,本机的 2222 (或者是你自己指定的)端口映射到 Gitea 容器的 22 端口,这样便实现了把用户从宿主机 22 端口传来的 SSH 直通给了 Gitea 容器的 22 端口。

数据库使用 Navicat 但不暴露端口

数据库如果对外暴露端口,可能遭到加密勒索等黑客攻击,因此大部分场合我们都不会把数据库的端口对外开放,通常 Docker 容器直接不做端口映射,或者是通过防火墙禁用掉数据库的端口。

但是,不开放数据库端口,会使我们导致难以调试,无法使用 Navicat 之类的工具。
虽然可以通过一些数据库连接工具来对外暴露一个 Web 界面,通过 Web 界面调试,例如 adminermongo-express 等工具,但是便利性还是比不上 Navicat 的,所以还是希望能用一种方法来支持工具调试。

此时,有一种方案就比较适合我们的需求:
Navicat 允许我们使用 SSH 隧道来连接数据库,会先 SSH 连接到主机,然后再尝试连接数据库,这便相当于做了一次 “中转”。
下面就以 PostgreSQL 举例,演示这种配置方式:

首先,为了避免数据库端口意外对外暴露,可以先在云服务供应商的防火墙处配置,禁用数据库端口的连通,一般来说禁用 TCP 即可,当然也可以把 UDP 也一起禁用了;也可以使用 Linux 的防火墙 iptablesfirewalld,关闭掉这些端口的连接。

MySQL 端口为 3306,PostgreSQL 端口为 5432,MongoDB 端口为 27017

然后,修改数据库的 Docker Compose 文件:

services:
	postgres:
    ports:
      - '127.0.0.1:5432:5432'
      # 注意这里的 127.0.0.1 不能省略

这里虽然做了 5432 端口映射,但指定了 127.0.0.1 这个 IP,只允许来自宿主机本地的连接。
这样做,即使 5432 端口对外放开,外部的连接也无法传入 PostgreSQL。

然后这一步是可选的,如果你使用域名来连接,建议做这一步;如果用 IP 直连,则无需操作。
修改 hosts,把本机的域名指向本地。
执行指令:

sudo vim /etc/hosts

添加:

127.0.0.1 example.com

把这里的 example.com 换成你自己的域名。
更换后,可能要重启服务器或重启网络服务,这个新的 hosts 才能生效。

然后,打开 Navicat 配置连接:
先配置 SSH:

此处配置和 SSH 连接用的配置一样,端口是 22;认证方式我是用 SSH 密钥,如果是密码的方式,可以在下拉框处修改。
服务器地址我这里填写了域名,如果你使用 IP 直连,改成 IP 即可。

然后配置数据库连接:

这里需要注意的是,如果你通过 SSH 连接到服务器,那么此时这里的 “Host” 地址是相对于服务器而言的了。如果数据库就在这台服务器上,你可以把 “Host” 直接填写 localhost如果数据库部署在内网其他机器上,改成对应的 IP 或域名即可;
如果想像图中这样填写本机的域名,那么上一步配置 hosts 便是必须的步骤,不然服务器走外网 DNS 到自身的地址,会发现 5432 端口被防火墙挡了。

这样便配置完成了,点击左下角的 “Test Connection” 测试连通。
完成时:


如果连接失败了,也可以通过图示化界面查看是哪一步出错。

例如,如果 SSH 连接没配对,会显示成这样:

如果 SSH 连接配置正确,但是访问数据库的步骤出错,会显示成这样:

这样调试起来也会更容易。