使用 Docker 創建鏡像

Dockerfile 是一個文本格式的配置文件,風格卻像是一段代碼,用戶可以通過編寫 Dockerfile 來創建自定義鏡像。

本文將從 Dockerfile 的基本格式入手,羅列其支持的指令并進行介紹,并說明如何通過這些指令來構建我們想要的鏡像。最后還會講下一些編寫 Dockerfile 的經驗。

1. 基礎結構

Docker 由一行行命令語句和以 # 開頭的注釋語句構成。主體內容分為四個部分:基礎鏡像信息、維護者信息、鏡像操作指令和容器啟動時執行指令。

下面從一個簡單的 Dockerfile 例子開始說明:

# escape=\ (backslash)
# This dockerfile uses the ubuntu:xenial image
# VERSION 2 - EDITION 1
# Author: docker_user
# Command format: Instruction [arguments / command] ..

# Base image to use, this must be set as the first line
FROM ubuntu:xenial

# Maintainer: docker_user <docker_user at email.com> (@docker_user)
LABEL maintainer docker_user<docker_user@email.com>

# Command to update the image 
RUN echo "deb http://archive.ubuntu.com/ubuntu/ xenial main universe" >> /etc/apt/sources.list
RUN apt-get update && apt-get install -y nginx
RUN echo "\ndaemon off;" >> /etc/nginx/nginx.conf

# Command when creating a new container 
CMD /usr/sbin/nginx

1.1 解析器指令

這個例子中,第一行是解析器命令,而不是普通的注釋,它指定了這個 Dockerfile 文本的轉義字符。

# escape=\ (backslash) 或者 # escape=` (backtick),默認轉義字符是 \ 。

轉義字符既用于轉義一行中的字符,也用于轉義換行符。這使得一條 Dockerfile 指令可以跨越多行。注意,無論解析器指令 escape 是否包含在 Dockerfile 中,轉義都不會在 RUN 指令中執行,除非要轉義的字符出現在行末尾。

在Windows環境下,將轉義字符設置為反引號而不是 \ 將非常有用,這樣,\ 就可以保持其作為目錄路徑分隔符的原有的含義,與 Windows PowerShell 中保持一致。

考慮下面的這個例子,它在Windows上可能會非常詭異地執行失敗。第二行末尾的第二個 \ 將被視為用來轉義換行符的轉義字符,而不是第一個 \ 的轉義目標。類似地,假設第三行末尾的 \ 本來是要作為一條指令進行處理的,但是它卻將被視為行延續字符。這個 Dockerfile 的結果是,第二行和第三行被認為是一條指令:

FROM microsoft/nanoserver
COPY testfile.txt c:\\
RUN dir c:\

解決方式就是指定轉義字符為 `

更多關于解析器指令的資料可以參考 Docker 官方文檔

1.2 主體部分

一般而言,大部分鏡像會包含一個 FROM 標簽指定基礎鏡像信息,即要構建的新鏡像是基于哪個舊鏡像構建的。接下來是使用 LABEL 指令說明維護者信息,后面則是鏡像操作指令,需要注意的是每運行一條 RUN 指令,鏡像添加新的一層,并提交,最后是 CMD 指令,指定容器啟動時的操作命令。

2. 指令說明

Dockerfile 中指令的一般格式是 INSTRUCTION arguments,類型有配置指令(配置鏡像信息)和操作指令(具體執行操作)。

2.1 配置指令

2.1.1 ARG

定義了創建鏡像過程中的變量。
格式為 ARG <name>[=<default value>]。
在執行 docker build 時,可以通過 -build-arg[=] 來為變量賦值。當鏡像編譯成功后, ARG 變量將不再存在(ENV 變量將一直保留在環境變量中)。
Docker 中內置了一些鏡像創建變量,可以直接使用(無需區分大小寫):HTTP_PROXY、hTTPS_PROXY、FTP_PROXY、NO_PROXY。

2.1.2 FROM

指定所創建鏡像的基礎鏡像。
格式為 FROM <image> [AS <name>] 或 FROM <image>:<tag> [AS <name>] 或 FROM <image>@<digest> [AS <name>]。
任何 Dockerfile 的第一條指令(不算解析器指令),必須是 FROM 指令。如果在同一個 Dockerfile 中有多個鏡像,可以使用多個 FROM 指令。

例子為

ARG VERSION=9.3
FROM debian:${VERSION}

2.1.3 LABEL

LABEL 指令可以為生成的鏡像添加元數據標簽信息。之后可以使用 docker ps -f lable key=value 快速過濾出特定鏡像。
格式為 LABEL <key>=<value> <key>=<value> <key>=<value>....

例如:

LABEL author="wean" date="2019-1-16"
LABEL description="This is a mult-line \
    text"

2.1.4 EXPOSE

聲明鏡像內服務監聽的端口。
格式為 EXPOSE <port> [<port>/<protocol>...]
例如:

EXPOSE 22 80 443

需要注意的是,該指令只是起到聲明的作用,并不會自動完成端口映射。
如果要映射端口出來,在啟動容器時可以使用 -P 或 -p 參數,可以參考 操作 Docker 容器,查看端口映射的具體操作。

2.1.5 ENV

指定環境變量,在編譯鏡像過程中會被后續 RUN 指令使用,在鏡像啟動后也會存在于環境變量中。
例如:

ENV APP_VERSION=1.0.0
ENV PATH $PATH:/usr/local/bin

指令指定的環境變量在啟動時也可以覆蓋掉:docker run --env <key>=<value> built_image

注意當一條 ENV 指令中同時為多個環境變量賦值并且值也是從環境變量讀取時;會為變量先賦值后更新。如下面指令,最終結果是 key1=value1 key2=value2

ENV key1=value2
ENV key1=value1 key2=${key1}

2.1.6 ENTRYPOINT

指定鏡像的默認入口命令,該命令會在啟動容器時作為跟命令執行,所有傳入值作為該命令的參數。
支持兩種格式:

  • ENTRYPOINT ["executable", "param1", "param2"]: exec 調用執行
  • ENTRYPOINT command param1 param2: shell 調用執行。
    此時,若是指定了 CMD 指令或者 docker run 指定了啟動命令,則會作為 ENTRYPOINT 根命令的參數。
    每個 Dockerfile 中只能有一個 ENTRYPOINT,當指定多個時,只有最后一個生效。
    在運行時,可以被 --entrypoint 參數覆蓋掉。

關于 ENRTYPOINT 和 CMD 的更多差異可以參考 https://www.cnblogs.com/lienhua34/p/5170335.html

2.1.7 VOLUME

創建數據卷掛載點,掛載到本地的一個隨機目錄下。
格式為 VOLUME ["/data"]。

2.1.8 USER

指定運行容器時的用戶名或者 UID,后續的 RUN 指令也會默認使用特定的用戶身份。
格式為 USER daemon。
當服務不需要管理員權限時,可以在 Dockerfile 中創建需要所需要的用戶,并通過 USER 命令指定該運行用戶。例如:

RUN groupadd -r postgres && useradd --no-log-init -r -g postgress postgress

后續需要獲取臨時管理員權限可以使用 gosu 命令。

2.1.9 WORKDIR

為后續的 RUN、CMD、ENTRYPOINT 指令配置工作目錄。
格式為 WORKDIR /path/to/workdir
可以使用多個 WORKDIR 指令,但是后續指令如果是相對路徑,基于之前的指令指定的路徑。
例如:

WORKDIR /a
WORKDIR b

結果是路徑 /a/b,所以推薦要是需要寫多個 WORKDIR,則每個 WORKDIR 都寫絕對路徑。

2.1.10 ONBUILD

指定當基于所生成的鏡像創建子鏡像時,自動執行的操作指令。
格式為 ONBUILD [INSTRUCTION]
由由于這個命令是隱式執行的,子鏡像創建過程中察覺不到,所以應該在鏡像標簽中進行標注,例如 ruby:2.1-onbuild。
這個指令在創建專門用于自動編譯、檢查等操作的基礎鏡像時十分有用。

2.1.11 STOPSIGNAL

默認的stop-signal是SIGTERM,在docker stop的時候會給容器內PID為1的進程發送這個signal,通過--stop-signal可以設置自己需要的signal,主要的目的是為了讓容器內的應用程序在接收到signal之后可以先做一些事情,實現容器的平滑退出,如果不做任何處理,容器將在一段時間之后強制退出,會造成業務的強制中斷,這個時間默認是10s。
格式為 STOPSIGNAL signal

2.1.12 HEALTHCHECK

配置所啟動容器如何進行健康檢查(如何判斷健康與否)。格式有兩種:

  • HEALTHCHECK [OPTIONS] CMD command:根據所執行命令返回值是否為 0 來判斷
  • HEALTHCHECK NONE: 禁止基礎鏡像中的健康檢查

OPTIONS 支持如下參數:

  • -interval=DURATION (default: 30s): 過多久檢查一次。
  • -timeout=DURATION (default: 30s): 每次檢查的超時時間。
    • retries=N (default: 3): 如果失敗了,重試幾次才最終確定失敗。

2.1.13 SHELL

指定其他命令使用 shell 時的默認 shell 類型。
SHELL ["executable", "parameters"]。
默認值是 ["/bin/sh", "-c"]。

2.2 操作指令

2.2.1 RUN

運行指定命令。
格式為 RUN <command> 或 RUN ["executable", "param1", "param2"]。命令過長可以使用轉義字符換行,比如默認的
每條 RUN 指令會生成一個新的鏡像層。

2.2.2 CMD

CMD 容器用來指定容器啟動時默認執行的命令。支持三種格式:

  • CMD ["executable", "param1", "param2"]: 相當于執行 executable param1 param2,推薦方式
  • CMD command parame1 param2: 在默認的 shell 中執行,提供給需要交互的應用
  • CMD ["param1", "param2"]: 提供給 ENTRYPOINT 的默認參數
    每個鏡像只能有一個 CMD,多條會覆蓋,可以在運行 (run) 容器時通過參數覆蓋

2.2.3 ADD

添加內容到鏡像。格式為 ADD <src> <dest>。
該命令復制指定的 <src> 下的內容到容器中的 <dest> 路徑下。
其中 <src> 可以是 Dockerfile 所在目錄的一個相對路徑(文件或目錄);也可以是一個 URL;還可以是一個 tar 文件(會自動解壓為目錄)。<dest> 可以是鏡像內的絕對路徑,或者相對于工作目錄 WORKDIR 下的相對路徑。且路徑支持正則格式。

2.2.4 COPY

COPY 指令與 ADD 相似,語義上 CPOY 更貼切,推薦使用 COPY。

3. 創建鏡像

編寫完 Dockerfile 后就可以通過 docker [image] build [OPTIONS] PATH|URL|- 命令來創建鏡像。
要注意的點有:

  • 碰到 ADD、COPY 和 RUN 指令會生成一層新的鏡像
  • 執行命令的過程中會把 Dockerfile 文件所在的上下文都發給服務端。因此應該避免無關的文件存在上下文,或者使用 .dockerignore 文件。
  • 非上下文路徑的 Dockerfile 使用 -f 選項來指定其路徑
  • 要指定生成鏡像的標簽信息,可以使用 -t 選項。該選項可以重復使用多次來添加多個標簽。

3.1 OPTIONS 命令選項

選項 說明
-add-host list 添加自定義的主機名到 IP 的映射
-build-arg list 添加創建時的變量
-cache-from strings 使用指定鏡像作為緩存源
-cgroup-parent string 繼承上層 cgroup
-compress 使用 gzip 來壓縮創建上下文數據
-cpu-period int 分配的 CFS 調度器時長
-cpu-quota int CPU 調度器總份額
-c, -cpu-shares int CPU 權重
-cpuset-cpus string 多 CPU 允許使用的 CPU
-cpuset-mems string 多 CPU 允許使用的內存
-disable-content-trust 不進行鏡像校驗,默認為真
-f,-file string Dockerfile 名稱
-force-rm 總是刪除中間過程的容器
-iidfile string 將鏡像 ID 寫入到文件
-isolation string 容器的隔離機制
-label list 配置鏡像的元數據
-m,-memory bytes 限制使用內存量
-memory-swap bytes 限制內存和緩存的總量
-network string 指定 RUN 命令時的網絡模式
-no-cache 創建鏡像時不適用緩存
-platform string 指定平臺類型
-pull 總是嘗試獲取鏡像的最新版本
-q,-quiet 不打印創建過程中的日志信息
-rm 創建成功后自動刪除中間過程容器,默認為真
-security-opt strings 指定安全相關的選項
-shm-size bytes /dev/shm 的大小
-squash 將新創建的多層擠壓放到一層中
-stream 持續獲取創建的上下文
-t,-tag list 指定鏡像的標簽列表
-target string 指定創建的目標階段
-ulimit ulimit 指定 ulimit 變量

3.2 選擇父鏡像

鏡像的繼承關系

從這個鏡像的繼承圖來看,我們可以選擇兩種鏡像作為父鏡像,一種是基礎鏡像(灰色),另一種是普通鏡像(白色)。

3.3 使用 .dockerignore 文件

從前面我們可以知道,Dockerfile build 的過程中會把上下文一起發給服務端,如果有不想發送的文件(加快傳輸過程),可以使用一下模式忽略文件:

  • /temp
  • //temp*
  • tmp?
  • ~*
  • Dockerfile
  • !README.md

其中 * 表示任意多的字符, ? 代表單個字符, ! 代表不匹配(即不忽略指定的路徑或文件)。

3.4 多步驟創建

從 17.05 版本開始,Docker 支持多步驟鏡像創建,用來精簡最終生成的鏡像的大小。

舉個簡單的例子,制作一個 Java 程序鏡像,分成兩個步驟創建,第一步用包含 maven 庫及 JDK 的編譯鏡像編譯程序,第二部把生成的 jar 文件復制到運行鏡像中,打包運行鏡像就得到精簡的鏡像。

以 Go 語言為例,提供一個例子:

package main 

import (
    "fmt"
)

func main() {
    fmt.Println("Hello, Docker")
}
FROM golang:1.9 as builder
RUN mkdir -p /go/src/test
WORKDIR /go/src/test
COPY main.go .
RUN CGO_ENABLED=0 GOOS=linux go build -o app .

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR/root/
COPY --from=builder /go/src/test/app .
CMD ["./app"]

編譯鏡像,可以看到最終大小只有 6.83 MB

>docker build -t wean2018/test-multistage:latest .

> docker images
REPOSITORY                                      TAG                 IMAGE ID            CREATED             SIZE
wean2018/test-multistage                        latest              2e2eed03a664        53 seconds ago      6.83MB
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容