Docker 從入門到實(shí)踐

Introduction

Docker 簡介

  • 虛擬機(jī)(VMs):在 Host OS(主操作系統(tǒng)) 上虛擬化硬件,并使用 Guest OS(虛擬OS) 控制虛擬化硬件
    • Hypervisor 將 Host OS 中計(jì)算資源進(jìn)行分配構(gòu)成虛擬化硬件
    • 內(nèi)核作為 os 的最基本的組成部分,是硬件和軟件進(jìn)行交流的媒介
    • 因此一個(gè) VM 由三部分組成:Guest os, Bins/Libs, App
  • 容器:容器只包含 App 和它所依賴的環(huán)境 Bins/Libs。同時(shí)因?yàn)槿萜骱?VMs 的主要區(qū)別只有內(nèi)核不同,因此能夠?qū)崿F(xiàn) VMs 的大部分功能,它相比 VM 還具有以下優(yōu)點(diǎn)
    • 不需要將硬件虛擬化,具有更高的計(jì)算資源使用效率
    • 不需要啟動(dòng)完整的 os,啟動(dòng)更快
    • 更易維護(hù)和拓展,可移植性更高
  • Docker 在容器的基礎(chǔ)上進(jìn)行再封裝,簡化了容器的創(chuàng)建和維護(hù),使得 Docker 技術(shù)比虛擬機(jī)技術(shù)更為輕便、快捷。

基本概念

鏡像 (image)

  • 鏡像相當(dāng)于一個(gè) root 文件系統(tǒng)
  • 鏡像不包含任何動(dòng)態(tài)數(shù)據(jù),其內(nèi)容在構(gòu)建之后不再發(fā)生改變 (或者說鏡像的內(nèi)容不可寫)
  • 利用 Union FS 技術(shù),將鏡像設(shè)計(jì)為分層存儲(chǔ)的架構(gòu),它有以下好處
    • 節(jié)省存儲(chǔ)空間
    • 使得鏡像的復(fù)用、定制更容易
  • 參考

容器 (Container)

  • 從原理上來說,容器是鏡像運(yùn)行時(shí)的實(shí)體,當(dāng)容器運(yùn)行時(shí),以鏡像為基礎(chǔ)層,并在其上創(chuàng)建一個(gè)容器存儲(chǔ)層。從功能上來說,容器是獨(dú)立運(yùn)行的一組應(yīng)用,以及它們的運(yùn)行態(tài)環(huán)境
  • 容器的實(shí)質(zhì)是進(jìn)程,但與直接在 host 中執(zhí)行的進(jìn)程不同,容器進(jìn)程運(yùn)行于屬于自己的獨(dú)立的 命名空間,因此容器運(yùn)行在一個(gè)獨(dú)立于 host 的隔離的環(huán)境中
  • 按照 Docker 最佳實(shí)踐的要求,容器不應(yīng)該向其存儲(chǔ)層內(nèi)寫入任何數(shù)據(jù),容器存儲(chǔ)層要保持無狀態(tài)化。所有的 (數(shù)據(jù)) 文件寫入操作,都應(yīng)該使用 數(shù)據(jù)卷 (Volume)、或者 掛載 host 目錄
  • 參考

倉庫 (Repository)

  • 倉庫是一個(gè)集中的存儲(chǔ)、分發(fā)鏡像的服務(wù),例如 Docker Registry
    • Docker Registry 公開服務(wù):例如 Docker Hub,可以使用國內(nèi)鏡像提高下載速度
    • 私有 Docker Registry
  • 一個(gè) Docker Registry 中可以包含多個(gè) 倉庫Repository);每個(gè)倉庫可以包含多個(gè) 標(biāo)簽Tag);每個(gè)標(biāo)簽對(duì)應(yīng)一個(gè)鏡像

安裝 Docker

  • 介紹了不同操作系統(tǒng)下 Docker 的安裝
  • 介紹了 Docker Hub 國內(nèi)鏡像的安裝

使用鏡像

獲取鏡像

