Docker與Dockerfile極簡入門文檔

引言


記錄docker知識的一篇文檔。

docker的安裝


docker的安裝我就不多說,建議按照docker的官方安裝文檔一步一步來https://docs.docker.com/install/。需要注意的是,Docker的windows版本只支持Win10的專業(yè)版和企業(yè)版,像我們普通學(xué)生用的Win10家庭版它是不支持的,但是docker官方也提供一個docker toolbox(下載地址:https://docs.docker.com/toolbox/toolbox_install_windows/),通過它可以在windows上運(yùn)行docker,只不過這個docker toolbox是基于一個linux虛擬機(jī)運(yùn)行的,所以性能肯定是不如原生的好。

安裝好docker后,不要忘了用service docker start或者systemctl start docker(如果是centos7以上版本的話)啟動docker的服務(wù)

配置docker不需要sudo


剛裝好的docker每次使用docker命令都需要sudo,比較麻煩,可以通過以下命令省去sudo:

sudo usermod -aG docker 當(dāng)前用戶名

然后一定要記得重新登錄該用戶才能生效。

docker核心概念


要理解docker,最核心的是理解三個概念,分別是:倉庫(Registry)、鏡像(image)和容器(Container)。我們結(jié)合一些常見的Docker命令對這些概念進(jìn)行講解。

倉庫(Registry)

所謂倉庫,其實(shí)是個鏡像倉庫,里面有很多別人已經(jīng)打包好的鏡像,可以直接使用docker pull命令將倉庫中的鏡像拉到本地,默認(rèn)的倉庫Docker的官方倉庫Docker Hub Registry

因?yàn)閴Φ木壒剩俜絺}庫的速度會比較慢,可以配一個官方的中國加速鏡像,方法是:修改 /etc/docker/daemon.json,加上如下的鍵值:

{
  "registry-mirrors": ["https://registry.docker-cn.com"]
}

之后重啟docker服務(wù)即可生效。

使用以下命令可以去倉庫中搜索含有tutorial關(guān)鍵字的鏡像:

docker search tutorial

輸出如下:

NAME                                          DESCRIPTION                                     STARS               OFFICIAL            AUTOMATED
learn/tutorial                                                                                38                                      
georgeyord/reactjs-tutorial                   This is the backend of the React comment box…   5                                       [OK]
chris24walsh/flask-aws-tutorial               Runs a simple flask webapp demo, with the op…   1                                       [OK]
......(以下省略)

可以使用docker pull命令從倉庫中拉取剛剛搜索到的鏡像到本地,這里為了避免learn/tutorial那個鏡像被取消,我自己重建了一個一樣的,這里拉取我創(chuàng)建的那個鏡像:

docker pull ddfddf/tutorial

輸出如下:

Using default tag: latest
latest: Pulling from ddfddf/tutorial
271134aeb542: Pull complete 
483756d37259: Pull complete 
Digest: sha256:4615baf428c61a6feafd8f77f91e0cadc7f8f080710cc82362ac89a5f66a329c
Status: Downloaded newer image for ddfddf/tutorial:latest

ddfddf/tutorial為剛剛搜索到鏡像名,默認(rèn)情況下會拉去最新版本的鏡像,即ddfddf/tutorial:latest,你也可以用:指定某個特定版本的鏡像,比如docker pull ubuntu:14.04。其實(shí)冒號后面的這個東西的官方術(shù)語叫做tag,這個tag可以是一串?dāng)?shù)字(比如unbantu:14.04的tag是14.04),也可以是一個單詞(比如debian:stretch的tag是stretch),latest也只是一個普通的tag而已,只是當(dāng)docker pull不去專門指定tag時,默認(rèn)會去下載tag為latest的鏡像,通過在Docker Hub Registry中搜索你想要的鏡像,點(diǎn)進(jìn)去之后可以查看到有哪些tag可以提供下載,如下圖:

搜索鏡像的tag

鏡像(Image)

通過docker images命令可以看到本地已有的鏡像:

REPOSITORY                              TAG                 IMAGE ID            CREATED             SIZE
ddfddf/tutorial                         latest              48a0196af3c3        3 hours ago         140MB

每個鏡像都有一個IMAGE ID作為唯一標(biāo)識,可以看出這個鏡像的IMAGE ID為48a0196af3c3,使用鏡像的id可以將它刪除,命令如下:

docker rmi 鏡像id

這里先不要著急把剛剛拉下來的鏡像刪掉,待會實(shí)驗(yàn)還要用。

容器(Container)

然后使用docker run來運(yùn)行這個鏡像(運(yùn)行之后鏡像就變成一個容器):

