项目作者: travishen

项目描述 :
:fire::fire: Docker Image, Container, Network; Docker Swarm Stack, Service, Task, Secret, Healthcheck :fire::fire:
高级语言:
项目地址: git://github.com/travishen/docker-tutorial.git
创建时间: 2018-12-04T08:53:37Z
项目社区:https://github.com/travishen/docker-tutorial

开源协议:MIT License

下载


TOC

image

Docker簡介

  • Docker是2013年由DotCloud開發的開源專案,因為軟體的成功,公司之後也改名為Docker.Inc
  • 其實Docker的前身是象龜(@gordonTheTurtle),之後改成鯨魚

Docker的重要性

  • 容器將會是未來最有影響力的基礎架構
  • 不管你是開發、維運、系統管理、部屬都會須要學一下
  • 成長最快的雲端技術
  • Infrastructure as code

過去到現在基礎技術的發展,可以參考以下文章

Docker的優點

  • 過去底層的基礎架構都是為sysops或sysadmins設計的,但Docker的解決方案考慮了developer的使用情境
  • 就是快:develop faster, build faster, test faster, deploy faster, update faster, recover faster
  • 使用容器來減少不同應用程式、不同系統、不同相依性的複雜度
  • 大部分現存的軟體開發得少維護得多,Docker可以減輕我們維護的難度,增加開發的時間

相關工具

學會了Docker你可能需要多了解其他工具

安裝前要注意的

在不同環境下的安裝:

  • Linux: 直接安裝,不過有分版本
  • Windows: Win10不直接支援,需要透過virtual machine來安裝,在安裝的流程中會設定到;因為Windows Containers的出現,新的Windows Server 2016之後會開始直接支援Docker,不用再透過Linux Container
  • Mac: 也是要透過vm,據說不要用brew來裝
  • Cloud: Docker for GCP/AWS/Azure

看更多有關windows container:

Stable vs Edge(beta)

開源版本的Edge每個月會更新一次,Stable大概四個月更新一次,付費的企業版本會有更穩定的支援

安裝

Windows 10 Pro / Enterprise

  • 使用Pro跟Enterprice的用戶算是比較吃香,會有比較好的體驗
  • 請在此下載安裝Docker for Windows
  • 命令界面(CLI)會建議使用PowerShell
  • 如果你的系統已經有在使用VirtualBox或VMware,Hyper-V啟用可能會發生資源互搶的問題

Windows 7, 8, or 10 Home Edition

  • Win7, 8與Pro/Enterprise的主要差別是Hyper-V過舊不支援,Win10 Home則是沒有Hyper-V,因此要額外安裝Docker Toolbox,然後透過VirtualBox安裝Linux VM(Linux Container),也就是docker-machine
    使用網路位址轉換(NAT)存取網路
  • 需要把Toolbox的http://localhost改成http://192.168.99.100

Mac

  • OSX Yosemite 10.10.3以下版本的需要裝Toolbox,以上版本安裝Docker for Mac

Linux

  • 不要用系統預設的packages直接下指令安裝,例如apt install docker.io,你可能會裝到過舊的版本
  • 可以經由Docker automated script來安裝相依的程式,例如curl -ssl https://get.docker.com/ | sh
  • 或者先看過官網上的要求來手動下載安裝檔
  1. $ sudo curl -ssl https://get.docker.com/ | sh

Docker會動用到系統核心的功能,需要root權限來操作
你可以將使用者加入Docker group,註:有些版本的linux如Red Hat, Fedora沒有此選項,每個指令都要透過sudo

  1. $ sudo usermod -aG docker <--username-->
  2. $ docker version
  3. Client:
  4. Version: 18.06.1-ce
  5. API version: 1.38
  6. Go version: go1.10.3
  7. Git commit: e68fc7a
  8. Built: Tue Aug 21 17:24:56 2018
  9. OS/Arch: linux/amd64
  10. Experimental: false
  11. Server:
  12. Engine:
  13. Version: 18.06.1-ce
  14. API version: 1.38 (minimum version 1.12)
  15. Go version: go1.10.3
  16. Git commit: e68fc7a
  17. Built: Tue Aug 21 17:23:21 2018
  18. OS/Arch: linux/amd64
  19. Experimental: false
Docker Machine & Docker Compose

Windows和Mac會自動幫你安裝好,Linux系統則要自己安裝這兩個項目

  1. $ base=https://github.com/docker/machine/releases/download/v0.14.0 &&
  2. curl -L $base/docker-machine-$(uname -s)-$(uname -m) >/tmp/docker-machine &&
  3. sudo install /tmp/docker-machine /usr/local/bin/docker-machine

Docker compose

  1. $ sudo curl -L "https://github.com/docker/compose/releases/download/1.22.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

使用官方文件提供的指令可能會安裝到非最新的Docker compose版本,你可以到github/docker/compose去安裝最新的版本

  1. $ sudo -i curl -L https://github.com/docker/compose/releases/download/1.23.0-rc3/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
  2. $ sudo -i chmod +x /usr/local/bin/docker-compose
  3. $ docker-compose version
  4. docker-compose version 1.23.0-rc3, build ea3d406e
  5. docker-py version: 3.5.0
  6. CPython version: 3.6.6
  7. OpenSSL version: OpenSSL 1.1.0f 25 May 2017

版本格式

現今版本的格式為YY.MM,如18.06.0就是2018年6月出的版本的第一個release

其他選項

你可以試試Play with Docker,無須安裝任何環境就可以透過瀏覽器體驗Docker的強大,可以參考以下文章:

指令格式

舊的指令格式為:docker <command> (options),例如docker run,而新的Docker指令的格式已改成:

  1. docker <command> <sub-command> (options)

例如docker run現在要寫成docker container run,不過舊的指令格式目前還能使用

安裝完成你可以試著執行以下指令:

  • docker: 檢視所有管理選項
  • docker version: 確認你可以跟docker engine溝通,和檢查你的版本號
  • docker info: 檢視docker engine的各項設定值
  • docker image ls: 檢視本機所有映像檔
  • docker pull <-- IMAGE -->: 取得映像檔

容器(Containers)

什麼是容器

容器是基於特定映像檔所創造出來的程序

映像檔(image)和容器(container)的差別

  • 映像檔是我們想執行的應用程式的模板,一個映像檔可以包含一個完整的作業系統環境,裡面安裝了需要的應用程式,同一個映像檔可以建立多個容器
  • 容器是從映像檔建立的執行實例,可以被啟動、開始、停止、刪除
  • 大部分的映像檔都可以從Docker Hub下載並取用

基本指令

  1. $ docker container run --publish 80:80 --name webhost1 nginx
  2. Unable to find image 'nginx:latest' locally
  3. latest: Pulling from library/nginx
  4. f17d81b4b692: Pull complete
  5. d5c237920c39: Pull complete
  6. a381f92f36de: Pull complete
  7. Digest: sha256:b73f527d86e3461fd652f62cf47e7b375196063bbbd503e853af5be16597cb2e
  8. Status: Downloaded newer image for nginx:latest
  1. 從Docker Hub下載nginx映像檔
  2. 建立一個映像檔實例(容器),命名為webhost1並執行
  3. Port Forwarding: 啟用主機的80 port並將容器內部使用的80 port映射到主機上

註:

  • nginx server預設的對外端口是80 port
  • 你可以更改要映射到主機的哪個port,例如8888:80,然後用localhost:8888訪問

加入detach參數只在背景執行,並回傳容器的id

  1. $ docker container run --publish 80:80 --detach --name webhost2 nginx
  2. 6950bd542f5d281cddd7691c5032d4424d7f72585e7e36623028212ddebb4490

