由於格式和圖片解析問題,為了更好體驗可前往 閲讀原文
前面我們學習了可以使用docker commit命令式構建新的鏡像,而此方式相對來説比較繁瑣且對於旁人來説內部都是黑箱操作,無法瞭解製作的具體細節。很有可能很長時間後製作者也會對其忘卻,且製作多鏡像時相同階段也無法共用已構建的產物,Dockerfile便可以完美解決這些問題
掃碼關注攻粽號,查看更多優質文章
概念
Dockerfile是用於定義Docker鏡像的文件,是一種純文本文件,其中包含了一系列的指令和配置信息,以描述如何構建和運行一個Docker容器。Dockerfile的內容可以基於現有的鏡像進行擴展和定製化,也可以自己編寫底層的操作系統配置和應用程序安裝腳本。
鏡像是分層構建的,每一層都對應了具體的構建指令,而Dockerfile中每一條指令也代表着每層的構建過程
作用
使用Dockerfile為鏡像的構建使得更加便利,其優勢大致如下:
- 自動化構建:使用Dockerfile可以自動構建Docker鏡像,避免了手動操作的繁瑣和錯誤
- 可重複性:通過Dockerfile可以確保每次構建出的鏡像都是相同的,避免了因為不同的環境或依賴出現的差異
- 可維護性:使用Dockerfile可以清晰地描述容器的配置和依賴,方便維護和升級
- 靈活性:Dockerfile提供了靈活的自定義選項,可以根據需要對容器進行個性化配置
- 可分享性:Dockerfile可以作為代碼一樣進行版本控制和分享,方便團隊協作和共享
初識Dockerfile
和大部分配置文件不同,Dockerfile其實就是個普通的文本文件,內部由一條條具體的構建指令組合而成,可以説Dockerfile是加強版的命令構建版本。以下是一個簡單的Dockerfile:
FROM nginx:alpine
ENV DESC="DOCKERFILE_DEMO"
WORKDIR /var/nginx/html
RUN echo $DESC
COPY . .
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
以上配置指定了構建以nginx:alpine鏡像為基礎,定義容器的工作目錄為/var/nginx/html,並將宿主機當前目錄下的文件複製到容器的工作目錄下,暴露端口80,並規定容器啓動後讓nginx以<u>前台</u>的形式運行。除此之外還定義了環境變量DESC的值為DOCKERFILE_DEMO,並打印其結果
接下來構建生成目標鏡像:nginx:dockerfile
➜ docker build -t nginx:dockerfile .
[+] Building 15.3s (9/9) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 182B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/nginx:alpine 15.3s
=> [1/4] FROM docker.io/library/nginx:alpine@sha256:eb05700fe7baa6890b74278e39b66b2ed1326831f9ec3ed4bdc6361a4ac2f333 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 182B 0.0s
=> CACHED [2/4] WORKDIR /var/nginx/html 0.0s
=> CACHED [3/4] RUN echo DOCKERFILE_DEMO 0.0s
=> [4/4] COPY . . 0.0s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:bcd9c0828a32f7f2816cc128ac1f1f7368066caf401c93057825d0f3b5df4864 0.0s
=> => naming to docker.io/library/nginx:dockerfile
查看並運行:
# 查看鏡像已經存在
➜ docker images | grep dockerfile
nginx dockerfile bcd9c0828a32 56 seconds ago 22.1MB
# 運行容器
➜ docker run --rm -p 8088:80 nginx:dockerfile
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf
10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
以上我們便使用Dockerfile配置文件定製nginx鏡像的完整流程,可以體驗出它相比命令式更加便捷,操作簡單透明,並且同階段的構建會被緩存起來複用,可以很好的提高構建效率,對於相同鏡像的共享也只需拿到配置文件即可製作完全一樣的鏡像。
相反不使用Dockerfile,需要我們一步一步使用命令進行構建,那樣會相對繁瑣,整體構建的操作也會是黑箱操作,無法滿足簡單、易維護、高效的特性。
瞭解了Dockerfile的便捷之處後,接下來學習詳細語法。
語法
FROM
初始化基礎的鏡像,在Dockerfile中必須的也是最開始就需要的,後面的命令需要基於最初的鏡像進行操作。FROM一般是最開始執行的,ARG命令是唯一可以在FROM前執行的命令。
語法:
FROM [--platform=<platform>] <image>[:tag] [AS <name>]
AS name:可選,指定構建階段的名稱,適用於多階段構建--platform:指定平台,如arm,amd等等tag:可選,不指定鏡像tag默認為latest
ARG
ARG 指令用於設置構建時的參數,這些參數可以在構建時傳遞給 Docker 以影響鏡像的構建,使用ARG可以簡化鏡像的構建過程、將容器的配置信息與容器的代碼分離,提高容器的可維護性
語法:
ARG <name>[=<default value>]
可以在構建時使用 --build-arg <key>=<value> 參數來傳遞一個新的值給指定的參數
例子:
# Dockerfile-ARG
FROM nginx:alpine
ARG VERSION=0.0.1
ENV VERSION=$VERSION
RUN echo $VERSION
構建鏡像重新賦值:從第9行看到已經被賦新值0.1.0
➜ docker build --build-arg VERSION=0.1.0 -t nginx:ARG --file Dockerfile-ARG .
[+] Building 15.9s (6/6) FINISHED
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load build definition from Dockerfile-ARG 0.0s
=> => transferring dockerfile: 119B 0.0s
=> [internal] load metadata for docker.io/library/nginx:alpine 15.6s
=> CACHED [1/2] FROM docker.io/library/nginx:alpine@sha256:eb05700fe7baa6890b74278e39b66b2ed1326831f9ec3ed4bdc6361a4ac2 0.0s
=> [2/2] RUN echo 0.1.0 0.3s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:c073ffd0029a4a3e64b68f2a9e450a4389d0b81fc7221bfcd727448be48bea36 0.0s
=> => naming to docker.io/library/nginx:ARG
然後可以通過docker inspect查看鏡像的變量:
➜ docker inspect nginx:ARG | grep -A 6 Env
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"NGINX_VERSION=1.21.5",
"NJS_VERSION=0.7.1",
"PKG_RELEASE=1",
"VERSION=0.1.0"
],
ENV
該指令用於設置環境變量,可以在容器運行時使用這些環境變量
語法:
ENV <key>=<value> ...
其中,<key> 表示要設置的環境變量的名稱,<value>表示要設置的環境變量的值。如果要設置多個環境變量,可以在<u>ENV指令中指定多個鍵值對,每個鍵值對用空格分隔,雙引號將會被移除如果沒有使用轉義</u>。Dockerfile中ENV定義的變量會持久保存在鏡像中,可以使用docker inspect image查看內部的環境變量,可以在<u>運行容器時</u>使用-e <key>=<value>來覆蓋原來的值,或設置新的環境變量
例子:
# Dockerfile
FROM nginx:alpine
# 設置時區
ENV TZ=Asia/Shanghai
ENV k1=v1
RUN echo $k1
構建鏡像:
docker build --file Dockerfile -t nginx:ENV .
[+] Building 15.7s (6/6) FINISHED
=> [internal] load build definition from Dockerfile-ENV 0.0s
=> => transferring dockerfile: 86B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/nginx:alpine 15.5s
=> CACHED [1/2] FROM docker.io/library/nginx:alpine@sha256:eb05700fe7baa6890b74278e39b66b2ed1326831f9ec3ed4bdc6361a4ac2 0.0s
=> [2/2] RUN echo v1 0.2s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:c6e1d107c5aa56a9c70fcd377c539e6ae28267e4f1660d9dd8ee5d7a40ac79e6 0.0s
=> => naming to docker.io/library/nginx:ENV
查看鏡像內部持久化的變量:第8行看到鏡像內部設置的k1變量
docker inspect nginx:ENV | grep -A 5 ENV
# ...
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"NGINX_VERSION=1.21.5",
"NJS_VERSION=0.7.1",
"PKG_RELEASE=1",
"k1=v1"
],
運行容器並覆蓋鏡像中默認的k1變量值:
docker run -d -p 8080:80 -e k1=v2 --name nginx-env nginx:ENV
# 查看容器 nginx-env 內部配置
docker inspect nginx-env | grep -A 6 Env
"Env": [
"k1=v2",
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"NGINX_VERSION=1.21.5",
"NJS_VERSION=0.7.1",
"PKG_RELEASE=1"
],
WORKDIR
WORKDIR 指令用於設置容器中的工作目錄。當我們在容器中運行命令時,它們將在 WORKDIR 指定的目錄中運行。如果在 Dockerfile 中沒有使用 WORKDIR 指令,則默認的工作目錄是根目錄(/)。
語法:
WORKDIR /path/to/workdir
例子:
FROM ubuntu:latest
# 指定容器工作目錄為/app
WORKDIR /app
# 將當前目錄下的內容複製到容器中的 /app 目錄中
COPY . .
RUN apt-get update && apt-get install -y python3
CMD ["python3", "app.py"]
LABEL
給鏡像添加標籤,label使用鍵值對的方式定義,有空格的值要用雙引號包括,儘量在一行中定義完所有的label,自己使用意義不大瞭解下即可
語法:
LABEL <key>=<value> <key>=<value> <key>=<value> ...
例子:
LABEL label1=xxxxx \
label2=123143 \
label3="sdfsdfs"
例子:
# 使用 arm架構的 nginx:1.15.3 的鏡像作為 build 階段的基礎鏡像
FROM --platform=arm nginx:1.15.3 AS build
RUN
在當前鏡像上層執行相關命令,並commited一個新的鏡像層提供給下一層階段使用。由於一條RUN指令便會產生新的鏡像層,使得構建體積變大,在生產構建時儘量採用 && 把RUN合併成一條指令執行,當然在測試構建時分開寫更容易排查構建錯誤
:::warning 注意
使用RUN執行命令時當前階段必須包含所執行的命令才可以,否則需要先安裝相關命令
:::
語法:
RUN <command>
RUN ["executable", "param1", "param2"...]
RUN支持兩種執行形式:<u>shell格式和exec格式</u>。shell格式就是在終端執行命令一樣,可以使用管道、重定向、變量的一些特性,而exec格式不會採用shell執行,而是直接在容器中執行,寫法就像一個一維數組,將每個參數放在每個位置即可,使用exec可以減少啓動新shell進程的開銷,且會作為第一進程執行。
例子:
RUN curl -I http://localhost
RUN ["curl", "-I", "http://localhost"]
COPY
複製本地文件或路徑到容器的文件系統,支持通配符匹配,不支持遠程url文件地址,不支持壓縮包自動解壓,相比ADD指令一般使用這個也夠用了
語法:
COPY [--chown=<user>:<group>] [--chmod=<perms>] <src>... <dest>
COPY [--chown=<user>:<group>] [--chmod=<perms>] ["<src>",... "<dest>"]
複製文件也支持設置指定的用户和權限組,同時支持--from=xx多階段產物的複製,這種特性對於減小鏡像構建體積作用很大,你可以從多階段構建瞭解其作用
例子:
# 絕對路徑
COPY *.md /app
COPY hom?.txt /app
# 相對路徑 work相對於 WORKDIR
COPY *.sh work
# 修改文件權限
COPY --chown=1 files* /somedir/
ADD
複製本地文件、路徑或<u>遠程文件</u>到容器文件系統,相比COPY可以遠程文件,並自動下載複製到目標位置。如果是壓縮文件,也會自動解壓。
語法:
ADD [--chown=<user>:<group>] [--chmod=<perms>] [--checksum=<checksum>] <src>... <dest>
ADD [--chown=<user>:<group>] [--chmod=<perms>] ["<src>",... "<dest>"]
例子:
ADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz /
ADD *.md /app
VOLUME
定義容器數據掛載點,在運行容器時使用-v或--mount參數指定宿主機目錄或文件掛載到容器的掛載點,實現容器與主機之間的數據共享,將容器的數據和應用程序分離開來,保持容器的數據獨立性,也便於在主機上修改相應的配置文件。
語法:
VOLUME /dir1 /dir2 /dir3 ...
VOLUME ["dir1", ...., "dir9999"]
VOLUME支持多個掛載點,在運行容器時通過-v進行掛載
例子:
# ...
# 創建文件夾 static
mkdir /static
# 將容器外部的html文件複製到 static中
COPY /html /static
# 創建掛載點
VOLUME /static
在運行時通過-v /somedir:/static可以覆蓋容器中的文件
EXPOSE
EXPOSE 指令用於聲明容器運行時要監聽的網絡端口號。該指令並不會在容器運行時自動將端口號映射到主機上,而是作為一種文檔形式的標記,用於告訴用户哪些端口可以被容器訪問。
:::warning 注意
EXPOSE指令並不會自動將容器的端口映射到主機上,如果需要將容器的端口映射到主機上,需要在運行容器時使用 -p 或 --publish 參數指定端口映射規則。同時,也需要確保容器運行的應用程序實際上監聽了指定的端口號,否則端口號將無法被訪問。
:::
語法:
EXPOSE <port> [<port>/<protocol>...]
其中,<port>表示要監聽的端口號,可以是1到65535之間的任意整數。如果要監聽多個端口號,可以在 EXPOSE 指令中指定多個端口號,每個端口號用空格分隔。如果要監聽的端口號是使用 TCP 或 UDP 協議,則可以在端口號後面添加 /tcp 或 /udp 表示協議類型。例如,EXPOSE 80/tcp 表示要監聽 TCP 協議的 80 端口。
例子:
FROM nginx
EXPOSE 80/tcp
指明該容器運行是會監聽容器中的80端口,然後在運行時可以通過-p和主機進行端口之間的映射:
# 將主機的 8088 端口映射到 容器的 80端口,就可以使用 宿主機的 IP:port 方式訪問到nginx
docker run -p 8088:80 nginx
CMD
CMD 指令用於指定容器啓動時默認要執行的命令。當我們使用docker run命令啓動容器時,如果沒有指定要執行的命令,那麼將會執行 CMD 中指定的命令。
:::warning 注意
Dockerfile 中只能使用一條 CMD 指令。如果在 Dockerfile 中使用了多個 CMD 指令,只有最後一個 CMD 指令會生效
:::
語法:
# exec執行
CMD ["executable","param1","param2"]
# 作為 ENTRYPOINT參數
CMD ["param1","param2"]
# shell 執行
CMD command param1 param2
CMD支持exec和shell兩種寫法,使用exec方式可以和ENTRYPOINT結合並作為其參數使用,強烈建議使用exec方式使用。
例子:
FROM busybox
# 使用ping localhost 3次後停止
CMD ["ping", "-c", "3", "localhost"]
構建鏡像:
docker build -t nginx:CMD --file Dockerfile-CMD .
運行容器:容器啓動會執行Dockerfile中指定的CMD命令,這裏執行ping localhost3次後停止
➜ docker run --rm nginx:CMD
PING localhost (127.0.0.1): 56 data bytes
64 bytes from 127.0.0.1: seq=0 ttl=64 time=0.064 ms
64 bytes from 127.0.0.1: seq=1 ttl=64 time=0.198 ms
64 bytes from 127.0.0.1: seq=2 ttl=64 time=0.196 ms
--- localhost ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.064/0.152/0.198 ms
運行容器時提供默認運行的命令來覆蓋默認的命令:這裏使用date命令代替了Dockerfile中CMD的命令
➜ docker run --rm nginx:CMD date
Tue Mar 28 07:23:07 UTC 2020
:::warning 注意
容器的運行必須提供默認的運行命令,可以運行時提供(CMD/ENTRYPOINT)或者構建時提供,沒有默認運行命令則會自動退出,低版本可能會報錯
:::
我們去掉以上的CMD命令重新構建鏡像:
docker build -t nginx:CMD --file Dockerfile-CMD .
然後運行容器:
➜ docker run --rm nginx:CMD
ENTRYPOINT
和CMD一樣ENTRYPOINT命令也是用於指定容器啓動時默認要執行的命令,也是隻能最後一個生效。若Dockerfile中CMD和ENTRYPOINT都存在,則ENTRYPOINT作為容器的運行命令,CMD將作為ENTRYPOINT的參數;若運行容器時再次指定了CMD的參數,將會覆蓋默認的CMD參數,但參數也會追加到ENTRYPOINT作為參數使用。
語法:
ENTRYPOINT ["executable", "param1", "param2"]
ENTRYPOINT command param1 param2
儘管ENTRYPOINT支持兩種形式的寫法,但是也是強烈建議使用exec形式,避免和CMD參數追加的問題。
若想覆蓋Dockerfile中的ENTRYPOINT時,可以在運行容器時通過--entrypoint來指定要運行的程序
例子:
FROM busybox
ENTRYPOINT ["ping"]
CMD ["localhost", "-c", "3"]
以上指定容器運行程序為ping,CMD作為ENTRYPOINT的參數,整體命令為ping localhost -c 3,ping完localhost 3次就會停止
運行容器:
➜ docker run --rm nginx:ENTRYPOINT
PING localhost (127.0.0.1): 56 data bytes
64 bytes from 127.0.0.1: seq=0 ttl=64 time=0.069 ms
64 bytes from 127.0.0.1: seq=1 ttl=64 time=0.256 ms
64 bytes from 127.0.0.1: seq=2 ttl=64 time=0.248 ms
--- localhost ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.069/0.191/0.256 ms
運行時通過傳參覆蓋掉默認的 CMD,並且會將動態添加的參數追加到 ENTRYPOINT:這裏改為ping qq.com 1次就停止
➜ docker run --rm nginx:ENTRYPOINT qq.com -c 1
PING qq.com (183.3.226.35): 56 data bytes
64 bytes from 183.3.226.35: seq=0 ttl=127 time=34.314 ms
--- qq.com ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max = 34.314/34.314/34.314 ms
通過運行容器時使用--entrypoint替換默認的ENTRYPOINT命令:
➜ docker run --rm --entrypoint "ls" nginx:ENTRYPOINT -ll
total 16
drwxr-xr-x 2 root root 12288 Dec 29 2021 bin
drwxr-xr-x 5 root root 340 Mar 28 10:07 dev
drwxr-xr-x 1 root root 66 Mar 28 10:07 etc
drwxr-xr-x 2 nobody nobody 6 Dec 29 2021 home
dr-xr-xr-x 294 root root 0 Mar 28 10:07 proc
drwx------ 2 root root 6 Dec 29 2021 root
dr-xr-xr-x 12 root root 0 Mar 28 10:07 sys
drwxrwxrwt 2 root root 6 Dec 29 2021 tmp
drwxr-xr-x 3 root root 18 Dec 29 2021 usr
drwxr-xr-x 4 root root 30 Dec 29 2021 var
ONBUILD
當使用包含 ONBUILD 指令的鏡像作為下一個 Dockerfile 的基礎鏡像時,ONBUILD 指令中的命令將自動執行,並將其結果添加到子鏡像中。這樣可以在構建鏡像時預先處理一些操作,以便在基於該鏡像構建更高級別的應用程序時,執行這些操作不必重複編寫代碼。
語法:
ONBUILD <INSTRUCTION>
例子:
FROM ubuntu:latest
RUN apt-get update && apt-get install -y python3
WORKDIR /app
ONBUILD COPY . .
ONBUILD RUN pip3 install -r requirements.txt
ONBUILD CMD ["python3", "app.py"]
當使用這個鏡像作為基礎鏡像創建一個新的 Dockerfile 時,Docker 將自動複製當前 Dockerfile 目錄中的所有文件到新的鏡像中,並安裝 requirements.txt 中指定的 Python 庫。然後,在新的鏡像中,CMD 指令將自動運行 Python 應用程序。
假如以上的鏡像名為myimage,使用其作為基礎鏡像再次構建新的鏡像:
FROM myimage
EXPOSE 5000
CMD ["python3", "run.py"]
在這個 Dockerfile 中,我們使用 myimage 鏡像作為基礎鏡像,並指定了一些額外的操作,例如暴露端口和運行run.py文件。由於 myimage 鏡像中包含 ONBUILD 指令,因此 Docker 將自動執行 COPY 和 RUN 命令,並安裝 requirements.txt 中指定的 Python 庫。然後,在新的鏡像中,CMD 指令將自動運行 Python 應用程序。
HEALTHCHECK
指定容器健康檢查的方式和頻率,以及容器健康檢查失敗時的行為,Docker 支持多種檢查方式,例如 HTTP 接口、TCP 端口、命令行輸出等。可以根據應用程序的特點和需求選擇適合的健康檢查方式。
語法:
HEALTHCHECK [OPTIONS] CMD command
# 禁止健康檢查
HEALTHCHECK NONE
例子:
FROM nginx
HEALTHCHECK --interval=10s --timeout=10s \
CMD curl -I http://localhost || exit 1
CMD ["nginx", "g", "daemon off;"]
在這個 Dockerfile 中,我們使用HEALTHCHECK指令設置了容器健康檢查的方式。具體來説,我們使用 curl 命令檢查容器中是否能夠訪問 http://localhost,並將檢查間隔設置為10s,檢查超時時間設置為10s。如果健康檢查失敗,則容器將退出。
在運行容器時,可以使用 --health-cmd、--health-interval、--health-timeout 等參數覆蓋 Dockerfile 中設置的健康檢查參數
⭐️⭐️⭐️⭐️⭐️
到這裏關於Dockerfile的語法知識已經講的差不多了,如果你還需要更多有關Dockerfile語法請閲讀官方文檔
注意事項
- 保留字指令必須大寫,後面要跟隨至少一個參數
- 指令從上到下執行
- 每條指令都會創建一個新的鏡像層並對鏡像進行提交
- docker會對前面相同的步驟進行構建緩存
構建上下文
Dockerfile 的構建上下文(Build Context)是指在構建 Docker 鏡像時,Docker 引擎需要讀取的文件和目錄的集合。
構建上下文通過docker build命令的-f和 . 參數指定。其中,-f 參數用於指定 Dockerfile 文件的路徑,. 參數用於指定構建上下文的路徑。例如,下面是一個使用 docker build 命令構建 Docker 鏡像的示例:
docker build -f Dockerfile -t myimage .
在這個命令中,我們使用 -f 參數指定 Dockerfile 文件的路徑為當前目錄下的 Dockerfile 文件,使用 . 參數指定構建上下文的路徑為當前目錄。Docker引擎將讀取當前目錄下的所有文件和目錄,並根據 Dockerfile 文件中的指令構建 Docker 鏡像。
需要注意的是,構建上下文中包含的所有文件和目錄都會被上傳到 Docker 引擎中進行構建。因此,構建上下文的大小會直接影響構建時間和構建過程中網絡傳輸的數據量。建議使用.dockerignore將構建上下文限制在必要的文件和目錄範圍內,避免上傳無用文件和目錄,以提高構建效率。
鏡像構建
:::warning 注意
規定在Dockerfile中第一條指令必須是FROM,作為製作鏡像的最基礎的鏡像層,基礎鏡像可以是空鏡像如:scratch 鏡像
:::
或許你已經注意到了,以上鏡像構建使用的是docker build命令,它就是用來構建鏡像的。<u>構建是在服務端(docker引擎)進行的,我們知道docker是典型的C/S架構,通過構建命令會將當前上下文的文件傳遞給服務端,然後進行構建,這種架構也天然的支持分佈式遠端構建,</u>這裏不用瞭解太多。
語法:
docker build [OPTIONS] PATH | URL | -
支持本地Dockerfile文件,還支持遠程URL,如果文件是個tar壓縮包,將會自動解壓。常用的參數如下:
構建上下文:指定上下文目錄,默認.,可以是任何文件系統路徑-f:指定Dockerfile文件路徑,其命名可以隨意,默認當前路徑下的Dockerfile文件-t:鏡像名及標籤,如 nginx:1.1--build-arg:Dockerfile構建arg參數--no-cache:禁用構建緩存,強制重新構建鏡像--pull:強制每次重新拉取遠程鏡像
在構建命令前加上 DOCKER_BUILDKIT=1 參數可以查看構建構建日誌和進度,便於調試
例子:
docker build -t nginx:1.1 -f ../Dockerfile . --pull --no-cache
更多關於docker build的用法可以使用docker build -h瞭解
多階段構建
到這裏你基本上已經會簡單的構建鏡像了,但往往構建的鏡像體積比較大,內部往往包含了一些無用的內容文件。如前端靜態項目可能需要用node進行打包,最後用nginx提供http服務,實際並不需要node環境及文件,但還是一併放進了鏡像中,體積會變大好多,這種情況可以使用多階段構建解決。
Dockerfile 多階段構建是一種優化 Docker 鏡像構建過程的技術。它可以在一個 Dockerfile 文件中定義多個階段,每個階段可以使用不同的基礎鏡像和構建步驟,並且可以挑取上一階段的產物,最終生成一個精簡的鏡像。
語法:
FROM xxx AS stageName
同樣的每個階段都是以FROM開始,使用AS可以為當前階段命名,名字需小寫
例子:
# build階段
FROM node:14.16.2 AS build
WORKDIR /app
COPY package.json .
COPY src .
RUN npm install && npm run build
# 最後一個階段
FROM nginx:alpine
COPY --from=build /app/dist /var/etc/nginx/html
CMD ["nginx", "g", "daemon off;"]
這個Dockerfile定義了兩個階段的構建,第一階段為build階段,使用node:14.16.2為基礎鏡像,安裝前端依賴並進行打包,產物為dist目錄;第二階段以nginx:alpine為基礎鏡像使用COPY --from=build將build階段的/app/dist產物複製到第二階段的nginx靜態目錄,最終設置容器的啓動命令。最終打包出來的鏡像只包含了最後一階段的文件,不會包含build階段的node內容,這樣會使鏡像的體積減小很多,也能在CI/CD中減小打包交付時間,提高效率
你可以嘗試將以上的Dockerfile分別用單階段和多階段進行構建,對比下兩者的大小,加深其作用印象
dockerignore
使用多階段構建是一種優化手段,另一個重要的概念便是dockerignore,它是用來做什麼的?前面也提到了docker是一個C/S架構,鏡像的構建是在Docker Engine構建的,構建時會將構建上下文的文件全部上傳到服務端,如果上傳了一些不必要的文件,就會影響總體耗時。
docker也支持像.gitignore類似的配置文件.dockerignore,在構建上傳文件時將會忽略掉.gitignore中匹配的文件或路徑,從而縮短文件上傳耗時。
.DS_Store
node_modules
*.md
*/dist
構建前端項目
本次就分別以前端的靜態項目和NodeJS項目為例子做個簡單的構建演示
靜態項目
靜態項目用node打包,最終以nginx發佈,源碼地址點擊這裏
這裏使用vite簡單創建一個demo,使用react+ts模板
npm create vite
編寫Dockerfile:
# docker發佈前端靜態項目簡單demo
FROM node:alpine as builder
WORKDIR /app
COPY . .
RUN npm install && npm run build
# 大小隻有20MB左右
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
CMD [ "nginx", "-g", "daemon off;" ]
構建鏡像:
docker build -t blog:v1 .
運行容器:
docker run --rm -p 8088:80 blog:v1
以本人為例以上鏡像大小隻有22.4MB,瀏覽器輸入IP:8088訪問頁面就可以看到靜態頁面了
NodeJS項目
NodeJS服務項目,使用pm2進行發佈,源碼地址點擊這裏。
初始化項目,並安裝相應的依賴:
npm init -y
# package.json文件
{
"name": "frontend-nodejs",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"dayjs": "^1.11.7",
"express": "^4.18.2"
}
}
創建server.js文件:
const express = require("express");
const app = express();
const dayjs = require("dayjs");
app.use((req, res) => {
console.log("【request】:" + req.url);
res.json({
code: 200,
date: dayjs().format("YYYY-MM-DD HH:mm:ss"),
path: req.url,
query: {
...req.query,
},
});
});
app.listen(10001, () => console.log("server is running on port 10001."));
創建pm2.json配置文件:
{
"apps": [
{
"name": "frontend-nodejs",
"script": "server.js",
"watch": false,
"instance": 3,
"autorestart": true,
"max_memory_restart": "1G",
"env": {
"NODE_ENV": "development"
},
"env_production": {
"NODE_ENV": "production"
}
}
]
}
Dockerfile配置文件:
FROM node:alpine
WORKDIR /app
COPY package.json .
RUN npm install && npm install pm2 -g
COPY . .
EXPOSE 10001
ENTRYPOINT ["pm2-runtime", "start", "pm2.json", "--env", "production"]
構建鏡像:
docker build -t blog:v2 .
運行容器:
docker run --rm -p 8088:10001 blog:v2
以本人為例以上鏡像大小有200MB大小,瀏覽器輸入IP:8088訪問頁面就可以頁面了,你還可以控制枱查看日誌輸出
構建優化
構建優化在生產環境中是非常必要的,常見的優化手段有:
- 多階段構建:使用多階段構建可以減小鏡像大小,提高構建速度。多階段構建可以將構建環境和運行環境分離,只保留必要的文件和組件。
- 使用 .dockerignore 文件:使用 .dockerignore 文件可以避免將不必要的文件和目錄複製到鏡像中,從而減小鏡像大小。
- 構建時使用緩存:使用 Dockerfile 構建緩存可以加速鏡像構建過程。例如,在 Dockerfile 中,可以將不經常更改的指令放在前面,從而利用緩存。
- 使用更小的基礎鏡像:使用更小的基礎鏡像可以減小鏡像大小,提高鏡像構建和部署的效率。
參考文檔
- https://docs.docker.com/engine/reference/builder
- https://docs.docker.com/engine/reference/commandline/build
- https://docs.docker.com/language/nodejs/build-images
- https://docs.docker.com/engine/reference/run
總結
Dockerfile 是構建和管理 Docker 鏡像的重要工具,使用它可以簡化應用程序的部署和管理,並提高可重複性、可自動化性、可定製性和可擴展性。