SSD Nodes Learn 🎉 VPS $5.50/月起
指南 Matt Connor作者: Matt Connor

Docker Compose 的 build 與 image 在 VPS 上怎麼用?

了解 Docker Compose 的 image 與 build 差異:為何修改 Dockerfile 後執行 docker compose up 仍使用舊映像,以及如何強制重新建置並部署。

Docker Compose build 與 image:簡短答案

在 Docker Compose 檔案中,image: 指定要從 registry 拉取的 image,build: 則告訴 Compose 在本機使用 Dockerfile 建置 image。只設定 image: 時,Compose 會拉取該 tag 並執行。只設定 build: 時,Compose 會在此處建置 image,並使用由 project name 與 service name 衍生的名稱。兩者都設定時,Compose 會在本機建置,然後使用 image: 中的名稱為結果加上 tag。這就是使用自訂名稱建置 image 並推送的方法。

以上就是兩者的全部差異。以下內容說明這項差異在伺服器上的實際運作方式。本文假設 Docker Engine 與 Compose plugin 已經安裝完成;在 VPS 上執行 Docker涵蓋這部分。

三種完整形式

拉取已發布的 tag 並執行。整個過程不會使用 Dockerfile。

services:
  web:
    image: nginx:1.27
    restart: unless-stopped
    ports:
      - "80:80"

使用目前目錄中的 Dockerfile 建置。除了 FROM 指定的基礎映像之外,不會拉取其他內容。

services:
  web:
    build: .
    restart: unless-stopped
    ports:
      - "80:80"

在本機建置並標記結果。接著可使用 docker compose push,將該確切 tag 傳送至 registry。

services:
  web:
    build:
      context: .
      dockerfile: Dockerfile
    image: registry.example.com/acme/web:1.4.2
    restart: unless-stopped
    ports:
      - "80:80"

context 是傳送給 builder 的目錄。dockerfile 會相對於該 context 解析,因此搭配 dockerfile: docker/prod.Dockerfile 使用 context: . 是正常且正確的做法。執行 docker compose images,即可查看每個服務容器背後的映像名稱與映像 ID。這是確認實際撰寫的是上述三種形式中的哪一種最快的方法。

為什麼修改 Dockerfile 後,執行 docker compose up 仍不會重新建置?

因為 up 檢查的是映像是否存在,而不是映像是否為最新版本。

Compose 啟動包含 build: 區段的服務時,會先在本機映像儲存區中尋找該映像。如果已經存在同名映像,Compose 就會使用它。Compose 不會讀取 Dockerfile、比較來源檔案,或檢查任何時間戳記。Compose 規格將這項規則定義為 pull_policy 屬性;預設行為只有在映像不存在時才會建置映像。只要映像存在,就會被視為足夠。

因此,你修改 app.py 後執行 docker compose up -d,看到 Compose 回報容器正在執行,實際提供的卻是舊程式碼。過程中沒有失敗,自然也不會顯示警告。這是 Compose 中最常見的「我的修改沒有生效」問題。判斷線索是 Compose 顯示在容器名稱旁的狀態文字:Compose 重新建立的容器會顯示 recreated 或 started,而 Compose 判定不需處理的容器則會顯示 running。

執行兩項檢查即可確認。docker compose images 會列出每個容器使用的映像 ID,因此請在部署前記錄,並在部署後比較。docker image ls 包含 CREATED 欄位;如果映像的建立時間早於你上次提交的時間,無論部署腳本輸出了什麼,都代表它是過時的映像。

哪些旗標會強制重新建置

  • docker compose up -d --build 會先建置映像檔,接著重建映像檔已變更的容器。這是大多數人要找的旗標。
  • docker compose build web 只建置一個服務,不啟動任何服務。接著搭配 docker compose up --no-deps -d web,即可只替換該容器,讓其餘 stack 持續執行。
  • docker compose build --no-cache web 捨棄所有快取層,從第一個指令開始重新建置。
  • docker compose build --pull 會嘗試在 FROM 中拉取較新版的基礎映像檔,因此像 node:22 這類會變動的 tag,會取得目前的內容,而不是使用你在 March 下載的副本。
  • docker compose up -d --force-recreate 會使用容器目前採用的映像檔重新建立容器。它永遠不會進行建置。原本應使用 --build 卻改用這個旗標,是常見的排查死路。

你也可以將這項決策寫入檔案。依 Compose 規格的定義,pull_policy: build 表示由 Compose 建置映像檔;如果映像檔已存在,也會重新建置。每次執行 up 都會進行建置。這適合在 laptop 上使用,但在 server 上通常不是理想做法。

services:
  web:
    build: .
    image: registry.example.com/acme/web:dev
    pull_policy: build

還有一個值得了解的互動行為。docker compose pull 也會嘗試拉取具有 build 區段的服務映像檔;如果拉取失敗,便會提示你必須改為建置映像檔。傳入 --ignore-buildable 可略過這些服務,且不顯示錯誤。