顯示目前所有容器

  1. $ docker container ls -a
  2. 6950bd542f5d nginx "nginx -g 'daemon of…" 3 minutes ago Up 3 minutes 0.0.0.0:80->80/tcp webhost2
  3. eb05ed09c19c nginx "nginx -g 'daemon of…" 3 hours ago Exited (0) 3 minutes ago webhost1

關閉正在運行的容器

  1. $ docker container stop 6950bd542f5d

顯示webhost2的log

  1. $ docker container logs webhost2

檢視webhost2的程序,或檢查是否有正在運行的webhost2

  1. $ docker container top webhost2
  2. UID PID PPID C STIME TTY TIME CMD
  3. root 18871 18852 0 13:59 ? 00:00:00 nginx: master process nginx -g daemon off;
  4. systemd+ 18917 18871 0 13:59 ? 00:00:00 nginx: worker process

強制關閉運行中的容器

  1. $ docker container rm -f 6950bd542f5d

以上這些指令發生了什麼事

docker container run

  1. 尋找特定的映像檔快取,找不到該映像檔的話會從遠端倉庫(remote image repo)尋找(預設是Docker Hub,若沒提供映像檔版本將下載最新的版本)
  2. 新增映像檔實例(容器)
  3. Docker引擎會給這個容器一個實體IP(virtual ip)
  4. 啟用host的特定埠號將容器的特定埠號映射到host
  5. 以映像檔的Dockerfile CMD來執行容器

容器(container)和虛擬機(virtual machine)的差別

很多介紹容器的文章把容器拿來和vm比較,雖然它們相似的地方很多,但事實上它們是完全不同的概念,因為:

  • 容器只是程序(processes)
  • 容器可用的資源是受限的
  • 容器關閉=程序停止

舉個例子來說:

  1. $ docker run --name mongo -d mongo

檢視此容器是否正在運行

  1. $ docker top mongo

檢視所有運行中的docker容器,註:docker ps會對象是host,如果系統是mac或windows,需要先連到docker vm

  1. $ docker ps
  2. CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
  3. 2db362bba01a mongo "docker-entrypoint.s…" 5 minutes ago Up 5 minutes 27017/tcp mongo

檢視系統上所有程序,可以清楚的看到容器是一個系統上正在執行的程序

  1. $ ps aux | grep mongo
  2. USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
  3. 999 19617 0.4 0.9 1089996 75788 ? Ssl 14:28 0:02 mongod --bind_ip_all

練習:開啟多個容器

  1. 執行一個nginx實例,在背景執行且聽80port
  2. 執行一個httpd(apche)實例,在背景執行且聽8080port
  3. 執行一個mysql實例,在背景執行、密碼設定為自動產生,聽3306port
    1. $ docker container run --name nginx -p 80:80 -d nginx
    2. $ docker container run --name httpd -p 8080:80 -d httpd
    3. $ docker container run --name mysql -p 3306:3306 -e MYSQL_RANDOM_ROOT_PASSWORD=yes -d mysql
    檢視這三個容器的狀態
    1. $ docker container ls -a
    2. CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
    3. 0e723b2571e0 mysql "docker-entrypoint.s…" 28 seconds ago Up 27 seconds 0.0.0.0:3306->3306/tcp, 33060/tcp mysql
    4. 22068e0c1ad2 httpd "httpd-foreground" 38 seconds ago Up 38 seconds 0.0.0.0:8080->80/tcp httpd
    5. 45b3a7dc26c9 nginx "nginx -g 'daemon of…" About a minute ago Up About a minute 0.0.0.0:80->80/tcp nginx
    檢視mysql密碼
    1. $ docker container logs mysql
    2. Initializing database
    3. ...
    4. Database initialized
    5. MySQL init process in progress...
    6. ...
    7. GENERATED ROOT PASSWORD: eiCheu9Wae3ohmooxaebooR7quohphai
    8. ...
    9. MySQL init process done. Ready for start up.
    10. ...

基本指令:監控執行中的容器

檢視容器的設定(metadata),會回傳json陣列

  1. $ docker container inspect <-- container id or container name -->

檢視所有容器的即時狀態(live performance)

  1. $ docker container stats

基本指令:在容器中使用終端機

  • run -t: Allocate a pseudo-TTY
  • run -i: Keep STDIN open even if not attached

根據格式docker container run [OPTIONS] IMAGE [COMMAND] [ARG...],在指令後面可以再帶入[COMMAND]及參數[ARG...],以nginx為例,預設程式(default program)是nginx,參數是-g'daemon off;'

  1. {
  2. ...
  3. "Path": "nginx",
  4. "Args": [
  5. "-g",
  6. "daemon off;"
  7. ],
  8. ...
  9. }

加入bash參數來改變預設程式,進入bash shell之後exit,可以看到容器隨之停止:

  1. $ docker container run --name proxy -it nginx bash
  2. root@e78f7e17edbc:/# exit
  3. $ codker container ls -a
  4. CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
  5. e78f7e17edbc nginx "bash" 25 seconds ago Exited (0) 5 seconds ago proxy

這邊可以看出預設的程式變成bash:

  1. {
  2. ...
  3. "Path": "bash",
  4. "Args": [],
  5. ...
  6. }
  • start -a: Attach STDOUT/STDERR and forward signals
  • start -i: Attach container’s STDIN

重啟容器,注意這裡一樣會開啟bash shell因為我們建立容器時就把command改成bash了:

  1. $ docker container start -ai proxy
  2. root@e78f7e17edbc:/#

如果容器正在執行中,我們如何透過殼程式操作呢(這很常用,在容器執行時debug或設定參數):

  • exec: 在執行中的容器上執行額外的程序
  • exec -t: Allocate a pseudo-TTY
  • exec -i: Keep STDIN open even if not attached
    1. $ docker container run --name mysql -e MYSQL_RANDOM_ROOT_PASSWORD=yes -d mysql
    2. $ docker container exec -it mysql bash
    另外要注意的是,並非所有映像檔都有bash程式,例如Linux的超迷你分支alpine[ 延伸閱讀 ]:
    1. $ docker container run --it alpine bash
    2. docker: Error response from daemon: OCI runtime create failed: container_linux.go:348: starting container process caused "exec: \"bash\": executable file not found in $PATH": unknown.
    要使用bash要先透過alpine內建的殼程式sh來安裝
    1. docker container run -it alpine sh
    2. / # apk add bash
    3. / # bash
    4. bash-4.4#

Docker背後的網路運作

docker讓你可以建立虛擬網路(virtual network),並將container加到網路內,建立起屬於你自己應用程式的網路拓墣
[來源]

  1. $ docker container run --name nginx -p 80:80 -d nginx
  2. $ docker container port nginx
  3. 80/tcp -> 0.0.0.0:80

docker daemon運作的時候,會建立三個網路

  1. $ docker network ls
  2. NETWORK ID NAME DRIVER SCOPE
  3. 81978b8f756d bridge bridge local
  4. 4bad3f6e57ed host host local
  5. da7c350f0dc0 none null local

當你建立容器時,使用的ip跟主機並不同,例如以下例子:我的主機內部ip是192.168.43.63,而我的nginx容器的內部ip則是172.17.0.2

  1. $ docker container inspect --format '{{ .NetworkSettings.IPAddress }}' nginx
  2. 172.17.0.2
  3. $ ip addr show wlp58s0
  4. 3: wlp58s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
  5. link/ether 9c:b6:d0:f1:f2:55 brd ff:ff:ff:ff:ff:ff
  6. inet 192.168.43.63/24 brd 192.168.43.255 scope global dynamic wlp58s0
  7. valid_lft 2871sec preferred_lft 2871sec
  8. inet6 fe80::6228:4a16:e94a:7822/64 scope link
  9. valid_lft forever preferred_lft forever

