開發者必學 Docker:映像、容器與 Compose
你克隆了一個專案,照著安裝說明一步步操作,結果卻卡住了:缺少系統函式庫、資料庫啟動不了,或是 Node 版本不符。Docker 的存在就是為了消除這類問題。與其直接把服務安裝在你的機器上,不如用一次描述、到處都能以相同方式運行的隔離容器來執行。
本指南將解釋你真正需要掌握的三個核心概念:映像(image)、容器(container)與 Compose。內容著重實用,並提供你可以邊讀邊執行的指令。
映像與容器:心智模型
映像是一個唯讀範本,內含檔案系統快照,以及預設指令、對外連接埠等中繼資料。容器則是該映像的執行中(或已停止)實例,上頭多了一層薄薄的可寫層。
如果你學過物件導向程式設計,這個類比很貼切:映像就像類別(class),容器就像物件(object)。你可以從一個映像啟動多個容器,而且容器內的變更不會修改到映像本身。
| 概念 | 是什麼 | 常用指令 |
|---|---|---|
| 映像 | 不可變範本 | docker build、docker pull |
| 容器 | 執行中的實例 | docker run、docker ps |
| 磁碟區 | 持久化儲存 | docker volume create |
| Compose | 多容器定義 | docker compose up |
用 Dockerfile 建立你的第一個映像
Dockerfile 是一份食譜。每一道指令都會建立一層,而 Docker 會快取這些層,所以在沒有變更的情況下重新建置會非常快。以下是一個簡單的 Node.js 範例:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]這裡有兩個習慣很重要。第一,先複製相依性清單,再複製其餘原始碼,這樣當只有應用程式碼變更時,npm ci 這一層就能被快取。第二,盡量選用像 alpine 這類精簡的基礎映像,以縮小體積並減少攻擊面。
建置並執行它:
docker build -t myapp:1.0 .
docker run --rm -p 3000:3000 myapp:1.0-p 3000:3000 這個旗標會將主機的 3000 埠對應到容器的 3000 埠。--rm 會在容器結束時將其刪除,讓你的機器在開發過程中保持整潔。
日常容器指令
以下這些涵蓋了大部分日常工作:
docker ps— 列出執行中的容器;加上-a可一併顯示已停止的容器。docker logs -f <container>— 即時串流應用程式輸出。docker exec -it <container> sh— 在執行中的容器內開啟 shell。docker stop <container>— 優雅地停止容器。docker images— 列出本機映像及其大小。docker system prune— 移除未使用的容器、網路與懸空映像。
當容器一啟動就結束時,請先讀取日誌。最常見的原因包括 CMD 寫錯、缺少環境變數,或是行程在容器內綁定到 127.0.0.1 而非 0.0.0.0。
為什麼需要 Compose
真實的應用程式通常需要不只一個行程:一個 API、一個資料庫,或許再加一個快取。用三個 docker run 指令搭配一長串旗標來執行,很容易出錯。Docker Compose 讓你在單一 YAML 檔案中宣告所有服務,並一次啟動它們。
services:
api:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://app:secret@db:5432/app
depends_on:
- db
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: app
volumes:
- db_data:/var/lib/postgresql/data
volumes:
db_data:請注意,API 連線的是 db,而不是 localhost。Compose 會建立一個私有網路,每個服務都能透過其服務名稱互相存取。這是新手最容易混淆的地方。
用 docker compose up 啟動所有服務,或加上 -d 在背景執行。程式碼變更後用 docker compose up --build 重新建置。用 docker compose down 拆除環境,若想同時刪除具名磁碟區,再加上 -v。
磁碟區、綁定掛載與即時重載
容器是短暫的;裡面的資料在容器被移除後就會消失。有兩種機制可以解決這個問題:
- 具名磁碟區(Named volumes) — 由 Docker 管理,非常適合存放資料庫檔案。它們在容器重新啟動後依然存在,並在
volumes:區段中宣告。 - 綁定掛載(Bind mounts) — 將主機目錄對應到容器內,非常適合在開發期間存放原始碼。
services:
api:
build: .
volumes:
- ./src:/app/src使用綁定掛載時,你在主機上編輯檔案會即時反映到容器內。搭配檔案監看工具(nodemon、uvicorn --reload、air),就能在不重新建置映像的情況下實現即時重載。
實用的開發流程
- 為你的應用程式撰寫 Dockerfile,並保持精簡。
- 新增一個
docker-compose.yml,包含應用程式及其相依服務。 - 原始碼使用綁定掛載,資料使用具名磁碟區。
- 不要把機密資訊寫進檔案;改用
.env檔案,並以${VAR}引用變數。 - 新增
.dockerignore,內容包含node_modules、.git與建置產物,以加快建置速度。 - 執行
docker compose up --build並反覆迭代。
如果你的服務會產生需要日後檢視的日誌,請記得容器日誌就只是純文字。能解析伺服器日誌的工具,例如 nginx 日誌分析器,與容器搭配得很好,因為你可以將日誌導出或複製出來,而不必在主機上安裝任何東西。
常見陷阱
- 映像過於肥重:使用多階段建置,在一個階段編譯,只將產物複製到精簡的執行階段映像中。
- 每次都全部重新建置:將 Dockerfile 指令依「最不常變動到最常變動」的順序排列。
- 硬編碼主機名稱:在 Compose 內使用服務名稱;在外部則使用環境變數。
- 忽略清理:懸空映像與已停止的容器會不斷累積。請定期執行
docker system prune。 - 以 root 身分執行:在生產環境映像中加上非 root 的
USER。
常見問題
映像與容器有什麼差別?
映像是一個不可變的範本,內含檔案系統與中繼資料。容器則是該映像的執行中實例,額外加上一層可寫層。你可以從單一映像啟動多個容器。
單一服務也需要 Docker Compose 嗎?
不需要。單一容器用一行 docker run 指令就夠了。當你有多個服務、共享網路,或想集中宣告環境變數時,Compose 才會顯現價值。
容器之間如何互相通訊?
Compose 會將所有服務放在同一個共享網路上,每個服務都能透過其服務名稱互相存取。請用該名稱作為主機名稱,例如 db:5432,而不是 localhost。
Docker 的前期投資很小,回報卻很大。一旦你搞懂映像、容器與 Compose,要讓隊友上手或換一台機器,就不再是繁瑣的安裝儀式,而是一行指令就能搞定的事。