Docker學習筆記之常見 Dockerfile 使用技巧


0x00 概述

在掌握 Dockerfile 的基本使用方法后,我們再來了解一些在開發中使用 Dockerfile 的技巧。這一小節的展現方式與之前的略有不同,其主要來自閱讀收集和我自身在使用中的最佳實踐。也許這里面介紹的不是最為標准或是合乎規范的方式,但一定是能夠直接幫助大家在開發中使用 Docker 提升生產力的方式。下面就讓我們來看看這些關於 Dockerfile 的使用技巧吧。

 

0x01 構建中使用變量

在實際編寫 Dockerfile 時,與搭建環境相關的指令會是其中占有大部分比例的指令。在搭建程序所需運行環境時,難免涉及到一些可變量,例如依賴軟件的版本,編譯的參數等等。我們可以直接將這些數據寫入到 Dockerfile 中完全沒有問題,有問題的是這些可變量我們會經常調整,在調整時就需要我們到 Dockerfile 中找到它們並進行更改,如果只是簡單的 Dockerfile 文件尚且好說,但如果是相對復雜或是存在多處變量的 Dockerfile 文件,這個工作就變得繁瑣而讓人煩躁了。

在 Dockerfile 里,我們可以用 ARG 指令來建立一個參數變量,我們可以在構建時通過構建指令傳入這個參數變量,並且在 Dockerfile 里使用它。

例如,我們希望通過參數變量控制 Dockerfile 中某個程序的版本,在構建時安裝我們指定版本的軟件,我們可以通過 ARG 定義的參數作為占位符,替換版本定義的部分。

FROM debian:stretch-slim

## ......

ARG TOMCAT_MAJOR
ARG TOMCAT_VERSION

## ......

RUN wget -O tomcat.tar.gz "https://www.apache.org/dyn/closer.cgi?action=download&filename=tomcat/tomcat-$TOMCAT_MAJOR/v$TOMCAT_VERSION/bin/apache-tomcat-$TOMCAT_VERSION.tar.gz"

## ......

在這個例子里,我們將 Tomcat 的版本號通過 ARG 指令定義為參數變量,在調用下載 Tomcat 包時,使用變量替換掉下載地址中的版本號。通過這樣的定義,就可以讓我們在不對 Dockerfile 進行大幅修改的前提下,輕松實現對 Tomcat 版本的切換並重新構建鏡像了。

如果我們需要通過這個 Dockerfile 文件構建 Tomcat 鏡像,我們可以在構建時通過 docker build 的 --build-arg 選項來設置參數變量。

$ sudo docker build --build-arg TOMCAT_MAJOR=8 --build-arg TOMCAT_VERSION=8.0.53 -t tomcat:8.0 ./tomcat

 

0x02 環境變量

環境變量也是用來定義參數的東西,與 ARG 指令相類似,環境變量的定義是通過 ENV 這個指令來完成的。

FROM debian:stretch-slim

## ......

ENV TOMCAT_MAJOR 8
ENV TOMCAT_VERSION 8.0.53

## ......

RUN wget -O tomcat.tar.gz "https://www.apache.org/dyn/closer.cgi?action=download&filename=tomcat/tomcat-$TOMCAT_MAJOR/v$TOMCAT_VERSION/bin/apache-tomcat-$TOMCAT_VERSION.tar.gz"

環境變量的使用方法與參數變量一樣,也都是能夠直接替換指令參數中的內容。

與參數變量只能影響構建過程不同,環境變量不僅能夠影響構建,還能夠影響基於此鏡像創建的容器。環境變量設置的實質,其實就是定義操作系統環境變量,所以在運行的容器里,一樣擁有這些變量,而容器中運行的程序也能夠得到這些變量的值。

另一個不同點是,環境變量的值不是在構建指令中傳入的,而是在 Dockerfile 中編寫的,所以如果我們要修改環境變量的值,我們需要到 Dockerfile 修改。不過即使這樣,只要我們將 ENV 定義放在 Dockerfile 前部容易查找的地方,其依然可以很快的幫助我們切換鏡像環境中的一些內容。

