Docker 映象的製作和使用
映象 Layer(層)
映象裡的內容是按「層」來組織的,「層」可以複用,一個完整的映象也可以看做是一個「層」。多個「層」疊加在一起就形成了一個新的映象,這個映象也可以作為別的映象的基礎「層」進行更加複雜的映象構建。下圖展示了一個映象的內部結構。
這個目標映象使用 Debian 映象作為基礎映象開始構建,也就是說 Debian 映象是目標映象的第一「層」;往上的兩層分別使用了 ADD
指令將 emacs
和 apache
新增到了目標映象中,每一個 ADD
指令都將產生新的一個「層」,最後這個目標映象就是一個擁有三「層」的映象。每新增一「層」時,將要生成的這一「層」映象都會預設使用上一步構建出的「層」作為自己的基礎映象,上圖中的箭頭表示的就是這種引用關係。
所以,「層」和「映象」是等價的,當這一「層」之上沒有其他「層」時,我們就可以將這一「層」及其下面的所有「層」合起來稱作一個「映象」。如果這一「層」只是在構建映象過程中生成的一個「中間層」,即這一「層」不會被用來啟動容器,那麼就可以稱作「層」。總的來說,能用來啟動容器的就稱作「映象」,其他都稱作「層」。
製作映象
製作映象的過程和在作業系統上安裝軟體的過程幾乎是完全一樣的,唯一的區別是製作映象需要使用 Dockerfile 檔案來編寫要執行的操作。請注意,Dockerfile 裡的所有指令,除了 CMD
和 ENTRYPOINT
,都是給 Docker 引擎執行的,目的是製作出目標映象,這些指令不是
下面的例子將一步步演示從 0 開始製作一個在 CentOS7.2 作業系統上安裝了 openjdk
和 nginx
並執行一個 Java 應用程式的映象,這個過程同時也將體現映象分層複用的思想。
映象分層複用思想
Dockerfile 中的每個指令都會生成一個「層」,最終的目標映象就是由多個「層」組成的。如果製作 B 映象的 Dockerfile 中存在某個指令與製作 A 映象的 Dockerfile 中的某個指令完全一致,那麼製作 B 映象時就會複用製作 A 映象時生成的「中間層」,而不會再去建立一個新的「層」,這就是「映象分層複用」思想。
第一步,製作一個支援中文的 CentOS 映象
官方為我們提供了 Linux 各種發行版的映象,我們日常的所有映象構建都是基於這些映象來完成的。由於官方的 CentOS 映象並不支援中文字符集,所以我們需要先製作一個支援中文的映象出來
編寫 Dockerfile
FROM centos:7.2.1511
RUN yum install -y telnet kde-l10n-Chinese net-tools vim inetutils-ping unzip \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& localedef -c -f UTF-8 -i zh_CN zh_CN.utf8 \
&& yum clean all
ENV LC_ALL "zh_CN.UTF-8"
CMD ["/bin/bash"]
FROM
指令表示我們從官方提供的 CentOS 映象開始構建我們自己的映象。centos
是映象的名稱,7.2.1511
是映象的版本。
RUN
指令表示在構建映象時我們要執行的 shell 命令。之前的 FROM
指令相當於給了我們一個乾淨的作業系統,我們在這個系統上要執行的各種操作,如安裝軟體、建立目錄等就都要書寫在這個 RUN 指令之後。理論上你可以對每一個要執行的 shell 命令都使用一個 RUN 指令,比如我們將上面的 RUN 指令改寫為下面的樣子:
RUN yum install -y telnet kde-l10n-Chinese net-tools vim inetutils-ping unzip
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
RUN localedef -c -f UTF-8 -i zh_CN zh_CN.utf8
RUN yum clean all
這樣編寫出來的 Dockerfile 檔案是沒有任何問題的,映象最終也能夠製作成功,但是這並不切合映象分層複用的思想,因為我們幾乎不會用到上面單個 RUN 指令生成的「中間層」。這樣編寫指令只會增加磁碟空間的佔用,也讓 Dockerfile 變得非常臃腫。
需要特別注意的是,如果 RUN 指令中有安裝軟體的操作,那就一定要在 RUN 指令的最後清除掉軟體倉庫的快取,這樣可以有效的瘦身映象。
ENV
指令表示在構建映象時要在作業系統中設定的環境變數。這個指令每次只能設定一個環境變數,如果需要設定多個環境變數,則需要編寫多個 ENV 指令。
CMD
指令表示的是容器啟動時要執行的操作,通常會設定為應用程式的啟動指令碼,這個指令一定是出現在 Dockerfile 的最後。被指定的操作一定是能夠掛起一個程序的操作,否則容器啟動並執行完這個操作後就會退出。
構建映象
構建映象時需要告訴 Docker 引擎 Dockerfile 的位置、映象的名稱和構建位置三個資訊,下面是一個簡單的映象構建命令:
docker build -t myorg/centos:7.2 .
由於我們沒有使用 -f
引數指定 Dockerfile 檔案的位置,Docker 引擎將預設使用當前目錄下的 Dockerfile 檔案進行構建。映象的名稱為 myorg/centos:7.2
,其中 myorg
是組織名,但不是必須的。如果你需要將映象釋出到公網去,或者儘可能的避免和別人製作的映象發生衝突,通常還是建議加上組織名。最後的 .
表示構建位置在當前目錄。通常建議將 Dockerfile 和構建所需要的檔案放在一個目錄下,然後在這個目錄下執行構建。由於在構建開始前 Docker 引擎會讀取構建目錄下的所有檔案,為了提高構建速度,請不要將構建中不需要的檔案放到構建目錄下。下面是執行上述構建命令後的輸出,其中 shell 命令的輸出內容被裁減掉了:
Sending build context to Docker daemon 2.048kB
Step 1/4 : FROM centos:7.2.1511
---> 4cbf48630b46
Step 2/4 : RUN yum install -y telnet kde-l10n-Chinese net-tools vim inetutils-ping unzip && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && localedef -c -f UTF-8 -i zh_CN zh_CN.utf8 && yum clean all
---> Running in 724ac4950fc9
// shell 命令執行的輸出
---> 2703f1dd2526
Removing intermediate container 724ac4950fc9
Step 3/4 : ENV LC_ALL "zh_CN.UTF-8"
---> Running in 2f49ec282e95
---> f6919bceb45f
Removing intermediate container 2f49ec282e95
Step 4/4 : CMD /bin/bash
---> Running in aea69f51eefd
---> e8e1d37c61a1
Removing intermediate container aea69f51eefd
Successfully built e8e1d37c61a1
Successfully tagged myorg/centos:7.2
Sending build context to Docker daemon 2.048kB
表示在構建開始前,Docker 引擎讀取到了構建目錄下共有 2.048k 的檔案。這裡也印證了前文提到的不要將構建無關的檔案放到構建目錄下,否則會影響構建速度的結論。
Step 1/4 : FROM centos:7.2.1511
表示構建映象的第一步是使用 centos:7.2.1511
映象作為基礎映象,由於沒有任何變更操作,所以下面輸出的 4cbf48630b46
就是原本這個 CentOS 映象的 ID。如果在構建時本地沒有 centos:7.2.1511
這個映象,那麼這裡還將輸出 Docker 引擎從映象倉庫拉取這個映象的資訊。
Step 2/4 ...
表示構建映象的第二步是執行這些 shell 命令。其下的 Running in 724ac4950fc9
表示 Docker 引擎啟動了一個 ID 為 724ac4950fc9
的容器並在容器內部執行這些操作。接著 ---> 2703f1dd2526
表示這些 shell 命令執行完成後生成了 ID 為 2703f1dd2526
的中間「層」。最後的 Removing intermediate container 724ac4950fc9
表示當中間「層」生成完成後,刪除了剛才使用的容器。
Step 3/4 ...
和 Step 4/4 …
表示的意義和 Step 2/4
類似,這裡不再贅述。
Successfully built e8e1d37c61a1
表示最終構建出來的映象 ID 是 e8e1d37c61a1
。
Successfully tagged myorg/centos:7.2
表示把映象的名稱設定為了構建命令中指定的 myorg/centos:7.2
。
檢視映象
構建完成的映象會直接被 Docker 管理,而不會給我們生成一個檔案。使用 docker images
命令可以檢視到當前已有的映象,如下所示:
REPOSITORY TAG IMAGE ID CREATED SIZE
myorg/centos 7.2 e8e1d37c61a1 14 minutes ago 272MB
centos 7.2.1511 4cbf48630b46 3 months ago 195MB
可以看到第一個映象就是剛才建立的映象,大小是 272MB,比原本官方的映象多了 77MB。在製作這個映象的過程中還生成了 2 箇中間「層」,我們可以使用 docker images -a
命令看到它們。
REPOSITORY TAG IMAGE ID CREATED SIZE
myorg/centos 7.2 e8e1d37c61a1 18 minutes ago 272MB
<none> <none> f6919bceb45f 18 minutes ago 272MB
<none> <none> 2703f1dd2526 18 minutes ago 272MB
centos 7.2.1511 4cbf48630b46 3 months ago 195MB
由於中間「層」沒有名字,所以名稱和 TAG 都顯示為 <none>
。你可以嘗試使用 docker rmi f6919bceb45f
命令來刪除一箇中間「層」,你會得到一個如下的錯誤提示:
Error response from daemon: conflict: unable to delete f6919bceb45f (cannot be forced) - image has dependent child images
從上面構建映象的輸出可以看出,f6919bceb45f
這一「層」,即 Step 3/4
這一步生成的「層」被 e8e1d37c61a1
所引用,所以這裡不能夠直接刪除這個中間「層」。回想一下前文的那張映象層次圖中的引用箭頭,這就是「層」與「層」直接的引用關係。
使用映象
使用映象就是利用製作好的映象來啟動容器,如下面的命令:
docker run --name mycontainer myorg/centos:7.2
docker run
是啟動容器的命令,--name
用於指定容器的名稱,最後面是啟動容器所使用的映象名稱。命令執行完成後使用 docker ps
檢視執行中的容器,這時你會發現並沒有任何容器出現;再使用 docker ps -a
檢視所有容器將會有如下資訊:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b4cae07cb40c myorg/centos:7.2 "/bin/bash" Less than a second ago Exited (0) 1 second ago mycontainer
可以看到剛才啟動的 mycontainer
容器的狀態(STATUS)為 Exited
,這表示容器已經退出了,即沒有在執行狀態,那為什麼容器啟動後就會退出呢?前文已經提到過,Dockerfile 中 CMD
指令和 ENTRYPOINT
指令指定的是容器啟動後的操作,這個操作必須要能夠掛起一個程序,否則容器啟動完成後就會退出。檢視剛才編寫的 Dockerfile 可以看到,CMD
指令指定的命令是 /bin/bash
,這個命令並不會掛起程序。
這個問題可以通過增加 docker run
命令的引數來解決,如下面的命令:
docker run -d -i --name mycontainer2 myorg/centos:7.2
命令執行完成後再次執行 docker ps
檢視執行中的容器就能看到這個名為 mycontainer2
的容器了。其中 -d
引數表示讓容器在後臺執行,-i
引數表示保持標準輸入開啟,這樣容器就不會在啟動完成後立即退出了。
第二步,製作一個帶有 openjdk 和 nginx 的映象
這一次的映象構建使用我們第一步構建出的映象作為基礎映象。
編寫 Dockerfile
FROM myorg/centos:7.2
RUN echo "[nginx]" >> /etc/yum.repos.d/nginx.repo \
&& echo "name=nginx repo" >> /etc/yum.repos.d/nginx.repo \
&& echo "baseurl=http://nginx.org/packages/centos/7/\$basearch/" >> /etc/yum.repos.d/nginx.repo \
&& echo "gpgcheck=0" >> /etc/yum.repos.d/nginx.repo \
&& echo "enabled=1" >> /etc/yum.repos.d/nginx.repo \
&& yum makecache \
&& rpm --rebuilddb \
&& yum install -y java-1.8.0-openjdk-devel.x86_64 nginx \
&& yum clean all
ENV JAVA_HOME /usr
CMD ["/bin/bash"]
這一份 Dockerfile 中的指令在上文中已經解釋過了,這裡不再贅述。注意 RUN
指令後 shell 命令多行排版的方式是以 \
結尾,以 &&
開頭。
構建映象
這一次構建的映象命名為 myorg/base:centos7.2.x64-ngx-java8
。在為映象命名時,應當在名稱和版本兩個部分充分描述這個映象,這樣便於快速瞭解映象的功能,構建命令如下:
docker build -t myorg/base:centos7.2.x64-ngx-java8 .
第三步,製作可部署的應用系統映象
經過第二步的構建,我們已經擁有了一個帶有 Java 執行環境和 Nginx 的映象,這一步就是要將應用系統也放入映象中,並通過指令讓容器啟動後就去執行應用系統啟動操作。
編寫 Dockerfile
FROM myorg/base:centos7.2.x64-ngx-java8
COPY login-deploy-1.0 /home/admin/login/
COPY login-ui /home/admin/login-ui/
COPY nginx.conf /etc/nginx/nginx.conf
COPY entrypoint.sh /home/admin/entrypoint.sh
RUN chmod +x /home/admin/entrypoint.sh
EXPOSE 80
VOLUME ["/home/admin/logs"]
ENTRYPOINT ["sh", "/home/admin/entrypoint.sh"]
COPY
指令用於將檔案或目錄拷貝到映象中指定的位置。如果拷貝的是一個目錄,指定映象中的位置時通常建議在最後加上 /
,以避免將目錄拷貝成檔案的情況。與 COPY
相似的指令是 ADD
,後者可以將一個壓縮檔案拷貝到映象中並自動解壓。由於 ADD
的自動解壓功能可能導致解壓出來的檔案的名稱不可控,所以通常是推薦使用 COPY
命令來完成拷貝工作,壓縮檔案在拷貝前手動解壓即可。
EXPOSE
指令用於指定使用這個映象啟動的容器可以通過哪個埠和外界進行通訊。換言之,只有 EXPOSE 指令指定的端口才能夠和宿主機上的埠做對映。比如這裡 EXPOSE 了 80 埠,那麼在啟動容器的時候就可以將宿主機的 8888 埠對映到容器的 80 埠,這樣外界訪問宿主機的 8888 埠就相當於訪問容器內部的 80 埠。
VOLUME
指定用於指定容器資料的掛載點。容器在執行時會產生各種資料,由於容器和宿主機天然是隔離的,所以在宿主機上並不能看到容器內的資料,當容器被銷燬時,這些資料也會隨之銷燬,無法找回。為了將容器內產生的資料存放到宿主機上,我們可以在製作映象時指定某些目錄為掛載點,然後將容器執行時產生的資料指定輸出到這些目錄中。當容器啟動時,Docker 就會自動在宿主機上建立資料捲來對映掛載點,這樣容器中產生的資料就會儲存在宿主機上的這個資料卷內。資料卷有自己獨立的生命週期,即使刪掉了容器,資料卷也還會存在。
Docker 會使用隨機 ID 給資料卷命名,這非常不便於管理。在啟動 Docker 容器時可以使用 -v
引數來指定資料卷的名稱,如 -v myappdata:/home/admin/logs
。這樣當我們啟動容器時,Docker 就會在宿主機上建立名為 myappdata
的資料卷。檢視資料卷使用命令 docker volume ls
。
ENTRYPOINT
指令的作用和前文介紹的 CMD
指令的作用是基本一致的。區別在於前者指定的命令不會被覆蓋,而後者指定的命令會被啟動容器時附帶的命令所覆蓋。對於應用程式映象來說,通常建議使用 ENTRYPOINT 指令。在這份 Dockerfile 中,ENTRYPOINT 指令表示在容器啟動後執行 /home/admin/entrypoint.sh
這份指令碼。
構建映象
這一次構建的映象命名為 myorg/login:20190108
。
docker build -t myorg/login:20190108 .
使用映象
經過上面的三個步驟,一個可以使用的應用系統映象就製作完成了,使用下面的命令來啟動容器:
docker run -d --name loginService -p 8800:80 -p 9090:8080 -v myappdata:/home/admin/logs myorg/login:20190108
-d
引數表示讓容器在後臺執行。
--name
引數指定容器的名稱。
-p
引數指定埠對映關係。命令中的關係為將宿主機 8800 埠對映到容器中的 80 埠,將宿主機 9090 埠對映到容器中的 8080 埠。
-v
引數指定掛載點對應資料卷的名稱。需要特別說明的是,掛載點也可以指定一個宿主機目錄去掛載,這樣 Docker 將不會建立資料卷。比如使用宿主機的 /data/appdata
目錄去掛載,引數值修改為 -v /data/appdata:/home/admin/logs
。掛載前宿主機目錄必須存在,Docker 不會自動建立,並且要保證具有讀寫許可權。
修改映象
對於常用如 MySQL、Kafka、Redis 等中介軟體,官方已經為我們提供了經過測試的映象,我們可以直接拿來使用。但是由於業務的具體需求等原因,我們通常需要對這些映象進行修改。所謂修改映象,其實就是基於這些官方映象製作出新的映象。在下面的這個例子中,我們將一步步把官方的 mysql:5.7
映象進行修改。
第一步,修改時區
很多官方映象都使用的零時區,這顯然不符合國情,所以 通常我們都需要把官方映象的時區調整為東八區。調整時區需要安裝 tzdata
這個軟體,所以我們需要事先確定官方映象是基於哪種 Linux 發行版進行構建的,否則我們將不知道該使用什麼軟體安裝命令。你可以登入 Docker Hub 搜尋對應的映象,然後檢視官方放置在 GitHub 上的 Dockerfile 檔案來確定相關資訊。
編寫 Dockerfile
FROM mysql:5.7
ENV TZ=Asia/Shanghai
COPY customer.cnf /etc/mysql/conf.d/
RUN apt-get update \
&& apt-get install -y tzdata \
&& ln -s -f /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& rm -rf /var/lib/apt/lists/*
修改時區的同時我們通過增加自定義 MySQL 配置檔案來調整 MySQL 字符集、時區、連線數等配置,內容如下:
[client]
default-character-set=utf8mb4
[mysql]
default-character-set=utf8mb4
[mysqld]
character-set-client-handshake=FALSE
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci
max_connections=1000
default-time_zone='+8:00'
需要注意的是我們並沒有使用 CMD
指令或 ENTRYPOINT
指令來指點容器啟動後要執行的操作,因為在很多情況下,除非我們查閱官方映象的 Dockerfile 檔案,否則我們無法獲知原本的啟動操作是什麼。所以只要我們變更的操作不影響啟動流程,那麼就可以不指定啟動操作,讓映象預設使用基礎映象的啟動操作。
構建映象
這一步我們將映象命名為 myorg/mysql:5.7_bjtime_utf8mb4
,執行構建命令:
docker build -t myorg/mysql:5.7_bjtime_utf8mb4 .
第二步,自動建庫
在系統部署情況下,我們系統 MySQL 容器啟動後就能將所需要的資料庫建立好,這樣可以避免我們再手動去建庫。
編寫 Dockerfile
FROM myorg/mysql:5.7_bjtime_utf8mb4
COPY db_login.sql /sqls/db_login.sql
COPY privileges.sql /sqls/privileges.sql
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENV MYSQL_ALLOW_EMPTY_PASSWORD yes
ENTRYPOINT ["sh", "/entrypoint.sh"]
本次構建基於上一步的映象繼續構建,db_login.sql
是建庫建表的 SQL 指令碼,privileges.sql
是新增資料庫使用者資訊的指令碼, entrypoint.sh
是容器啟動後要執行的指令碼。
privileges.sql
中主要是修改了 ROOT 使用者的密碼並允許遠端登入,內容如下:
update mysql.user set authentication_string=password("123456") where user = "root";
create user 'root'@'%' identified by '123456';
grant all privileges on *.* to 'root'@'%';
flush privileges;
entrypoint.sh
指令碼負責啟動 MySQL Server 並執行 db_login.sql
和 privileges.sql
,內容如下:
#! /bin/sh
service mysql start
sleep 3
mysql < /sqls/db_login.sql
mysql < /sqls/privileges.sql
tail -f /dev/null
最後的 tail -f /dev/null
是為了讓進城掛起,禁止容器退出。
ENV MYSQL_ALLOW_EMPTY_PASSWORD yes
表示允許容器啟動時 MySQL ROOT 使用者沒有密碼。官方預設要求啟動時必須設定 ROOT 使用者密碼,否則容器無法啟動。
構建映象
至此,一個啟動即建庫並支援 utf8mb4 和東八區的 MySQL 映象就修改完成了,這個映象中關於 MySQL 安裝和配置部分完全是複用的官方映象,我們只做了定製化的修改。最後,我們將映象命名為 myorg/mysql:5.7_login_20190108
。
docker build -t myorg/mysql:5.7_login_20190108 .
使用映象
docker run -d --name login_db -p 3306:3306 -v /data/mysqldata:/var/lib/mysql myorg/mysql:5.7_login_20190108
官方映象
當你需要製作一個映象,尤其是中介軟體映象時,最好的選擇是先去 Docker Hub 搜尋是否已有相關的官方映象。基於官方映象或者別人釋出的映象來進行定製化比自己從頭做一個映象更方便更可靠。
Docker Hub 是世界上最大的 Docker 映象倉庫,Docker 官方和世界各地的開發者都在這上面釋出自己製作的映象。在這裡你可以找到各種映象的使用說明,也能找到其 Dockerfile 來學習。比如我們上面使用官方的 MySQL 映象來定製化,那麼 ROOT 密碼該怎麼設定,資料掛載點在哪裡,開放了哪些埠這些問題,你都能在映象文件中找到答案。
最後
Docker 映象的製作技術非常簡單,難點在於你是否能夠事先規劃好映象內容。當你編寫 Dockerfile 時,你的腦海裡應該具有映象製作完成之後的一個全貌,這樣你編寫的 Dockerfile 才是可靠有效的。編寫 Dockerfile 其實就像是給你一個乾淨的作業系統,讓你去安裝軟體,設定目錄,啟動應用類似,明確了目的,流程就會很清晰。