引言
記錄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可以提供下載,如下圖:
鏡像(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 ID
是0299878039f0
.
我們嘗試一下如下的命令:
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ī)制能極大地縮小鏡像占用的硬盤空間,如下圖:
剛剛我們運(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 start
與docker 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)系:
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中包括的指令有FROM
、ARG
、ENV
、RUN
,下面的表格中我對其含義進(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)目地址,看下面的截圖也大概知道是什么樣子了:
可以看出這是一個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
感謝閱讀