建置快取如何決定部署時間

Dockerfile 中的每個指令都會產生一個 layer。當該指令及其輸入未變更時,建置器會重複使用快取的 layer。對 COPY 而言,輸入是要複製之檔案的內容。只要有一個 layer 未命中快取,後續每個 layer 都會重新建置,因為每個 layer 都是以之前一層產生的檔案系統為基礎建置。

這項規則會決定部署需要幾秒或幾分鐘。請依照變更頻率由低至高排列 Dockerfile,將最少變更的內容放在前面,最常隨每次 commit 變更的內容放在後面。

FROM node:22-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
CMD ["node", "server.js"]

npm ci 位於 COPY . . 上方,因此編輯原始碼檔案時,安裝 layer 仍可使用快取,建置會從複製步驟繼續。若交換這兩行,一個字元的變更就會重新安裝所有相依套件,因為 COPY . . 會使 npm ci 所依賴的 layer 失效。相同的結構也適用於 pip install -r requirements.txtgo mod download

當你懷疑有過時的 layer 掩蓋修正內容時,--no-cache 是適合使用的工具。但不應將它作為預設做法,因為它會捨棄 Dockerfile 排列順序原本要保留的快取重複使用效果。

有一項設定可由 image sets 與 Compose 覆寫:Dockerfile 的 CMD 是 image 預設執行的內容,而服務中的 command: key 會取代它。command 與 entrypoint 如何互動 在此很重要,因為 Compose 覆寫設定可能讓剛建置的 image 表現得與舊 image 完全相同。

建置內容與 .dockerignore

context: . 表示 Compose 會先將該目錄打包並傳送給建置器,之後才執行第一個指令。目錄中的所有內容都會被傳送,包括 .git,以及你可能放在原始碼旁的任何資料目錄。如果專案沒有變更,但建置在傳送內容的步驟停頓,表示建置內容過大。

在建置內容的根目錄建立 .dockerignore 檔案,即可排除不需傳送的路徑。其語法接近 .gitignore

.git
node_modules
*.log
data/
.env

這有兩項好處。傳送的內容變少,因此每次建置都會更快。COPY . . 也無法再將 .env 複製到映像檔中,避免任何取得該映像檔的人讀取其中內容。

會隨時間變慢的建置通常與 bind mount 有關。named volume 位於專案目錄之外,但像 ./data:/var/lib/postgresql/data 這樣的 bind mount 位於建置內容內,因此資料庫每週增長時,建置也會越來越慢。在 .dockerignore 中加入一行即可修正。Bind mount 與 named volume 的取捨 說明更完整的差異。

Build argument 也有較小但相同的風險。透過 args: 傳入的值會顯示在映像檔歷程中,任何持有該映像檔的人都能查看。因此請在其中放置版本號碼,絕不要放入 token。Compose 中的環境檔案與 secrets 說明憑證應放置的位置。

應在 VPS 上建置,還是在其他地方建置後再拉取?

在提供網路流量服務的主機上建置是預設做法,因為流程最短:git pull,接著 docker compose up -d --build。對於尚未有人依賴的小型伺服器,這樣做沒有問題。但有兩個可測量的原因,以及一個只會在狀況不佳時出現的原因,會讓這種做法不再適用。

記憶體。 建置會在執行中的應用程式旁啟動編譯器與 bundler,而這些通常是多數技術堆疊中最耗用記憶體的元件。在 1 GB VPS 上,JavaScript bundler 或 Rust 編譯程序通常會成為主機上占用記憶體最多的程序。核心耗盡記憶體時,會終止占用量最大的程序:建置可能會以 Killed 和結束狀態 137 停止,或資料庫反而被終止,導致網站在部署過程中斷線。dmesg -T | grep -i oom 會列出終止訊息與程序名稱,因此你可以判斷究竟是哪一個程序被終止,不必猜測。

磁碟。 每次建置都會留下 image layer,而建置工具也會在映像檔之外另外保存自己的快取。docker system df 會同時顯示這兩者,而建置快取列只會持續增長。使用 docker image prune 回收 dangling image,使用 docker builder prune 回收快取的 layer。磁碟滿載造成的問題不只會停止建置,資料庫也會停止寫入,而這類故障的代價遠高於部署變慢。

可重現性。 在伺服器上建置的映像檔只存在於該伺服器。回復舊版本時,必須切換到舊的 commit 並重新建置;而且無法保證這次建置會產生原本的結果,因為 base tag 可能已變更,套件 mirror 也可能隨之變更。在其他地方建置並推送 tag 後,回復就只需修改設定:讓 image: 指向先前的 tag,再執行 docker compose up -d