由於環境變量在容器運行時依然有效,所以運行容器時我們還可以對其進行覆蓋,在創建容器時使用 -e 或是 --env 選項,可以對環境變量的值進行修改或定義新的環境變量。

$ sudo docker run -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:5.7

事實上,這種用法在我們開發中是非常常見的。也正是因為這種允許運行時配置的方法存在,環境變量和定義它的 ENV 指令,是我們更常使用的指令,我們會優先選擇它們來實現對變量的操作。

關於環境變量是如何能夠幫助我們更輕松的處理 Docker 鏡像和容器使用等問題,我們會在下一節中進行實際展示,通過例子大家能夠更容易理解它的原理。

另外需要說明一點,通過 ENV 指令和 ARG 指令所定義的參數,在使用時都是采用 $ + NAME 這種形式來占位的,所以它們之間的定義就存在沖突的可能性。對於這種場景,大家只需要記住,ENV 指令所定義的變量,永遠會覆蓋 ARG 所定義的變量,即使它們定時的順序是相反的。

 

0x03 合並命令

在上一節我們展示的完整的官方 Redis 鏡像的 Dockerfile 中,我們會發現 RUN 等指令里會聚合下大量的代碼。

事實上,下面兩種寫法對於搭建的環境來說是沒有太大區別的。

