目錄

Docker 安全最佳實務:讓容器更安全的 10 個做法

容器提供了一定程度的隔離,但不是安全的保證。以下是從開發到部署都應該做到的基本防護,每一項都附上實際執行的結果。

1. 不要以 root 執行

$ docker run --rm alpine:3.23 id
uid=0(root) gid=0(root) groups=0(root),0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)

$ docker run --rm -u 10001:10001 alpine:3.23 id
uid=10001 gid=10001 groups=10001

$ docker run --rm node:24-alpine id node
uid=1000(node) gid=1000(node) groups=1000(node),1000(node)
  • 容器內預設是 root(uid=0)。如果程式有漏洞被入侵,攻擊者在容器內就有最高權限,再搭配其他漏洞就有機會影響主機。
  • -u 10001:10001 在執行時指定使用者,不需要修改映像檔。
  • 很多官方映像檔已經內建一般使用者,例如 node 映像檔的 node(uid 1000),distroless 映像檔的 nonroot。

最好的做法是在 Dockerfile 中就切換使用者:

RUN adduser -D -u 10001 app      # Alpine
# RUN useradd --system --uid 10001 app   # Debian / Ubuntu
USER app

實際範例見 Python 應用 與 Node.js 應用。

2. 使用可信任的基礎映像檔,並固定版本

  • 優先選擇 Docker Official Images(例如 python、node、nginx)或知名組織發布的映像檔。
  • 不要用來路不明的映像檔,特別是名稱跟熱門映像檔很像的(例如 ngnix)。
  • 固定版本,例如 node:24-alpine 或更精確的 node:24.21-alpine,而不是 node 或 latest。要完全固定,可以加上 digest:node:24-alpine@sha256:...。

3. 讓映像檔越小越好

映像檔裡的每個套件都可能有漏洞。用 slim、alpine、distroless 或 多階段建置,只保留執行需要的東西。不需要的工具(編譯器、curl、ssh)越少,攻擊者能利用的也越少,漏洞掃描的結果也越乾淨。

4. 不要把機密資料放進映像檔

實測:機密會留在映像檔中

以下 Dockerfile 犯了三個常見錯誤:

FROM alpine:3.23
ARG NPM_TOKEN
ENV DB_PASSWORD=super-secret
COPY .env /app/.env
RUN echo "use the token: $NPM_TOKEN" > /dev/null && rm /app/.env
CMD ["sh"]

最後一行用完 .env 就刪除了,看起來沒問題。建置並檢查:

$ docker build -q -f Dockerfile.bad --build-arg NPM_TOKEN=npm_abc123secret -t leaky .
sha256:c9523bb4d0484e3abf049d15dec57df3282ff8203574c03127a8dfc6ee37bf9b

$ docker run --rm leaky ls /app

$ docker inspect -f "{{.Config.Env}}" leaky
[PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin DB_PASSWORD=super-secret]

$ docker history --no-trunc --format "{{.CreatedBy}}" leaky | head -n 4
CMD ["sh"]
RUN |1 NPM_TOKEN=npm_abc123secret /bin/sh -c echo "use the token: $NPM_TOKEN" > /dev/null && rm /app/.env # buildkit
COPY .env /app/.env # buildkit
ENV DB_PASSWORD=super-secret
  • ls /app 是空的,.env 看起來已經刪除了。
  • 但 docker inspect 直接顯示了 ENV 設定的 DB_PASSWORD=super-secret。
  • docker history 顯示了建置時傳入的 NPM_TOKEN=npm_abc123secret:ARG 的值會記錄在使用它的那一層。

.env 呢?映像檔是由多個層疊加而成,rm 只是在新的一層標記「刪除」,COPY 那一層的內容還在。用 docker save 匯出映像檔,從各層中找出 .env:

