目錄

Dockerfile 指令大全:FROM、RUN、COPY、CMD、ENTRYPOINT 完整說明

上一節用一個小範例寫出第一個 Dockerfile。這一節把每個指令說明清楚,並用小實驗實際比較最容易搞混的幾組指令。

基本規則

  • 指令不分大小寫,但慣例用大寫,方便和參數區分。
  • 由上往下依序執行,第一個指令(除了 ARG 和註解外)必須是 FROM。
  • # 開頭是註解。
  • 一行太長可以用 \ 換行。
Dockerfile 指令分為建置時生效與執行時生效兩類,以及 ENTRYPOINT 與 CMD 的組合方式
建置時(docker build)與執行時(docker run)生效的指令,以及 ENTRYPOINT + CMD 的組合

FROM:基礎映像檔

FROM python:3.14-slim
FROM --platform=linux/amd64 node:24-alpine
FROM scratch                     # 完全空白的映像檔,用來放靜態編譯的程式
FROM golang:1.27 AS builder      # 多階段建置時幫階段命名

RUN:建置時執行指令

# shell 格式:透過 /bin/sh -c 執行,可以用 && 和管線
RUN apt-get update && apt-get install -y --no-install-recommends curl \
    && rm -rf /var/lib/apt/lists/*

# exec 格式:不經過 shell
RUN ["pip", "install", "--no-cache-dir", "flask"]

每個 RUN 都會產生一層。apt-get update 和 apt-get install 要寫在同一個 RUN 裡,否則快取的 update 層可能過期,裝到舊版本的套件。最後清掉套件清單(rm -rf /var/lib/apt/lists/*)可以讓映像檔小一點,實際差多少見 建置快取與最佳化。

COPY 與 ADD:複製檔案

COPY package.json package-lock.json ./
COPY src/ ./src/
COPY --chown=app:app . .
COPY --from=builder /app/dist ./dist     # 從其他建置階段複製

ADD 和 COPY 很像,但遇到 tar 壓縮檔時行為不同。實驗:準備一個 files.tar.gz(裡面有 files/a.txt、files/b.txt),分別用兩種指令複製:

FROM alpine:3.23
COPY files.tar.gz /copy/
ADD files.tar.gz /add/
CMD ["sh", "-c", "echo '--- COPY:'; ls -R /copy; echo '--- ADD:'; ls -R /add"]
$ docker build -q -t add-demo .
sha256:2466246e51b76cd547fa913fce223fccc227ae19b9e53006b9ce203e1afc8ec4

$ docker run --rm add-demo
--- COPY:
/copy:
files.tar.gz
--- ADD:
/add:
files

/add/files:
a.txt
b.txt
  • COPY 原封不動地複製了 files.tar.gz。
  • ADD 自動解壓縮了,/add 底下直接是 files/a.txt、files/b.txt。

ADD 的來源還可以是網址(自動下載)。這些「自動行為」容易造成意外,官方建議一般情況都用 COPY,只在需要解壓縮時才用 ADD。

WORKDIR:工作目錄

WORKDIR /app

之後的 RUN、COPY、CMD 等都在這個目錄下執行,目錄不存在會自動建立。請用 WORKDIR,不要寫 RUN cd /app:每個 RUN 都是獨立的 shell,cd 不會延續到下一行。

ENV 與 ARG:變數

ARGENV
何時有效只在建置時建置時與容器執行時
怎麼設定docker build --build-arg NAME=值Dockerfile 或 docker run -e
容器裡看得到嗎看不到看得到

實驗:

FROM alpine:3.23
ARG APP_VERSION=dev
ENV APP_ENV=production
RUN echo "building version $APP_VERSION" > /build-info.txt
CMD ["sh", "-c", "cat /build-info.txt; echo APP_ENV=$APP_ENV; echo APP_VERSION=${APP_VERSION:-<not set>}"]
$ docker build -q --build-arg APP_VERSION=1.2.0 -t args-demo .
sha256:3537c3e70baef148c3481f97bf325963b4809a15b806a2e38fed7e8eb5f50619

$ docker run --rm args-demo
building version 1.2.0
APP_ENV=production
APP_VERSION=<not set>

$ docker history --no-trunc --format "{{.CreatedBy}}" args-demo | head -n 3
CMD ["sh" "-c" "cat /build-info.txt; echo APP_ENV=$APP_ENV; echo APP_VERSION=${APP_VERSION:-<not set>}"]
RUN |1 APP_VERSION=1.2.0 /bin/sh -c echo "building version $APP_VERSION" > /build-info.txt # buildkit
ENV APP_ENV=production
  • 建置時傳入的 APP_VERSION=1.2.0 有被 RUN 用到(寫進了 /build-info.txt),但容器執行時已經看不到它(<not set>)。
  • ENV 設定的 APP_ENV 在容器裡看得到。
  • 注意 docker history 的第二行:RUN |1 APP_VERSION=1.2.0 ...,ARG 的值被記錄在映像檔的歷史裡,任何拿到映像檔的人都看得到。

所以絕對不要用 ARG 或 ENV 傳密碼、API 金鑰。建置時需要機密資料,請用 RUN --mount=type=secret,見 環境變數與 .env 檔。

FROM 之前宣告的 ARG 只能用在 FROM 裡;FROM 之後要再用,需要重新寫一次 ARG 名稱。

EXPOSE:說明連接埠

EXPOSE 3000
EXPOSE 53/udp

EXPOSE 只是文件用途,告訴使用者這個容器會用哪個連接埠,不會真的開放,實際對應還是要靠 docker run -p。唯一的作用是搭配 -P,把所有 EXPOSE 的連接埠對應到隨機的主機連接埠(nginx 映像檔 EXPOSE 80):

$ docker run --rm -d --name exp -P nginx:1.30
857560427b1218d038314394f03cc0eb8edf950062304efaf2f5f3dc995f6ef6

$ docker port exp
80/tcp -> 0.0.0.0:32768
80/tcp -> [::]:32768

$ docker rm -f exp
exp

USER:以非 root 身分執行

預設容器內是 root。改用一般使用者執行,即使程式被入侵,影響範圍也比較小:

FROM alpine:3.23
RUN addgroup -S app && adduser -S -G app app
USER app
CMD ["sh", "-c", "whoami; id"]
$ docker build -q -t user-demo .
sha256:bdcaab4c6133a5446cdaaa1ca3353e01a690dc9fd0647458543abcec0d6041f1

$ docker run --rm user-demo
app
uid=100(app) gid=101(app) groups=101(app),101(app)

$ docker run --rm alpine:3.23 whoami
root

很多官方映像檔已經內建一般使用者,例如 node 映像檔有 node 使用者,可以直接 USER node。

VOLUME:宣告資料目錄

VOLUME /var/lib/data

宣告這個目錄應該存放持久資料。執行時如果沒有掛載,Docker 會自動建立一個匿名 Volume(Volume 一節有實際的例子)。實務上通常直接在 docker run -v 或 Compose 裡指定,自己的 Dockerfile 不一定需要寫 VOLUME。

HEALTHCHECK:健康檢查

Docker 會定期執行健康檢查指令,結束代碼 0 代表健康,非 0 代表不健康:

FROM nginx:1.30-alpine
HEALTHCHECK --interval=5s --timeout=3s --retries=3 \
  CMD wget -q --spider http://localhost/ || exit 1

實驗:啟動後等幾秒,再故意刪掉首頁讓 nginx 回傳錯誤:

$ docker run -d --name health health-demo
52bfa4cce8024630797ba6f33ba99dea44f80399fbadd617b2d04a8ac7f8e3b1

$ docker ps --filter name=health
CONTAINER ID   IMAGE         COMMAND                  CREATED        STATUS                                     PORTS     NAMES
52bfa4cce802   health-demo   "/docker-entrypoint.…"   1 second ago   Up Less than a second (health: starting)   80/tcp    health

$ docker ps --filter name=health
CONTAINER ID   IMAGE         COMMAND                  CREATED         STATUS                   PORTS     NAMES
52bfa4cce802   health-demo   "/docker-entrypoint.…"   9 seconds ago   Up 8 seconds (healthy)   80/tcp    health

$ docker exec health rm /usr/share/nginx/html/index.html

$ docker ps --filter name=health
CONTAINER ID   IMAGE         COMMAND                  CREATED          STATUS                      PORTS     NAMES
52bfa4cce802   health-demo   "/docker-entrypoint.…"   29 seconds ago   Up 28 seconds (unhealthy)   80/tcp    health

$ docker inspect --format '{{range .State.Health.Log}}{{.ExitCode}} {{.Output}}{{println}}{{end}}' health
0
1 wget: server returned error: HTTP/1.1 403 Forbidden
1 wget: server returned error: HTTP/1.1 403 Forbidden
1 wget: server returned error: HTTP/1.1 403 Forbidden
1 wget: server returned error: HTTP/1.1 403 Forbidden

$ docker rm -f health
health

狀態的變化:health: starting(剛啟動)→ healthy(檢查通過)→ 刪掉首頁後連續失敗 3 次(--retries=3)→ unhealthy。.State.Health.Log 記錄了每次檢查的結束代碼與輸出。

  • 映像檔裡要有 curl 或 wget 等工具才能這樣寫(alpine 內建 wget)。
  • Compose 可以等服務 healthy 之後才啟動依賴它的服務,見 compose.yaml 語法。

CMD 與 ENTRYPOINT:啟動指令

兩者都決定容器啟動時執行什麼,差別在於能不能被覆蓋以及怎麼組合。

CMD:可以被整個取代

FROM alpine:3.23
CMD ["echo", "hello from CMD"]
$ docker build -q -t cmd-demo .
sha256:45b2af259ce828e227961bcc620b973758a1f757d948a7e23e056781afba324d

$ docker run --rm cmd-demo
hello from CMD

$ docker run --rm cmd-demo echo overridden
overridden

docker run 時在映像檔名稱後面寫的指令,會整個取代 CMD。

ENTRYPOINT:固定的主程式

FROM alpine:3.23
ENTRYPOINT ["echo", "ENTRYPOINT says:"]
CMD ["default argument"]
$ docker build -q -t ep-demo .
sha256:32414489000b71c255d808cfd4cda73764b526525229dece3a8f1fd5487fc6d1

$ docker run --rm ep-demo
ENTRYPOINT says: default argument

$ docker run --rm ep-demo my argument
ENTRYPOINT says: my argument

$ docker run --rm --entrypoint date ep-demo
Mon Oct 12 23:59:17 UTC 2026
  • ENTRYPOINT 和 CMD 同時存在時,實際執行的是 ENTRYPOINT + CMD。
  • docker run 後面寫的東西只會取代 CMD,變成 ENTRYPOINT 的參數。
  • 要換掉 ENTRYPOINT 本身,用 --entrypoint。
CMDENTRYPOINT
作用預設指令或預設參數容器的主要執行檔
docker run 映像檔 xxxxxx 取代 CMDxxx 附加在 ENTRYPOINT 後面
怎麼覆蓋直接在映像檔名稱後寫指令--entrypoint

選擇原則:

  • 一般應用程式:只寫 CMD,使用者想進 shell 時可以直接 docker run -it myapp sh。
  • 把容器當成「一個指令」來用(例如 CLI 工具):用 ENTRYPOINT 指定程式,CMD 放預設參數。

exec 格式與 shell 格式

CMD ["node", "server.js"]    # exec 格式(JSON 陣列)
CMD node server.js           # shell 格式

shell 格式實際執行的是 /bin/sh -c "node server.js",可以使用環境變數展開和 && 等 shell 語法;exec 格式則直接執行程式,不經過 shell。一般建議使用 exec 格式,行為最明確。

PID 1 與 docker stop 要等 10 秒的問題

容器生命週期 提到,docker stop 有時候要等滿 10 秒。下面用同一個 Node.js 程式(上一節 的 server.js,沒有處理 SIGTERM),比較四種 CMD 寫法,用 docker top 看容器裡的程序,再計時 docker stop:

$ docker top shell-npm
PID                 USER                TIME                COMMAND
4155                root                0:00                npm start
4182                root                0:00                {MainThread} node server.js

$ time docker stop shell-npm
shell-npm
real	0m 0.66s
user	0m 0.00s
sys	0m 0.00s

$ docker top exec-npm
PID                 USER                TIME                COMMAND
4318                root                0:00                npm start
4347                root                0:00                {MainThread} node server.js

$ time docker stop exec-npm
exec-npm
real	0m 0.67s
user	0m 0.00s
sys	0m 0.00s

$ docker top shell-node
PID                 USER                TIME                COMMAND
4488                root                0:00                {MainThread} node server.js

$ time docker stop shell-node
shell-node
real	0m 10.14s
user	0m 0.01s
sys	0m 0.01s

$ docker top exec-node
PID                 USER                TIME                COMMAND
4660                root                0:00                {MainThread} node server.js

$ time docker stop exec-node
exec-node
real	0m 10.14s
user	0m 0.01s
sys	0m 0.00s
CMD 寫法容器裡的 PID 1docker stop
CMD npm startnpm0.66 秒
CMD ["npm", "start"]npm0.67 秒
CMD node server.jsnode10.14 秒
CMD ["node", "server.js"]node10.14 秒

關鍵不在 shell 格式或 exec 格式,而在誰是 PID 1:

  • Linux 對 PID 1 有特別的規則:PID 1 沒有自己處理 SIGTERM 時,訊號會被忽略。node 直接當 PID 1、程式裡又沒有處理 SIGTERM,docker stop 只能等 10 秒後用 SIGKILL 強制結束。
  • 由 npm 啟動時,npm 是 PID 1,它會把 SIGTERM 轉給 node;node 不是 PID 1,收到 SIGTERM 就照預設行為結束。
  • 第一列的 shell 格式之所以沒有多一層 sh,是因為 Alpine 的 sh 在只有單一指令時會直接用 npm 取代自己。

解決方法(擇一):

  1. 在程式裡處理 SIGTERM,優雅地關閉伺服器(最好的做法,見 Node.js 應用)。
  2. 執行時加 --init(Compose 寫 init: true),由一個小型 init 程序當 PID 1 並轉發訊號。

其他指令

指令用途
LABEL加上中繼資料,例如 LABEL org.opencontainers.image.source="https://github.com/..."
SHELL改變 shell 格式使用的 shell,例如 Windows 容器用 PowerShell
STOPSIGNAL改變 docker stop 送出的訊號(nginx 映像檔設定為 SIGQUIT,見 映像檔 中的 docker history)
ONBUILD當其他映像檔以此為基礎時才執行的指令(少用)

完整範例:Python 網站

FROM python:3.14-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

RUN useradd --system app
USER app

EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]

PYTHONUNBUFFERED=1 讓 Python 的輸出立即寫出,docker logs 才能即時看到。這個 Dockerfile 實際建置與執行的過程見 用 Docker 部署 Python 應用。

重點回顧

  • RUN、COPY、ARG 在建置時生效;CMD、ENTRYPOINT、USER、ENV 影響執行時。
  • docker run 映像檔 xxx 的 xxx 會取代 CMD,或附加在 ENTRYPOINT 之後。
  • CMD、ENTRYPOINT 用 exec 格式(JSON 陣列)。
  • 不要用 ARG、ENV 傳遞密碼。

動手練習

練習 1:映像檔設定 ENTRYPOINT ["ping"] 和 CMD ["-c", "3", "localhost"]。執行 docker run 映像檔 -c 1 example.com 實際執行的是什麼?

答案

ping -c 1 example.com。執行時的參數取代 CMD,再附加在 ENTRYPOINT 之後。

練習 2:為什麼 ENV NODE_ENV production 會被 docker build --check 警告?

答案

這是舊的寫法,應該寫成 ENV NODE_ENV=production,警告名稱是 LegacyKeyValueFormat。

常見問題

COPY 和 ADD 該用哪個?

一律優先用 COPY。ADD 會自動解壓縮 tar 檔、可以下載網址,行為比較不直覺,只有需要這些功能時才用。

CMD 和 ENTRYPOINT 該用哪個?

大多數應用程式只用 CMD,方便被覆蓋。映像檔像一個指令工具(每次都執行同一個程式、只換參數)時才用 ENTRYPOINT。

怎麼檢查 Dockerfile 寫法有沒有問題?

執行 docker build --check .,它會列出例如 JSONArgsRecommended、LegacyKeyValueFormat 等警告,見 [docker init](/zh-tw/docker/docker-init/)。

更新於 2026年10月14日