鏡像裏的內容是按「層」來組織的,「層」能夠複用,一個完整的鏡像也能夠看作是一個「層」。多個「層」疊加在一塊兒就造成了一個新的鏡像,這個鏡像也能夠做爲別的鏡像的基礎「層」進行更加複雜的鏡像構建。下圖展現了一個鏡像的內部結構。java
這個目標鏡像使用 Debian 鏡像做爲基礎鏡像開始構建,也就是說 Debian 鏡像是目標鏡像的第一「層」;往上的兩層分別使用了 ADD
指令將 emacs
和 apache
添加到了目標鏡像中,每個 ADD
指令都將產生新的一個「層」,最後這個目標鏡像就是一個擁有三「層」的鏡像。每新增一「層」時,將要生成的這一「層」鏡像都會默認使用上一步構建出的「層」做爲本身的基礎鏡像,上圖中的箭頭表示的就是這種引用關係。mysql
因此,「層」和「鏡像」是等價的,當這一「層」之上沒有其餘「層」時,咱們就能夠將這一「層」及其下面的全部「層」合起來稱做一個「鏡像」。若是這一「層」只是在構建鏡像過程當中生成的一個「中間層」,即這一「層」不會被用來啓動容器,那麼就能夠稱做「層」。總的來講,能用來啓動容器的就稱做「鏡像」,其餘都稱做「層」。nginx
製做鏡像的過程和在操做系統上安裝軟件的過程幾乎是徹底同樣的,惟一的區別是製做鏡像須要使用 Dockerfile 文件來編寫要執行的操做。請注意,Dockerfile 裏的全部指令,除了 CMD
和 ENTRYPOINT
,都是給 Docker 引擎執行的,目的是製做出目標鏡像,這些指令不是啓動容器的時候執行的。sql
下面的例子將一步步演示從 0 開始製做一個在 CentOS7.2 操做系統上安裝了 openjdk
和 nginx
並運行一個 Java 應用程序的鏡像,這個過程同時也將體現鏡像分層複用的思想。docker
Dockerfile 中的每一個指令都會生成一個「層」,最終的目標鏡像就是由多個「層」組成的。若是製做 B 鏡像的 Dockerfile 中存在某個指令與製做 A 鏡像的 Dockerfile 中的某個指令徹底一致,那麼製做 B 鏡像時就會複用製做 A 鏡像時生成的「中間層」,而不會再去建立一個新的「層」,這就是「鏡像分層複用」思想。shell
官方爲咱們提供了 Linux 各類發行版的鏡像,咱們平常的全部鏡像構建都是基於這些鏡像來完成的。因爲官方的 CentOS 鏡像並不支持中文字符集,因此咱們須要先製做一個支持中文的鏡像出來數據庫
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
是鏡像的版本。apache
RUN
指令表示在構建鏡像時咱們要執行的 shell 命令。以前的 FROM
指令至關於給了咱們一個乾淨的操做系統,咱們在這個系統上要執行的各類操做,如安裝軟件、建立目錄等就都要書寫在這個 RUN 指令以後。理論上你能夠對每個要執行的 shell 命令都使用一個 RUN 指令,好比咱們將上面的 RUN 指令改寫爲下面的樣子:vim
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 變得很是臃腫。centos
須要特別注意的是,若是 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
參數表示保持標準輸入打開,這樣容器就不會在啓動完成後當即退出了。
這一次的鏡像構建使用咱們第一步構建出的鏡像做爲基礎鏡像。
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 的鏡像,這一步就是要將應用系統也放入鏡像中,並經過指令讓容器啓動後就去執行應用系統啓動操做。
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 文件來肯定相關信息。
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 容器啓動後就能將所須要的數據庫創建好,這樣能夠避免咱們再手動去建庫。
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 其實就像是給你一個乾淨的操做系統,讓你去安裝軟件,設置目錄,啓動應用相似,明確了目的,流程就會很清晰。