穩定的配置很簡單。continuous integration 執行建置並推送 registry.example.com/acme/web:<git-sha>,VPS 上的 Compose 檔案則使用 image:,完全不包含 build: key。如此一來,部署只需執行兩個幾乎不占用記憶體的命令。

docker compose pull
docker compose up -d

在伺服器上執行一次 docker login registry.example.com,之後 Compose 就能拉取 private tag。

保留 development 使用的 build 區段,不要刪除;請將它放在你自行命名的檔案中。

# compose.dev.yaml
services:
  web:
    build:
      context: .
    pull_policy: build
docker compose -f compose.yaml -f compose.dev.yaml up -d --build

將該檔案命名為 compose.dev.yaml,不要命名為 compose.override.yaml。只要 override 檔案存在,Compose 就會自動載入,因此若將多餘的 override 檔案複製到伺服器,就會在不知情的情況下再次於伺服器上建置。疊加多個 Compose 檔案說明合併程序如何解析各個 key。

在其他環境建置時的架構陷阱

映像檔會包含建置時所使用的 CPU 架構。在 Apple Silicon 筆電上建置並推送映像檔,接著將該標籤拉取到 x86_64 VPS 時,Docker 會警告要求的映像檔平台與偵測到的主機平台不相符。程序接著會因 exec format error 終止。這看起來像二進位檔損毀,但實際上並不是。請明確指定目標平台進行建置:

docker buildx build --platform linux/amd64 \
  -t registry.example.com/acme/web:1.4.2 --push .

如果筆電使用 x86,而您執行的是 ARM VPS,而非 x86 VPS,也會發生相反的架構不相符問題。讓 CI 在您部署所用的架構上進行建置,就能避免這個問題。

部署後要檢查的項目

  • docker compose images 會列出每個執行中容器所使用的 image 與 tag。image ID 已變更,表示新建置版本已在服務中。
  • docker compose config 會在變數替換後列印合併的檔案內容,讓你在執行任何操作前確認 Compose 最終會使用的 image 名稱。
  • docker compose logs -f web 監看切換後的前 30 秒。容器若啟動後立即結束,會持續重啟而無法保持執行;除非主動查看,否則這個迴圈通常不會顯示任何訊息。
  • docker image ls 會顯示 CREATED 欄位。image 的建立時間早於最近一次 commit,表示它從未重新建置。

如果你仍在組合這些檢查所使用的檔案,VPS 上 Compose 檔案的基本結構涵蓋相關設定鍵,而 Compose command 速查表列出其餘子命令。

FAQ

我可以在同一個服務中同時使用 build 和 image 嗎?

可以。對於自行建置的專案,這是標準設定。Compose 會根據 build: 區段進行建置,並使用 image: 的值為結果加上標籤。docker compose push 會將這個標籤推送至 registry,其他機器也會使用這個標籤拉取映像。若沒有 image: 金鑰,Compose 仍會進行建置,但會依專案名稱和服務名稱為映像命名,並警告缺少該屬性,因此無法推送映像。

為什麼 docker compose up 沒有套用我的 Dockerfile 變更?

因為 up 只會檢查是否存在具有該名稱的映像。只要映像存在,Compose 就會啟動它,不會比對 Dockerfile 或原始檔案。請執行 docker compose up -d --build;若只要替換單一服務,則執行 docker compose build web,接著執行 docker compose up --no-deps -d web。在服務上設定 pull_policy: build 後,每次執行 up 都會重新建置,適合開發機器使用。

--build 和 --force-recreate 有什麼差異?

--build 會重新建置映像,接著重新建立映像已變更的容器。--force-recreate 會使用容器現有的映像重新建立容器,因此無法套用程式碼變更。如果變更位於原始檔案或 Dockerfile,應使用 --build 旗標。--force-recreate 用於重設容器本身,例如在保留相同映像的情況下清除可寫入層。

我應該在 VPS 上建置 Docker 映像,還是在其他地方建置?

請在其他地方建置;當該主機同時提供網路流量服務時,再拉取標籤。建置會與應用程式競爭記憶體,而小型 VPS 可能讓 kernel 終止佔用記憶體最多的程序;被終止的可能是建置程序,也可能是資料庫。建置也會在磁碟上留下無法自動回收的快取。對於沒有使用者的小型專案,在伺服器上建置仍然可行。只要將 build: 區段保留在僅供開發使用的 Compose 檔案中,日後遷移的成本就很低。

如何避免 Docker build cache 填滿磁碟?

執行 docker system df,查看映像和 build cache 各自佔用多少空間。docker builder prune 會移除快取層,docker image prune 會移除先前建置留下的懸置映像。在任一指令中加入 -a 會採取更積極的清理方式,並強制下一次建置從空白快取開始。請勿在伺服器上排程執行 docker system prune -af --volumes,因為 --volumes 會刪除目前沒有任何容器使用的 volume;而停止進行維護的 stack,正可能將資料庫儲存在這類 volume 中。