https://mengz.me/posts/docker...
在進行應用容器化的實踐中,我們可以使用多種方式來建立容器映象,而使用Dockerfile是我們最常用的方式。
而且在實現CI/CD Pipeline的過程中,使用Dockerfile來構建應用容器也是必須的。
本文不具體介紹Dockerfile的指令和寫法,僅僅是在實踐中積累的一些寫好一個Dockerfile的小提示,體現在一下幾個方面:
- 減少構建時間
- 減小映象大小
- 映象可維護性
- 重複構建一致性
- 安全性
減小構建時間
首先來看看下面這個Dockerfile
FROM ubuntu:18.04
COPY . /app
RUN apt-get update
RUN apt-get -y install ssh vim openjdk-8-jdk
CMD [“java”,”-jar”,”/app/target/app.jar”]
要減小構建的時間,那我們可以例如Docker構建的快取特性,儘量保留不經常改變的層,而在Dockerfile的指令中, COPY
和RUN
都會產生新的層,而且快取的有效是與命令的順序有關係的。
在上面的Dockerfile中,COPY . /app
在RUN apt-get ...
之前,而COPY是經常改變的部分,所以每次構建都會到導致RUN apt-get ...
快取失效。
Tip-1 : 合理利用快取,而執行命令的順序是會影響快取的可用性的。
要減小構建時間,另一方面是應該僅僅COPY需要的東西,對於上面這個Dockerfile的目的,應該僅僅需要COPY Java應用的jar檔案。
Tip-2 : 構建過程中僅僅COPY需要的東西。
上面的Dockerfile對apt-get命令分別使用了兩個RUN指令,會生成兩個不同的層。
Tip-3 : 儘量合併最終映象的層數。
還有對於這個示例,我們最終是想要一個JRE環境來執行Java應用,因此可以選擇一個jre的映象來作為基礎映象,這樣不用花時間再去安裝jdk。
Tip-4 : 選擇合適的基礎映象
這樣我們可以把Dockerfile寫成:
FROM ubuntu:18.04
RUN apt-get update \
&& apt-get y install ssh vim openjdk-8-jdk
COPY target/app.jar /app
CMD [“java”,”-jar”,”/app/app.jar”]
減小映象大小
進一步,我們如何儘量減小最終應用映象的大小,來加速我們的CI構建,以及減小映象在網路上傳輸的效率。
在上例中,ssh, vim
應該都是不必要的軟體包,它們會我們用映象的空間。
Tip-5 : 移除不必要的軟體包安裝(包括一些debug工具)。
其次,類似apt-get之類的系統包管理工具會產生快取資料,我們也應該清除。
Tip-6 : 在使用系統包管理工具安裝軟體包後清理快取資料。
另外我們應該使用Docker提供的多階段構建特性來減小最終的映象大小,我們在後面介紹。
我們進一步改進Dockerfile:
FROM ubuntu:18.04
RUN apt-get update \
&& apt-get y install –no-install-recommends openjdk-8-jdk \
&& rm -rf /var/lib/apt/lists/*
COPY target/app.jar /app
CMD [“java”,”-jar”,”/app/app.jar”]
映象的可維護性
我們看看上面的Dockerfile,使用了一個ubuntu
的映象來安裝jdk包,而在安裝jdk包的不同時間點,可能會導致不同的版本,這樣就導致了映象的不易維護。
Tip-7 : 儘可能使用應用(語言)執行時的官方基礎映象,並指定Tag版本
一般來說,官方會維護一些變種映象來提供多樣性,例如基於 alpine的,還有 -slim 精簡版本的,其次對於像Java應用,最終我們需要的應該只是JRE,因此應該選擇jre的映象,這樣既保證了可維護性,同時也可以減小映象的大小。
FROM openjdk:8-jre-alpine
COPY target/app.jar /app
CMD [“java”,”-jar”,”/app/app.jar”]
重複構建一致性
我們應該保障我們的映象構建在任何時候,以及任何構建伺服器上是一致的,但是我們看上面的Dockerfile,是將jar檔案COPY到容器中,但是這個jar檔案是在什麼環境構建的呢?
Tip-8 : 在一致的環境中從原始碼構建
同樣,Docker的多階段構建提供了最好的解決方案,將原始碼編譯構建放到構建階段,將最終生成的軟體包COPY放到執行是階段。
Tip-9 : 使用多階段構建
FROM maven:3.6-jdk-11 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn -e -B dependency:resolve
COPY src ./src
RUN mvn -e -B package
FROM openjdk:11-jre-slim
COPY –from=builder /app/target/app.jar /
CMD [“java”,”-jar”,”/app.jar”]
例如上面的Dockerfile,我們使用了maven
的映象來構建程式碼,使用openjdk:jre
的映象來執行。
安全性
最後我們來看看安全性,如何使我們的應用容器更加安全。首先,容器裡包含的軟體包越少,那可能的漏洞就會越少,所以這也是 Tip-5 所強調的。
Tip-10 : 使用非root使用者執行容器應用程式
其次,我們應該使用非root使用者來執行我們的應用,預設情況下容器都是使用root使用者來執行,我們可以使用以下兩種方法來使用非root使用者來執行。
- 使用
USER
指令,記得在使用USER
指令前建立相應的使用者 - 在
CMD
或者ENTRYPOINT
中使用su-exec
,gosu
等工具來啟動應用
我推薦使用第二種方法,因為第一種方式,在啟動容器後進入容器會預設使用非root使用者,這樣不便於安裝某些除錯工具來執行除錯(當然也可以通過配置sudo)。
而第二種方式需要安裝su-exec
等工具,我建議自己基於官方的基礎映象維護一些自己的執行時基礎映象,這樣避免在每次構建應用映象的時候都進行一次安裝。
FROM gradle:6.4-jdk11 as builder
WORKDIR /code
COPY . .
RUN gradle assemble
FROM mengzyou/openjdk:11-jre-alpine
ENV APP_HOME="/opt/app" \
APP_USER="appuser" \
JAR_OPTS="--spring.profiles.active=prod"
RUN addgroup ${APP_USER} && \
adduser -D -h ${APP_HOME} -S -G ${APP_USER} ${APP_USER}
COPY --from=builder --chown=${APP_USER}:${APP_USER} /code/build/libs/*.jar ${APP_HOME}/app.jar
EXPOSE 8080/tcp
WORKDIR ${APP_HOME}
CMD su-exec app java ${JAVA_OPTS} -jar ${APP_HOME}/app.jar ${JAR_OPTS}
# CMD [“su-exec”,”appuser”,”sh -c”,”java -jar /opt/app/app.jar"]
在來一個golang的示例
FROM golang:1.14-alpine AS builder
RUN apk add --no-cache git && \
mkdir -p $GOPATH/src/app \
WORKDIR $GOPATH/src/app
COPY . $GOPATH/src/app
RUN go mod tidy \
&& go build -o /go/bin/app
FROM mengzyou/alpine:3.12
ENV APP_HOME=/opt/app \
APP_USER=appuser
RUN addgroup ${APP_USER} && \
adduser -D -h ${APP_HOME} -S -G ${APP_USER} ${APP_USER}
COPY --from=builder --chown=${APP_USER}:${APP_USER} /go/bin/app ${APP_HOME}/
EXPOSE 8080/tcp
WORKDIR ${APP_HOME}
CMD su-exec ${APP_USER} ${APP_HOME}/app
總結
這裡僅僅是在Dockerfile實踐中的一些提示,要寫好Dockerfile,還有很多方面需要注意的地方,可參考Docker官方的Best practices for writing Dockerfiles。