它們之間透過docker birdge network的模式(預設),此模式透過一個叫docker0的NAT server來掌管容器的網路連線
image
@tomdeore/docking-a-docker-container-part-3-networking-edd779f068cb">[圖片來源]

透過以下指令可以看出bridge網路裡有哪些容器

  1. $ docker network inspect bridge
  2. {
  3. ...
  4. "Containers": {
  5. "b8b8b86a8825166954069c7884d8ed8181234f5399a9d8e7bac860493c5a132f": {
  6. "Name": "nginx",
  7. "EndpointID": "b80f6f698bcf305689752dd9356ecc14c114275caa28ff7cd9815545d92f74be",
  8. "MacAddress": "02:42:ac:11:00:02",
  9. "IPv4Address": "172.17.0.2/16",
  10. "IPv6Address": ""
  11. }
  12. },
  13. ...
  14. }

新增一個網路(預設是bridge driver),然後建立新的容器

  1. $ docker network create new_net
  2. $ docker container run --name new_nginx -d --network new_net nginx

連結容器至多個網路

  1. $ docker network connect new_net nginx
  2. $ docker container inspect --format '{{ .NetworkSettings.Networks }}' nginx
  3. map[bridge:0xc4205300c0 my_app_net:0xc420530180]

DNS設置

容器隨著不同設置會改變其狀態,容器間用ip位址來連線是不可靠的,因此我們會需要DNS server

先來看看新網路的DNS能不能運作

  1. $ docker network insepct new_net
  2. {
  3. ...
  4. "Containers": {
  5. "1a6c3e7c03caba424cbc5d3cbbdb9e86fd5c39741f5af1835d7446b1758e35a7": {
  6. "Name": "nginx",
  7. "EndpointID": "3958183cb1514d597067aeeb01931ab3e62ce4267848f4471c5c465cecbd6b91",
  8. "MacAddress": "02:42:ac:14:00:03",
  9. "IPv4Address": "172.20.0.3/16",
  10. "IPv6Address": ""
  11. },
  12. "e285eb6071b133177db6ec2cc922b01b1acfd7000199c927659fd4e0f023742f": {
  13. "Name": "new_nginx",
  14. "EndpointID": "3d451d2884418b5ee7efb2311530ceb255a86688148e9ec637f280fd7f24289c",
  15. "MacAddress": "02:42:ac:14:00:02",
  16. "IPv4Address": "172.20.0.2/16",
  17. "IPv6Address": ""
  18. }
  19. },
  20. ...
  21. }
  22. $ docker container exec -it new_nginx bash
  23. root@e285eb6071b1:/# apt-get update
  24. root@e285eb6071b1:/# apt-get install -y iputils-ping
  25. root@e285eb6071b1:/# exit
  26. $ docker container exec -it new_nginx ping nginx
  27. PING nginx (172.20.0.3) 56(84) bytes of data.
  28. 64 bytes from nginx.new_net (172.20.0.3): icmp_seq=1 ttl=64 time=0.074 ms
  29. 64 bytes from nginx.new_net (172.20.0.3): icmp_seq=2 ttl=64 time=0.121 ms

同樣在nginx容器安裝ping套件後,測試能不能雙向溝通

  1. $ docker container exec -it nginx ping new_nginx
  2. PING new_nginx (172.20.0.2) 56(84) bytes of data.
  3. 64 bytes from new_nginx.new_net (172.20.0.2): icmp_seq=1 ttl=64 time=0.063 ms
  4. 64 bytes from new_nginx.new_net (172.20.0.2): icmp_seq=2 ttl=64 time=0.071 ms

註:如果我以上的測試在預設的bridge network做的話會出現錯誤訊息Name or service not known,原因是預設的bridge網路並沒有內建的DNS server,容器要連線必須手動使用--link指令為容器設定連線到bridge網路。建議建立新的網路來省去這一步驟

練習:快速更新Linux分支的CLI套件

  1. 分別檢查容器不同分支的Linux上的curl版本
  2. 分別在centos:7和ubuntu:14.04的容器中開啟終端機

分別取得各版本的映像檔

  1. $ docker image pull centos:7
  2. $ docker image pull ubuntu:14.04

建立ubuntu映像檔實例,開啟終端機並檢查curl版本

  1. $ docker container run --name ubuntu -it --rm ubuntu:14.04
  2. root@66c81252095f:/# curl --version
  3. bash: curl: command not found
  4. root@66c81252095f:/# apt-get update && apt-get install -y curl

container run --rm: exit之後預期容器被移除

  1. root@66c81252095f:/# exit
  2. $ docker container ls -f "name=ubuntu"
  3. CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES

建立centos映像檔實例,開啟終端機並檢查curl版本

  1. $ docker container run --name ubuntu -it --rm centos:7
  2. root@66c81252095f:/# curl --version
  3. curl 7.29.0 (x86_64-redhat-linux-gnu) libcurl/7.29.0 NSS/3.34 zlib/1.2.7 libidn/1.28 libssh2/1.4.3
  4. root@66c81252095f:/# yum update curl
  5. root@66c81252095f:/# exit

練習:輪替式DNS(DNS Round Robin aka poor man’s load balancer)

  1. 建立一個network包含兩個elasticsearch:2的映像檔實例
  2. 將兩個容器的網路拓墣別名(network-alias)都命名為search
  1. $ docker network create new_net
  2. $ docker pull elasticsearch:2
  3. $ docker container run --name es1 -d --network new_net --network-alias search elasticsearch:2
  4. $ docker container run --name es2 -d --network new_net --network-alias search elasticsearch:2

透過一個內網centos容器來測試連線

curl -s: Silent mode. Don’t output anything

  1. $ docker container run --name centos --network new_net -it --rm centos:7
  2. [root@570493921bec /]# curl -s search:9200
  3. {
  4. "name" : "Stuntmaster",
  5. "cluster_name" : "elasticsearch",
  6. "cluster_uuid" : "dRpxX5qNQTqm-RDSKy8gaw",
  7. "version" : {
  8. "number" : "2.4.6",
  9. "build_hash" : "5376dca9f70f3abef96a77f4bb22720ace8240fd",
  10. "build_timestamp" : "2017-07-18T12:17:44Z",
  11. "build_snapshot" : false,
  12. "lucene_version" : "5.5.4"
  13. },
  14. "tagline" : "You Know, for Search"
  15. }
  16. [root@570493921bec /]# curl -s search:9200
  17. {
  18. "name" : "Midas",
  19. "cluster_name" : "elasticsearch",
  20. "cluster_uuid" : "k-8YUFb5TrCFpOqQWLP2Xg",
  21. "version" : {
  22. "number" : "2.4.6",
  23. "build_hash" : "5376dca9f70f3abef96a77f4bb22720ace8240fd",
  24. "build_timestamp" : "2017-07-18T12:17:44Z",
  25. "build_snapshot" : false,
  26. "lucene_version" : "5.5.4"
  27. },
  28. "tagline" : "You Know, for Search"
  29. }

映像檔(image)

什麼是映像檔?根據官方定義跟我隨翻:

An image is an ordered collections of root filesystem changes and the corresponding execution parameters for use within a container runtime

映像檔 = 檔案系統變動的有序集合 + 執行一個實例時相對應的執行參數

  • 不是作業系統、沒有內核(kernal)、沒有核心模組(kernal module)[ 延伸閱讀 ]
  • 體積非常輕量,小則幾KB(golang static binary),大則幾GB(ubuntu distro + apache + php)

Docker Hub

目前最多人用的映像檔的集散地(image registry),映像檔repo分成officialnon-official,映像檔會用tag做區別(正確來說tag不算版本或分支),舉例來說,官方的nginx repo:

  • 1.15.5, mainline, 1, 1.15, latest (mainline/stretch/Dockerfile)
  • 1.15.5-perl, mainline-perl, 1-perl, 1.15-perl, perl (mainline/stretch-perl/Dockerfile)
  • 1.15.5-alpine, mainline-alpine, 1-alpine, 1.15-alpine, alpine (mainline/alpine/Dockerfile)

