關於 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 通訊的範例:
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 原則有兩種方式:
- 使用
extends關鍵字 - 對 docker compose yaml 中可重複使用的設定片段使用 YAML anchor/merge 語法
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.101Dockerfile 中的變數替換規則 #
- 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)。
Dockerfile 的
ENV命令這種方法會讓
containerd在對容器內執行的任何程序進行 fork-exec 時加入指定的環境變數。因此,entrypoint 程序與針對執行中容器的docker exec都能使用,且與 shell 或任何 shell rc 無關。ENV支援的變數替換只有ARG與其他ENV,因此$PATH不會生效(其他常見的 Unix 環境變數也一樣)。docker run -e或compose.yaml的 environment機制與 Dockerfile 的
ENV命令相同:非 shell entrypoint 能看到環境變數,非 shell 的exec也能看到;但有以下差異:- 適用於一次性容器,以及之後對該容器執行的所有
docker exec。 - 所有展開都由主機 shell 完成。除了主機 shell 的變數之外,不允許其他變數。
- 適用於一次性容器,以及之後對該容器執行的所有
使用 Dockerfile 的
RUN命令修改 shell rc
優點:
- 此時位於真正的客體機器 shell 環境中,環境變數的值可以是某個程序的輸出,例如在 rc 中加入一行
export foo="$(...)"。這在 1 和 2 的ENV中從未可行。(範例請見此處)。 - 如果
docker exec命令和容器 entrypoint 都是 shell,兩者現在都能看到相同的環境變數。
缺點:
- 需要建置新的映像。
- 如果 entrypoint 或 exec 命令不是 shell,則無法運作。
大多數 Linux 發行版的 Docker 映像使用非登入 bash。請務必修改正確的系統或使用者層級 rc,而不是 profile。
- 在 entrypoint init script 中使用
export
這只是 3 的弱化版本。export 的環境變數只能被該 init script 的子程序觀察到,docker exec 則什麼也看不到。
不過,限制變數的可見範圍始終是良好的做法。
在為
docker exec -it ... script.sh準備的獨立 script 中使用export只有在 entrypoint 只執行
sleep infinity,而你準備透過docker exec進入容器、輸入 shell 命令以操作隔離環境時,這種設定才有意義。init script 的所有子程序都看不到環境變數(只有你自行 source 它時才能看到),但對sleep infinity而言,這樣做是合理的。
Docker 磁碟區的迷思 #
Docker 磁碟區是由 dockerd 管理的持久化儲存空間。
- 任一容器結束或刪除後,磁碟區都不會被刪除;兩者彼此獨立。
- 建立容器時,可以將任意磁碟區附加至任意容器。但是,按照設計,容器建立後無法新增或移除磁碟區。
許多關於磁碟區的混淆來自 docker run|create 命令的 -v 旗標。這確實是糟糕的設計。語法為 -v [LHS:]RHS:
- 如果省略
LHS:,此旗標會建立匿名磁碟區,並將其掛載至容器內的路徑RHS。 - 否則,如果
LHS是以/或./開頭的字串,則會被解讀為主機上的路徑:絕對路徑,或相對於輸入docker run命令時 PWD 的路徑;該處的目錄或檔案會被 bind mount 至容器內的 RHS 路徑。如果LHS解析為主機上不存在的路徑,則會在該處建立空目錄。這不會建立新的 Docker 磁碟區。 - 否則,如果
LHS符合現有 Docker 磁碟區的名稱(可透過docker volume ls查看),則該磁碟區會掛載至容器內的RHS。 - 否則,會建立新的 Docker 磁碟區並命名為
LHS,然後掛載至容器內的RHS。
只有 1 和 4 會建立新的磁碟區,3 會重複使用舊磁碟區,2 則與 Docker 磁碟區無關。然而,這些操作全都透過 -v 旗標完成。現代 Docker 實務建議以 -mount type=..., 旗標取代 -v,因為它的表達更明確。
以下不是磁碟區,也不會建立磁碟區:
LHS以/或./開頭時的docker run -v LHS:RHSdocker run --mount type=bind,...- 建立新容器會在 Docker 內部建立一個寫入層,但那不是磁碟區。你可以在
docker inspect <container>中查看它在主機上的位置;在 Linux 上通常位於/var/lib/docker/overlay2/下。該儲存空間不是磁碟區,而是 Docker 的實作細節。
以下操作涉及磁碟區:
- 明確建立新的磁碟區:
docker volume create LHS不以/或./開頭,或省略LHS:時的docker run -v LHS:RHSdocker 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 上閱讀或評論(發表評論需要登入)