RUN apt-get update; \
    apt-get install -y --no-install-recommends $fetchDeps; \
    rm -rf /var/lib/apt/lists/*;
RUN apt-get update
RUN apt-get install -y --no-install-recommends $fetchDeps
RUN rm -rf /var/lib/apt/lists/*

那為什么我們更多見的是第一種形式而非第二種呢?這就要從鏡像構建的過程說起了。

看似連續的鏡像構建過程,其實是由多個小段組成。每當一條能夠形成對文件系統改動的指令在被執行前,Docker 先會基於上條命令的結果啟動一個容器,在容器中運行這條指令的內容,之后將結果打包成一個鏡像層,如此反復,最終形成鏡像。

所以說,我們之前談到鏡像是由多個鏡像層疊加而得,而這些鏡像層其實就是在我們 Dockerfile 中每條指令所生成的。

了解了這個原理,大家就很容易理解為什么絕大多數鏡像會將命令合並到一條指令中,因為這種做法不但減少了鏡像層的數量,也減少了鏡像構建過程中反復創建容器的次數,提高了鏡像構建的速度。

 

0x04 構建緩存

 Docker 在鏡像構建的過程中,還支持一種緩存策略來提高鏡像的構建速度。

由於鏡像是多個指令所創建的鏡像層組合而得,那么如果我們判斷新編譯的鏡像層與已經存在的鏡像層未發生變化,那么我們完全可以直接利用之前構建的結果,而不需要再執行這條構建指令,這就是鏡像構建緩存的原理。

那么 Docker 是如何判斷鏡像層與之前的鏡像間不存在變化的呢?這主要參考兩個維度,第一是所基於的鏡像層是否一樣,第二是用於生成鏡像層的指令的內容是否一樣。

基於這個原則,我們在條件允許的前提下,更建議將不容易發生變化的搭建過程放到 Dockerfile 的前部,充分利用構建緩存提高鏡像構建的速度。另外,指令的合並也不宜過度,而是將易變和不易變的過程拆分,分別放到不同的指令里。

在另外一些時候,我們可能不希望 Docker 在構建鏡像時使用構建緩存,這時我們可以通過 --no-cache 選項來禁用它。

$ sudo docker build --no-cache ./webapp

 

0x05 搭配 ENTRYPOINT 和 CMD

上一節我們談到了 ENTRYPOINT 和 CMD 這兩個命令,也解釋了這兩個命令的目的,即都是用來指定基於此鏡像所創建容器里主進程的啟動命令的。

兩個指令的區別在於,ENTRYPOINT 指令的優先級高於 CMD 指令。當 ENTRYPOINT 和 CMD 同時在鏡像中被指定時,CMD 里的內容會作為 ENTRYPOINT 的參數,兩者拼接之后,才是最終執行的命令。

為了更好的讓大家理解,這里索性列出所有的 ENTRYPOINT 與 CMD 的組合,供大家參考。

有的讀者會存在疑問,既然兩者都是用來定義容器啟動命令的,為什么還要分成兩個,合並為一個指令豈不是更方便嗎?

這其實在於 ENTRYPOINT 和 CMD 設計的目的是不同的。ENTRYPOINT 指令主要用於對容器進行一些初始化,而 CMD 指令則用於真正定義容器中主程序的啟動命令。

另外,我們之前談到創建容器時可以改寫容器主程序的啟動命令,而這個覆蓋只會覆蓋 CMD 中定義的內容,而不會影響 ENTRYPOINT 中的內容。

我們依然以之前的 Redis 鏡像為例,這是 Redis 鏡像中對 ENTRYPOINT 和 CMD 的定義。

## ......

COPY docker-entrypoint.sh /usr/local/bin/

ENTRYPOINT ["docker-entrypoint.sh"]

## ......

CMD ["redis-server"]

可以很清晰的看到,CMD 指令定義的正是啟動 Redis 的服務程序,而 ENTRYPOINT 使用的是一個外部引入的腳本文件。

事實上,使用腳本文件來作為 ENTRYPOINT 的內容是常見的做法,因為對容器運行初始化的命令相對較多,全部直接放置在 ENTRYPOINT 后會特別復雜。

我們來看看 Redis 中的 ENTRYPOINT 腳本,可以看到其中會根據腳本參數進行一些處理,而腳本的參數,其實就是 CMD 中定義的內容。

#!/bin/sh
set -e

# first arg is `-f` or `--some-option`
# or first arg is `something.conf`
if [ "${1#-}" != "$1" ] || [ "${1%.conf}" != "$1" ]; then
    set -- redis-server "$@"
fi

# allow the container to be started with `--user`
if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then
    find . \! -user redis -exec chown redis '{}' +
    exec gosu redis "$0" "$@"
fi

exec "$@"

這里我們要關注腳本最后的一條命令,也就是 exec "$@"。在很多鏡像的 ENTRYPOINT 腳本里,我們都會看到這條命令,其作用其實很簡單,就是運行一個程序,而運行命令就是 ENTRYPOINT 腳本的參數。反過來,由於 ENTRYPOINT 腳本的參數就是 CMD 指令中的內容,所以實際執行的就是 CMD 里的命令。

所以說,雖然 Docker 對容器啟動命令的結合機制為 CMD 作為 ENTRYPOINT 的參數,合並后執行 ENTRYPOINT 中的定義,但實際在我們使用中,我們還會在 ENTRYPOINT 的腳本里代理到 CMD 命令上。

相對來說,Redis 的 ENTRYPOINT 內容還是簡單的,在掌握了 ENTRYPOINT 的相關作用后,大家可以嘗試閱讀和編寫一些復雜的 ENTRYPOINT 腳本。

 

0x06 臨摹案例

上面提及的幾項只是幾個比較常見的 Dockerfile 最佳實踐,其實在編寫 Dockerfile 時,還有很多不成文的小技巧。

想要學好 Dockerfile 的編寫,閱讀和思考前人的作品是必不可少的。

前面我們介紹了,Docker 官方提供的 Docker Hub 是 Docker 鏡像的中央倉庫,它除了鏡像豐富之外,給我們帶來的另一項好處就是其大部分鏡像都是能夠直接提供 Dockerfile 文件給我們參考的。

要得到鏡像的 Dockerfile 文件,我們可以進入到鏡像的詳情頁面,在介紹中,鏡像作者們通常會直接把 Dockerfile 的連接放在那里。

 

除此之外,進入到 Dockerfile 這個欄目下,我們也能夠直接看到鏡像 Dockerfile 的內容。在頁面的右側,還有進入 Dockerfile 源文件的連接,如果在 Dockerfile 中有引入其他的文件,我們可以通過這個連接訪問到。

 

另外,我自己也制作了一些軟件的鏡像,大家可以訪問 GitHub 上的項目地址,查閱其中的 Dockerfile 內容:github.com/cogset 。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM