我踩到了什麼坑

本文的背景延續自我之前的文章《一個專案需要多個 Dockerfile - 淺談建構上下文 (build context)

因為我們目前經手的專案會需要針對不同環境或是測試 Build 不同的 Image,為了讓目錄架構更具組織性且容易理解,我們根據不同的環境把各個環境的 Dockerfile, docker-compose.yaml, .sh 等等放在各個環境的目錄下,現在的目錄架構大概像這樣 ↓:

E2Eproject/
├── testenvironment1/
│   └── Dockerfile
├── testenvironment2/
│   ├── docker-compose.yaml
│   └── Dockerfile
├── start-headless-tests.sh
└── requirements.txt

由於我正在本地 Debug 這個專案,需要在本地運行 Docker-compose,我便很自然的執行 docker-compose -f ./cicd/headless/docker-compose.yaml up,結果 Docker 容器運行起來時,遇了一個錯誤:/bin/bash: /usr/src/app/cicd/headless/start-headless-tests.sh: No such file or directory

這個錯誤讓我想也想不透哪邊出錯,我一開始都是針對 Dockerfile 去做動作,但完全沒有用

這邊就附上當時錯誤發生時的 Dockerfiledocker-compose.yaml,大家可以試著猜猜看是哪個環節導致錯誤!

E2Eproject/cicd/headless/Dockerfile ↓

# Dockerfile

# Use the official Python base image from the DockerHub.
FROM python:3.12

# Set the working directory within the container.
WORKDIR /usr/src/app

# 略.....
# 略.....
# 略.....

# Copy all files from the current directory on the host to the working directory in the container.
# This includes the application source code and any additional required files.
COPY . /usr/src/app/

# Copy the start-tests.sh script into the container's work directory.
RUN chmod +x /usr/src/app/start-headless-tests.sh

# The command to run the application.
CMD ["/bin/bash", "/usr/src/app/cicd/headless/start-headless-tests.sh"]

E2Eproject/cicd/headless/docker-compose.yaml ↓

# docker-compose.yaml
version: '3'

services:
  selenium-hub:
    # 略 ...

  chrome-node:
    # 略 ...

  E2Eproject:
    container_name: E2Eproject
    build:
      context: ../../
      dockerfile: ./cicd/headless/Dockerfile
      args:
        NO_CACHE: ${NO_CACHE:-false}
    depends_on:
      - selenium-hub
    networks:
      - network-grid
    volumes:
      - .:/usr/src/app

networks:
  network-grid:

Docker Volume 介紹 (Bind Mount vs Volume)

types-of-mounts-volume (1) 圖片取自:https://docs.docker.com/storage/volumes/

要解掉這個坑,會牽扯到 Docker Volume 的概念,但本篇踩坑紀錄不會做太深入的講解,就簡單介紹一下

在 Docker 中,Volume 是用來持久化和共享數據的重要機制。大致上,我們可以將其分為兩類:Bind Mount 和 Volume。今天,我們不打算討論第三種類型,tmpfs mount,因為它與今天的話題不太相關。

Bind Mount 使用時機

Bind Mount 是一種將宿主機(Host)的文件或目錄掛載到容器中的方法。適合以下情況:

  1. 開發階段:當你需要對程式碼進行快速迭代時,使用 Bind Mount 可以即時反映宿主機上的更改。
    • 這也是為何我 Debug 時,使用 Bind Mount 的原因,因為我在 Host 上的改動可以立即顯現出來
    • 但這不意味著 Bind Mount 不適合用在 Production 環境,只是 Bind Mount 會依賴宿主機(Host)目錄系統的結構,在安全和一致性上讓你更難處理
  2. 日誌文件的處理:將日誌文件直接掛載到宿主機,方便進行日誌的收集和分析。

Volume 使用時機

Volume 則是由 Docker 管理的一種更加隔離和安全的數據持久化方法,官方也推薦使用 Volumes,我這邊就節錄一小段 Docker 官方列舉的優點,詳細可以去看一下官方文件(連結):

