容器

其实我一直不喜欢容器,我认为处理复杂依赖是包管理器和打包者的职责,推给 container 是比较粗暴的做法。然而比起虚拟机,容器的性能开销足够小,空间开销换可复现性可以接受,并且在微服务架构下单元化的优势确实很大。对于这种已经成为了事实标准的东西,还是有必要去学习了解一下的。

选择

最流行的容器工具应该就是 docker 了吧,然而因为几次负面消息[1][2]让我对其没有任何好感。

我也尝试在 wsl2 上部署了一下 docker,然而由于 ArchWSL 没有 systemctl 而无法启动其守护进程。docker-systemctl-replacement 不可用,需要 nohup 启动 dockerd。我的 wsl2 只有纯命令行,折腾许久,浪费了许多时间。

于是我转向了 podman,这是一个 daemonless, open source(开源的、无守护进程的)容器工具,解决了我对 docker 的偏见。并且其兼容性也不错:Most users can simply alias Docker to Podman (alias docker=podman) without any problems.。不过 podman 也有缺点,例如开机自启要麻烦一些。可以前往 external 1. 查看详细信息。

再说说平台兼容性:

提示

本篇中 docker 和 podman 命令是混用的。

工具

基础

看看 tutorial。

容器和镜像是两个概念,可以类比为系统和系统盘。

sh
docker ps                                 # 查看运行状态
docker ps -a                              # 查看所有容器状态
docker run -d ...                         # -d 表示后台运行
docker rm <name>                          # 删除容器
docker logs <name>                        # 查看输出 (stdout + stderr)
docker logs -f <name>                     # 持续查看输出 (stdout + stderr)
docker pull <repository>:<tag>            # 拉取镜像到本地
docker images                             # 查看镜像
docker tag <image ID> <repository>:<tag>  # 重命名镜像
docker stop $(docker ps -aq)              # 停止所有容器
docker rm $(docker ps -aq)                # 删除所有容器
docker rmi $(docker images -q)            # 删除所有镜像

调试

sh
docker run -it --entrypoint /bin/bash <image>     # 从 image 创建容器并进入(作为调试用)
docker build .                                    # 构建容器镜像(需要先下载 docker-buildx,否则会吃一个 deprecated。
                                                  # 下载完后,docker build 相当于 docker buildx build 的 alias)
docker system prune                               # 清理所有缓存、非运行中的镜像
docker builder prune -a                           # 清理构建缓存

代理

docker 代理需要特殊配置的原因是它访问网络的部分是以 systemd service 形式跑的 daemon ,它拿不到你在 shell 环境设置的代理相关的环境变量,群友推荐 podman 是因为它没有 daemon ,直接在你调用命令的进程里访问网络,就不需要特别地配置代理了。——DDoSolitary

src

简言之:守护进程和 docker 都需要配置,/etc/docker/daemon.json 和 ~/.docker/config.json 都需要写入代理内容。

而且注意,容器内部无法直接通过 localhost,127.0.0.1 访问宿主机代理端口,解法在容器中访问宿主机服务。

如果你的容器有指定虚拟子网,记得将互相的容器名加入 NO_PROXY,否则可能出现通信错误。

dockerhub mirror

在 2024 年,中国大规模下架了 docker 镜像。所以现在想要使用 docker 仓库会有一些麻烦。Using docker in China 2024 这篇文章介绍了一些方法,我尝试了 cloudflare 反代。不过反代的后果也是 UNAUTHORIZED。

然后还踩了应用镜像的坑。

容器中访问宿主机服务

最佳解法:使用宿主机的局域网 ip,并且关掉防火墙。

其他办法都挺麻烦的,例如 host.docker.internal 只在 Docker Desktop 上能用,而 linux ip addr show docker0 的 172.17.0.1 试过了用不了。

Dockerfile

如果你需要制作一个镜像,那么本质上就是写一个 Dockerfile 用来描述镜像。

指令 解释
ENV 设置环境变量。对所有之后的指令生效,可以多次覆盖。
COPY 将外部文件复制到容器内。了解更多请阅读坑。

每个 ADD,COPY,RUN 指令会为 image 创建一个只读 UnionFS layer。layer 是用于缓存的,当修改了 Dockerfile 的某一行后,系统只会构建此行和所有之后的指令,之前的 layer 可以被跳过构建。也正是因为 layer 的存在,尽可能不要写紧邻的 RUN 语句,如果每个 command 都分配一个 RUN 语句会导致 layer 过多,影响存储和性能。

坑

Dockerfile 的坑实在是太多了,真是他妈的屎一样的设计。

网络

docker 网络有四种模式,Host(和宿主共用网卡 IP 端口), Container(和其他容器共用网卡 IP 端口), None, Bridge(虚拟子网)。

一般容器间通信可以用 Container 也可以用 Bridge,Container 不用额外配置,Bridge 则更灵活。

Bridge 组网大致如下:

sh
docker network create xxx                                           # 先创建虚拟子网
# 通过 --network (--net) 指定虚拟子网
docker run -d --name AAA -p 8000:8000 --network xxx <image>         # 假设这是一个提供服务的容器
docker run -it --name BBB -p 8000:8000 --network xxx <image> sh     # 假设这是一个访问服务的容器
NO_PROXY=AAA curl -v http://AAA:8000                                # 访问 AAA 的服务。如果你设置了代理,要保证虚拟子网内的请求不走代理。

可以看出,Bridge 需要修改容器内程序的源码或 env 以适配新的 base url,还是有一些麻烦的。好处是可以加新的容器到子网,或者随便更换服务提供容器都行。

为离线机器下载镜像

假设我有无法联网的内网服务器,我需要使用其跑镜像。

如果你的电脑上安装了 docker,你应该优先选择这个方法。

  1. 使用 docker pull 拉取镜像。注意你的目标架构需要与服务器的架构相同。
    sh
    docker pull --platform linux/arm64 hectorqin/reader:openj9-latest
  2. 使用 docker save 导出指定镜像。如果你有多个镜像,并且 docker 内所有镜像都需要导出,你可以用脚本批量导出:
    sh
    #!/bin/bash
    output_dir="docker_images"
    mkdir -p "$output_dir"
    for image in $(docker images --format "{{.Repository}}:{{.Tag}}"); do
        echo "Saving $image..."
        file_name=$(echo "$image" | sed 's/[\/:]/_/g').tar
        docker save -o "$output_dir/$file_name" "$image"
    done
    echo "All images saved to $output_dir/"
  3. 通过 rsync 或者其他方法将所有 .tar 传输到目标机器上
  4. 使用 docker load 加载所有镜像。

docker-drag 用不了,别看了。我用的算是官方的 moby 脚本吧。

sh
wget https://raw.githubusercontent.com/moby/moby/master/contrib/download-frozen-image-v2.sh
chmod +x download-frozen-image-v2.sh
bash download-frozen-image-v2.sh reader hectorqin/reader:openj9-latest
tar -cvaf reader.tar reader
# 这样就把镜像下载并打成了 tar 包。

不过这个脚本在 20241204 时还有 bug,就是必须指定 image 的 tag,否则下载链接会 404。

好不容易下载完了,结果打出的 tar 还不能直接被 docker load 加载,非常神奇。我不知道怎么办了。

docker-compose

docker-compose 是 docker 的上一层抽象,一言:一个 yaml 文件,指明了多个容器的参数。实际上使用一个 bash 脚本也能做到 docker-compose 做的事。

关于开机自启动

docker 容器非常方便,用 --restart always(总是自动重启)或 --restart unless-stopped(除非手动停止,否则总是自动重启)启动容器就行。

但是 podman 是 rootless 的,没有一个守护进程去叫醒容器,--restart 参数对其无效。使用 systemd 用户服务的话,按照网上教程 podman generate systemd 又会发现 command deprecated,而且这样无法启动 podman-compose 生成的 pod。

然后我又折腾了一段时间。遇到了太多无语的事情。

最后我的解决方法:回归本源,每次开机时运行 /usr/bin/podman pod start pod_root 来启动 pod。至于开机自动执行命令,可以用我的 user-startup,一行搞定。

遇到的问题

无法应用镜像

解法:ps | grep dockerd,可以看到应用的 config 是 --config-file=/tmp/dockerd/daemon.json,根本就不是 /etc/docker/daemon.json。

继续看 /etc/init.d/dockerd,可以发现其 config 是写在奇怪的地方,并且格式也是自定义格式。(被我喷了)

podman 指定 registry

/var/run/docker.sock: connect: permission denied

这个也是常见问题了,守护进程运行在 root 下,普通用户无法访问。解法(ref):

sh
sudo usermod -aG docker $USER

然后重新登录。

安装问题

正常的 Linux 发行版的包管理器安装都不会出现问题。不过 WSL 就折磨了。我用 ArchWSL,结果 docker 没有守护进程,podman 爆 subuid 错误。

docker

ArchWSL 上没有 systemd 用,所以自然也不会自行启动 dockerd 守护进程。因此我必须要在另一个 console 上手动运行 sudo dockerd 才能正常使用 docker。

podman

在 ArchWSL 上 podman 找不到 /etc/subuid,报错。subuid 和 subgid 是用于 User Namespaces 的一部分,主要用于容器化软件,以创建特权分离的容器。

运行指令:

sh
sudo usermod --add-subuids 100000-165535 <user>
sudo usermod --add-subgids 100000-165535 <user>

解决。

external

  1. Exploring Podman: A More Secure Docker Alternative
  2. podman 踩坑记录 - Aya | source code

  1. Docker 向所有 Docker Hub 用户发去邮件,如果他们是以组织的名义创建账号,那么他们的账号将被删除,所有镜像也将一并删除,除非他们升级到一个付费的团队方案——其年费为 420 美元。src: solidot ↩︎

  2. Docker 公司将限制其 Docker Desktop 工具仅供个人或小企业免费使用,大企业将需要付费订阅。该公司要求员工人数在 250 人以上或年收入超过一千万美元的企业如果需要使用 Docker Desktop 那么就必须付费订阅。src: solidot ↩︎