昨天已經介紹了幾個寫 Dockerfile 時該注意的地方,但其實需要注意的地方非常非常多,所以今天會再補充兩個,話不多說馬上開始吧!
平常在寫 Dockerfile 時,有些人為了一時方便,會直接用 COPY . /app
把整個專案資料夾複製到 container 的 /app
資料夾內。這樣不用動太多腦筋,在 container 裡面也可以直接存取到所有檔案,但這樣的做法可以說是糟透了
FROM node:14
WORKDIR /app
# Very BAD
COPY . .
RUN npm install
RUN npm run test
CMD ["node", "index.js"]
首先是這樣會讓 image 變得很肥(連 node_modules
都進去了能不肥嗎XD),而且一不小心就會把 .envrc
這類敏感資料一起放進去,如果哪天這個 image 被駭客拿到,裡面的 AWS 憑證、資料庫密碼等等超機密資料就會直接外洩出去,哪天突然被刪庫也是有可能的
為了避免這種事情發生,在 build image 時應該只把需要的東西複製進去,譬如說你馬上就要跑 npm install
,這才把 package.json
跟 package-lock.json
放進去,而程式碼也是把真的會跑到的那些放進去就好
FROM node:14
WORKDIR /app
# Good
COPY package.json package-lock.json ./
RUN npm install
# Good
COPY src/ index.js ./
RUN npm run test
CMD ["node", "index.js"]
而且這樣還有另外一個好處,就是如果你改了 src
裡面的程式碼,但沒有安裝新的 package(開發時大部分都是這樣吧~),因為 Docker 會自動做 cache,所以就不需要重新跑一次 npm install,會直接從第 10 行的 npm run test
開始跑,因此可以大幅縮減需要等待的時間
這跟上一點有點類似,簡單來說就是不要留任何不需要的東西在 image 裡面(沒用的東西都給我滾),即便那是 build image 過程中產生的東西也是一樣
譬如說原本 Go API server 的 Dockerfile 可能長這樣,因為要在 build image 時編譯出執行檔,所以第 5 行的 COPY *.go
是一定要的,沒有他們就無法進行編譯
FROM golang:1.17
WORKDIR /app
COPY *.go go.mod go.sum ./
RUN go build -o main
CMD ["./main"]
但說真的一旦編譯出執行檔之後,那些 Go 程式碼就用不到了,所以應該來個爽快的過河拆橋,用 multi-stage build 把編譯完的執行檔保留下來就好,程式碼什麼的就直接拜拜
像下面這個這個 Dockerfile 經過 multi-stage build 後 image 裡面就只有 /app/main
這個編譯好的執行檔,沒有任何程式碼以及 Go 的編譯器,非常的存粹
# compile stage
FROM golang:1.17 as compile-env
WORKDIR /app
COPY *.go go.mod go.sum ./
RUN go build -o main
# final stage
FROM gcr.io/distroless/base
COPY --from=compile-env /app/main /app
CMD ["/app/main"]
那這樣有什麼好處呢?除了 image 可以變小很多之外,即便 image 被駭客拿到了,程式碼也不會外流出去(有很多攻擊都是拿到程式碼後從裡面找到漏洞),因此只保留執行檔可以提高安全性
除此之外,因為環境越複雜就越可能有沒發現的漏洞,而把 base image 從原本的 golang:1.17
換成 Google 提供的 distroless image 剛好可以大幅減少環境的複雜度(distroless 幾乎沒裝什麼東西,連 shell 都沒有),也就可以提高安全性
這兩天介紹了一些在寫 Dockerfile 時的注意事項,雖然很多都是小地方,但畢竟魔鬼藏在細節裡,想要讓你的 Docker image 更安全,那就連這些小細節都不能放過哦~