> ## Documentation Index
> Fetch the complete documentation index at: https://felimet-hub.jmcores.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Dockerfile 指令逐一

> Dockerfile 每個指令的語義與範例，以及 CMD vs ENTRYPOINT、COPY vs ADD、ARG vs ENV 三組常混的差異。

`Dockerfile` `指令`

每個指令的用意與 copy-exact 範例。以 Docker 官方 Dockerfile 參考為準（截至 2026-06）。

## 指令一覽

| 指令            | 用意                                       |
| ------------- | ---------------------------------------- |
| `FROM`        | 指定基底映像，必須是第一條有效指令；`AS <name>` 替 stage 命名 |
| `RUN`         | build 期間執行指令（裝套件、編譯）。每條 `RUN` 是一層        |
| `CMD`         | 容器啟動時的預設指令，可被 `docker run` 後接的指令覆蓋       |
| `ENTRYPOINT`  | 容器啟動時固定執行的進入點，`docker run` 的參數不覆蓋它       |
| `COPY`        | 把 context 的檔案複製進映像（最常用）                  |
| `ADD`         | 同 COPY，但多了自動解壓 tar 與下載 URL（一般優先 COPY）    |
| `ENV`         | 設環境變數，留在映像、容器執行時可見                       |
| `ARG`         | build 期變數，`--build-arg` 傳入，不留在映像         |
| `WORKDIR`     | 設工作目錄（不存在會自動建），取代 `RUN cd`               |
| `EXPOSE`      | 文件性質宣告監聽埠，不會真的開埠                         |
| `VOLUME`      | 標記某路徑為外部 volume 掛載點                      |
| `USER`        | 設後續指令與容器的執行身分（建議切非 root）                 |
| `LABEL`       | 加中繼資料（取代已棄用的 `MAINTAINER`）               |
| `HEALTHCHECK` | 定義健康檢查指令                                 |
| `ONBUILD`     | 延遲觸發：本映像被當基底時才執行                         |
| `SHELL`       | 覆蓋 shell form 用的 shell                   |
| `STOPSIGNAL`  | `docker stop` 時送的信號（預設 SIGTERM）          |

## RUN 的 shell form 與 exec form

```dockerfile theme={null}
# Shell form（包在 /bin/sh -c，會做變數展開與管線）
RUN apt-get update && apt-get install -y --no-install-recommends \
    curl git \
    && rm -rf /var/lib/apt/lists/*

# Exec form（JSON 陣列，不過 shell）
RUN ["/bin/bash", "-c", "echo $HOME"]
```

合併多條 `RUN` 成一條（用 `&&` 與 `\`）可減少層數，並把 apt 快取在同一層清掉。

## 三組常混的差異

<Tabs>
  <Tab title="CMD vs ENTRYPOINT">
    * `CMD` 是「預設指令」，`docker run myimg 其他指令` 會**整個覆蓋** CMD。
    * `ENTRYPOINT` 是「固定進入點」，`docker run` 後接的參數會**附加**在 ENTRYPOINT 之後，不覆蓋它。
    * 常見組合：`ENTRYPOINT` 放固定執行檔、`CMD` 放可被覆蓋的預設參數。
    * 兩者都用 exec form（JSON 陣列），PID 1 才是應用程式本身，才能正確收到 SIGTERM 做優雅關閉；shell form 會包一層 `/bin/sh -c`，signal 傳不進去。

    ```dockerfile theme={null}
    ENTRYPOINT ["python", "app.py"]
    CMD ["--port", "8000"]
    # docker run img             → python app.py --port 8000
    # docker run img --port 9000 → python app.py --port 9000
    ```

    完整互動：

    |               | ENTRYPOINT 無     | `ENTRYPOINT ["ep"]`（exec） | `ENTRYPOINT ep`（shell）   |
    | ------------- | ---------------- | ------------------------- | ------------------------ |
    | CMD 無         | 錯誤               | `ep`                      | `/bin/sh -c ep`          |
    | `CMD ["cmd"]` | `cmd`            | `ep cmd`                  | `/bin/sh -c ep`（CMD 被忽略） |
    | `CMD cmd`     | `/bin/sh -c cmd` | `ep /bin/sh -c cmd`       | `/bin/sh -c ep`（CMD 被忽略） |
  </Tab>

  <Tab title="COPY vs ADD">
    |                  | COPY | ADD |
    | ---------------- | ---- | --- |
    | 本地檔案複製           | 是    | 是   |
    | 自動解壓本地 tar       | 否    | 是   |
    | 下載遠端 URL         | 否    | 是   |
    | `--from` 跨 stage | 是    | 否   |

    官方建議**優先用 COPY**：語義單純、可預期。只有真的需要「解壓 tar」或「下載 URL」時才用 ADD，而且 ADD 下載 URL 的快取與認證行為複雜，多數情況用 `RUN curl` 反而更清楚可控。

    ```dockerfile theme={null}
    COPY requirements.txt .
    COPY --from=builder /app/dist /usr/share/nginx/html   # 跨 stage
    COPY --chown=appuser:appgroup . /app                  # 設擁有者
    ```
  </Tab>

  <Tab title="ARG vs ENV">
    |                    | ARG           | ENV                |
    | ------------------ | ------------- | ------------------ |
    | 可在 FROM 前宣告        | 是             | 否                  |
    | 留在最終映像             | 否             | 是                  |
    | 容器執行時可見            | 否             | 是                  |
    | CLI 傳入             | `--build-arg` | `docker run --env` |
    | 進 `docker history` | 否（預設）         | 是                  |

    `ARG` 是 build 期的暫時變數，`ENV` 會固化進映像。**ARG 有個作用域雷**：在 `FROM` 前宣告的全域 ARG，進到 stage 裡會失效，要在 stage 內重新 `ARG` 宣告一次才用得到。同名時 `ENV` 蓋過 `ARG`。

    ```dockerfile theme={null}
    ARG PYTHON_VERSION=3.12-slim
    FROM python:${PYTHON_VERSION}
    ARG PYTHON_VERSION          # 進 stage 後要重新宣告才有值
    RUN echo "built on ${PYTHON_VERSION}"
    ```

    僅 build 期需要的變數（如 `DEBIAN_FRONTEND`）別用 ENV（會污染執行期），改 inline 或 ARG：

    ```dockerfile theme={null}
    ARG DEBIAN_FRONTEND=noninteractive
    RUN apt-get install -y tzdata
    ```
  </Tab>
</Tabs>

## 其他常用指令範例

```dockerfile theme={null}
WORKDIR /app                    # 設工作目錄，相對路徑相對於前一個 WORKDIR
EXPOSE 8000                     # 文件性質，實際開埠靠 docker run -p
ENV APP_ENV=production PORT=8080
USER 1001:1001                  # 切非 root（用 UID:GID 不依賴 user database）
LABEL org.opencontainers.image.authors="dev@example.com"
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD curl -f http://localhost/ || exit 1
STOPSIGNAL SIGTERM
```

`EXPOSE` 只是宣告，不會真的開埠；`USER` 不存在的使用者要先 `RUN useradd` 建立。

## 接下來

* [配合 Python 程式](/notes/docker/dockerfile/python/)：把這些指令兜成可用的 Dockerfile。
* [Layer cache 與最佳實踐](/notes/docker/dockerfile/caching/)：指令順序與快取。

官方參考：[docs.docker.com/reference/dockerfile](https://docs.docker.com/reference/dockerfile/)
