Docker 杂项问答

最后更新: 2022-04-27 17:02:27 +0800 +0800
tags: Docker
原文以英文撰写。您正在阅读AI翻译版本。

关于 Docker 的有趣事项与备忘录。

从容器引用宿主机的 IP 地址 #

  • 对于 MacOS 宿主机 + Docker Desktop

    名称 docker.for.mac.host.internal 会解析为 MacOS 上安装的虚拟 NIC 的 IP 地址,并且无需任何配置。当然,VM 层(https://github.com/linuxkit/linuxkit/blob/master/examples/docker-for-mac.md)仍然存在,但至少 IPv4 路由已经正常工作,可以传输真实的 TCP 流量。

  • 对于 Linux 宿主机 + Docker engine

    docker run 和 create 命令接受 --add-host=name:<ip> 标志,用于向容器的 /etc/hosts 添加一行 <ip> name。<ip> 可以使用特殊字面值 host-gateway,以引用宿主机 Linux 内核中虚拟 NIC 的 IP 地址(由 Docker engine 负责设置)。因此最终会得到类似 172.0.0.1 name 的一行。

下面是允许容器化 GUI 与宿主机 X11 服务器套接字通信的示例:

sh
docker run --name gugu -it --entrypoint bash --user jose --add-host=hostgg:host-gateway -v $AA:$AA -v $BB:$CC -e DISPLAY=:0 -e XAUTHORITY=$CC kk

# in the container
getent hosts hostgg
172.17.0.1      hostgg

在 docker compose yaml 中复用组件 #

在 docker compose yaml 文件中实践 DRY 原则有两种方式:

  1. 使用 extends 关键字
  2. 对 docker compose yaml 中可复用的配置片段使用 YAML anchor/merge 语法
yaml
networks:
  gugunet:
    driver: bridge
    enable_ipv6: false
    ipam:
      driver: default
      config:
        - subnet: 99.99.99.0/24
          gateway: 99.99.99.1

x-hdfs-commons: &hdfs-commons
  # hadoop launcher scripts requires bash, don't use alpine.
  image: ubuntu:latest
  volumes:
    - ../rofiles/zulu17.58.21-ca-jdk17.0.15-linux_aarch64:/opt/jdk:ro
    - ../rofiles/hadoop-3.4.1:/opt/hadoop:ro
    - ./core-site.xml:/opt/hadoop/etc/hadoop/core-site.xml:ro
#    - ./hdfs-site.xml:/opt/hadoop/etc/hadoop/hdfs-site.xml:ro
    - ./hadoop-logs:/tmp/hadoop-logs
  environment:
    - JAVA_HOME=/opt/jdk
    - PATH=$PATH:/opt/jdk/bin:/opt/hadoop/bin
    - HADOOP_LOG_DIR=/tmp/hadoop-logs


services:
  hdfs-name-node:
    <<: *hdfs-commons
    networks:
      gugunet:
        ipv4_address: 99.99.99.100
    command: bash -c "hdfs namenode -format && hdfs --daemon start namenode && sleep infinity"
  hdfs-data-node-1:
    <<: *hdfs-commons
    command: bash -c "hdfs --daemon start datanode && sleep infinity"
    networks:
      gugunet:
        ipv4_address: 99.99.99.101

Dockerfile 中的变量替换规则 #

  • Dockerfile 同时使用 $VAR 和 ${VAR} 语法。
  • VAR 必须是之前的 ENV(在自身或基础镜像的 Dockerfile 中定义)或 ARG。
  • 无法引用任何 shell 变量:既不能引用客户机 shell 的变量,也不能引用宿主机 shell 的变量(这是有意设计的隔离)。

有时确实可以引用 $PATH。这是因为基础镜像在其 Dockerfile 中写入了 ENV PATH,而不是因为执行了客户机 shell 的 init rc。

任何需要在客户机 shell 中通过 shell eval 实现的动态能力都无法使用。也就是说,如果环境变量的值必须由客户机中的命令计算,ENV 就无能为力。请参阅下文。

设置环境变量 #

经验法则:

  • 阅读“Dockerfile 中的变量替换规则”。如果这对你来说足够,则使用 (1) 和 (2)。
  • 否则,如果希望环境变量能够被 entrypoint 进程及其子进程看到,则使用 (3) 和 (4)。
  • 否则,如果希望 Docker 容器永久运行,并通过 docker exec 进入其中进行操作,则使用 (5)。
  1. Dockerfile 的 ENV 命令

    此方法会使 containerd 在对容器内运行的任意进程执行 fork-exec 时添加指定的环境变量。因此,它同时适用于 entrypoint 进程和对运行中容器执行的 docker exec,与 shell 或任何 shell rc 无关。

    ENV 支持的变量替换只有 ARG 和其他 ENV,因此 $PATH 不会生效(其他常见的 Unix 环境变量也同样不会生效)。

  2. docker run -e 或 compose.yaml 的 environment

    机制与 Dockerfile 的 ENV 命令相同:非 shell entrypoint 可以看到该环境变量,非 shell 的 exec 也可以看到;区别在于:

    • 它适用于一次性容器,以及之后对该容器执行的所有 docker exec。
    • 所有展开都由宿主机 shell 完成。除了宿主机 shell 的变量之外,不允许使用其他变量。
  3. 使用 Dockerfile 的 RUN 命令修改 shell rc

优点:

  • 此时处于真实客户机的 shell 环境中,环境变量的值可以是某个进程的输出,例如向 rc 添加一行 export foo="$(...)"。这是 1 和 2 中使用 ENV 时无法实现的。(示例见此)。
  • 如果 docker exec 命令和容器 entrypoint 都是 shell,则二者现在可以看到相同的环境变量。

缺点:

  • 需要构建新镜像。
  • 如果 entrypoint 或 exec 命令不是 shell,则无法工作。

大多数 Linux 发行版的 Docker 镜像使用非登录 bash。请确保修改正确的系统级或用户级 rc,而不是 profile。

  1. 在 entrypoint init 脚本中使用 export

这只是 3 的弱化版本。export 的环境变量只能被该 init 脚本的子进程观察到;docker exec 看不到任何内容。

不过,限制变量的可见范围始终是良好的做法。

  1. 在为 docker exec -it ... script.sh 准备的独立脚本中使用 export

    只有在 entrypoint 仅执行 sleep infinity,并且你准备通过 docker exec 进入容器、通过输入 shell 命令来操作隔离环境时,这种设置才有意义。init 脚本的所有子进程都看不到该环境变量(只有你自己 source 它时才能看到),但对于 sleep infinity 来说,这样做是合理的。

Docker 卷的误解 #

Docker 卷是由 dockerd 管理的持久化存储。

  1. 任意容器退出或删除后,卷都不会被删除;二者彼此独立。
  2. 创建容器时,可以将任意卷挂载到任意容器。但是,按照设计,容器创建后无法添加或删除卷。

关于卷的许多困惑来自 docker run|create 命令的 -v 标志。这个设计确实很糟糕。语法是 -v [LHS:]RHS:

  1. 如果省略 LHS:,该标志会创建一个匿名卷,并将其挂载到容器内的路径 RHS。
  2. 否则,如果 LHS 是以 / 或 ./ 开头的字符串,它会被解释为宿主机上的路径:绝对路径,或相对于输入 docker run 命令时的 PWD 的路径;该路径下的目录或文件会被绑定挂载到容器内的 RHS 路径。如果 LHS 解析为宿主机上不存在的路径,则会在那里创建一个空目录。这不会创建新的 Docker 卷。
  3. 否则,如果 LHS 与现有 Docker 卷的名称匹配(可通过 docker volume ls 查看),则该卷会被挂载到容器内的 RHS。
  4. 否则,会创建一个新的 Docker 卷并命名为 LHS,然后将其挂载到容器内的 RHS。

只有 1 和 4 会创建新卷,3 会复用旧卷,2 与 Docker 卷无关。然而,所有这些操作都通过 -v 标志完成。现代 Docker 实践建议使用 -mount type=..., 标志替代 -v,因为前者表达得更明确。

以下不是卷,也不会创建卷:

  1. LHS 以 / 或 ./ 开头时的 docker run -v LHS:RHS
  2. docker run --mount type=bind,...
  3. 创建新容器会在 Docker 内部创建一个写入层,但它不是卷。可以通过 docker inspect <container> 查看其在宿主机上的位置;在 Linux 上通常位于 /var/lib/docker/overlay2/ 下。这种存储不是卷,而是 Docker 的实现细节。

以下操作涉及卷:

  1. 显式创建新卷:docker volume create
  2. LHS 不以 / 或 ./ 开头,或省略 LHS: 时的 docker run -v LHS:RHS
  3. docker run --mount type=volume,src=...,dest=...

/tmp 上的 tmpfs #

Docker 默认不会为容器中的 /tmp 提供内存中的 tmpfs。如果需要,必须显式指定:

  • 对于 compose 文件,使用服务下的 tmpfs 关键字。
  • 对于 docker run / create 命令,使用可重复指定的 --tmpfs /MOUNTING/POINT 标志。

只读容器 rootfs #

要创建 rootfs 为只读的容器,可以使用:

  • docker run / create 命令的 --read-only 标志;或
  • compose 文件服务中的 read_only: true

如果希望谨慎观察某个软件的行为,或者容器提供远程 SSH 时限制用户权限,这一功能很有用。

此设置只会使 rootfs 只读;其他已挂载的文件系统(例如 /proc 上的 tmpfs 和 /sys 上的 sysfs)仍然像平常一样可读写,因为内核运行需要这些文件系统可变。不过,这通常已经足够。需要特别注意的是,/tmp 也会变为只读;如果需要可写的 /tmp,请显式将 tmpfs 挂载到 /tmp。

如果希望在 --read-only 容器中让某个路径可读写,请挂载自己的 Docker 卷。

评论

在 GitHub 上阅读或评论(发表评论需要登录)