目錄
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-cacheVolume 保存漏洞資料庫,下次掃描不用重新下載。 - 結果顯示
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日