分別pull下來,可以看出同一個映像檔(相同image id)可以有多個tags

  1. $ docker image ls
  2. REPOSITORY TAG IMAGE ID CREATED SIZE
  3. nginx latest dbfc48660aeb 2 weeks ago 109MB
  4. nginx 1 dbfc48660aeb 2 weeks ago 109MB
  5. nginx alpine aae476eee77d 4 weeks ago 17.7MB

Docker Hub的操作跟Github很類似,可以fork其他repo或是上傳新的repo,你所需要的資訊也都能在detail page或github上找到。

  • 映像檔可以從Github或Bitbucket的repo來建置,從首頁點選Create Automated Build連結帳戶即可
    • 可以選擇你的映像檔要從哪個分支來build
    • 對於dockerfile的FROM指令,Docker Hub可以設定Repository Links,設定連結後,來源映像檔如果有偵測到更新,你的映像檔就會跟著rebuild
    • 自定義Rebuild的Trigger

Union檔案系統(Union file system) && 映像檔的資料層(Image layers)

映像檔的形成並非一個大的資料區塊,例如當我pull nginx:alpine時,可以看到有些資料層我已經有了

  1. $ docker pull nginx:alpine
  2. alpine: Pulling from library/nginx
  3. 4fe2ade4980c: Already exists
  4. c3f09dfaf47d: Pull complete
  5. 83283d0e9bb9: Pull complete
  6. e2e530da9538: Pull complete

透過image history來檢視映像檔的資料更動紀錄:

  1. $ docker image histroy nginx:alpine
  2. IMAGE CREATED CREATED BY SIZE
  3. aae476eee77d 4 weeks ago /bin/sh -c #(nop) CMD ["nginx" "-g" "daemon… 0B
  4. <missing> 4 weeks ago /bin/sh -c #(nop) STOPSIGNAL [SIGTERM] 0B
  5. <missing> 4 weeks ago /bin/sh -c #(nop) EXPOSE 80/tcp 0B
  6. <missing> 4 weeks ago /bin/sh -c #(nop) COPY file:1d1ac3b9a14c94a7… 1.09kB
  7. <missing> 4 weeks ago /bin/sh -c #(nop) COPY file:af94db45bb7e4b8f… 643B
  8. <missing> 4 weeks ago /bin/sh -c GPG_KEYS=B0F4253373F8F6F510D42178 13.3MB
  9. <missing> 4 weeks ago /bin/sh -c #(nop) ENV NGINX_VERSION=1.15.5 0B
  10. <missing> 5 weeks ago /bin/sh -c #(nop) LABEL maintainer=NGINX Do… 0B
  11. <missing> 7 weeks ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B
  12. <missing> 7 weeks ago /bin/sh -c #(nop) ADD file:25c10b1d1b41d46a1… 4.41MB

註:IMAGE ID為<missing>只是為了區別——這些資料層不完整代表這個映像檔,只是被這個映像檔所用

所有映像檔都是繼承於特定基礎映像檔(blank layer, scratch),再往上繼承堆疊,不同映像檔之間可以共享基礎的檔案系統層,提升儲存效率 [ 延伸閱讀 ]

image [ 圖片來源 ]

image [ 圖片來源 ]

透過image inspect來檢視映像檔的metadata,可以看到這個映像檔開了哪些port、有哪些環境變數以及建立的時候會執行的指令等

  1. $ docker image inspect nginx:alpine
  2. {
  3. "ContainerConfig": {
  4. ...
  5. "ExposedPorts": {
  6. "80/tcp": {}
  7. },
  8. "Env": [
  9. "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
  10. "NGINX_VERSION=1.15.5"
  11. ],
  12. "Cmd": [
  13. "/bin/sh",
  14. "-c",
  15. "#(nop) ",
  16. "CMD [\"nginx\" \"-g\" \"daemon off;\"]"
  17. ],
  18. ...
  19. },
  20. }

fork一份映像檔到自己的repo

  1. $ docker login
  2. $ docker image tag nginx:alpine ssivart/nginx
  3. $ docker image tag ssivart/nginx ssivart/nginx:testing
  4. docker image push ssivart/nginx:testing

註:

  • 在遠端機器使用docker login的時候,操作完要登出才會移除機器上儲存的密碼
  • 欲上傳private映像檔要先建立一個私人的repo

Dockerfile基本指令