Volumes are the preferred mechanism for persisting data generated by and used by Docker containers. While bind mounts are dependent on the directory structure and OS of the host machine, volumes are completely managed by Docker. Volumes have several advantages over bind mounts:

  • Volumes are easier to back up or migrate than bind mounts.
  • Volumes work on both Linux and Windows containers.
  • You can manage volumes using Docker CLI commands or the Docker API.
  • Volumes can be more safely shared among multiple containers.
  • Volume drivers let you store volumes on remote hosts or cloud p -roviders, encrypt the contents of volumes, or add other functionality.
  • New volumes can have their content pre-populated by a container.
  • Volumes on Docker Desktop have much higher performance than bind mounts from Mac and Windows hosts.

然後我也列舉一些 Volume 適合的情境:

  1. 生產環境:在生產環境中,我們更關注數據的持久化和安全,Volume 提供了更好的隔離。
  2. 不希望與宿主機的文件系統直接交互:當需要對數據進行持久化存儲,並且不希望與宿主機的文件系統直接交互時。
    • 相比 Bind Mount,Volume 就不用擔心宿主機因為不同作業系統表示路徑的方式不太一樣,因為 Volume 由 Docker 完全管理,例如:
      • Windows: C:/Users/shiun/Documents/my_folder
      • Mac: C:/Users/shiun/Documents/my_folder
      • Linux: /home/shiun/my_folder

為什麼會發生錯誤

首先一定要了解在 docker-compose.yaml 中,Bind Mount 在指定宿主機的目錄路徑時,路徑的相對路徑是基於 docker-compose.yaml 的所在目錄:

    # 略...
    volumes:
      - .:/usr/src/app # <docker-compose.yaml 所處當前目錄路徑>: <容器目標目錄路徑>

再回到我遇到的問題,原因其實很簡單:

  • 在我使用的 Dockerfile 中,我使用了 COPY . /usr/src/app/ 指令將文件從建構上下文中複製到容器內。
  • 但是,當容器啟動時,docker-compose.yaml 中定義的 volume 又將我本地的 E2Eproject/cicd/headless 目錄掛載到了同一位置。
    • 這導致了容器中的 /usr/src/app 目錄內容被覆蓋,而且 start-headless-tests.sh 腳本並不存在於 E2Eproject/cicd/headless 中,因此容器找不到這個文件,進而出現 /bin/bash: /usr/src/app/cicd/headless/start-headless-tests.sh: No such file or directory

解決方法

解決這個問題其實很簡單:

  1. 註解掉或移除 docker-compose.yaml 的 volume 指令,每一次有 code 改動都重新 Build Image
    • 這樣就不會有掛載覆蓋的問題,畢竟我再 Dockerfile 裡面就有把整個專案目錄 COPY 到 WORKDIR
  2. 修改 volumes 設定:將它改為 ../../,這樣就會掛載 E2Eproject 目錄,而不是僅僅掛載 E2Eproject/cicd/headless
    • 再次提醒,這邊的相對路徑是基於 docker-compose.yaml 的所在目錄

改好的樣子會像這樣:

E2Eproject/cicd/headless/docker-compose.yaml ↓

# docker-compose.yaml
version: '3'

services:
  # 略...
  # 略...
  # 略...

  E2Eproject:
    container_name: E2Eproject
    build:
      context: ../../
      dockerfile: ./cicd/headless/Dockerfile
      args:
        NO_CACHE: ${NO_CACHE:-false}
    depends_on:
      - selenium-hub
    networks:
      - network-grid
    volumes:
      - ../../:/usr/src/app # 關鍵修改的地方

networks:
  network-grid:

當然後續我還有做一些簡單的細節調整,但就不多加贅述,也不影響本篇踩坑紀錄的內容

若文章內容有誤,歡迎隨時連絡我!你們的回饋對我來說相當重要!

也歡迎跟我交流或是分享你的想法