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 伺服器 socket 通訊的範例:

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 script 中使用 export

這只是 3 的弱化版本。export 的環境變數只能被該 init script 的子程序觀察到,docker exec 則什麼也看不到。

不過,限制變數的可見範圍始終是良好的做法。

  1. 在為 docker exec -it ... script.sh 準備的獨立 script 中使用 export

    只有在 entrypoint 只執行 sleep infinity,而你準備透過 docker exec 進入容器、輸入 shell 命令以操作隔離環境時,這種設定才有意義。init script 的所有子程序都看不到環境變數(只有你自行 source 它時才能看到),但對 sleep infinity 而言,這樣做是合理的。

Docker 磁碟區的迷思 #

Docker 磁碟區是由 dockerd 管理的持久化儲存空間。

  1. 任一容器結束或刪除後,磁碟區都不會被刪除;兩者彼此獨立。
  2. 建立容器時,可以將任意磁碟區附加至任意容器。但是,按照設計,容器建立後無法新增或移除磁碟區。

許多關於磁碟區的混淆來自 docker run|create 命令的 -v 旗標。這確實是糟糕的設計。語法為 -v [LHS:]RHS:

  1. 如果省略 LHS:,此旗標會建立匿名磁碟區,並將其掛載至容器內的路徑 RHS。
  2. 否則,如果 LHS 是以 / 或 ./ 開頭的字串,則會被解讀為主機上的路徑:絕對路徑,或相對於輸入 docker run 命令時 PWD 的路徑;該處的目錄或檔案會被 bind mount 至容器內的 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,請明確在 /tmp 上掛載 tmpfs。

如果希望在 --read-only 容器中讓指定路徑可讀寫,請掛載自己的 Docker 磁碟區。

評論

在 GitHub 上閱讀或評論(發表評論需要登入)