$ docker pull [選項(xiàng)] [Docker Registry 地址[:端口號(hào)]/]倉庫名[:標(biāo)簽]
  • 鏡像名稱的格式
    • Docker 鏡像倉庫地址:地址的格式一般是 <域名/IP>[:端口號(hào)]。默認(rèn)地址是 Docker Hub(docker.io)。
    • 倉庫名:如之前所說,這里的倉庫名是兩段式名稱,即 <用戶名>/<軟件名>。對(duì)于 Docker Hub,如果不給出用戶名,則默認(rèn)為 library,也就是官方鏡像
  • 從下載過程中可以看到我們之前提及的分層存儲(chǔ)的概念,鏡像是由多層存儲(chǔ)所構(gòu)成。下載也是一層層的去下載,并非單一文件
  • 運(yùn)行鏡像,例如 $ docker run -it --rm ubuntu:18.04 bash

列出鏡像

$ docker image ls
  • 鏡像 ID 是鏡像的唯一標(biāo)識(shí),相對(duì)地,一個(gè)鏡像可以對(duì)應(yīng)多個(gè) Tag
    • 唯一標(biāo)識(shí)意思為一個(gè)標(biāo)識(shí)能唯一確定一個(gè)鏡像,不同鏡像擁有不同的標(biāo)識(shí)
  • 鏡像在下載和上傳過程中是保持壓縮狀態(tài)的,Docker Hub 中展示的大小是壓縮后的大小 (下載所需流量),而 docker image ls 中顯示的大小是展開后各層所占空間的總和 (由于分層存儲(chǔ)的優(yōu)勢,實(shí)際消耗硬盤空間的大小一般遠(yuǎn)小于顯示的大小)
  • 虛懸鏡像(dangling image)
    • 不是所有沒有標(biāo)簽的鏡像都是虛懸鏡像,許多中間層鏡像也沒有標(biāo)簽。區(qū)別是前者是頂層鏡像,而后者不是 (增加參數(shù) -a 顯示中間層鏡像)
    • $ docker image ls -f dangling=true 查看虛懸鏡像
    • $ docker image prune 刪除虛懸鏡像
  • $ docker image ls (--filter|-f) <filter> 過濾器,列出部分鏡像
  • $ docker image ls --format <format string> 以特定格式顯示鏡像

刪除鏡像

$ docker image rm [選項(xiàng)] <鏡像1> [<鏡像2> ...]
  • <鏡像> 可以由 鏡像短ID鏡像長ID,鏡像名,鏡像摘要(digest) 指定

  • 刪除鏡像實(shí)際上在做什么:先 UntaggedDeleted

  • 當(dāng)刪除一個(gè)鏡像時(shí),首先將該鏡像標(biāo)簽取消,即 Untagged

  • 如果還有別的標(biāo)簽指向該鏡像,那么不進(jìn)行實(shí)際的刪除

  • 如果該鏡像無標(biāo)簽指向,由上層向基礎(chǔ)層方向依次進(jìn)行判斷刪除:如果沒有任何層依賴當(dāng)前層,執(zhí)行 Deleted

  • 批量刪除鏡像:docker image rm $(docker image ls -q <篩選鏡像>)

    • 類似 Linux Shell,使用 $(<cmd>) 獲取 cmd 的標(biāo)準(zhǔn)輸出
    • 參數(shù) -q 控制 docker image ls 的標(biāo)準(zhǔn)輸出格式:僅輸出 鏡像短ID

利用 commit 理解鏡像構(gòu)成

  • 當(dāng)運(yùn)行一個(gè)容器時(shí),我們在容器內(nèi) (不使用卷和掛載 host) 所作的任何文件修改都會(huì)被記錄于容器的存儲(chǔ)層里,docker commit 直接將容器的存儲(chǔ)層保存下來成為鏡像

  • docker commit 有一些特殊的應(yīng)用場合,但不要用于定制鏡像,它有以下缺點(diǎn)

    • 輔助文件等無關(guān)內(nèi)容也被記錄:在容器內(nèi)修改文件(包括安裝和編譯等)時(shí),還會(huì)有大量無關(guān)的內(nèi)容(例如日志 .log 和臨時(shí)文件 .tmp)被修改并添加進(jìn)容器存儲(chǔ)層,這會(huì)導(dǎo)致鏡像非常臃腫
    • 黑箱鏡像:使用 docker commit 意味著黑箱操作,生成的鏡像被稱為黑箱鏡像。無從得知在該鏡像中執(zhí)行過什么命令、怎么生成的鏡像,不易于使用和維護(hù)
    • 只做加法不做減法:因?yàn)殓R像是分層存儲(chǔ)的,而 docker commit 只是將容器存儲(chǔ)層保存為鏡像,所刪除的上一層的東西不會(huì)丟失,因此鏡像的每一次后期修改都只會(huì)讓它更加臃腫

使用 Dockerfile 定制鏡像

鏡像分層存儲(chǔ)的結(jié)構(gòu)帶來諸多好處的同時(shí),也帶來很多問題。因此希望能在 Docker 引擎控制下設(shè)計(jì)一系列新的指令,并按照我們的需求構(gòu)建和定制鏡像,將記錄這些指令的純文本文件稱為 Dockerfile。

Dockerfile 是純文本文件,其內(nèi)包含了一條條的 指令(Instruction),每一條指令構(gòu)建鏡像的一層。通過 commit 容器存儲(chǔ)層會(huì)得到一個(gè)黑箱層,相反 Dockerfile 中的指令被設(shè)計(jì)為在 Docker 引擎下進(jìn)行鏡像定制,因此容易記錄鏡像多層結(jié)構(gòu)的歷史變動(dòng)(使用 docker history <image> 查看),接下來首先了解兩種最基礎(chǔ)的 Dockerfile 指令。

FROM 繼承自繼承鏡像

FROM <基礎(chǔ)鏡像>,基礎(chǔ)鏡像的選擇可分為三種

  • 直接在官方提供的鏡像作為基礎(chǔ)鏡像進(jìn)行定制

  • 如果沒有合適的官方基礎(chǔ)鏡像,使用更基礎(chǔ)的操作系統(tǒng)鏡像,不同操作系統(tǒng)的軟件庫為我們提供了廣闊的擴(kuò)展空間

  • 使用空白的基礎(chǔ)鏡像 FROM scratch

    對(duì)于 Linux 下靜態(tài)編譯的程序來說,并不需要有操作系統(tǒng)提供運(yùn)行時(shí)支持,所需的一切庫都已經(jīng)在可執(zhí)行文件里了,因此直接 FROM scratch 會(huì)讓鏡像體積更加小巧。

RUN 執(zhí)行命令

觀察 RUN 執(zhí)行命令的一個(gè)例子

FROM debian:stretch

RUN set -x; buildDeps='gcc libc6-dev make wget' \
&& apt-get update \
&& apt-get install -y $buildDeps \
&& wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" \
&& mkdir -p /usr/src/redis \
&& tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \
&& make -C /usr/src/redis \
&& make -C /usr/src/redis install \
&& rm -rf /var/lib/apt/lists/* \ # 清理 apt 緩存文件
&& rm redis.tar.gz \ # 清理下載文件:redis 源碼壓縮包 
&& rm -r /usr/src/redis \ # 清理 redis.tar.gz 的展開
&& apt-get purge -y --auto-remove $buildDeps # 清理 wget 以及依賴關(guān)系
  • RUN 有兩種格式:shell 格式 RUN <命令>;exec 格式 RUN ["可執(zhí)行文件", "參數(shù)1", "參數(shù)2"]
  • 精簡的鏡像:記得在 RUN 的最后清理所有無用的文件
  • trick of 清理:使用字符串 buildDeps='gcc libc6-dev make wget' 記錄軟件包,以及使用 $buildDeps 展開安裝軟件的依賴關(guān)系,最后干凈地清理了 wget

構(gòu)建鏡像

$ docker build [選項(xiàng)] <(上下文路徑|URL|-)>
傳遞上下文目錄

docker build .. 傳遞的是上下文 context 目錄。要理解這一點(diǎn),Docker 在運(yùn)行時(shí)分為 Docker 引擎和客戶端工具。Docker 引擎提供 Docker Remote API,而 docker 命令作為客戶端工具通過 API 和 Docker 引擎交互。因此表面上我們在 host 中執(zhí)行各種 docker 功能,實(shí)際上一切都是通過遠(yuǎn)程調(diào)用的形式在服務(wù)端(Docker 引擎)完成。

關(guān)于命令 docker build <path>,它首先指定 <path> 為上下文目錄,并將 <path> 下的所有文件打包交給 Docker 引擎。在此之后,諸如 docker copy, docker add 等指令通過指定基于上下文目錄的相對(duì)路徑移動(dòng)文件。

注意,Docker 會(huì)將上下文目錄中的所有文件打包交給 Docker 引擎,即 Docker 認(rèn)為上下文目錄中的所有文件都是構(gòu)建鏡像時(shí)需要的文件,因此我們在構(gòu)建上下文目錄時(shí)需要謹(jǐn)慎。建議做加法:每次定制鏡像時(shí),新建一個(gè)文件夾作為上下文目錄,然后僅將所需要的文件放進(jìn)去。此外為了方便維護(hù)等需求 .git, .log 文件也需要放入上下文目錄中,使用文本文件 .dockerignore 忽略它們。

指定 Dockerfile

前文提到過應(yīng)該使用 Dockerfile 定制鏡像,在 docker build 指令中,有不同的方式指定 Dockerfile 文件

  • 默認(rèn)情況下,將上下文目錄下的名為 Dockerfile 的文件作為 Dockerfile
  • 通過參數(shù) -f <dockerfile> 指定 Dockerfile
  • 從標(biāo)準(zhǔn)輸入中讀取 Dockerfile:如果標(biāo)準(zhǔn)輸入傳入文本文件,則將其視為 Dockerfile,例如
    • docker build - < Dockerfile
    • cat Dockerfile | docker build -

此外,docker build 也支持使用不同的方式指定上下文目錄

  • 指定 Host 中的本地路徑
  • 從 URL 構(gòu)建,包括 Git repo 和壓縮包(例如 http://<path>/context.tar.gz
  • 從標(biāo)準(zhǔn)輸入中讀取上下文壓縮包

Dockerfile 指令詳解

COPY 復(fù)制文件

  • COPY <path of file1> [<path of file2> ...] <path of directory> 將文件或目錄下的文件復(fù)制到鏡像的路徑下
  • 使用參數(shù) --chown=<user>:<group> 可修改文件所屬用戶和所屬組

ADD 更高級(jí)的復(fù)制文件

ADD 相比 COPY 增加了一些功能,但僅建議在需要自動(dòng)解壓縮的場合使用 ADD

CMD 容器啟動(dòng)命令

CMD 指令是用于指定默認(rèn)的容器啟動(dòng)命令(顯然 CMD 只能指定一次),例如 ubuntu 鏡像默認(rèn)的 CMD/bin/bash。

關(guān)于 CMD 指令格式,推薦使用 exec 格式。實(shí)際上,shell 格式會(huì)被自動(dòng)解釋為 exec 格式進(jìn)行執(zhí)行,下面二者等價(jià)

  • shell 格式:CMD echo <command>
  • exec 格式:CMD ["sh", "-c", "<command>"]

這是因?yàn)槟J(rèn)的 SHELL 指令是 ["/bin/sh", "-c"],具體見 SHELL 指令一節(jié)。

容器是進(jìn)程

容器的啟動(dòng)程序 CMD 就是容器的主進(jìn)程,容器就是為了主進(jìn)程而存在的,當(dāng)主進(jìn)程結(jié)束,容器就失去了存在的意義,從而退出。因此容器沒有后臺(tái)服務(wù),當(dāng)有類似需求時(shí)應(yīng)將對(duì)應(yīng)的后臺(tái)服務(wù)以前臺(tái)形式運(yùn)行,例如 CMD ["nginx", "-g", "daemon off;"]。

ENTRYPOINT 入口點(diǎn)

當(dāng)指定了 ENTRYPOINT 后,CMD 的含義就發(fā)生了改變,不再是直接的運(yùn)行其命令,而是將 CMD 的內(nèi)容作為參數(shù)傳給 ENTRYPOINT 指令:<ENTRYPOINT> "<CMD>"。它的設(shè)計(jì)目的是進(jìn)行靈活的定制,它主要應(yīng)用于

  • 讓鏡像在使用時(shí)像命令一樣添加可選參數(shù):將原本 CMD 的內(nèi)容傳給 ENTRYPOINT,在運(yùn)行容器時(shí)輸入可選參數(shù),它們會(huì)自動(dòng)傳給 CMD
  • 在執(zhí)行 CMD 前進(jìn)行一些準(zhǔn)備工作:使用 ENTRYPOINT 執(zhí)行一個(gè) .sh 腳本,它由兩部分組成,執(zhí)行 CMD 前的準(zhǔn)備工作以及在最后執(zhí)行 CMD

ENV 設(shè)置環(huán)境變量

  • 設(shè)置環(huán)境變量,它有兩種格式 ENV <key> <value>ENV <key>=<value>
  • 使用 $<key> 展開環(huán)境變量
  • <key> 一般使用大寫字母表示,提高可讀性

ARG 構(gòu)建參數(shù)

  • ARG 設(shè)置環(huán)境變量,并且它們在容器運(yùn)行時(shí)不存在。在構(gòu)建完鏡像后 ARG 環(huán)境變量仍是存在的,例如在 CMD 中使用 ARG 環(huán)境變量時(shí)就需要這一特性
  • 靈活的定制ARG 被設(shè)計(jì)用于定義參數(shù)名稱以及它的默認(rèn)值,進(jìn)而使得在不修改 Dockerfile 的情況下構(gòu)建出不同的鏡像。在 docker build 中添加參數(shù) --build-arg <參數(shù)名>=<值> 修改 ARG 環(huán)境變量的值,實(shí)現(xiàn)靈活的定制
  • ARG 指令有生效范圍,它的生效范圍由 FROM 指令截?cái)?/li>

VOLUME 構(gòu)建匿名卷

舉例說明,使用 VOLUME /data 可以將工作目錄下的 /data 目錄掛載為匿名卷。當(dāng)運(yùn)行容器時(shí),如果用戶

  • 不指定掛載,應(yīng)用可以將數(shù)據(jù)文件寫入 /data 以保證正常運(yùn)行,容器刪除以后匿名卷中的數(shù)據(jù)消失
  • 需要指定掛載,那么使用 docker run -v mydata:/datamydata 掛載到 /data 目錄下,保存了容器運(yùn)行時(shí)數(shù)據(jù)的修改

綜上,VOLUME 實(shí)現(xiàn)了一種 靈活的定制:在配置容器內(nèi)的應(yīng)用時(shí),將應(yīng)用程序的數(shù)據(jù)寫入目錄 /data 下。此時(shí)使用 VOLUME 構(gòu)建匿名卷可以使得鏡像的定制更加靈活,當(dāng)用戶

  • 不需要數(shù)據(jù)時(shí),不指定掛載,此時(shí)不需要的數(shù)據(jù)不會(huì)寫入容器存儲(chǔ)層
  • 需要數(shù)據(jù)時(shí),只需要將 mydata 掛載到 /data 目錄下就可以獲取數(shù)據(jù)

EXPOSE 暴露端口

要將 EXPOSE 和在運(yùn)行時(shí)使用 -p <宿主端口>:<容器端口> 區(qū)分開來

  • -p 將容器的對(duì)應(yīng)端口服務(wù)公開給外界訪問
  • EXPOSE 僅聲明容器打算使用什么端口,不進(jìn)行端口映射

WORKDIR 指定工作目錄

使用 WORKDIR <工作目錄路徑> 指定工作目錄,如果該目錄不存在,則自動(dòng)建立目錄并指定。以后各層(各指令)的當(dāng)前目錄就被改為指定的目錄,包括 WORKDIR 指令,例如

WORKDIR /a # 絕對(duì)路徑 /a
WORKDIR b # 目錄 /a 下相對(duì)路徑 b -> /a/b
WORKDIR c # 目錄 /a/b 下相對(duì)路徑 c -> /a/b/c

RUN pwd # RUN pwd 的工作目錄為 /a/b/c

USER 指定當(dāng)前用戶

  • USER 指令和 WORKDIR 相似,都是改變環(huán)境狀態(tài)并影響以后的層,改變以后各層執(zhí)行命令(例如 RUN, CMD, ENTRYPOINT)的用戶身份
  • 如果希望在執(zhí)行指令期間改變身份,使用 gosu

HEALTHCHECK 健康檢查

為什么要進(jìn)行健康檢查

如果沒有 HEALTHCHECK,容器只能根據(jù)主進(jìn)程是否退出來判斷容器狀態(tài)是否異常。如果容器內(nèi)的程序進(jìn)入死鎖或死循環(huán),該容器已經(jīng)無法提供正常服務(wù)了,但是容器無法退出。

如何防止容器進(jìn)入死循環(huán)而不退出

首先每隔一段時(shí)間執(zhí)行檢查程序檢查容器是否異常,但這是不夠的,因?yàn)闄z查程序可能無法正常啟動(dòng)或運(yùn)行(例如容器崩潰或檢查程序進(jìn)入死循環(huán));因此設(shè)置檢查程序運(yùn)行的超時(shí)時(shí)長;除此之外,一次失敗就認(rèn)為容器異常太過苛刻,允許多嘗試幾次。以上分別對(duì)應(yīng)了 HEALTHCHECK 的三個(gè)參數(shù):時(shí)間間隔 --interval,超時(shí)時(shí)長 --timeout,連續(xù)失敗次數(shù)--retries

容器的健康狀態(tài)變化

當(dāng)運(yùn)行鏡像后,容器最初的狀態(tài)為 starting;等待一段時(shí)間后迎來第一次 HEALTHCHECK,如果檢查成功則狀態(tài)變?yōu)?healthy;之后如果檢測失敗則狀態(tài)變?yōu)?unhealthy??梢杂?docker inspect 查看歷史健康狀態(tài),有助于維護(hù)和排障。

HEALTHCHECK 如何判斷健康狀態(tài)

關(guān)于在 HEALTHCHECK CMD <命令><命令> 返回的值,如果

  • 返回 0,認(rèn)為健康檢查結(jié)果為成功,即程序健康
  • 返回 1,認(rèn)為健康檢查結(jié)果為失敗,即程序異常
  • 返回 2,保留,或者說未失敗,但也不標(biāo)記為成功導(dǎo)致終止連續(xù)失敗次數(shù)的計(jì)數(shù)

奇怪的是,為什么返回 0 認(rèn)為成功,而返回 1 認(rèn)為失?。ㄍǔUJ(rèn)為 1 表示 true 而 0 表示 false)。這是因?yàn)?HEALTHCHECK 的目的是檢查是否有異常,因此返回 0 表示沒有異常,返回 1 表示有異常。

補(bǔ)充,進(jìn)程的 exit 返回值。當(dāng)進(jìn)程正常結(jié)束時(shí),執(zhí)行 exit 0 或者說 exit 返回值為 0;相反,當(dāng)進(jìn)程出現(xiàn)異常時(shí),為了讓使用該程序的人知道異常的發(fā)生,執(zhí)行 exit 1,即 exit 的返回值為 1。

ONBUILD 構(gòu)建基礎(chǔ)鏡像

ONBUILD <#1:其它 Dockerfile 指令> 只有以當(dāng)前鏡像為基礎(chǔ)鏡像,構(gòu)建下一級(jí)鏡像的時(shí)候 #1 才會(huì)被執(zhí)行。

為什么我們需要 ONBUILD
  • ****基礎(chǔ)鏡像****:當(dāng)需要定制許多類似的鏡像時(shí),使用基礎(chǔ)鏡像進(jìn)行繼承更易于維護(hù)
  • ****靈活的定制****:在基礎(chǔ)鏡像中使用 ONBUILD,可以將基礎(chǔ)鏡像寫成模板鏡像,更靈活地定制鏡像

關(guān)于書中的例子

FROM node:slim
RUN mkdir /app
WORKDIR /app
ONBUILD COPY ./package.json /app
ONBUILD RUN [ "npm", "install" ]
ONBUILD COPY . /app/
CMD [ "npm", "start" ]

ONBUILD COPY 后面的 ./package.json 以及 . 都是基于上下文目錄的相對(duì)路徑,因此這里的上下文目錄相當(dāng)于提供了定制鏡像的模板。當(dāng)我們使用以上例子作為基礎(chǔ)鏡像時(shí),提供不同的上下文目錄,就可以引用不同的 package.json 以及執(zhí)行不同的應(yīng)用。

LABEL 為鏡像添加元數(shù)據(jù)

  • LABEL 指令給鏡像提供元數(shù)據(jù) LABEL <key>=<value> <key>=<value> <key>=<value> ...
  • 補(bǔ)充:元數(shù)據(jù) 簡單來說就是描述數(shù)據(jù)的數(shù)據(jù)

SHELL 指令

SHELL ["executable", "parameters"] 指定默認(rèn)運(yùn)行的 Shell 以及運(yùn)行參數(shù),Linux 中默認(rèn)為 ["/bin/sh", "-c"]

參考文檔

Dockerfile 多階段構(gòu)建

單個(gè) Dockerfile

上一節(jié)中構(gòu)建鏡像時(shí)將所有的構(gòu)建過程(項(xiàng)目及其依賴庫的編譯、測試、打包等流程)包含在一個(gè) Dockerfile 中,在前面已提過:一個(gè)好的 Dockerfile 需要在繼續(xù)下一層之前清除所有不需要的資源,例如我們在 “利用 Dockerfile 定制鏡像” 這一節(jié)的 RUN 指令中手動(dòng)清除下載文件、源碼和 apt 緩存。

思考一下構(gòu)建鏡像時(shí)的需求:我們需要一個(gè)精簡的鏡像,這個(gè)鏡像將生成一個(gè)容器來運(yùn)行程序,因此我們的唯一要求就是良好地執(zhí)行程序。在 “利用 Dockerfile 定制鏡像” 這一節(jié)中,我們在做減法:手動(dòng)刪除不需要的資源以實(shí)現(xiàn)鏡像的精簡。但這樣做是麻煩的,并且也是不足夠精簡的:因?yàn)榫幾g的時(shí)候也生成了一些層,它們不是執(zhí)行程序時(shí)所需要的數(shù)據(jù)。

多個(gè) Dockerfile

相反,參考 軟件開發(fā)中常見的流程,也可以做加法:每一個(gè)階段結(jié)束后只傳遞必要的文件到下一階段,即編譯結(jié)束后僅將可執(zhí)行程序傳給下一階段。要實(shí)現(xiàn)這個(gè)目的,可以構(gòu)建兩個(gè)鏡像 image-buildimage-server。在書中 “分散到多個(gè) Dockerfile” 一節(jié)的具體例子中得知,我們需要一個(gè) .sh 腳本才能將兩個(gè)鏡像聯(lián)系起來,它需要做到以下幾件事

  • 根據(jù)鏡像 image-build 生成的容器獲得可執(zhí)行程序 app
  • app 復(fù)制到 host,再由 host 復(fù)制到 image-server
  • 運(yùn)行鏡像 image-server 為用戶提供服務(wù)
  • 執(zhí)行清理工作:刪除鏡像 image-build 以及在 host 中刪除 app

這樣確實(shí)構(gòu)建了精簡的鏡像,但也有一些缺點(diǎn)

  • 定制多個(gè)鏡像需要同時(shí)維護(hù)多個(gè) Dockerfile 文件
  • 還需要一個(gè)額外的 shell 腳本文件來執(zhí)行復(fù)制和清理工作

多階段構(gòu)建

為解決這個(gè)問題,Docker v17.05 開始支持多階段構(gòu)建 (multistage builds),使用兩個(gè)語法 FROM <image> as <name> 以及 COPY --from=<name> 就可以完成多階段構(gòu)建:在一個(gè)鏡像中可以使用多個(gè) FROM,每個(gè) FROM 對(duì)應(yīng)一個(gè)鏡像構(gòu)建的階段,并根據(jù) <name> 指定從哪個(gè)鏡像中獲取當(dāng)前階段需要的文件。

補(bǔ)充,也可以不指定標(biāo)簽 <name>,通過 --from=0 從上一個(gè)階段復(fù)制文件到當(dāng)前階段。但是為了提高可讀性,建議每個(gè)階段指定標(biāo)簽 <name> 進(jìn)行階段命名和復(fù)制文件。

構(gòu)建多種系統(tǒng)架構(gòu)支持的 Docker 鏡像

使用鏡像創(chuàng)建一個(gè)容器,該鏡像必須與 Docker 宿主機(jī)系統(tǒng)架構(gòu)一致,例如在 Linux x86_64 架構(gòu)的系統(tǒng)中只能創(chuàng)建 Linux x86_64 架構(gòu)的鏡像,同時(shí)也只能使用 Linux x86_64 的鏡像創(chuàng)建容器。

Windows、macOS 除外,其使用了 binfmt_misc 提供了多種架構(gòu)支持,在 Windows、macOS 系統(tǒng)上 (x86_64) 可以運(yùn)行 arm 等其他架構(gòu)的鏡像。

因此需要為不同的系統(tǒng)架構(gòu)準(zhǔn)備對(duì)應(yīng)的鏡像,但是要做到這一點(diǎn),需要將鏡像的名稱加長,例如從 python:3.10 加長為 python:3.10:linux_x86_64,這樣很不方便。我們希望僅提供 python:3.10,然后 Docker 引擎能夠根據(jù)系統(tǒng)架構(gòu)自動(dòng)獲取正確的鏡像。

使用 manifest 列表 (manifest list) 可以記錄<image name> 對(duì)應(yīng)的不同系統(tǒng)架構(gòu)鏡像的 digest 值。當(dāng) pull image 時(shí),Docker 引擎會(huì)先查看 <image name> 是否存在 manifest

  • 如果存在,查找對(duì)應(yīng)的 digest 值并獲取正確的鏡像
  • 如果不存在,直接獲取當(dāng)前鏡像

操作容器

Again: 容器是獨(dú)立運(yùn)行的一個(gè)或一組應(yīng)用,以及它們的運(yùn)行態(tài)環(huán)境

  • 啟動(dòng):docker run <image> 新建并啟動(dòng);docker container start <container> 啟動(dòng)運(yùn)行一個(gè)已終止(exited)的容器
  • -d 后臺(tái)運(yùn)行:輸出容器 ID,并且不會(huì)把輸出的結(jié)果輸出到 Host(或者說 stdout 重定向?yàn)槟硞€(gè) log 文件,使用 docker container log 查看)
  • docker container stop 終止容器(但不刪除),此外當(dāng)容器的主程序(CMDENTRYPOINT)終結(jié)時(shí)容器自動(dòng)終止
  • 進(jìn)入在后臺(tái)運(yùn)行的容器:docker attach <container> 將 stdin, stdout, stderr 等附著在容器上,此時(shí)若在 stdin 中輸入 exit,則容器終止;docker exec <container> CMD 在已運(yùn)行的容器中執(zhí)行一條命令,不創(chuàng)建和啟動(dòng)新的容器,在 stdin 中輸入 exit 容器也不會(huì)終止
  • docker export 導(dǎo)出容器快照到本地文件;docker import 從容器快照導(dǎo)入為鏡像,容器快照文件將丟棄所有的歷史記錄和元數(shù)據(jù)信息(即僅保存容器當(dāng)時(shí)的快照狀態(tài))
  • 刪除容器:docker container rm 刪除已終止容器;docker container rm -f 強(qiáng)制刪除容器,包括運(yùn)行中的容器;docker container prune 清理所有已終止容器;docker run --rm <image> 當(dāng)容器終止時(shí)自動(dòng)刪除

訪問倉庫

Todo

Notes

  • 操作系統(tǒng)與內(nèi)核
    • os: Operating System,操作系統(tǒng)
    • 內(nèi)核:參考 內(nèi)核 in wiki。內(nèi)核是現(xiàn)代操作系統(tǒng) (os) 中最基本的組成成分,它接受并管理軟件的 I/O 需求,并將這些需求轉(zhuǎn)化為指令交由 CPU, Memory 等硬件進(jìn)行處理,是軟件和硬件進(jìn)行交流的媒介
  • 無標(biāo)簽中間層鏡像出現(xiàn)的一種方式
    • 使用 base-image:latest 作為基礎(chǔ)層構(gòu)建 app-image
    • 更新鏡像 base-image:latest。此時(shí)舊版本的 base-image 不會(huì)在鏡像的更新中被刪除,因?yàn)樗?app-image 依賴,但是它已經(jīng)失去了標(biāo)簽,因此舊版本的 base-image 成為了無標(biāo)簽中間層鏡像
  • Bash 簡介
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評(píng)論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容