docker run ddfddf/tutorial apt-get install -y ping

這個命令會在docker容器中執(zhí)行"apt-get install -y ping",也就是安裝一個ping命令,運(yùn)行完之后容器就自動退出了。之后使用docker ps命令可以查看所有當(dāng)前正在運(yùn)行的容器,輸出如下:

CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

空的,沒有任何一個容器正在運(yùn)行,之所以會這樣是因?yàn)閯倓側(cè)萜鲌?zhí)行完命令后就退出了。使用docker ps -a命令可以查看到所有容器,不管它正在運(yùn)行還是已經(jīng)退出,輸出如下:

CONTAINER ID        IMAGE                COMMAND                  CREATED             STATUS                      PORTS               NAMES
0299878039f0        ddfddf/tutorial       "apt-get install -y …"   1 minutes ago      Exited (0) 11 minutes ago                       peaceful_wozniak

每個容器也會有一個ID作為唯一標(biāo)識,從上面的輸出中可以看出CONTAINER ID0299878039f0.

我們嘗試一下如下的命令:

docker run ddfddf/tutorial ping www.baidu.com

這個命令會報如下的錯誤:

docker: Error response from daemon: OCI runtime create failed: container_linux.go:348: starting container process caused "exec: \"ping\": executable file not found in $PATH": unknown.

"ping": executable file not found in $PATH這句話中可以看出是因?yàn)閜ing命令沒有裝導(dǎo)致的,剛剛明明裝過ping命令了,為什么沒有呢?這是因?yàn)樵谌萜髦械男薷牟⒉粫绊戠R像,通過docker commit命令可以將這個容器提交成一個新的鏡像。命令如下:

docker commit 0299878039f0 ddfddf/ping

命令中0299878039f0是之前用docker ps -a查詢出來的容器ID。

試一下我們新提交的鏡像learn/ping:

docker run ddfddf/ping ping www.baidu.com

輸出如下:

PING www.a.shifen.com (111.13.100.92) 56(84) bytes of data.
64 bytes from promote.cache-dns.local (111.13.100.92): icmp_req=1 ttl=127 time=60.3 ms
64 bytes from promote.cache-dns.local (111.13.100.92): icmp_req=2 ttl=127 time=61.8 ms
......(以下省略)

可見ping命令可以使用了。在上面這個現(xiàn)象的背后,容器其實(shí)只是在鏡像上面添加一個可寫層,每當(dāng)對這個容器進(jìn)行修改都會在可寫層標(biāo)明與原本鏡像的不同之處,當(dāng)你使用docker commit命令時,只是提交了一個可寫層,將它變成一個不可寫的鏡像層,而這個新的鏡像和原本的鏡像共享原本鏡像的所有層,這就是所謂的docker分層機(jī)制,其實(shí)每個docker鏡像都是由好多層構(gòu)成的,這個機(jī)制能極大地縮小鏡像占用的硬盤空間,如下圖:

docker分層機(jī)制

剛剛我們運(yùn)行ping命令的時候都是在前臺運(yùn)行的,在它運(yùn)行的時候我們命令行做不了別的事情,只能眼睜睜地看它輸出,使用如下命令可以讓它后臺運(yùn)行:

docker run -d ddfddf/ping ping www.baidu.com

這個時候在控制臺就看不到它的輸出,使用docker ps可以看到這個容器的id,它是目前唯一正在運(yùn)行的容器,如果想看它的輸出的話可以使用docker logs 容器ID的方式查看。

使用docker stop命令可以停止這個后臺運(yùn)行的容器:

docker stop 2f371d67d92d

2f371d67d92d為這個容器在我的電腦上的ID

如果你想繼續(xù)讓這個容器運(yùn)行,可以使用docker start命令:

docker start 2f371d67d92d

這個容器又會從剛才停止的地方重新運(yùn)行,在初學(xué)的時候總是會搞混docker startdocker run命令,其實(shí)在理解了容器與鏡像的區(qū)別之后,就很容易理解了,docker run使用來啟動鏡像的,而docker start是用來重新啟動被停止的容器的。
只要鏡像被執(zhí)行了一次啊,都會生成一個新的容器,所以此時用docker -a會看到好多廢棄的容器,可以使用docker rm 容器ID的方式將他們刪除掉,也可以使用如下的小技巧一次性刪除所有容器:

docker rm $(docker ps -a -q)

不用擔(dān)心,這里只是刪除所有容器而已,鏡像還完好無損的保留在那里。

這里把容器和鏡像容易混淆的命令總結(jié)在了下表中:

刪除 啟動
鏡像 docker rmi docker run
容器 docker rm docker start

上文中一直使用容器ID來標(biāo)明容器,其實(shí)在啟動的時候可以通過--name選項(xiàng)來指定一個別名,之后就可以使用這個別名來代替容器ID使用,注意這個別名必須在本臺機(jī)器中唯一,示例如下:

docker run -d --name=test ddfddf/ping ping www.baidu.com
docker stop test
docker rm test

上面的命令中給容器起了一個叫做test的別名,然后使用它的別名將其停止與刪除。

如果有些情況下不得已要進(jìn)入容器內(nèi)部進(jìn)行操作的話,可以使用如下命令進(jìn)入容器內(nèi)部的shell:

docker run -d --name=test ddfddf/tutorial ping www.baidu.com
docker exec -ti test /bin/bash

docker exec用于在正在運(yùn)行的容器中執(zhí)行命令,-ti選項(xiàng)表示分配一個虛擬終端。注意docker exec只能在“正在運(yùn)行”的容器中執(zhí)行命令,所以在docker run的時候執(zhí)行ping就是為了讓這個容器一直運(yùn)行,而不是立即退出。

再聊聊倉庫(Registry)

之前講過,Docker Hub Registry是Docker的官方倉庫,其實(shí)這是一個有點(diǎn)類似于Github的地方,任何人都可以在上面提交與下載鏡像,我們可以先去上面注冊一個賬戶(地址:https://hub.docker.com/),注冊賬戶時會有谷歌的人機(jī)認(rèn)證系統(tǒng),所以需要一些科學(xué)上網(wǎng)技巧。
注冊完成后可以用如下命令在shell中登錄:

docker login -u 用戶名 -p 密碼

在將鏡像push到自己新建的賬戶之前,要用docker tag重命名一下,將鏡像命名你的用戶名/鏡像名這種形式,不然會push認(rèn)證不通過,代碼如下:

docker tag ddfddf/tutorial 你的用戶名/tutorial 

docker tag并沒有干太多的事情,只是創(chuàng)建了一個到ddfddf/tutorial鏡像的引用。之后就可以使用docker push命令將自己的鏡像推送到賬戶中去了,方便自己和別人的使用:

docker push 你的用戶名/tutorial 

搞定之后就可以使用以下命令登出了:

docker logout

登錄上Docker Hub,你將能看到剛剛你push上去的鏡像。

總結(jié)

最后我再總結(jié)一下Docker的三個核心概念間的關(guān)系:

docker核心概念及其關(guān)聯(lián)

Dockerfile


如果你想要從一個基礎(chǔ)鏡像開始建立一個自定義鏡像,可以選擇一步一步進(jìn)行構(gòu)建,也可以選擇寫一個配置文件,然后一條命令(docker build)完成構(gòu)建,顯然配置文件的方式可以更好地應(yīng)對需求的變更,這個配置文件就是Dockerfile。
學(xué)習(xí)Dockerfile的最好方式就是閱讀別人寫的Dockerfile,遇到不會的指令就查一查Dockerfile的文檔,文檔地址如下:
https://docs.docker.com/engine/reference/builder/
大家遇到不知道的指令多去里面翻一翻,下面我?guī)е蠹易x幾個開源Dockerfile,在讀的過程中學(xué)習(xí)相關(guān)知識。

第一個Dockerfile以阿里中間件大賽給的debian-jdk8鏡像為例,Dockerfile文件如下:

FROM debian:stretch

ARG DEBIAN_FRONTEND=noninteractive
ARG JAVA_VERSION=8
ARG JAVA_UPDATE=172
ARG JAVA_BUILD=11
ARG JAVA_PACKAGE=jdk
ARG JAVA_HASH=a58eab1ec242421181065cdc37240b08

ENV LANG C.UTF-8
ENV JAVA_HOME=/opt/jdk
ENV PATH=${PATH}:${JAVA_HOME}/bin

