容器

其实我一直不喜欢容器,我认为处理复杂依赖是包管理器和打包者的职责,推给 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 ↩︎