範例1

  1. FROM debian:stretch-slim
  2. ENV NGINX_VERSION 1.13.6-1~stretch
  3. ENV NJS_VERSION 1.13.6.0.1.14-1~stretch
  4. RUN apt-get update \
  5. && apt-get install --no-install-recommends --no-install-suggests -y gnupg1 \
  6. && \
  7. NGINX_GPGKEY=573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62; \
  8. found=''; \
  9. for server in \
  10. ha.pool.sks-keyservers.net \
  11. hkp://keyserver.ubuntu.com:80 \
  12. hkp://p80.pool.sks-keyservers.net:80 \
  13. pgp.mit.edu \
  14. ; do \
  15. echo "Fetching GPG key $NGINX_GPGKEY from $server"; \
  16. apt-key adv --keyserver "$server" --keyserver-options timeout=10 --recv-keys "$NGINX_GPGKEY" && found=yes && break; \
  17. done; \
  18. test -z "$found" && echo >&2 "error: failed to fetch GPG key $NGINX_GPGKEY" && exit 1; \
  19. apt-get remove --purge -y gnupg1 && apt-get -y --purge autoremove && rm -rf /var/lib/apt/lists/* \
  20. && echo "deb http://nginx.org/packages/mainline/debian/ stretch nginx" >> /etc/apt/sources.list \
  21. && apt-get update \
  22. && apt-get install --no-install-recommends --no-install-suggests -y \
  23. nginx=${NGINX_VERSION} \
  24. nginx-module-xslt=${NGINX_VERSION} \
  25. nginx-module-geoip=${NGINX_VERSION} \
  26. nginx-module-image-filter=${NGINX_VERSION} \
  27. nginx-module-njs=${NJS_VERSION} \
  28. gettext-base \
  29. && rm -rf /var/lib/apt/lists/*
  30. RUN ln -sf /dev/stdout /var/log/nginx/access.log \
  31. && ln -sf /dev/stderr /var/log/nginx/error.log
  32. EXPOSE 80 443
  33. CMD ["nginx", "-g", "daemon off;"]
  • FROM: 開頭一定要加的,指定一個Base Image來初始化,通常是取用較輕量的分支如alpine
  • ENV: 設定環境變數
  • RUN: 每行指令都會往上建築新的layer(new layer on top),上面的範例用&&來連結每一行指令是常見的作法
  • EXPOSE: 容器沒有任何預設開啟的TCP/UDP埠號,而加了也不代表這些埠號會自動打開,要開啟埠號還是要透過container run -p
  • CMD: 容器執行或停止的時候都會執行的指令
    註:docker能自動幫我們處理logging,上面的範例將log送到stdout跟stderr,讓docker可以捕捉到這些log然後做後續處理

在dockerfile所在的目錄下build image

  1. $ docker image build -t customnginx:customtag .

稍微更改一下dockerfile再安裝一次,可以看出union file system怎麼運作的,dockfile頂端指令會動到的資料幾乎沒什麼變動:Using cache,而越接近後頭的指令,會有較多的變動

  1. $ docker image build -t customnginx:customtag .
  2. Sending build context to Docker daemon 4.096kB
  3. Step 1/7 : FROM debian:stretch-slim
  4. ---> 4b4471f624dc
  5. Step 2/7 : ENV NGINX_VERSION 1.13.6-1~stretch
  6. ---> Using cache
  7. ---> 5993fe34a11a
  8. Step 3/7 : ENV NJS_VERSION 1.13.6.0.1.14-1~stretch
  9. ---> Using cache
  10. ---> b1b9fa913700
  11. Step 4/7 : RUN apt-get update && apt-get install --no-install-recommends --no-install-suggests -y gnupg1 && NGINX_GPGKEY=573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62; found=''; for server in ha.pool.sks-keyservers.net hkp://keyserver.ubuntu.com:80 hkp://p80.pool.sks-keyservers.net:80 pgp.mit.edu ; do echo "Fetching GPG key $NGINX_GPGKEY from $server"; apt-key adv --keyserver "$server" --keyserver-options timeout=10 --recv-keys "$NGINX_GPGKEY" && found=yes && break; done; test -z "$found" && echo >&2 "error: failed to fetch GPG key $NGINX_GPGKEY" && exit 1; apt-get remove --purge -y gnupg1 && apt-get -y --purge autoremove && rm -rf /var/lib/apt/lists/* && echo "deb http://nginx.org/packages/mainline/debian/ stretch nginx" >> /etc/apt/sources.list && apt-get update && apt-get install --no-install-recommends --no-install-suggests -y nginx=${NGINX_VERSION} nginx-module-xslt=${NGINX_VERSION} nginx-module-geoip=${NGINX_VERSION} nginx-module-image-filter=${NGINX_VERSION} nginx-module-njs=${NJS_VERSION} gettext-base && rm -rf /var/lib/apt/lists/*
  12. ---> Using cache
  13. ---> 10da6c1aa8b6
  14. Step 5/7 : RUN ln -sf /dev/stdout /var/log/nginx/access.log && ln -sf /dev/stderr /var/log/nginx/error.log
  15. ---> Using cache
  16. ---> 9e33d35615e8
  17. Step 6/7 : EXPOSE 80 443 8080
  18. ---> Running in 1a79c9e87968
  19. Removing intermediate container 1a79c9e87968
  20. ---> efa93a97a5e1
  21. Step 7/7 : CMD ["nginx", "-g", "daemon off;"]
  22. ---> Running in d53275212fcd
  23. Removing intermediate container d53275212fcd
  24. ---> 873793860fdf
  25. Successfully built 873793860fdf
  26. Successfully tagged customnginx:customtag

範例2

  1. FROM nginx:latest
  2. WORKDIR /usr/share/nginx/html
  3. COPY index.html index.html
  • 如果你能從更末端的映像檔來build,例如FROM官方的nginx再做一些客製化,在維護dockerfiles時就會更加容易。如範例A到範例B
  • WORKDIR: 意同RUN cd /some/path,不過使用WORKDIR會更好
  • COPY: 這個範例用本地的index.html複寫原本nginx的index.html
  • 這個範例並沒有改寫CMD,這個指令會繼承nginx:latest的command

建立一個容器,訪問主頁面時預期會看到客製後的index.html

  1. $ docker image build -t customnginx:customtag .
  2. $ docker container run --rm -p 80:80 customnginx:customtag

練習:建立Dockerfile(alpine + node.js + tini)

  1. FROM node:6-alpine
  2. RUN apk update \
  3. && apk add --update tini \
  4. && mkdir -p /usr/src/app
  5. WORKDIR /usr/src/app
  6. COPY package.json /usr/src/app/
  7. RUN npm install && npm cache clean --force
  8. COPY . /usr/src/app/
  9. EXPOSE 3000
  10. CMD ["/sbin/tini", "--", "node", "./bin/www"]
  1. $ docker image build -t node:tini .
  2. $ docker container run -p 80:3000 node:tini

發布到Docker Hub

  1. $ docker image tag node:tiny ssivart/node:tini
  2. $ docker image push ssivart/node

持久化數據(Persisting Data)

容器的設計理念有兩個特性:

  1. immutable intrastucture: 當你需要更改設定,皆是透過重新建立新的容器
  2. ephemeral:無狀態,代表容器可以被關閉、銷毀或取代

延伸閱讀:
Pets vs Cattle Analogy

容器的特性帶來可靠性(iability)和一致性(consistency),不過相對地帶來一個問題,即如何維持持久化數據(如資料庫),針對這種關注點分離(Seperation of concerns, SOC)的架構設計問題,Docker提供了幾個方案:

  1. Volumes: 由Docker管理,存儲在宿主機的某個地方(在linux上是/var/lib/docker/volumes/)。非Docker應用程序不能改動這一位置的數據。Volumes是Docker最好的數據持久化方法
  2. Bind mounts: 數據可以存放在宿主機的任何地方。數據甚至可以是重要的系統文件或目錄。非Docker應用程序可以改變這些數據;適用於local 開發測試
  3. tmpfs mounts: 數據只存儲在宿主機的內存中,不會寫入到宿主機的文件系統
    [ 來源 ]

image

Volumes

先來看一下mysql:8 Dockerfile的VOLUME指令

  1. VOLUME /var/lib/mysql

意即當mysql的容器建立的時候,docker會在本機上新增一個volume location然後跟容器裡的數據目錄互通,這兩個路徑指向host同一個位址,資料只有靠手動方式才能移除,不會隨著容器被移除而消失

建立一個mysql容器然後看metadata

  1. $ docker container run --name mysql -d -e MYSQL_ALLOW_EMPTY_PASSWORD=True mysql
  2. $ docker container inspect mysql
  3. {
  4. ...
  5. "Mounts": [
  6. {
  7. "Type": "volume",
  8. "Name": "c7eaf7ddfa3ad5d90abbe0372d628f97ddcc2384dae45a35d60c74bfdac37416",
  9. "Source": "/var/lib/docker/volumes/c7eaf7ddfa3ad5d90abbe0372d628f97ddcc2384dae45a35d60c74bfdac37416/_data",
  10. "Destination": "/var/lib/mysql",
  11. "Driver": "local",
  12. "Mode": "",
  13. "RW": true,
  14. "Propagation": ""
  15. }
  16. ],
  17. ...
  18. "Config": {
  19. ...
  20. "Volumes": {
  21. "/var/lib/mysql": {}
  22. },
  23. ...
  24. },
  25. }

可以看到data儲存在主機/var/lib/docker/volumes/c7eaf7ddfa3ad5d90abbe0372d628f97ddcc2384dae45a35d60c74bfdac37416/_data的位置

而這在使用上會比較不友善:沒辦法從volume看出哪個容器是連結到自身

  1. $ docker volume ls
  2. DRIVER VOLUME NAME
  3. local 0ad326a8fbd002aed0d6ab977140b9ac45a3b056f9885d6cc308b981ff599e4c
  4. local 0ef3e7bc6e024c53b166fb6a2b5a1dead036d703f40608f7094c93569016fd62

這裡稍微改善的方式是用參數-v為volume命名(以專案來命名之類的)

  1. $ docker container run --name mysql -d -e MYSQL_ALLOW_EMPTY_PASSWORD=True -v mysql-db:/var/lib/mysql mysql
  2. $ docker volume ls
  3. DRIVER VOLUME NAME
  4. local mysql-db

source的path也變得比較乾淨易讀

  1. ...
  2. "Source": "/var/lib/docker/volumes/mysql-db/_data",
  3. ...

另外Volume也可以是匿名的(anonymous volume),會分配一個隨機的名字,在同一個主機中不會重覆[ 官方文件 ]

Bind Mounting

  • host優先於container
  • 不能在Dockerfile裡使用,只能透過container run
  • 通常是用-v指令,或--mount,格式為/path/host:/path/container,如
    1. $ docker container run -v /Users/ssivart/stuff:/path/container # linux / mac
    2. $ docker container run -v //c/Users/ssivart/stuff:/path/container # windows

    範例

    新增一個nginx容器,把/home/ssivart/桌面目錄mount至容器的/usr/share/nginx/html
    1. $ cd /home/ssivart/桌面
    2. $ docker container run --name nginx -d -p 80:80 -v $(pwd):/usr/share/nginx/html nginx
    接著我在/home/ssivart/桌面新增一個index.html,內容為<h1>Hello World! Bind Mount</h1>
    1. $ vim index.html
    預期在localhost:80會看到我更改過的首頁內容

-v--mount的差別[ 文件 ]

現在這兩個指令差別僅在於如果host上目錄不存在,使用-v會幫你建立新的目錄,使用--mount會顯示錯誤

練習:postgres版本更新用Volumes保持數據

  1. $ docker container run --name pg1 -d -v psql:/var/lib/postgresql/data postgres:9.6.1-alpine
  2. $ docker container stop pg1
  3. $ docker container run --name pg2 -d -v psql:/var/lib/postgresql/data postgres:9.6.2-alpine
  1. $ docker volume ls
  2. DRIVER VOLUME NAME
  3. local psql
  1. $ docker container logs pg1
  2. ...
  3. LOG: database system is ready to accept connections
  4. LOG: autovacuum launcher started
  5. LOG: received smart shutdown request
  6. LOG: autovacuum launcher shutting down
  7. LOG: shutting down
  8. LOG: database system is shut down
  9. $ docker container logs pg2
  10. LOG: database system was shut down at 2018-11-05 08:07:27 UTC
  11. LOG: MultiXact member wraparound protections are now enabled
  12. LOG: database system is ready to accept connections
  13. LOG: autovacuum launcher started

練習:使用Bind Mount架設ruby + jekyll[ 來源 ]

  1. $ docker container run -p 80:4000 -v $(pwd):/site -it bretfisher/jekyll-serve sh
  2. /site # bundle lock --add-platform x86-mingw32 x86-mswin32 x64-mingw32 java
  3. /site # bundle exec jekyll serve --force_polling -H 0.0.0.0 -P 4000

Docker Compose[ 官方文件 ]

  • 應用程式常常需要結合多個容器如SQL、proxy、網頁和後端排程等,docker compose便是用來設置容器之間的關係
  • 一鍵完成
  • 可以透過Docker Swarm 1.13以上版本部屬compose file

Docker compose包含兩個部份:

  1. docker-compose.yml: YAML格式的文件來設定容器、網路、Volumes的hierarchy

    • 有區分版本,如1, 2, 2.1
    • -f來讀取特定檔案,預設讀取的檔名是docker-compose.yml
  2. docker-compose: 命令列(CLI)工具用來測試compose file

以下是compose file範例:

  1. version: '3.1' # 預設是v1,但v1功能有限,建議最少設為v2
  2. services:
  3. servicename: # 容器名稱,這個名稱就是容器的DNS name
  4. image: # 非必須,如果你需要build映像檔使用
  5. command: # 非必須, 同docker container run [OPTIONS] IMAGE [COMMAND]的[COMMAND]
  6. environment: # 非必須, 同docker run的-e
  7. volumes: # 非必須, 同docker run的-v
  8. servicename2:
  9. volumes: # 非必須, 同docker volume create
  10. networks: # 非必須, 同docker network create

一個簡單的proxy network設定如下

  1. services:
  2. proxy:
  3. image: nginx:1.13 # this will use the latest version of 1.13.x
  4. ports:
  5. - '80:80' # expose 80 on host and sent to 80 in container
  6. volumes:
  7. - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
  8. web:
  9. image: httpd # this will use httpd:latest

docker compose CLI

  • 請參考上方提到的安裝流程
  • 僅用於開發測試端,非正式部屬使用

常用指令

  • docker-compose up: 設定volumes/networks,執行所有容器,使用-d讓程序於背景執行
  • docker-compose down: 停止並移除所有容器,常用-v來移除所有volumes/networks

練習: drupal + postgres架站

  1. version: '3'
  2. services:
  3. psql:
  4. image: postgres:latest
  5. environment:
  6. POSTGRES_USER: postgres
  7. POSTGRES_PASSWORD: mypassword
  8. restart: always
  9. web:
  10. image: drupal:latest # this will use the latest version
  11. ports:
  12. - '8080:80' # expose 80 on host and sent to 80 in container
  13. volumes:
  14. - drupal-modules:/var/www/html/modules
  15. - drupal-profiles:/var/www/html/profiles
  16. - drupal-sites:/var/www/html/sites
  17. - drupal-themes:/var/www/html/themes
  18. links:
  19. - psql:postgres
  20. volumes:
  21. drupal-modules:
  22. drupal-profiles:
  23. drupal-sites:
  24. drupal-themes:

使用compose來建置(build)客製化映像檔

  • 使用docker-compose up,如果找不到該映像檔的cache,會在當下建置
  • re-build使用docker-compose up --builddocker-compose build

先來看以下compose範例:

  1. version: '2'
  2. # based off compose-sample-2, only we build nginx.conf into image
  3. # uses sample site from https://startbootstrap.com/template-overviews/agency/
  4. services:
  5. proxy:
  6. build:
  7. context: .
  8. dockerfile: nginx.Dockerfile
  9. image: nginx-custom
  10. ports:
  11. - '80:80'
  12. web:
  13. image: httpd
  14. volumes:
  15. - ./html:/usr/local/apache2/htdocs/

當proxy容器執行時,會先在cache找nginx-custom這個映像檔,找不到會build,這邊示範了在context提供的目錄下用dockerfile來建置映像檔

  1. $ docker-compose down
  2. $ docker image ls
  3. REPOSITORY TAG IMAGE ID CREATED SIZE
  4. nginx-custom latest 4e0717a21563 6 minutes ago 109MB

執行完down,docker並不會主動移除客製化映像檔,針對要移除上面範例經過命名的映像檔,down指令加上--rmi all參數,如果是不提供映像檔名稱,docker會以<bulid directory>_<container name>的規則來命名映像檔,可以透過--rmi local移除

composefile配置

這個部份官方還在持續更新
往後針對部署端的compose還會有許多變動(swarm、stack、secrets…),之後會持續更新

  1. compose files
  2. ├── docker-compose.yml # base
  3. ├── docker-compose.override.yml # local
  4. ├── docker-compose.test.yml # ci test
  5. └── docker-compose.prod.yml # deploy

docker-compose會自動辨識.override.yml的檔案,.test.yml.prod.yml或其他自訂的compose file則需要手動透過指令-f來操作

本地開發端:docker-compose.yml + docker-compose.override.yml

  1. $ docker-compose up

CI測試:docker-compose.yml + docker-compose.test.yml

  1. $ docker-compose -f docker-compose.yml -f docker-compose.test.yml up -d

部署通常會用config輸出成一個完整的compose file:docker-compose.yml + docker-compose.prod.yml

  1. $ docker-compose -f docker-compose.yml -f docker-compose.prod.yml config > production.yml
  2. $ docker-compose -f production.yml

Swarm Mode

  • Swarm用來做(叢集)架構管理
  • Swarm內建於Docker
  • 多個Docker host組成1個Swarm mode
  • Swarm程式獨立於Docker,在Docker環境下透過swarmkit運行(須先啟用)

Node

Swarm中的Docker的實例(Docker host)

image

  • Worker:
    • 負責容器的執行
  • Manager:
    • 在Swarm中透過Raft Database的設定進行協調與同步
    • 負責管理Worker與協調容器的部署工作
    • 也可以當Worker,Manager可以想像成有Swarm控制權限的Worker

兩個角色可以互換,node之間透過雙向TLS(mutual Transport Layer Security, 前身是SSL)協定溝通

image

Service and Task

  • Task: Container + Command(怎麼run這個容器)
  • Service: Task A + Task B + Task C(任務的堆疊),基於docker run的再封裝

Swarm會確保services持續運作

在新的處事方式上,服務器被編好號,就像牛在牛群中。比如,www001到www100。當一個服務器宕機了,它將會被取出替換上線 —— Randy Bias

image

Swarm運作

image [ 圖片來源 ]

基本指令

docker swarm init可以新增一個swarm,其中完成這些動作[ 文件 ]

  1. 初始公開金鑰基礎架構(public key infrastructure, PKI)
    • Docker先扮演第一個Manager node(root manager)
    • 產生一組root certificate authority(.ca)
    • 產生token: worker token + manager token
    • 其他node可以用這組token加入(join)swarm
  2. 初始Raft database
    • 儲存憑證
    • 在control plane讓Manager之間共享log,透過TLS
    • 儲存config data[ 文件 ]

初始後可以看到第一個manager node

  1. $ docker swarm init
  2. $ docker swarm ls
  3. ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS
  4. uvemtlsi7743iqxih5tkwt9v8 * xps13 Ready Active Leader

建立一個service,指定task的容器執行的指令及參數(docker run)

  1. $ docker service create --name myservice -d alpine ping 8.8.8.8
  2. $ docker service ls
  3. ID NAME MODE REPLICAS IMAGE
  4. u2br67fsvqsi myservice replicated 1/1 alpine:latest

檢視service的task,可以看到一個執行中的容器

  1. $ docker service ps myservice
  2. ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
  3. sfge35webgpo myservice.1 alpine:latest xps13 Running Running about a minute ago

更新service的參數,提高task數量(replicas)來sacle up

  1. $ docker service update myservice --replicas 3
  2. 1/3: running [==================================================>]
  3. 2/3: running [==================================================>]
  4. 3/3: running [==================================================>]
  5. $ docker service ps myservice
  6. ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
  7. sfge35webgpo myservice.1 alpine:latest xps13 Running Running 8 minutes ago
  8. 57mukap2p6ck myservice.2 alpine:latest xps13 Running Running 30 seconds ago
  9. mfui4zrgdop4 myservice.3 alpine:latest xps13 Running Running 30 seconds ago

手動停止其中一個容器,service會再add一個task到service queue取代之,確保同時會有3個正常運作的task

  1. $ docker container rm -f myservice.1.sfge35webgpo0nnmejuqsbvnz
  2. $ docker service ps myservice
  3. 1o792ip8k75i myservice.1 alpine:latest xps13 Running Running 6 seconds ago
  4. sfge35webgpo \_ myservice.1 alpine:latest xps13 Shutdown Failed 11 seconds ago "task: non-zero exit (137)"
  5. 57mukap2p6ck myservice.2 alpine:latest xps13 Running Running 4 minutes ago
  6. mfui4zrgdop4 myservice.3 alpine:latest xps13 Running Running 4 minutes ago

要終止task必須移除整個service,同時會把當中所有的程序清理掉

  1. $ docker service rm myservice

更新services

指令範例

  1. $ docker service scale web=2
  2. $ docker service update --image nginx:1.13.6 web
  3. $ docker service update --publish-rm 80 --publish-add 8080:80 web

service更新其中的tasks都會重建,task建立時docker會挑負載較小的node做較多分配,所以對service做force update可以平衡node之間的負載

  1. $ docker service update --force web

CLI更新

service createservice update在不同版本上的參數變更:

  1. 17.05前:必須都要透過service lsservice ps來檢查是否正常執行
  2. 17.05後:新增了--detach參數,預設為true
  3. 17.10後:--detach參數,預設改為false
    結論是17.12版本後要透過shell scripts或automation來建立service的話,記得設定--detach=true

使用GCP Compute Engine Instance Group來試作多節點Swarm

  1. node-group-1-5188:~$ docker swarm init
  2. Swarm initialized: current node (jxq78ujvpd196whvzos74vqjg) is now a manager.
  3. node-group-1-c1m7:~$ docker swarm join --token SWMTKN-1-0hjsesqene40g6w9pw5usrcfti72bwhqwwccp57xclcozqtn5
  4. h-ab4c4ihrdaekal7sbpww3ga38 10.132.0.2:2377
  5. This node joined a swarm as a worker.

worker node沒有swarm的控制權

  1. node-group-1-c1m7:~$ docker node ls
  2. Error response from daemon: This node is not a swarm manager. Worker nodes can't be used to view or modify cluster sta
  3. te. Please run this command on a manager node or promote the current node to a manager.

提昇host的worker成manager,node狀態變成Reachable

  1. node-group-1-5188:~$ docker node update --role manager node-group-1-c1m7
  2. node-group-1-5188:~$ docker node ls
  3. ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGIN
  4. E VERSION
  5. jxq78ujvpd196whvzos74vqjg * node-group-1-5188 Ready Active Leader 18.09
  6. .0
  7. exs01fiaqanf0efveae7d610c node-group-1-c1m7 Ready Active Reachable 18.09
  8. .0

Overley Network

跨Node間的網路拓樸,基於Routing Mesh

  1. node-group-1-5188:~$ docker network create --driver overlay mydrupal
  2. node-group-1-5188:~$ docker service create --name psql --network mydrupal -e POSTGRES_PASSWORD=example postgres
  3. node-group-1-5188:~$ docker service create --name drupal --network mydrupal -p 80:80 drupal

psql在node1,drupal在node2,node2可以透過dns name:psql來存取node1的資料庫,用80 port連到node1也能看到drupal的歡迎頁面

  1. node-group-1-5188:~$ docker service ps psql
  2. ID NAME IMAGE NODE DESIRED STATE CURRENT STATE
  3. ERROR PORTS
  4. w35eiu38yfii psql.1 postgres:latest node-group-1-5188 Running Running 10 minutes
  5. ago
  6. node-group-1-5188:~$ docker service ps drupal
  7. ID NAME IMAGE NODE DESIRED STATE CURRENT STATE
  8. ERROR PORTS
  9. 0blpaalwe5qi drupal.1 drupal:latest node-group-1-c1m7 Running Running about a mi
  10. nute ago

Routing Mesh

  • 為service底下的task提供路由(routes ingress packets)
    • container-to-container use Virtual IP
    • external traffic incomming to publish ports(all nodes listen)
  • 為services提供負載平衡
    • stateless load balancer,如果需要指向特定container的話,須另外設定(如cookie、session)
    • 使用linux既有的IPVS(IP Virtual Server)實現load balancing
    • load balancer表現於TCP層的(OSI第3層),針對一個swarm一個對外port的web架構,還會需要Nginx、HAProxy來做DNS層(第4層)的load balancer
  1. 無論訪問網路中的哪個節點,即使該節點上沒有運行該service的副本,最終都能訪問到該service
    舉例來說,如果後端資料庫有3個副本,當前端web server要取資料時,並非直接訪問某個資料庫副本的ip,而是透過swarm為所有service搭建的Virtual IP(VIP)

  2. 外部流量導向所有節點共同監聽的public port

image

image

練習:voting app[ dockersamples ]

image

首先先建立前後端的網路,跨node要用overlay模式

  1. $ docker network create --driver overlay backend
  2. $ docker network create --driver overlay frontend
  1. $ docker service create --name vote --network frontend --replicas 3 -p 80:80 dockersamples/examplevotingapp_vote:before
  2. $ docker service create --name redis --network frontend redis:3.2
  3. $ docker service create --name worker --network frontend --network backend dockersamples/examplevotingapp_worker
  4. $ docker service create --name db --network backend --mount type=volue,source=/var/lib/postgresql/data postgres:9.4
  5. $ docker service create --name result --network backend -p 5001:80 dockersamples/examplevotingapp_result:before
  1. ssivart@ds-1z84:~$ docker service ls
  2. ID NAME MODE REPLICAS IMAGE PORTS
  3. wxh8d5b9vb45 db replicated 1/1 postgres:9.4
  4. 8c9t5hehhl6c redis replicated 1/1 redis:3.2
  5. zka68kizvla8 result replicated 1/1 dockersamples/examplevotingapp_result:before *:5001->80/tcp
  6. gbed0pcrympq vote replicated 3/3 dockersamples/examplevotingapp_vote:before *:80->80/tcp
  7. s0ese5a8wlna worker replicated 1/1 dockersamples/examplevotingapp_worker:latest

Stacks

  • dockerfile版本3以上
  • 適用於部署端的compose(services + volumes + overlay networks + secrets…)
  • composefile指令跟local端的差異:開發端忽略deploy指令,部署端忽略build
  • 部署不透過docker-compose
  • 針對已經存在的service,再部署會update這些service

上述的範例只要透過一個Composefile就能完成所有部署

  1. $ docker stack deploy -c example-voting-app-stack.yaml voteapp
  2. $ docker stack services voteapp
  3. ID NAME MODE REPLICAS IMAGE PORTS
  4. 7alt1ix5vefl voteapp_worker replicated 1/1 dockersamples/examplevotingapp_worker:latest
  5. 7zx6l55sybey voteapp_vote replicated 2/2 dockersamples/examplevotingapp_vote:before *:5000->80/tcp
  6. bp4hdcrig4pb voteapp_redis replicated 1/1 redis:alpine
  7. lbejdwn5d9je voteapp_visualizer replicated 1/1 dockersamples/visualizer:stable *:8080->8080/tcp
  8. qvp0jwokpi40 voteapp_result replicated 1/1 dockersamples/examplevotingapp_result:before *:5002->80/tcp
  9. thzyq45ib40w voteapp_db replicated 1/1 postgres:9.4

swarm的可視化工具visualizer

image

Secrets

  • 儲存:
    • 使用者帳密
    • TLS憑證、金鑰
    • SSH金鑰
    • 自訂設定檔
  • 支援動態字串或二進位的內容
  • 儲存在Raft database,只存在於各個manager node的硬碟空間
  • secrets產生後會先存在swarm再分配到特定service,只有特定的container能訪問
  • 乍看之下是個實體檔案,但實際上是ramfs files system透過記憶體儲存
  • secrets只適用於swarm,而swarm只適用於部署端(production),但docker讓docker-compose也能使用,僅提供開發測試,不具真的功能(fake secure)
  1. $ docker secret create psql_user psql_user.txt
  2. zbqddocct68y2evhxy7krfxbd
  3. $ echo "mypassword" | docker secret create psql_pass -
  4. zy2da4ok40jxjc6t3kliw91io
  5. $ docker secret ls
  6. ID NAME DRIVER CREATED UPDATED
  7. zy2da4ok40jxjc6t3kliw91io psql_pass 2 minutes ago 2 minutes ago
  8. zbqddocct68y2evhxy7krfxbd psql_user

secrets無法透過指令檢視內容(廢話),容器要存取secret得先經過指定

  1. $ docker service create --secret psql_user --secret psql_pass

但這樣還少了怎麼配置這些secret,docker提供一個方便的查找方式,前提是映像檔要有這些變數

  1. $ docker service create --secret psql_pass -e POSTGRES_PASSWORD_FILE=/run/secrets/psql_pass

終端機進容器檢視密碼

  1. $ docker container exec -it psql.1.g2wh4ybeidc70vifm4xvtl9lf bash
  2. root@0727ea1882ad:/# cat /run/secrets/psql_pass
  3. mypassword

針對已經建立的service可以透過update --secret-rmupdate --secret-add來更新secret,但這樣做會導致整個service重新部署

secrets透過stack部署

  • dockerfile版本3.1以上
  1. version: "3.1"
  2. services:
  3. psql:
  4. image: postgres
  5. secrets:
  6. - psql_user
  7. - psql_password
  8. environment:
  9. POSTGRES_PASSWORD_FILE: /run/secrets/psql_password
  10. POSTGRES_USER_FILE: /run/secrets/psql_user
  11. secrets:
  12. psql_user:
  13. file: ./psql_user.txt
  14. psql_password:
  15. file: ./psql_password.txt

透過實體檔案儲存密碼的方式是有風險的,記得在部署完成後移除這些檔案

開發端測試secrets

  • docker-compose CLI版本11以上適用
  • 開發端是使用bind mount的方式把secrets mount到本機目錄,只是用於開發並無secret實際功能
  • 不適用內含external用法,如果部署是用external:true,可以另外維護一個開發用的compose file,secret的部份改用file來寫入
    1. $ docker-compose up -d
    2. $ docker-compose exec psql cat /run/secrets/psql_pass
    3. mypassword

Healthchecks

  • 回傳0(OK)或1(Error)
  • 支援Dockerfile、Compose、docker run及swarm services
  • 容器有三種健康狀態: starting, healthy, unhealthy

container

  1. $ docker container run --name psql -d --health-cmd="pg_isready -U postgres || exit 1" postgres
  2. $ docker container ls
  3. CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
  4. 44d6df2dcb5a postgres "docker-entrypoint.s…" 38 seconds ago Up 37 seconds (healthy) 5432/tcp psql
  1. $ docker container inspect psql
  2. {
  3. ...
  4. "State": {
  5. ...
  6. "Health": {
  7. "Status": "healthy",
  8. "FailingStreak": 0,
  9. "Log": [
  10. {
  11. "Start": "2018-11-14T05:52:14.562405278Z",
  12. "End": "2018-11-14T05:52:16.32755068Z",
  13. "ExitCode": 0,
  14. "Output": "/var/run/postgresql:5432 - accepting connections\n"
  15. },
  16. {
  17. "Start": "2018-11-14T05:52:46.338006182Z",
  18. "End": "2018-11-14T05:52:46.588202392Z",
  19. "ExitCode": 0,
  20. "Output": "/var/run/postgresql:5432 - accepting connections\n"
  21. },
  22. ...
  23. ]
  24. },
  25. ...
  26. }
  27. ...
  28. }

services

  1. $ docker service create --name psql -d --health-cmd="pg_isready -U postgres || exit 1" postgres

Docker Registry

  1. $ docker container run -d -p 5000:5000 --name registry registry
  2. $ docker image tag hello-world 127.0.0.1:5000/hello-world
  3. $ docker image ls
  4. 127.0.0.1:5000/hello-world latest 4ab4c602aa5e 2 months ago 1.84kB
  5. hello-world latest 4ab4c602aa5e 2 months ago 1.84kB
  1. $ docker image push 127.0.0.1:5000/hello-world

push之後映像檔的實體檔案儲存在/var/lib/registry/

Registry in Swarm mode

  • 主要問題是解決nodes之間要訪問同一份映像檔
  • 如果只在其中一個manager node build image,其他node是沒辦法取得該image
  • 解法:
    • 使用Docker hub、AWS、Quay來儲存管理映像檔
    • 借助Routing Mesh,所有node都能藉由127.0.0.1:5000訪問

以下示範如何使用Routing Mesh建立registry

  1. $ docker service create --name registry --publish 5000:5000 registry
  2. $ docker pull nginx
  3. $ docker tag hello-world 127.0.0.1:5000/nginx
  4. $ docker push 127.0.0.1:5000/nginx

將映像檔push到registry後,所有nodes都能從127.0.0.1:5000取得映像檔

  1. $ docker service create -p 80:80 --replicas 5 -d 127.0.0.1/nginx