Docker 入門之DockerFile

本篇記錄一下DockerFile怎麼寫。

 

1. 關於dockerFile

Dockerfile 是一個文本文件,其內包含了一條條的 指令(Instruction)每一條指令構建一層,因此每一條指令的內容,就是描述該層應當如何構建。  因此不要寫太多指令,可以將多個指令寫在一條。

首先創建一個文件夾然後進入這個文件夾創建一個名字叫  Dockerfile 的文件。如下我創建一個docker 文件夾。

vim  Dockerfile   添加如下命令

FROM nginx
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html

docker  build  -t  nginx:v2  .   注意後面這個點   意思是構建當前目錄。

2.FROM 指定基礎鏡像

所謂定製鏡像,那一定是以一個鏡像爲基礎,在其上進行定製。就像我們之前運行了一個 nginx 鏡像的容器,再進行修改一樣,基礎鏡像是必須指定的。而 FROM 就是指定 基礎鏡像,因此一個 Dockerfile 中 FROM 是必備的指令,並且必須是第一條指令。

3.RUN 執行命令

RUN 指令是用來執行命令行命令的。由於命令行的強大能力,RUN 指令在定製鏡像時是最常用的指令之一。其格式有兩種:

  • shell 格式:RUN <命令>,就像直接在命令行中輸入的命令一樣。剛纔寫的 Dockerfile 中的 RUN 指令就是這種格式。
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
  • exec 格式:RUN ["可執行文件", "參數1", "參數2"],這更像是函數調用中的格式。

對於下面這種指令,實際上用了兩次RUN 那麼就build就會創建了兩層鏡像。

RUN apt-get update
RUN apt-get install -y gcc libc6-dev make wget

Dockerfile 中每一個指令都會建立一層,RUN 也不例外。每一個 RUN 的行爲,就和commit手工建立鏡像的過程一樣:新建立一層,在其上執行這些命令,執行結束後,commit 這一層的修改,構成新的鏡像。

Union FS 是有最大層數限制的,比如 AUFS,曾經是最大不得超過 42 層,現在是不得超過 127 層。

應該做出如下 && 修改    下面就是編譯、安裝 redis 可執行文件

FROM debian:stretch