$ docker save leaky -o leaky.tar && mkdir x && tar -xf leaky.tar -C x && for l in x/blobs/sha256/*; do tar -tzf $l 2>/dev/null | grep -q "app/.env" && tar -xzOf $l app/.env; done; rm -rf x leaky.tar
API_KEY=sk-live-1234567890abcdef

.env 的完整內容被取出來了。任何能拉取這個映像檔的人都能這樣做。推送到 Registry 的映像檔,要假設所有人都能看到每一層的內容。

用 docker build --check 提早發現

docker build --check 會依變數名稱判斷 ARG、ENV 是否像是機密:

$ docker build --check -f Dockerfile.bad . 2>&1 | sed -n "/Check complete/,\$p"
Check complete, 2 warnings have been found!

WARNING: SecretsUsedInArgOrEnv - https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
Do not use ARG or ENV instructions for sensitive data (ARG "NPM_TOKEN")
Dockerfile.bad:2
--------------------
   1 |     FROM alpine:3.23
   2 | >>> ARG NPM_TOKEN
   3 |     ENV DB_PASSWORD=super-secret
   4 |     COPY .env /app/.env
--------------------

WARNING: SecretsUsedInArgOrEnv - https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
Do not use ARG or ENV instructions for sensitive data (ENV "DB_PASSWORD")
Dockerfile.bad:3
--------------------
   1 |     FROM alpine:3.23
   2 |     ARG NPM_TOKEN
   3 | >>> ENV DB_PASSWORD=super-secret
   4 |     COPY .env /app/.env
   5 |     RUN echo "use the token: $NPM_TOKEN" > /dev/null && rm /app/.env
--------------------

NPM_TOKEN、DB_PASSWORD 都被標出來了。但它只看 ARG、ENV 的名稱,COPY .env 這類把機密檔案複製進去的問題它檢查不到,所以 .dockerignore 仍然必要。

正確做法

  • 不要在 Dockerfile 用 ENV 或 ARG 傳入密碼、API 金鑰。
  • 用 .dockerignore 排除 .env、私鑰、.git,避免被 COPY . . 複製進去。
  • 執行時需要的機密,用環境變數、--env-file 或 Compose secrets 在執行時傳入,參考 環境變數與 .env 檔。
  • 建置時需要的機密(例如安裝私有套件的 token),用 BuildKit 的 secret 掛載:
FROM alpine:3.23
RUN --mount=type=secret,id=npm_token \
    test -s /run/secrets/npm_token && echo "token length: $(wc -c < /run/secrets/npm_token)"
CMD ["sh"]
$ NPM_TOKEN=npm_abc123secret docker build --progress plain --no-cache --secret id=npm_token,env=NPM_TOKEN -f Dockerfile.good -t safe . 2>&1 | grep "token length"
#5 [stage-0 2/2] RUN --mount=type=secret,id=npm_token     test -s /run/secrets/npm_token && echo "token length: $(wc -c < /run/secrets/npm_token)"
#5 0.261 token length: 16

$ docker history --no-trunc --format "{{.CreatedBy}}" safe | head -n 2
CMD ["sh"]
RUN /bin/sh -c test -s /run/secrets/npm_token && echo "token length: $(wc -c < /run/secrets/npm_token)" # buildkit

$ docker run --rm safe ls /run/secrets
ls: /run/secrets: No such file or directory
  • 建置時 RUN 可以從 /run/secrets/npm_token 讀到 token(長度 16)。
  • docker history 中只有指令本身,沒有 token 的值。
  • 映像檔中也沒有 /run/secrets,secret 只在那一個 RUN 執行期間掛載。

5. 不要對外開放不必要的連接埠

# 不好:資料庫對全世界開放
docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=secret postgres:18

# 好:只有本機可以連
docker run -d -p 127.0.0.1:5432:5432 -e POSTGRES_PASSWORD=secret postgres:18

在 Linux 上,Docker 會直接修改 iptables 規則,-p 開放的連接埠會繞過 ufw、firewalld 等防火牆的設定:即使 ufw 沒有允許 5432,外部仍然可以連進來。最安全的做法是資料庫、快取等內部服務完全不用 -p,只讓其他容器透過 Docker 網路連線,例如 Nginx 反向代理 中只有 Nginx 有 ports。

6. 限制容器的權限

唯讀檔案系統

$ docker run --rm --read-only alpine:3.23 touch /tmp/x
touch: /tmp/x: Read-only file system

$ docker run --rm --read-only --tmpfs /tmp alpine:3.23 sh -c "touch /tmp/x && echo ok"
ok

--read-only 讓容器的檔案系統變成唯讀,攻擊者就無法修改程式或寫入惡意檔案。程式需要寫入的地方(例如 /tmp),另外用 --tmpfs 掛載記憶體中的暫存空間,或掛載 Volume。

移除 Linux capabilities

root 的權限在 Linux 中被拆成許多 capabilities,例如 CHOWN(變更檔案擁有者)、NET_BIND_SERVICE(綁定 1024 以下的連接埠)。Docker 預設會給容器一部分 capabilities:

$ docker run --rm alpine:3.23 chown nobody /etc/hostname && echo "chown ok"
chown ok

$ docker run --rm --cap-drop ALL alpine:3.23 chown nobody /etc/hostname
chown: /etc/hostname: Operation not permitted

$ docker run --rm --cap-drop ALL --cap-add CHOWN alpine:3.23 chown nobody /etc/hostname && echo "chown ok"
chown ok
  • 預設情況下 chown 可以執行。
  • --cap-drop ALL 移除所有 capabilities 後,即使是 root 也得到 Operation not permitted。
  • 用 --cap-add 只加回真正需要的。

大多數網頁應用程式不需要任何 capabilities,可以直接 --cap-drop ALL。

禁止提升權限

$ docker run --rm alpine:3.23 grep NoNewPrivs /proc/self/status
NoNewPrivs:	0

$ docker run --rm --security-opt no-new-privileges alpine:3.23 grep NoNewPrivs /proc/self/status
NoNewPrivs:	1

NoNewPrivs: 1 表示容器中的程序無法透過 setuid 程式(例如 sudo、su)取得更高權限。即使以一般使用者執行,也建議加上。

綜合起來:

docker run -d \
  --user 10001:10001 \
  --read-only --tmpfs /tmp \
  --cap-drop ALL \
  --security-opt no-new-privileges \
  myapp

Compose 的寫法:

services:
  app:
    image: myapp
    user: "10001:10001"
    read_only: true
    tmpfs:
      - /tmp
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true

絕對不要隨便使用 --privileged,它會給容器所有 capabilities 並能存取主機的所有裝置,幾乎等於關掉所有隔離。

7. 限制資源

$ docker run --rm --pids-limit 50 alpine:3.23 sh -c "for i in \$(seq 100); do sleep 5 & done; wait" 2>&1 | sort | uniq -c
      1 sh: can't fork: Resource temporarily unavailable

$ docker run -d --name limited --memory 512m --cpus 1 --pids-limit 200 nginx:1.30-alpine
WARNING: Your kernel does not support swap limit capabilities or the cgroup is not mounted. Memory limited without swap.
2edbff0c81d73b83f0055b781d8d8ad20661b2b64ef1dfd5a6def334ef02723c

$ docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.PIDs}}" limited
NAME      CPU %     MEM USAGE / LIMIT   PIDS
limited   0.00%     3.406MiB / 512MiB   2

$ docker inspect -f "Memory={{.HostConfig.Memory}} NanoCpus={{.HostConfig.NanoCpus}} PidsLimit={{.HostConfig.PidsLimit}}" limited
Memory=536870912 NanoCpus=1000000000 PidsLimit=200

$ docker rm -f limited
limited
  • --pids-limit 50 時,容器想建立 100 個程序,超過上限就得到 can't fork: Resource temporarily unavailable。這能防止 fork 炸彈拖垮整台主機。
  • --memory 512m --cpus 1 限制記憶體和 CPU,docker stats 的 LIMIT 顯示 512MiB。超過記憶體上限的程序會被系統終止(OOM),而不是讓整台主機記憶體不足。
  • WARNING: Your kernel does not support swap limit capabilities 表示主機核心沒有啟用 swap 限制,記憶體限制仍然有效,只是無法同時限制 swap,可以忽略。

Compose 的寫法:

services:
  app:
    image: myapp
    mem_limit: 512m
    cpus: 1
    pids_limit: 200

8. 保護 Docker socket

/var/run/docker.sock 是 Docker 的控制介面,能存取它就能建立任何容器,包括掛載主機的根目錄。實測把主機的 / 掛載進容器:

$ docker run --rm -v /:/host alpine:3.23 sh -c "head -n 2 /host/etc/os-release; ls /host/var/lib/docker | head -n 5"
NAME="Alpine Linux"
ID=alpine
buildkit
containerd
containers
engine-id
image

容器直接讀到了 Docker 主機的 /etc/os-release 和 /var/lib/docker(本教學的測試環境是 Alpine Linux)。同樣的方法也能讀寫 /etc/shadow、SSH 金鑰等任何檔案。所以:

  • 能執行 docker 指令,就等同擁有主機的 root 權限。把使用者加入 docker 群組,就等於給了他 root。
  • 不要隨意把 -v /var/run/docker.sock:/var/run/docker.sock 掛給容器,除非你完全信任那個映像檔(例如 Traefik、Portainer、Trivy 這類需要它的工具)。
  • 不要把 Docker daemon 的 TCP 連接埠(2375)開放到網路上,這等於把主機的 root 權限公開。
  • 需要更強的隔離時,可以研究 Docker 的 rootless 模式。

9. 掃描映像檔漏洞

開源工具 Trivy 可以掃描映像檔中的作業系統套件和程式語言套件的已知漏洞。用容器執行,不需要安裝:

$ docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v trivy-cache:/root/.cache aquasec/trivy:latest image --quiet --scanners vuln --severity HIGH,CRITICAL nginx:1.30-alpine

Report Summary

┌───────────────────────────────────┬────────┬─────────────────┐
│              Target               │  Type  │ Vulnerabilities │
├───────────────────────────────────┼────────┼─────────────────┤
│ nginx:1.30-alpine (alpine 3.24.2) │ alpine │        3        │
└───────────────────────────────────┴────────┴─────────────────┘
Legend:
- '-': Not scanned
- '0': Clean (no security findings detected)


nginx:1.30-alpine (alpine 3.24.2)
=================================
Total: 3 (HIGH: 3, CRITICAL: 0)

┌──────────┬─────────────────┬──────────┬────────┬───────────────────┬───────────────┬─────────────────────────────────────────────────────────┐
│ Library  │  Vulnerability  │ Severity │ Status │ Installed Version │ Fixed Version │                          Title                          │
├──────────┼─────────────────┼──────────┼────────┼───────────────────┼───────────────┼─────────────────────────────────────────────────────────┤
│ libexpat │ CVE-2026-93990  │ HIGH     │ fixed  │ 2.8.4-r0          │ 2.8.5-r0      │ expat: Expat: XML Injection via Malformed UTF-16 Input  │
│          │                 │          │        │                   │               │ https://avd.aquasec.com/nvd/cve-2026-93990              │
├──────────┼─────────────────┤          │        ├───────────────────┼───────────────┼─────────────────────────────────────────────────────────┤
│ pcre2    │ CVE-2026-103111 │          │        │ 10.48-r0          │ 10.49-r0      │ pcre2: pcre2: Out-of-bounds write via crafted regular   │
│          │                 │          │        │                   │               │ expression                                              │
│          │                 │          │        │                   │               │ https://avd.aquasec.com/nvd/cve-2026-103111             │
├──────────┼─────────────────┤          │        ├───────────────────┼───────────────┼─────────────────────────────────────────────────────────┤
│ tiff     │ CVE-2026-4775   │          │        │ 4.7.1-r0          │ 4.7.2-r0      │ libtiff: libtiff: Arbitrary code execution or denial of │
│          │                 │          │        │                   │               │ service via signed integer...                           │
│          │                 │          │        │                   │               │ https://avd.aquasec.com/nvd/cve-2026-4775               │
└──────────┴─────────────────┴──────────┴────────┴───────────────────┴───────────────┴─────────────────────────────────────────────────────────┘
  • --severity HIGH,CRITICAL 只顯示高風險以上的漏洞,--quiet 隱藏下載漏洞資料庫的進度。
  • 掛載 docker.sock 讓 Trivy 讀取本機的映像檔;trivy-cache Volume 保存漏洞資料庫,下次掃描不用重新下載。
  • 結果顯示 nginx:1.30-alpine 的基礎 Alpine 有 3 個 HIGH 等級的漏洞,Status 都是 fixed,而且列出了 Fixed Version:也就是 Alpine 已經發布修補,只是這個映像檔建置時還沒有包含。

掃描結果會隨時間改變:新的漏洞每天都在發現,映像檔也會不斷更新。你執行時看到的結果很可能不同。

Docker Desktop 與新版 CLI 也內建了 Docker Scout(docker scout quickview、docker scout cves),登入 Docker 帳號後即可使用。建議在 CI 流程中自動掃描,發現嚴重漏洞時就阻止部署。

10. 定期更新

基礎映像檔會持續修補漏洞。接續上面的掃描結果,在 Dockerfile 中升級套件後重新掃描:

$ cat Dockerfile
FROM nginx:1.30-alpine
RUN apk upgrade --no-cache

$ docker build -q -t nginx-patched .
sha256:c88ea56e7062e7703dccc5eb2fedea74c333f33982d9735753056a50858646f2

$ docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v trivy-cache:/root/.cache aquasec/trivy:latest image --quiet --scanners vuln --severity HIGH,CRITICAL nginx-patched

Report Summary

┌───────────────────────────────┬────────┬─────────────────┐
│            Target             │  Type  │ Vulnerabilities │
├───────────────────────────────┼────────┼─────────────────┤
│ nginx-patched (alpine 3.24.2) │ alpine │        0        │
└───────────────────────────────┴────────┴─────────────────┘
Legend:
- '-': Not scanned
- '0': Clean (no security findings detected)

apk upgrade 把有漏洞的套件升級到修補版本後,HIGH 和 CRITICAL 的漏洞數變成 0。

實務上更好的做法是定期用 docker build --pull 重新建置(--pull 會先下載基礎映像檔的最新版本),並重新部署。可以使用 Dependabot、Renovate 等工具自動提醒基礎映像檔的新版本。只建置一次、之後好幾年都不更新的映像檔,漏洞只會越來越多。

檢查清單

  • Dockerfile 有 USER,不以 root 執行
  • 基礎映像檔來源可信、版本固定
  • 使用精簡映像檔或多階段建置
  • 映像檔中沒有密碼、金鑰、.env(檢查 docker history 和 docker inspect)
  • 建置時的機密用 RUN --mount=type=secret
  • 資料庫等內部服務沒有對外開放連接埠
  • 視情況使用 --read-only、--cap-drop ALL、no-new-privileges
  • 沒有使用 --privileged,沒有任意掛載 docker.sock
  • 設定了記憶體、CPU 與 PID 上限
  • CI 中有映像檔漏洞掃描
  • 定期用 --pull 重新建置以取得安全更新

重點回顧

  • Dockerfile 加上 USER,以非 root 執行。
  • ARG、ENV、被刪除的檔案都會留在映像檔中;建置時的機密用 --secret。
  • 不需要對外的服務不要用 -p;-p 會繞過 ufw。
  • 能使用 docker.sock 就等同主機 root。
  • 定期掃描並用 --pull 重新建置。

動手練習

練習 1:用 docker history --no-trunc 檢查你自己建置的映像檔,有沒有出現密碼或 token?

提示

看 CREATED BY 欄中的 ARG、ENV、RUN 指令內容,以及 docker inspect -f "{{json .Config.Env}}" 映像檔。有的話要改用 --secret 或執行時的環境變數,並撤銷已外洩的金鑰。

常見問題

容器裡的 root 就是主機的 root 嗎?

一般模式下是同一個 uid,只是被 namespace、capabilities、seccomp 限制。要讓它不是主機的 root,使用 [Rootless 模式](/zh-tw/docker/rootless-podman/)。

映像檔掃描出很多漏洞怎麼辦?

先處理有 Fixed Version 的高風險漏洞:重新建置(docker build --pull)或升級套件。沒有修補版本的,評估是否真的會用到該套件。

Docker Scout 和 Trivy 有什麼不同?

都是漏洞掃描工具。Scout 整合在 Docker Desktop 中,需要登入;Trivy 是開源工具,用容器就能執行。

更新於 2026年10月14日