RUN set -ex \
 && apt-get update \
 && apt-get -y install ca-certificates wget unzip \
 && wget -q --header "Cookie: oraclelicense=accept-securebackup-cookie" \
         -O /tmp/java.tar.gz \
         http://download.oracle.com/otn-pub/java/jdk/${JAVA_VERSION}u${JAVA_UPDATE}-b${JAVA_BUILD}/${JAVA_HASH}/${JAVA_PACKAGE}-${JAVA_VERSION}u${JAVA_UPDATE}-linux-x64.tar.gz \
 && CHECKSUM=$(wget -q -O - https://www.oracle.com/webfolder/s/digest/${JAVA_VERSION}u${JAVA_UPDATE}checksum.html | grep -E "${JAVA_PACKAGE}-${JAVA_VERSION}u${JAVA_UPDATE}-linux-x64\.tar\.gz" | grep -Eo '(sha256: )[^<]+' | cut -d: -f2 | xargs) \
 && echo "${CHECKSUM}  /tmp/java.tar.gz" > /tmp/java.tar.gz.sha256 \
 && sha256sum -c /tmp/java.tar.gz.sha256 \
 && mkdir ${JAVA_HOME} \
 && tar -xzf /tmp/java.tar.gz -C ${JAVA_HOME} --strip-components=1 \
 && wget -q --header "Cookie: oraclelicense=accept-securebackup-cookie;" \
         -O /tmp/jce_policy.zip \
         http://download.oracle.com/otn-pub/java/jce/${JAVA_VERSION}/jce_policy-${JAVA_VERSION}.zip \
 && unzip -jo -d ${JAVA_HOME}/jre/lib/security /tmp/jce_policy.zip \
 && rm -rf ${JAVA_HOME}/jar/lib/security/README.txt \
       /var/lib/apt/lists/* \
       /tmp/* \
       /root/.wget-hsts

在解釋含義之前,我們先體驗(yàn)一下如何用Dockerfile打包一個鏡像,新建一個空目錄,假設(shè)就是~/debian-jdk8吧,cd進(jìn)這個目錄,新建一個Dockerfile,然后把上面的內(nèi)容copy進(jìn)去,然后執(zhí)行下面的命令:

docker build -t debian-jdk8:v1.0 .

其中-t debian-jdk8:v1.0表示打包的鏡像名為debian-jdk,tag為v1.0(前面說過,tag是可以任意命名的,不一定要是這種格式),注意命令的最后有一個.,這個表示打包的上下文(其實(shí)就是Dockerfile所在目錄)是在當(dāng)前目錄,然后目錄下的Dockerfile就會被編譯執(zhí)行。

執(zhí)行完畢后運(yùn)行docker images就會發(fā)現(xiàn)多了一個debian-jdk8鏡像。

下面來解釋一下Dockerfile的結(jié)構(gòu),那些字母全部大寫的每行第一個單詞都是Dockerfile的指令,可以看出這個Dockefile中包括的指令有FROMARGENVRUN,下面的表格中我對其含義進(jìn)行了解釋:

指令 含義解釋
FROM FROM debian:stretch表示以debian:stretch作為基礎(chǔ)鏡像進(jìn)行構(gòu)建
RUN 可以看出RUN后面跟的其實(shí)就是一些shell命令,通過&&將這些腳本連接在了一行執(zhí)行,這么做的原因是為了減少鏡像的層數(shù),每多一行RUN都會給鏡像增加一層,所以這里選擇將所有命令聯(lián)結(jié)在一起執(zhí)行以減少層數(shù)
ARG 特地將這個指令放在RUN之后講解,這個指令可以進(jìn)行一些宏定義,比如我定義ENV JAVA_HOME=/opt/jdk,之后RUN后面的shell命令中的${JAVA_HOME}都會被/opt/jdk代替
ENV 可以看出這個指令的作用是在shell中設(shè)置一些環(huán)境變量(其實(shí)就是export)

多階段構(gòu)建(multi-stage build)

這個功能是在Docker17.05版本及以上才出現(xiàn)的,主要用于解決docker鏡像構(gòu)建的中間冗余文件的處理,這里只做簡要介紹,更詳細(xì)的解釋請看官方文檔:
https://docs.docker.com/develop/develop-images/multistage-build/

有的時候會看見一些古怪,里面會有多個FROM指令或者FROM...AS...指令(這都是多階段構(gòu)建的標(biāo)志),比如如下的dockerfile(來源于阿里中間件大賽的agent-demp,完整項(xiàng)目請見:https://github.com/DQinYuan/Agent-demo):

# Builder container
FROM registry.cn-hangzhou.aliyuncs.com/aliware2018/services AS builder

COPY . /root/workspace/agent
WORKDIR /root/workspace/agent
RUN set -ex && mvn clean package


# Runner container
FROM registry.cn-hangzhou.aliyuncs.com/aliware2018/debian-jdk8

COPY --from=builder /root/workspace/services/mesh-provider/target/mesh-provider-1.0-SNAPSHOT.jar /root/dists/mesh-provider.jar
COPY --from=builder /root/workspace/services/mesh-consumer/target/mesh-consumer-1.0-SNAPSHOT.jar /root/dists/mesh-consumer.jar
COPY --from=builder /root/workspace/agent/mesh-agent/target/mesh-agent-1.0-SNAPSHOT.jar /root/dists/mesh-agent.jar

COPY --from=builder /usr/local/bin/docker-entrypoint.sh /usr/local/bin
COPY start-agent.sh /usr/local/bin

RUN set -ex && mkdir -p /root/logs

ENTRYPOINT ["docker-entrypoint.sh"]

如果你懶得點(diǎn)進(jìn)去我剛剛給出的完整項(xiàng)目地址,看下面的截圖也大概知道是什么樣子了:

agent-demo項(xiàng)目結(jié)構(gòu)

可以看出這是一個Java的Maven項(xiàng)目。

先解釋幾個之前沒解釋過的指令:

指令 含義解釋
FROM...AS... 這是Docker 17.05及以上版本新出來的指令,其實(shí)就是給這個階段的鏡像起個別名:FROM ...(基礎(chǔ)鏡像) AS ...(別名),在后面引用這個階段的鏡像時直接使用別名就可以了
COPY 顧名思義,就是用來來回復(fù)制文件的,COPY . /root/workspace/agent表示將當(dāng)前文件夾(.表示當(dāng)前文件夾,即Dockerfile所在文件夾)的所以文件拷貝到容器的/root/workspace/agent文件夾中。通過--from參數(shù)也可以從前面階段的鏡像中拷貝文件過來,比如--from=builder表示文件來源不是本地文件系統(tǒng),而是之前的別名為builder的容器
WORKDIR 在執(zhí)行RUN后面的shell命令前會先cd進(jìn)WORKDIR后面的目錄
ENTRYPOINT 這個參數(shù)表示鏡像的“入口”,鏡像打包完成之后,使用docker run命令運(yùn)行這個鏡像時,其實(shí)就是執(zhí)行這個ENTRYPOINT后面的可執(zhí)行文件(一般是一個shell腳本文件),也可以通過["可執(zhí)行文件", "參數(shù)1", "參數(shù)2"]這種方式來賦予可執(zhí)行文件的執(zhí)行參數(shù),這個“入口”執(zhí)行的工作目錄也是WORKDIR后面的那個目錄

知道上面這些指令后嘗試閱讀一下之前給出的Dockerfile。按照官方文檔的說法,多階段構(gòu)建中每一個FROM指令表示開始一個階段,第一階段從第一個FROM開始,在第二個FROM之前結(jié)束,片段如下:

# Builder container
FROM registry.cn-hangzhou.aliyuncs.com/aliware2018/services AS builder

COPY . /root/workspace/agent
WORKDIR /root/workspace/agent
RUN set -ex && mvn clean package

可以看出第一階段做的事情其實(shí)就是把項(xiàng)目拷貝到鏡像(COPY . /root/workspace/agent)中用maven打包(mvn clean package)一下。

第二階段的片段如下:

# Runner container
FROM registry.cn-hangzhou.aliyuncs.com/aliware2018/debian-jdk8

COPY --from=builder /root/workspace/services/mesh-provider/target/mesh-provider-1.0-SNAPSHOT.jar /root/dists/mesh-provider.jar
COPY --from=builder /root/workspace/services/mesh-consumer/target/mesh-consumer-1.0-SNAPSHOT.jar /root/dists/mesh-consumer.jar
COPY --from=builder /root/workspace/agent/mesh-agent/target/mesh-agent-1.0-SNAPSHOT.jar /root/dists/mesh-agent.jar

COPY --from=builder /usr/local/bin/docker-entrypoint.sh /usr/local/bin
COPY start-agent.sh /usr/local/bin

RUN set -ex && mkdir -p /root/logs

ENTRYPOINT ["docker-entrypoint.sh"]

這個階段的核心就是大量的COPY指令,從上一階段別名為builder的鏡像中拷貝一些執(zhí)行文件過來。

這兩個階段構(gòu)建結(jié)束后,第一階段生成的鏡像會被拋棄,只有最后一個階段構(gòu)建的鏡像會被構(gòu)建成為最終的鏡像。

因?yàn)樵诘诙A段中只是拷貝了一些必要的執(zhí)行文件過去,第一階段在maven打包過程中產(chǎn)生的大量冗余文件就被拋棄了,所以說善于使用多階段構(gòu)建可以大大減小鏡像的大小。

更多的關(guān)于Dockerfile的最佳實(shí)踐請參考官網(wǎng)的一篇文章:
https://docs.docker.com/develop/develop-images/dockerfile_best-practices/

END


感謝閱讀

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容