Docker 簡介
Docker 是一個開源項目。
可以把它理解為是一種新興的超輕量級虛擬化技術。
傳統虛擬化技術需要模擬電腦的一整套硬體出來,而且還要有自己的一套操作系統。
而 Docker 卻不需要,它只需要與主機共享同一個內核,並充分利用 Linux 上內核的「環境隔離方案」來實現輕量級的虛擬化。
它在一些特定場景下與傳統虛擬化技術相比,效率大幅提高,而資源開銷卻大幅降低。
Docker 的遷移也是十分方便的,基本上只需要把整個 Docker 目錄搬過去即可。
Docker 使用 伺服器-客戶端 架構。
如果想在 Docker 上運行 exe 軟體的話,那不用看下去了,左轉找 KVM 去吧。
三、理解 Docker 的結構
四個基本結構:容器(Container)、鏡像(Image)、倉庫(Repository)、註冊點(Registry)。
看著一臉懵逼對吧!是的,這幾個概念確實比較難理解。但是我用類比法還是把它搞明白了。
先想象一個無盤系統是怎麼樣的,下面我們用一般的無盤系統來類比。
3.1 鏡像
無盤伺服器硬碟內有各種軟體。比如說有 Win 7,還有各類應用軟體。
而這些軟體是相互依賴的。比如微信需要裝 Win 7 系統才能運行。
各個無盤電腦(容器)想要運行什麼軟體可以直接告訴無盤伺服器。
無盤伺服器會準備好一切所需軟體,打成一個包(鏡像),然後推送給無盤電腦。
假設整個無盤系統中只有兩種包。一種包是 Win 7 & QQ,另一種包是 Win 7 & 微信。
但是無論這兩種包有多少個,都不會佔用額外的硬碟空間(利用 Union mount 實現鏡像分層)。只有 Win 7(某個鏡像層) 、QQ、微信 這三個軟體會佔用硬碟空間。
一個鏡像是這樣被標識的:<倉庫名>:<標籤名(版本號)> ,例如nginx:latest。如果不指定標籤,默認為 latest。
3.2 容器
相當於無盤電腦。
無盤電腦啟動(容器啟動)時,要從無盤伺服器上拉取所需文件。如果無盤電腦對硬碟有寫入操作的話,寫入的數據將保存到無盤伺服器的緩存區(容器存儲層)。
無盤電腦關機(容器停止)時,如果沒有額外設置,所有保存到無盤伺服器的緩存區的文件(容器存儲層)都將丟失。除非另外保存在 U 盤等外接設備(數據卷)中。
各個無盤電腦之間的運行互不干擾。(利用 cgroups 、namespace 實現隔離)
3.3 倉庫
相當於同一個軟體所有版本的集合。
3.4 註冊點
相當於一個應用商店。
無盤伺服器會來這裡查找並下載軟體。
四、操作環境
- 操作系統:CentOS 7.3.1611
- Linux 內核版本:3.10.0-514.26.2.el7.x86_64
- Docker 版本:17.06.2-ce
五、Docker 的安裝
5.1 安裝一些組件
yum -y install yum-utils device-mapper-persistent-data lvm2
5.2 添加 Docker 源
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
5.3 建立 yum 緩存
yum makecache fast
5.4 安裝最新版本的 Docker
yum -y install docker-ce
若出現密鑰警告,按 y 回車即可。
5.5 啟動 Docker 服務
systemctl start docker
如需開機自啟動,請執行:
systemctl enable docker
六、運行第一個容器
6.1 運行 hello-world 容器
docker run hello-world
若出現以下執行結果,說明運行成功!
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b04784fba78d: Pull complete
Digest: sha256:f3b3b28a45160805bb16542c9531888519430e9e6d6ffc09d72261b0d26ff74f
Status: Downloaded newer image for hello-world:latest
Hello from Docker!
#以下內容省略
6.2 過程解析
- Docker 客戶端向 Docker 守護進程發出運行命令。
- Docker 守護進程發現沒有 hello-world 這個鏡像,於是從倉庫中尋找並下載它。
- 下載完畢之後,Docker 守護進程以 hello-world 這個鏡像創建一個新的容器。
- 容器向 Docker 守護進程輸出內容之後,容器停止。Docker 守護進程把輸出的內容傳遞給 Docker 客戶端。
七、Docker 的基本操作
下面以創建一個 Ubuntu 系統的容器為例來了解一下 Docker 的基本操作。
為了方便理解,我把命令完整地寫出來。
本節的命令參數只有最基本的參數,需要其他設置(如數據卷)的話會在後面講到。
docker 命令支持自動補全,這點必須贊!
7.1 獲取一個鏡像
常用格式
docker image pull [<註冊點名>/]<倉庫名>[:<標籤名(版本號)>]
若不指定註冊點名,將使用默認的 library/。
若不指定標籤名,將使用默認的 latest。
例如
docker image pull ubuntu
執行結果
Using default tag: latest
latest: Pulling from library/ubuntu
d5c6f90da05d: Pull complete
1300883d87d5: Pull complete
c220aa3cfc1b: Pull complete
2e9398f099dc: Pull complete
dc27a084064f: Pull complete
Digest: sha256:34471448724419596ca4e890496d375801de21b0e67b81a77fd6155ce001edad
Status: Downloaded newer image for ubuntu:latest
可以明顯地看出,鏡像被分為了多個塊。
7.2 以某個鏡像建立一個容器
常用格式
docker container create --interactive --tty [--name=<容器名>] <鏡像名> [要運行的程序和參數]
如果不指定容器名,系統會自動為之分配一個無重複的容器名。
如果本地不存在指定的鏡像,會自動從註冊點中拉取。
–interactive 表示持續為容器打開 stdin 以便隨時接受操作。
–tty 表示為容器分配一個偽終端,這樣我們才可以方便的操作容器。
要運行的程序和參數 指定容器啟動后要運行鏡像里的哪一個程序。這個程序運行結束后,容器也會停止。如果不指定,則使用鏡像的默認值。
例如
以 ubuntu 為鏡像,建立一個名為 ubuntu_test 的容器
docker container create --interactive --tty --name=ubuntu_test ubuntu
執行結果
46cc818c92f0780ccd89811c12906c4527b554d18a61e72b0b2337b663ebab5f
這是自動生成的容器唯一長 ID。
7.3 啟動一個容器
常用格式
docker container start <容器名> [容器名] [容器名] ...
可同時啟動多個容器。
例如
啟動剛才創建的 ubuntu_test 容器。
docker container start ubuntu_test
執行結果
ubuntu_test
返回容器名稱,說明啟動成功。
7.4 查看容器訊息
常用格式
docker container ls [--all] [--no-trunc]
如果不加 --all 選項,則只顯示運行中的容器。
–no-trunc 表示完整顯示容器的長 ID (形如 7.2 中命令的執行結果)。為了方便查看,一般不需要加此選項。
例如
查看本機所有容器的狀態。
docker container ls --all
執行結果
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
#<容器ID> <鏡像名> <命令參數> <創建時間> <狀態> <打開的埠> <容器名稱>
d4bc3be9148d hello-world "/hello" 5 hours ago Exited (0) 5 hours ago practical_rosalind
46cc818c92f0 ubuntu "/bin/bash" 3 hours ago Up 2 minutes ubuntu_test
可以看到,除了剛創建的 ubuntu_test 容器之外,還有一個名為 practical_rosalind 容器。practical_rosalind 這個容器正是剛才運行 docker run hello-world 時生成的。
7.5 操作一個容器 & 容器內外進程簡析
7.5.1 操作一個容器
常用格式
docker container attach <容器名>
例如
docker container attach ubuntu_test
執行之後按幾下回車,如果出現類似 root@46cc818c92f0:/# 的提示符,那說明您已經在容器內操作了。
我們來查看下系統的版本。
uname -a
Linux 46cc818c92f0 3.10.0-514.26.2.el7.x86_64 #1 SMP Tue Jul 4 15:04:05 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
好像看不出是 Ubuntu 系統,沒關係,我們再執行下以下命令。
cat /etc/os-release
NAME="Ubuntu"
VERSION="16.04.3 LTS (Xenial Xerus)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 16.04.3 LTS"
VERSION_ID="16.04"
HOME_URL="http://www.ubuntu.com/"
SUPPORT_URL="http://help.ubuntu.com/"
BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"
VERSION_CODENAME=xenial
UBUNTU_CODENAME=xenial
好,已經確定了是在虛擬的 Ubuntu 系統中操作了!我們再來看一下容器內都有什麼進程吧。
7.5.2 容器內進程簡析
在容器內執行以下命令:
ps aux
執行結果
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 18304 2072 pts/0 Ss 05:19 0:00 /bin/bash
root 229 0.0 0.0 34416 1436 pts/0 R+ 07:27 0:00 ps aux
可以看到,容器中目前只存在 bash 和剛開啟的 ps 這兩個進程,而且 bash 的 PID 為 1 !
這說明了容器處在與實體機不同的 namespace 中,容器看不到實體機的進程。
容器進程數目與傳統虛擬機的進程數目相比大幅減少了,所以說容器的效率非常高,啟動基本上是毫秒級的!
7.5.3 容器外進程簡析
實體機可以看到容器內的進程嗎?答案是可以的。
我們來看下實體機上的進程樹。
systemd─┬─ModemManager───2*[{ModemManager}
├─dockerd─┬─docker-containe─┬─docker-containe─┬─bash
│ │ │ └─8*[{docker-containe}]
│ │ └─12*[{docker-containe}]
│ └─11*[{dockerd}]
#無關部分已省略
可以看出,bash 屬於 dockerd 的子進程。
這說明了容器處在實體機的子 namespace 中,同時需要依賴實體機中的進程才可以運行。
所以,容器並不是完全的「虛擬化」。
7.6 從容器中脫開
前面 7.2 我們已經說過了:容器啟動時會運行鏡像里指定的應用程序,而這個程序運行結束后,容器也會停止。
現在這個容器啟動時運行了鏡像默認設定的的 /bin/bash ,所以當 /bin/bash 關閉時,容器就會跟著關閉。
如果直接按 Ctrl + D 退出容器操作的話,bash 就會退出而使整個容器停止運行,我們顯然不希望這樣。
正確的脫開方法是 先按 Ctrl+P 再按 Q (跟 Screen 的操作方法非常相似)。
執行完該操作之後,如果出現 read escape sequence ,就說明已經從容器中脫開了。
7.7 停止一個容器
常用格式
docker container stop <容器名>
執行完以上命令之後,實體機將向容器內所有的進程發送 SIGTERM 信號,然後給 10 秒的時間,讓容器內的進程可以「優雅地」結束。
如果容器內的進程在 10 秒內沒有結束,則實體機向未結束的進程發送 SIGKILL 信號來強制結束。
如果想立即強制結束容器的話把 stop 換成 kill 就行了。
例如
docker container stop ubuntu_test
執行結果
ubuntu_test
返回容器名稱,說明容器已經停止。
7.8 刪除一個容器
常用格式
docker container rm <容器名>
請注意:運行中的容器不能被刪除。
例如
我們把剛才第一個使用 hello-world 鏡像的容器給刪掉,容器名從上面 7.4 中得到。
docker container rm practical_rosalind
執行結果
practical_rosalind
返回容器名稱,說明容器已經刪除。
7.9 查看鏡像訊息
常用格式
docker images [--all]
沒有加 -all 的話將只顯示頂層鏡像,加了 -all 的話除了顯示頂層鏡像之外還會顯示中間層(依賴)鏡像。
例如
docker images
執行結果
這裡我們順便回顧一下,一個鏡像是這樣標識的: <倉庫名>:<標籤名(版本號)> 。
REPOSITORY TAG IMAGE ID CREATED SIZE
#<所在倉庫> <標籤> <鏡像 ID> <創建時間> <大小>
ubuntu latest ccc7a11d65b1 4 weeks ago 120MB
hello-world latest 1815c82652c0 2 months ago 1.84kB
7.10 刪除一個鏡像
常用格式
docker image rm <鏡像名>
請注意:如果有基於要刪除鏡像的容器,則該鏡像不能被刪除。
例如
我們把剛才第一個下載的 hello-world:latest 鏡像給刪掉。
docker image rm hello-world:latest
執行結果
Untagged: hello-world:latest
Untagged: hello-world@sha256:f3b3b28a45160805bb16542c9531888519430e9e6d6ffc09d72261b0d26ff74f
Deleted: sha256:1815c82652c03bfd8644afda26fb184f2ed891d921b20a0703b46768f9755c57
Deleted: sha256:45761469c965421a92a69cc50e92c01e0cfa94fe026cdd1233445ea00e96289a
鏡像已經刪除。
7.11 使用一次性容器(推薦用於測試或開發環境)
上述步驟目的其實是為了讓大家更好地理解 Docker 的結構。
我認為 Docker 有一個缺點,那就是容器一旦創建完成之後,想要修改配置就有點麻煩。而在測試或開發環境中,經常需要修改容器的配置。
好在,容器非常輕,完全可以做到「用時創建、用完即刪」!
所以,我還是推薦大家使用以下命令,來做到容器創建、啟動、刪除三合一。
常用格式
docker container run --rm [--detach] --interactive --tty [--name=<容器名>] <鏡像名> [要運行的程序和參數]
–rm 表示容器停止之後刪除容器。
–detach 表示容器啟動之後不進入容器內操作。
其他選項請參考 7.2。
執行完該命令之後,容器會自動創建然後啟動。如果沒有加入 --detach 選項,容器啟動完成後會直接進入到容器中操作(可隨時脫開)。
而容器停止之後,容器就會馬上被刪除,非常方便。
下文均使用一次性容器。
7.12 配置容器的自重啟(推薦用於生產環境)
常用格式
在 7.2 或 7.11 的 --tty 後面插入如下格式的內容:
--restart on-failure|unless-stopped|always
有三種自重啟方式。
on-failure 表示容器停止時,若出現錯誤則自動重啟(進程返回值不為 0)。在 Docker 服務重啟時,不能自動重啟。
unless-stopped 表示容器停止時,若沒有出現錯誤則自動重啟(進程返回值為 0)。在 Docker 服務重啟時,通常可以自動重啟。
always 表示一旦容器停止,都將自動重啟。在 Docker 服務重啟時,可以自動重啟。
請注意,如果執行了 docker container stop 或者 docker container kill 命令,容器自重啟將失效。
–restart 不能和 --rm 同時使用,也就是說不適用於一次性容器。
7.13 查看容器的詳細配置訊息
把容器所有配置參數以 json 格式顯示出來,這裡只做了解。
常用格式
docker container inspect <容器名>
7.14 查看鏡像的詳細訊息
把鏡像所有參數以 json 格式顯示出來,這裡只做了解。
常用格式
docker image inspect <鏡像名>
7.15 快速刪除所有容器
常用格式
docker container rm $(docker container ls --all -q)
請注意:運行中的容器不能被刪除。
八、保存容器中的數據
前面我們已經說過,容器一旦停止,容器內文件的所有改動都將丟失。
所以,我們必須指定一個可以存儲數據的方法,才能保存容器內的數據。
8.1 使用數據卷
簡單地說,數據卷就是在容器內指定一個目錄,存儲在這個目錄下的數據都可以持久化保存。
常用格式
在 7.2 或 7.11 的 --tty 後面插入如下格式的內容
--volume [<實體機文件或目錄>:]<容器內文件或目錄> [--volume [<實體機文件或目錄>:]<容器內文件或目錄>] ...
為了方便容器的遷移以及維護工作,通常會指定實體機內的某個文件或目錄映射到容器內的某個文件或目錄中。
如果不指定實體機文件或目錄,Docker 將會自動分配一個實體機目錄。
可以創建多個數據卷。
例如
以 ubuntu 為鏡像,建立一個名為 ubuntu_test2 的容器並啟動,將實體機上的 /root/ubuntu_files1目錄掛載到容器中的 /test/ubuntu_files1 目錄中去(實體機上的目錄已存在)。
docker container run --rm --interactive --tty --volume /root/ubuntu_files1:/test/ubuntu_files1 --name=ubuntu_test2 ubuntu:latest
執行完該命令后,我們已經是在容器內操作了。此時我們來看看容器內是否出現了掛載的目錄。
ls /test/ubuntu_files1
如果沒有返回錯誤訊息,說明掛載成功。現在我們來向裡面寫一點東西,看下能不能保存。
echo "File saved" > /test/ubuntu_files1/1.txt
然後按 Ctrl + D 關閉容器。我們就來到實體機下了,接下來我們來看看實體機有沒有這個文件。
cat /root/ubuntu_files1/1.txt
如果返回了 File saved ,說明數據已經可以保存了!
您也可以再次創建容器,然後在容器內看看 /test/ubuntu_files1/1.txt 這個文件在不在。
8.2 打包一個新的鏡像(不推薦)
這種方法十分簡單粗暴,就是把容器內現有文件全部打包成一個新的鏡像,然後新建一個使用該映像的容器即可實現文件的保存。
之所以不推薦,主要是因為這樣做會把容器內運行程序的緩存等無用文件一併打包下來。如果多次執行該操作,容器會變得非常臃腫。其次也可能會造成一定的安全隱患。
常用格式
docker container commit <需要保存的容器名> <打包之後的鏡像名>
這裡就不舉例了。
九、容器的網路連接
9.1 容器聯網的基本方式
NAT 模式是容器默認的聯網模式。
在啟動 Docker 服務之後,Docker 會自動往實體機內添加一個名為 docker0 的網橋,這個網橋默認可以與實體機內所有的網路介面通信。
我們通過執行 brctl show 這條命令來看一下 docker0 網橋的狀態。
執行結果
bridge name bridge id STP enabled interfaces
docker0 8000.024229c84e5a no
當容器啟動之後,會生成一個形如 vethXXXXXXX 的容器專用介面。這個介面也會加入到 docker0 的橋接列表中。
docker0 上面有 IP 地址,也可以自動為每個容器分配 IP 地址(非 DHCP 協議)。
我們通過執行 ip addr show dev docker0 還有 brctl show docker0 這條兩命令來看一下。
執行結果
6: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
link/ether 02:42:29:c8:4e:5a brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 scope global docker0
valid_lft forever preferred_lft forever
inet6 fe80::42:29ff:fec8:4e5a/64 scope link
valid_lft forever preferred_lft forever
bridge name bridge id STP enabled interfaces
docker0 8000.024229c84e5a no vethcd2a637
其實 docker0 就相當於一個普通的路由器,通過 NAT 轉換實現容器間的相互通信和連接外網。
我們通過執行 iptables -t nat -L POSTROUTING -v -n 來查看相關的 NAT 規則。
執行結果
Chain POSTROUTING (policy ACCEPT 373 packets, 31640 bytes)
pkts bytes target prot opt in out source destination
12 729 MASQUERADE all -- * !docker0 172.17.0.0/16 0.0.0.0/0
很顯然,存在 docker0 網段的 SNAT 規則,說明容器都是通過 NAT 的方式與實體機共享網路的。
如需查看容器的 IP 地址,請執行以下命令:
docker container inspect --format='{{.NetworkSettings.IPAddress}}' <容器名>
9.2 自定義網橋 & 容器 IP 地址
使用默認網橋一般是不能自定義容器 IP 地址的,會提示以下錯誤:
User specified IP address is supported only when connecting to networks with user configured subnets
這時候,我們就需要自定義一個網橋。
9.2.1 自定義網橋
常用格式
docker network create --driver bridge --subnet <網橋網段> <網橋名>
例如
創建一個名為 docker_br1 的網橋,網段為 192.168.10.0/24
docker network create --driver bridge --subnet 192.168.10.0/24 docker_br1
執行結果
46cc818c92f0780ccd89811c12906c4527b554d18a61e72b0b2337b663ebab5f
這是自動生成的網橋唯一長 ID。
網橋的管理和刪除命令格式和上面鏡像管理的相似,這裡就不再說了。
9.2.2 自定義容器 IP 地址
常用格式
在 7.2 或 7.11 的 --tty 後面插入如下格式的內容
--network=<網橋名> --ip=<IP地址>
例如
創建並運行一個使用剛才創建的 docker_br1 網橋的容器,把 IP 地址設定為 192.168.10.211 ,然後驗證結果。
為了方便,這裡將直接運行一個包含 ifconfig 命令的鏡像的容器,然後執行 ifconfig eth0 命令來驗證。
docker container run --rm --interactive --tty --network=docker_br1 --ip=192.168.10.211 --name=see_ip_addr ianneub/network-tools ifconfig eth0
執行結果
#鏡像下載過程略
eth0 Link encap:Ethernet HWaddr 02:42:c0:a8:0a:02
inet addr:192.168.10.211 Bcast:0.0.0.0 Mask:255.255.255.0
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:2 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:180 (180.0 B) TX bytes:0 (0.0 B)
顯然,這裡的 IP 地址已經是我們設定的 192.168.10.211 。
9.3 埠映射
如果容器需要對外提供服務,在默認情況下需要把容器內的埠映射到實體機上。
常用格式
在 7.2 或 7.11 的 --tty 後面插入如下格式的內容
-p [<實體機介面 IP 地址>:]<實體機埠>:<容器內埠>[/<tcp|udp>] [-p [<實體機介面 IP 地址>:]<實體機埠>:<容器內埠>[/<tcp|udp>]] ...
可以創建多個埠映射。
如果不指定實體機介面 IP 地址,則容器內埠將映射到實體機的所有網路介面上。
如果不指定 TCP 或 UDP 協議,默認使用 TCP 協議。
例如
運行一個提供 HTTP 服務的鏡像(這裡用 nginx)的容器,然後把容器中的 80 埠映射到實體機上的 8888 埠,最後測試能否訪問。
docker container run --detach --rm --interactive --tty -p 8888:80 --name=nginx_test nginx && curl 127.0.0.1:8888
執行結果
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
......
如果含有以上內容的介面,說明埠映射成功。
9.4 為容器設置域名解析
在很多情況下,我們需要在容器之間進行網路通信。而它們的 IP 地址又是不固定的,這就需要為容器固定一個 DNS 名稱。
假設現在有一個容器 A ,而新建的容器 B 需要訪問容器 A 上的網路服務,在沒設置域名解析的情況下容器 B 只能通過容器 A 的 IP 地址來訪問容器 A 。而如果在容器 B 建立的時候設置了域名解析,容器 B 就可以通過容器 A 的名稱或別名來訪問容器 A 。
常用格式
在 7.2 或 7.11 的 --tty 後面插入如下格式的內容
--link <容器名稱>[:<容器別名>]
如果不設置容器別名,將自動使用容器名稱。
9.5 橋接到物理網路
個人不太推薦使用這種方法。
首先,容器都無法靠自身獲取 IP 地址,必須借助 pipework 工具來設置。
而且,容器內一般是沒有防火牆的,這樣會降低整個容器的安全性。
如果需要使用物理網路上的不同 IP 提供不同服務的話,建議在實體機的物理網卡上綁定多個地址,然後把容器特定埠映射到特定的地址上去。
以下簡單地說一下設置的方法。
9.5.1 設置網橋
請參考 Linux 網橋的相關教程,新建一個網橋,網橋成員為物理網路介面,並根據實際情況設置網橋的 TCP/IP 參數。
9.5.2 安裝 pipework
git clone https://github.com/jpetazzo/pipework.git && cp pipework/pipework /usr/bin && rm -rf pipework
9.5.3 修改 docker 默認使用的網橋
cp /lib/systemd/system/docker.service /etc/systemd/system/docker.service
然後修改 /etc/systemd/system/docker.service:
找到 ExecStart= 這一行,把這一行改成 ExecStart=/usr/bin/dockerd -b <網橋名> ,保存文件。
9.5.4 通過 pipework 指定容器 IP 地址
常用格式
pipework <網橋名> `<docker container run 完整命令>` <IP地址/前綴長度@網橋IP>
注意:pipework 只能修改運行中容器的網路配置,並且容器要持續運行。