九個編寫Dockerfiles的常見錯誤


【編者的話】我們每天基於Dockerfiles工作;所有運行的代碼都來自一系列的Dockerfiles。這篇文章將會討論編寫Dockerfile時人們經常犯的錯誤以及如何改進。對於Docker專家說,這篇文章裏的許多技巧可能會非常明顯進而會得到很多的認同。但是對於初級到中級開發者,該文章將會是一份很有用的指南,它有助於理清以及加速你們的工作流程。

1. 執行 apt-get

執行apt-get install是每一個Dockerfile都有的東西之一。你需要安裝一些外部的包來運行代碼。但使用apt-get相應地會帶來一些問題。

一個是運行apt-get upgrade 會更新所有包到最新版本 —— 不能這樣做的理由是它會妨礙Dockerfile構建的持久與一致性。

另一個是在不同的行之間運行apt-get updateapt-get install命令。不能這樣做的原因是,只有apt-get update的代碼會在構建過程中被緩存,而且你需要運行apt-get install命令的時候不會每次都被執行。因此,你需要將apt-get update跟所要安裝的包都在同一行執行,來確保它們正確的更新。

在以下 Golang Dockerfileapt-install命令就是一個不錯的例子:
RUN apt-get update && \
apt-get install -y --no-install-recommends \
g++ \
gcc \
libc6-dev \
make \
&& rm -rf /var/lib/apt/lists/*

2. 使用ADD而非COPY

ADDCOPY是完全不同的命令。COPY是這兩個中最簡單的,它只是從主機複製一份文件或者目錄到鏡像裏。ADD同樣可以這麼做,但是它還有更神奇的功能,像解壓TAR文件或從遠程URLs獲取文件。爲了降低Dockerfile的複雜度以及防止意外的操作,最好用COPY來複制文件。
FROM busybox:1.24

ADD example.tar.gz /add #解壓縮文件到add目錄
COPY example.tar.gz /copy #直接複製文件

3. 在一行內添加整個應用目錄

明確代碼的哪些部分以及什麼時候應該放在構建鏡像內或許是最重要的事了,它可以顯著加快構建速度。

Dockerfile裏經常會看到如下這些內容:
# !!! ANTIPATTERN !!!
COPY ./my-app/ /home/app/
RUN npm install # or RUN pip install or RUN bundle install
# !!! ANTIPATTERN !!!

這就意味着每次修改文件之後都需要重新構建那行以下的所有東西。多數情況下(包括上面的例子),它意味着重新安裝應用依賴。爲了儘可能地使用Docker的緩存,首先複製所有安裝依賴所需要的文件,然後執行命令安裝這些依賴。在複製剩餘文件(這一步儘可能放到最後一行)之前先做這兩個步驟,會使代碼的變更被快速的重建。
COPY ./my-app/package.json /home/app/package.json # Node/npm packages
WORKDIR /home/app/
RUN npm install

或許還要安裝python依賴?

COPY ./my-app/requirements.txt /home/app/requirements.txt
RUN pip install -r requirements.txt
COPY ./my-app/ /home/app/

這樣做會確保構建儘可能快的執行。

4. 使用:latest標籤

許多Dockerfiles在開頭都使用FROM node:latest模板,用來從Docker registry拉取最新的鏡像。簡單地說,使用latest標籤的鏡像意味着如果這個鏡像得到更新,那麼Dockerfile的構建可能會突然中斷。弄清這件事可能會非常難,因爲Dockerfile的維護者實際上並沒做任何修改。爲了防止這種情況,只需要確保鏡像使用特定的標籤(例如:node:6.2.1)。這樣就可以確保Dockerfile的一致性。

5. 構建鏡像時使用外部服務

很多人會忽視構建Docker鏡像與運行一個Docker容器的區別。在構建鏡像時,Docker讀取Dockerfile裏的命令並創建鏡像。在依賴或代碼修改之前,鏡像是保持不變以及可重複使用的。這個過程完全獨立於其它容器。需要與其它容器或服務(如數據庫)進行交互則會在容器運行的時候發生。

舉一個例子,執行數據庫遷移。很多人試圖在構建鏡像時執行此操作。這樣做會導致許多問題。首先,在構建時數據庫可能不可用,因爲它可能沒建在它將要運行的服務器上。其次,你可能想使用同一個鏡像來連接不同的數據庫(在開發或生產環境中),在這種情況下,如果它在構建過程中,遷移是不能進行的。
# !!! ANTIPATTERN !!!
COPY /YOUR-PROJECT /YOUR-PROJECT
RUN python manage.py migrate
# 嘗試遷移數據,但是並不能
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
# !!! ANTIPATTERN !!!

6. 在Dockerfile開始部分加入EXPOSE和ENV

EXPOSE和ENV是廉價的執行命令。如果你破壞它們的緩存,幾乎瞬時就可以重建。所以,最好儘可能晚地聲明這些命令。在構建過程中應該直到需要的時候才聲明ENV。如果在構建的時候不需要他們,那麼應該在Dockerfile的末尾附加EXPOSE

再次查看Golang的Dockerfile,你會看到,所有ENVS都是在使用前聲明的,並且在最後聲明其餘的:
ENV GOLANG_VERSION 1.7beta1
ENV GOLANG_DOWNLOAD_SHA256 a55e718935e2be1d5b920ed262fd06885d2d7fc4eab7722aa02c205d80532e3b
RUN curl -fsSL "$GOLANG_DOWNLOAD_URL" -o golang.tar.gz \
&& echo "$GOLANG_DOWNLOAD_SHA256 golang.tar.gz" | sha256sum -c - \
&& tar -C /usr/local -xzf golang.tar.gz \
&& rm golang.tar.gz
ENV GOPATH /go
ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH

如需修改ENV GOPATHENV PATH,鏡像幾乎會馬上重建成功。

7. 多個FROM聲明

嘗試使用多個FROM聲明來將不同的鏡像組合到一起,這樣不會起任何作用。Docker僅使用最後一個FROM並且忽略前面所有的。

所以如果你有這樣的Dockerfile:
# !!! ANTIPATTERN !!!
FROM node:6.2.1
FROM python:3.5
CMD ["sleep", "infinity"]
# !!! ANTIPATTERN !!!

那麼docker exec進入運行的容器中,會得到下面的結果:
$ docker exec -it d86fcf0775d3 bash
root@d86fcf0775d3:/# which python
/usr/local/bin/python
root@d86fcf0775d3:/# which node
root@d86fcf0775d3:/#

這其實是GitHub上的一個問題:合併不同的鏡像,但它看起來不會很快就增加的功能。

8. 多個服務運行在同一個容器內

這可能是瞭解Docker的開發者遇到的最大問題。而公認的最佳實踐是:每個不同的服務,包括應用,應該在它自己的容器中運行。在一個Docker鏡像裏面加入多個服務非常容易,但是有一定的負面影響。

首先,橫向擴展應用會變得很困難。其次,額外的依賴和層次會使鏡像構建變慢。最終,增大了Dockerfile的編寫、維護以及調試難度。

當然,像所有的技術建議一樣,你需要用你的最佳判斷。如果想快速安裝一個Django+Nginx的應用的開發環境,那麼讓它們運行在同一個容器裏面,同時生產環境中有一個不同的Dockerfile,讓他們分開運行,是合理可行的。

9. 在構建過程中使用VOLUME

Volume是在運行容器時候加入的,而不是構建的時候。與第五個誤區類似,在構建過程中不應該與你聲明的volume有交互。相反地,你只是在運行容器的時候使用它。例如,如果在以下構建過程中創建文件並且在運行那個鏡像時候使用它,一切正常:
FROM busybox:1.24
RUN echo "hello-world!!!!" > /myfile.txt
CMD ["cat", "/myfile.txt"]
...
$ docker run volume-in-build
hello-world!!!!

但是,如果我對一個存儲在volume上的文件做同樣的事,就不會起作用。
FROM busybox:1.24
VOLUME /data
RUN echo "hello-world!!!!" > /data/myfile.txt
CMD ["cat", "/data/myfile.txt"]
...
$ docker run volume-in-build
cat: can't open '/data/myfile.txt': No such file or directory

一個有趣的問題是:如果你前面的任何一個層次聲明瞭一個VOLUME(也可能是幾個FROMS)依然會遇到同樣的問題。因此,最好留意一下父類鏡像都聲明瞭什麼volume。如果遇到問題,請使用docker inspect檢查。

結論

理解怎樣寫好一個Dockerfile將會是一個漫長的路程,它會帶你理解Docker是如何工作的,同時也幫助你建立你的基礎架構。理解Docker緩存會爲你節省好多等待構建完成的時間!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章