RUN 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/* \
    && rm redis.tar.gz \
    && rm -r /usr/src/redis \
    && apt-get purge -y --auto-remove $buildDeps

僅僅使用一個 RUN 指令,並使用 && 將各個所需命令串聯起來。將多層簡化爲了 1 層。在撰寫 Dockerfile 的時候,要經常提醒自己,這並不是在寫 Shell 腳本,而是在定義每一層該如何構建。

並且,這裏爲了格式化還進行了換行。Dockerfile 支持 Shell 類的行尾添加 \ 的命令換行方式,以及行首 # 進行註釋的格式。良好的格式,比如換行、縮進、註釋等,會讓維護、排障更爲容易,這是一個比較好的習慣。

此外,還可以看到這一組命令的最後添加了清理工作的命令,刪除了爲了編譯構建所需要的軟件,清理了所有下載、展開的文件,並且還清理了 apt 緩存文件。這是很重要的一步,我們之前說過,鏡像是多層存儲,每一層的東西並不會在下一層被刪除,會一直跟隨着鏡像。因此鏡像構建時,一定要確保每一層只添加真正需要添加的東西,任何無關的東西都應該清理掉。

很多人初學 Docker 製作出了很臃腫的鏡像的原因之一,就是忘記了每一層構建的最後一定要清理掉無關文件。

4.構建

在Dockerfile文件所在目錄執行: 注意必須有Dockerfile文件才能 build  哪怕不叫這個名用 -f 參數指定dockerfile。

-t 參數意思是指定  鏡像的名字及標籤

從命令的輸出結果中,我們可以清晰的看到鏡像的構建過程。在step中,如同我們之前所說的那樣,RUN指令啓動了一個容器88baf8e5c516,執行了所要求的命令,並最後提交了這一層0475a6626276 ,隨後刪除了所用到的這個容器88baf8e5c516。

這裏我們使用了 docker build 命令進行鏡像構建。其格式爲:

docker build [選項] <上下文路徑/URL/->

5.構建鏡像上下文

docker build 命令最後有一個 .      . 表示當前目錄,而 Dockerfile 就在當前目錄,這是在指定 上下文路徑

(即在當前pwd的目錄下找dockerfile文件,並將當前目錄作爲上下文路徑)

首先我們要理解 docker build 的工作原理。Docker 在運行時分爲 Docker 引擎(也就是服務端守護進程)和客戶端工具。Docker 的引擎提供了一組 REST API,被稱爲 Docker Remote API,而如 docker 命令這樣的客戶端工具,則是通過這組 API 與 Docker 引擎交互,從而完成各種功能。因此,雖然表面上我們好像是在本機執行各種 docker 功能,但實際上,一切都是使用的遠程調用形式在服務端(Docker 引擎)完成。也因爲這種 C/S 設計,讓我們操作遠程服務器的 Docker 引擎變得輕而易舉。

當我們進行鏡像構建的時候,並非所有定製都會通過 RUN 指令完成,經常會需要將一些本地文件複製進鏡像,比如通過 COPY 指令、ADD 指令等。而 docker build 命令構建鏡像,其實並非在本地構建,而是在服務端,也就是 Docker 引擎中構建的。那麼在這種客戶端/服務端的架構中,如何才能讓服務端獲得本地文件呢?

這就引入了上下文的概念。當構建的時候,用戶會指定構建鏡像上下文的路徑,docker build 命令得知這個路徑後,會將路徑下的所有內容打包,然後上傳給 Docker 引擎。這樣 Docker 引擎收到這個上下文包後,展開就會獲得構建鏡像所需的一切文件。

如果在 Dockerfile 中這麼寫:

COPY ./package.json /app/

這並不是要複製執行 docker build 命令所在的目錄下的 package.json,也不是複製 Dockerfile 所在目錄下的 package.json,而是複製 上下文(context) 目錄下的 package.json(執行docker build 時已經指定好上下文路徑了 .   將上下文路徑下面的package.json文件複製到 docker 服務端中的 /app/目錄下 。後面會詳細記錄COPY目錄以及工作目錄的概念 )

因此,COPY 這類指令中的源文件的路徑都是相對路徑。 COPY ../package.json /app 或者 COPY /opt/xxxx /app 無法工作的原因是這些路徑已經超出了上下文的範圍,Docker 引擎無法獲得這些位置的文件。如果真的需要那些文件,應該將它們複製到上下文目錄中去。

現在就可以理解剛纔的命令 docker build -t nginx:v5 . 中的這個 .,實際上是在指定上下文的目錄,docker build 命令會將該目錄下的內容打包交給 Docker 引擎以幫助構建鏡像。

如果觀察 docker build 輸出,我們其實已經看到了這個發送上下文的過程:

docker build -t nginx:v5 .
Sending build context to Docker daemon  2.048kB

一般來說,應該會將 Dockerfile 置於一個空目錄下,或者項目根目錄下。如果該目錄下沒有所需文件,那麼應該把所需文件複製一份過來。如果目錄下有些東西確實不希望構建時傳給 Docker 引擎,那麼可以用 .gitignore 一樣的語法寫一個 .dockerignore,該文件是用於剔除不需要作爲上下文傳遞給 Docker 引擎的。

那麼爲什麼會有人誤以爲 . 是指定 Dockerfile 所在目錄呢?這是因爲在默認情況下,如果不額外指定 Dockerfile 的話,會將上下文目錄下的名爲 Dockerfile 的文件作爲 Dockerfile。

這只是默認行爲,實際上 Dockerfile 的文件名並不要求必須爲 Dockerfile,而且並不要求必須位於上下文目錄中,比如可以用 -f ../Dockerfile.php 參數指定某個文件作爲 Dockerfile

當然,一般大家習慣性的會使用默認的文件名 Dockerfile,以及會將其置於鏡像構建上下文目錄中。

docker build -f /path/to/a/Dockerfile .

構建的整體流程大概如下。

  • 執行 docker build -t <imageName:imageTag> . ;
  • Docker 客戶端會將構建命令後面指定的路徑(.)下的所有文件打包成一個 tar 包,發送給 Docker 服務端;
  • Docker 服務端收到客戶端發送的 tar 包,然後解壓,根據 Dockerfile 裏面的指令進行鏡像的分層構建;
  • 此時你的dockerFile有操作上下文目錄的可以正常操縱。

6.其它 docker build 的用法

直接用 Git repo 進行構建

或許你已經注意到了,docker build 還支持從 URL 構建,比如可以直接從 Git repo 中構建:

$ docker build https://github.com/twang2218/gitlab-ce-zh.git#:11.1

這行命令指定了構建所需的 Git repo,並且指定默認的 master 分支,構建目錄爲 /11.1/,然後 Docker 就會自己去 git clone 這個項目、切換到指定分支、並進入到指定目錄後開始構建。

用給定的 tar 壓縮包構建   docker build http://server/context.tar.gz

如果所給出的 URL 不是個 Git repo,而是個 tar 壓縮包 Docker 引擎會下載這個包,並解壓縮,以其作爲上下文來構建。

7. COPY複製文件

COPY 指令將從構建上下文目錄中 <源路徑> 的文件/目錄複製到新的一層的鏡像內的 <目標路徑> 位置。

  • COPY [--chown=<user>:<group>] <源路徑>... <目標路徑>
  • COPY [--chown=<user>:<group>] ["<源路徑1>",... "<目標路徑>"]
COPY package.json /usr/src/app/

<源路徑> 可以是多個,甚至可以是通配符,其通配符規則要滿足 Go 的 filepath.Match 規則,如:

COPY hom* /mydir/
COPY hom?.txt /mydir/

<目標路徑> 可以是容器內的絕對路徑,也可以是相對於工作目錄的相對路徑(工作目錄可以用 WORKDIR 指令來指定)。目標路徑不需要事先創建,如果目錄不存在會在複製文件前先行創建缺失目錄。

此外,還需要注意一點,使用 COPY 指令,源文件的各種元數據都會保留。比如讀、寫、執行權限、文件變更時間等。這個特性對於鏡像定製很有用。特別是構建相關文件都在使用 Git 進行管理的時候。

在使用該指令的時候還可以加上 --chown=<user>:<group> 選項來改變文件的所屬用戶及所屬組。

COPY --chown=55:mygroup files* /mydir/
COPY --chown=bin files* /mydir/
COPY --chown=1 files* /mydir/
COPY --chown=10:11 files* /mydir/

8.ADD 更高級的複製文件

ADD 指令和 COPY 的格式和性質基本一致。但是在 COPY 基礎上增加了一些功能。

ADD指令增加了 源路徑可以爲URL    並且對於

ADD   http://asd.com/a.tar.gz    /app

需要注意的是 / 這個是很敏感的。我第一次寫的  /app 然後進容器發現App創建成了文件而不是目錄。當寫成  /app/  就會創建成目錄了。  通過add 遠程下載的文件root文件系統文件權限會是 600  你可以chown像COPY一樣。

ADD http://mirrors.tuna.tsinghua.edu.cn/apache/tomcat/tomcat-8/v8.5.45/bin/apache-tomcat-8.5.45.tar.gz  /app/

ADD 指令會令鏡像構建緩存失效,從而可能會令鏡像構建變得比較緩慢。

個人不成熟的感覺使用 COPY就好了,除非你有需求一定要通過URL下載。或者解壓。(todo而且我如上這個ADD命令也沒有成功解壓,我去Tomcat官網取的html中寫的文件地址)

9.CMD

對於容器而言,其啓動程序就是容器應用進程,容器就是爲了主進程而存在的,主進程退出,容器就失去了存在的意義,從而退出,其它輔助進程不是它需要關心的東西。

所以你CMD的命令如果是一瞬間結束的話,docker  ps  是不會查詢到運行的容器的、

CMD 指令的格式和 RUN 相似,也是兩種格式:

  • shell 格式:CMD <命令>
  • exec 格式:CMD ["可執行文件", "參數1", "參數2"...]
  • 參數列表格式:CMD ["參數1", "參數2"...]。在指定了 ENTRYPOINT 指令後,用 CMD 指定具體的參數。

FROM  nginx

CMD   echo  $HOME 

可以看到有輸出但是直接就退出了,這是因爲CMD只能運行一個命令,而這個命令作爲主進程,主進程結束宣告退出。

在運行時可以指定新的命令來替代鏡像設置中的這個默認命令,比如,ubuntu 鏡像默認的 CMD 是 /bin/bash,如果我們直接 docker run -it ubuntu 的話,會直接進入 bash。我們也可以在運行時指定運行別的命令,如 docker run -it ubuntu cat /etc/os-release。這就是用 cat /etc/os-release 命令替換了默認的 /bin/bash 命令了,輸出了系統版本信息。

在指令格式上,一般推薦使用 exec 格式,這類格式在解析時會被解析爲 JSON 數組,因此一定要使用雙引號 ",而不要使用單引號。

如果使用 shell 格式的話,實際的命令會被包裝爲 sh -c 的參數的形式進行執行。比如:

CMD echo $HOME

在實際執行中,會將其變更爲:

CMD [ "sh", "-c", "echo $HOME" ]

提到 CMD 就不得不提容器中應用在前臺執行和後臺執行的問題。這是初學者常出現的一個混淆。

Docker 不是虛擬機,容器中的應用都應該以前臺執行,而不是像虛擬機、物理機裏面那樣,用 systemd 去啓動後臺服務,容器內沒有後臺服務的概念。

Docker 不是虛擬機,容器就是進程。既然是進程,那麼在啓動容器的時候,需要指定所運行的程序及參數。CMD 指令就是用於指定默認的容器主進程的啓動命令的。

之前我寫的nginx例子 實際上也是  CMD ["nginx", "-g", "daemon off;"]   來運行的。這是nginx鏡像的默認,我試着重寫CMD 輸出一句echo 就直接退出了。這個概念是很重要的,至少它矇蔽了我很久。

10.ENTRYPOINT 入口點

ENTRYPOINT 的格式和 RUN 指令格式一樣,分爲 exec 格式和 shell 格式。

ENTRYPOINT 的目的和 CMD 一樣,都是在指定容器啓動程序及參數。ENTRYPOINT 在運行時也可以替代,不過比 CMD 要略顯繁瑣,需要通過 docker run 的參數 --entrypoint 來指定。

當指定了 ENTRYPOINT 後,CMD 的含義就發生了改變,不再是直接的運行其命令,而是將 CMD 的內容作爲參數傳給 ENTRYPOINT 指令,換句話說實際執行時,將變爲:

<ENTRYPOINT> "<CMD>"

可能直接理解起來費點勁。實際上就是 ENREYPOINT這個指令將CMD 搞成動態的了。

FROM ubuntu:18.04
RUN apt-get update \
    && apt-get install -y curl \
    && rm -rf /var/lib/apt/lists/*
CMD [ "curl", "-s", "https://ip.cn" ]

這個加 -i  參數是沒用的。

上述這個命令是使用的CMD 沒有使用 ENTRYPONT  所以docker run  ubuntu:18.04的時候只能是輸出-s 參數的結果,不能再添加參數,因爲CMD已經將這個command寫死了。但是如果你想加個參數  -i  多查一些信息,你就可以使用ENTRYPOINT。下面展示一下ENTRYPOINT的執行結果。首先我就將CMD改成 ENTRYPOINT 其他沒變。

跟在鏡像名後面的是 command,運行時會替換 CMD 的默認值。因此這裏的 -i 替換了原來的 CMD,而不是添加在原來的 curl -s https://ip.cn 後面。而 -i 根本不是命令,所以自然找不到。

下面是運行的結果。

因爲當存在 ENTRYPOINT 後,CMD 的內容將會作爲參數傳給 ENTRYPOINT,而這裏 -i 就是新的 CMD,因此會作爲參數傳給 curl,從而達到了我們預期的效果。

ENTRYPOINT ["test.sh"]  這樣就是在運行這個鏡像時,預先執行了一個腳本test.sh  然後默認參數是AAA  run最後可以指定傳入的參數 比如改成BBB 那麼傳入到test.sh的參數就是BBB了,此時run成容器就會有不同的效果了。

CMD ['"AAA"]

因此ENTRYPOINT 可以將鏡像做成動態的,這樣可以用一個鏡像,通過run時傳遞的不同參數創建不同的容器。

11. ENV

Dockerfile的環境變量。這個指令很簡單,就是設置環境變量而已,無論是後面的其它指令,如 RUN,還是運行時的應用,都可以直接使用這裏定義的環境變量。

  • ENV <key> <value>
  • ENV <key1>=<value1> <key2>=<value2>...
ENV VERSION=1.0 DEBUG=on \
    NAME="Happy Feet"
#或者如下
ENV VERSION 1.0

下列指令可以支持環境變量展開: ADDCOPYENVEXPOSELABELUSERWORKDIRVOLUMESTOPSIGNALONBUILD

可以從這個指令列表裏感覺到,環境變量可以使用的地方很多,很強大。通過環境變量,我們可以讓一份 Dockerfile 製作更多的鏡像,只需使用不同的環境變量即可。

對於ARG 指令沒覺得很有用。請參考:https://yeasy.gitbooks.io/docker_practice/content/image/dockerfile/arg.html

12.EXPOSE

EXPOSE  8080

EXPOSE 指令是聲明運行時容器提供服務端口,這只是一個聲明,在運行時並不會因爲這個聲明應用就會開啓這個端口的服務。在 Dockerfile 中寫入這樣的聲明有兩個好處,一個是幫助鏡像使用者理解這個鏡像服務的守護端口,以方便配置映射;另一個用處則是在運行時使用隨機端口映射時,也就是 docker run -P時,會自動隨機映射 EXPOSE 的端口。

要將 EXPOSE 和在運行時使用 -p <宿主端口>:<容器端口> 區分開來。-p,是映射宿主端口和容器端口,換句話說,就是將容器的對應端口服務公開給外界訪問,而 EXPOSE 僅僅是聲明容器打算使用什麼端口而已,並不會自動在宿主進行端口映射。除非你使用docker run --net=host指定才能保證EXPOSE的發揮。

13.WORKDIR 指定工作目錄

格式爲 WORKDIR <工作目錄路徑>

使用 WORKDIR 指令可以來指定工作目錄(或者稱爲當前目錄),以後各層的當前目錄就被改爲指定的目錄,如該目錄不存在,WORKDIR 會幫你建立目錄。

這個工作目錄的作用是幫你操作一個文件時多個指令可以結合着用,否則每一層指令就是一層。每一層指令執行都是一個新的容器,然後執行你的指令,然後commit。這樣當兩條指令操作同一個文件時,你以爲能成功,實際上卻是在兩個容器中操作的。

寫一個Dockerfile 來驗證這個WORKDIR 指令的作用。

FROM ubuntu
WORKDIR /app
RUN echo hello > /app/a.txt
RUN echo zhangyong >> /app/a.txt
如上基於ubuntu鏡像,兩條RUN指令,第一條寫入hello  第二條追加zhangyong到 a.txt  如果不加WORKDIR,執行的時候會報

/bin/sh: 1: cannot create /app/a.txt: Directory nonexistent
當然  dockerfile中你這麼寫  RUN echo  hello > /tmp/a.txt  RUN echo zang >> /tmp/a.txt 也是ok的,雖然沒加WORKDIR。但是鏡像它是一層一層打的,後面那一層會基於上一層的RUN 。這麼搞的前提是你在ubuntu中存在這個目錄。

但是你要是RUN  cd /tmp    RUN echo  hello >a.txt  這麼搞就不行了。因爲第二次RUN指令時,相當於重新進入了容器,cd命令已經不復存在了。會將a.txt創建在  / 根目錄。

RUN  cd  /tmp  \

   &&  echo  hello >a.txt  這麼搞是可以的。因爲它操作在一層的容器中。  所以每條指令生成一個容器的概念很重要!

14.USER 指定當前用戶

格式:USER <用戶名>[:<用戶組>]

USER 指令和 WORKDIR 相似,都是改變環境狀態並影響以後的層。WORKDIR 是改變工作目錄,USER 則是改變之後層的執行 RUNCMD 以及 ENTRYPOINT 這類命令的身份。

當然,和 WORKDIR 一樣,USER 只是幫助你切換到指定用戶而已,這個用戶必須是事先建立好的,否則無法切換。

RUN groupadd -r redis && useradd -r -g redis redis
USER redis
RUN [ "redis-server" ]

在這種root文件系統中使用權限小的用戶看起來還是很有用的。但是我喜歡直接操作root  哈哈哈哈、

 

15.Volume

掛載數據卷。從宿主機映射文件/文件夾到容器裏。

volume  宿主機文件(文件夾):/容器文件(文件夾)

dockerfile的方式掛載數據卷的時候使用volume 關鍵字 但是隻能生成隨機的目錄,不能生成指定的目錄。

 

本篇博客摘自:https://yeasy.gitbooks.io/docker_practice/content

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章