2018年12月18日 星期二

Docker Compose 初步閱讀與學習記錄

上次在看 Dockerfile 時,有注意到一個工具,Docker Compose。透過這個工具,可以先寫一份檔案,預先定義好多個 service,然後透過單一命令來啟動多個 container 執行你定義的 service,讓他們組成一個你想要的應用服務。直覺這是個 docker run 命令的進階工具,值得看看,因此就花點時間閱讀了官方文件,以下是一些簡單的學習記錄。

什麼是 Docker Compose

Docker Compose 官方說明文件 Overview of Docker Compose 已經有些簡介,以下為官方文件的部分翻譯:

Compose 是一個工具,用來定義與執行多個 container 組成的 Docker Applications。你可以使用 Compose 檔案來組態設定你的應用服務。然後使用單一命令,透過你的組態設定來建立與啟動你的服務。

Compose 適合用來開發、測試、與建立 staging 環境,如同 CI workflows。

使用 Compose 有基本的三個處理步驟:

  1. 使用 Dockerfile 定義你的 app 環境,讓它可以在任何地方都能複製(reproduced)。
  2. 使用 docker-compose.yml 定義你的服務,讓他們可以在獨立環境內一起執行。
  3. 最後,執行 docker-compose up,Compose 將會開始與執行你所有的 app。

docker-compose.yml 檔案看起來像這樣

  version: '2'
  services:
    web:
      build: .
      ports:
      - "5000:5000"
      volumes:
      - .:/code
      - logvolume01:/var/log
      links:
      - redis
    redis:
      image: redis
  volumes:
    logvolume01: {}

docker-compose.yml 也就是組態設定文件,是一種 yaml 格式撰寫的文件,可以上維基看一下 YAML 格式說明,比較需要注意的是他的縮排規定要空白鍵,而不是 tab。

安裝

如果你和我一樣是 Mac 的使用者,在安裝 Docker for Mac 時,就會安裝 docker-compose 工具。

若是其他作業系統使用者,可以到 github - docker/compose 查看最新版本的 docker-compose 與如何下載安裝。

Compose file version 3

目前最新的 Compose file 為版本 3,此文件會分成三大組態設定,分別為 services、networks 以及 volumes:

  • services (top-level)

    services 主要讓你定義你應用服務啟動時,用來執行的 container 相關資訊,比如說 image 是用哪個、是不是要透過 Dockerfile 先進行編譯、要不要覆寫預設 command 或是 entrypoint、環境變數為何、port 導出與對應等等的,這些參數多半在 docker run 指令有相對應的參數。可以參照著看。

    service 雖然提供許多組態設定,但是如果你已經知道使用的 image 或是你自己寫的 Dockerfile 原本有寫了,就不用在這邊在寫一次,比如說像等等範例會用到的 wordpress image,他預設就有定義 entrypoint,這時你就不用在這邊重新定義一次。

    以下簡介一些 services 底下的組態設定:

    • build

      如果你的 service 要使用 Dockerfile 來建立,你可以透過此設定,指定你 Dockerfile build context 在哪,如果你 build 和 image 兩個設定一起使用,則透過 Dockerfile 建立起來的 image 會被命名成你在 image 設定寫的名稱。需要注意的是,要透過 docker stack 指令部署到 swarm 上時,這設定會失效,因為 docker stack 只接受預先建好的 image。

    • deploy

      只有在 Compose file 版本 3 才能使用。此設定只有在使用 docker stack deploy 指令將應用服務部署到 swarm 上才會有作用,在 docker-compose up、docker-compose down 會被忽略。

    • depends_on

      可以定義 service 之間的相依性,比如說官方範例:

      version: '2'
      services:
        web:
          build: .
          depends_on:
            - db
            - redis
        redis:
          image: redis
        db:
          image: postgres

      當下 docker-compose up 指令時,db service 和 redis service 會先啟動,然後在啟動 web service。

    • environment

      定義環境變數,注意,如果值是布林,要用單引號包起來,如 'true'、'false'。否則 YML 解析器會把它變成 True 或是 False。以下為官方範例:

      environment:
        RACK_ENV: development
        SHOW: 'true'
        SESSION_SECRET:
      
      environment:
        - RACK_ENV=development
        - SHOW=true
        - SESSION_SECRET
    • image

      指定 container 要從哪個 image 啟動。以下為官方範例:

      image: redis
      image: ubuntu:14.04
      image: tutum/influxdb
      image: example-registry.com:4000/postgresql
      image: a4bc65fd
    • networks

      container 要加入哪個網路,這邊的項目會參考到最外層 networks 的設定。

    • ports

      讓你指定要導出的 port,可以是 HOST:CONTAINER,或是只指定 CONTAINER,這時會隨機挑一個 HOST Port 來用。注意,port 指定最好都用字串,因為 YAML 解析器在你挑的 port 小於 60 時會出問題。以下為官方範例:

      ports:
       - "3000"
       - "3000-3005"
       - "8000:8000"
       - "9090-9091:8080-8081"
       - "49100:22"
       - "127.0.0.1:8001:8001"
       - "127.0.0.1:5000-5010:5000-5010"
       - "6060:6060/udp"
    • volumes, volume_driver

      設定 container 要使用的 volume,可以是一個路徑或是參考到最外層 volume 的設定,格式為 HOST:CONTAINER,你也可以只寫 CONTAINER,讓 Docker 自動幫你建立一個。參考官方範例:

      volumes:
        # Just specify a path and let the Engine create a volume
        - /var/lib/mysql
      
        # Specify an absolute path mapping
        - /opt/data:/var/lib/mysql
      
        # Path on the host, relative to the Compose file
        - ./cache:/tmp/cache
      
        # User-relative path
        - ~/configs:/etc/configs/:ro
      
        # Named volume
        - datavolume:/var/lib/mysql
  • networks (top-level)

    networks 讓你設定網路。類似 docker network 指令。可以和 servcies 區塊內的 network 搭配使用,例如:

    version: '3'
    
    services:
      t1:
        image: tomcat:8.5.11-jre8
        networks:
          - test-net
      t2:
        image: tomcat:8.5.11-jre8
        networks:
          - test-net
    networks:
      test-net:
  • volumes (top-level)

    volumes 讓你處理資料共享與資料持久(persist)。類似 docker volume 指令。可以和 servcies 區塊內的 volume 搭配使用,比如說像官方範例:

    version: "3"
    
    services:
      db:
        image: db
        volumes:
          - data-volume:/var/lib/db
      backup:
        image: backup-service
        volumes:
          - data-volume:/var/lib/backup/data
    
    volumes:
      data-volume:

特別要注意的地方是,雖然官方說 Compose file 分成三大組態設定,但是其實最上面還有一個 version 設定,這是一定要加的,不然在執行 docker-compose up 時會一直出現無法理解的錯誤。例如:

version: "3"
services:
  foobar:
    image: tomcat:8.5.11-jre8
    networks:
      - foobar-nets
volumes:
  foobar-volume:
networks:
  foobar-nets:

更詳細的 Compose file 組態設定介紹建議可以參考官方網站 Compose file version 3 reference

Sample

推薦官方 WordPress 範例 Quickstart: Compose and WordPress 。雖然他是版本 2 的文件,但是裡面其實已經包含了 docker-compose 的精神,使用單一文件定義服務,之後透過一個指令啟動服務。

首先開啟終端機,在自己本機上建立一個目錄,用來放置 docker-compose.yml 檔案 例如:

# cd /tmp
# mkdir -p docker/compose_wordpress
# cd docker/compose_wordpress
# pwd
/tmp/docker/compose_wordpress 

接著建立 docker-compose.yml

# vi docker-compose.yml

以下為官方網頁上的 docker-compose.yml 檔案內容:

version: '2'

services:
   db:
     image: mysql:5.7
     volumes:
       - db_data:/var/lib/mysql
     restart: always
     environment:
       MYSQL_ROOT_PASSWORD: wordpress
       MYSQL_DATABASE: wordpress
       MYSQL_USER: wordpress
       MYSQL_PASSWORD: wordpress

   wordpress:
     depends_on:
       - db
     image: wordpress:latest
     ports:
       - "8000:80"
     restart: always
     environment:
       WORDPRESS_DB_HOST: db:3306
       WORDPRESS_DB_PASSWORD: wordpress
volumes:
    db_data:

此範例定義從 services 區塊來看,定義了兩個服務,分別為 db 與 wordpress。

db service 指定 image 為 mysql:5.7,然後使用 volume db_data 對應到 container 的目錄 /var/lib/mysql,設定 volume 是因為當 docker-compose down 指令執行時,會將 container 停止並移除,所以資料是會完全消失的,必須把 MySQL 的儲存資料透過 volume 永久保存。然後他設定 restart 為 always。接著他設定 MySQL 預設環境變數,這部分可以參考 Docker Store - MySQL,這邊可以找到 MySQL 官方 image 環境變數的說明。

接著是 wordpress service,他先寫了 depends_on db 這組態設定,意思是 wordpress service 和 db service 有相依性的關係,因此在下 docker-compose up 指令時,會先啟動 db service 的 container,接著才是 wordpress service 的 container。container 使用的 image 為 wordpress:latest image,然後將 port 導到本機的 8000 port,restart 策略為 always,最後在設定環境變數。

docker-compose.yml 檔案準備好之後,在終端機輸入:

# docker-compose up -d
Starting composewordpress_db_1
Starting composewordpress_wordpress_1
Creating network "composewordpress_default" with the default driver
Creating volume "composewordpress_db_data" with default driver
Pulling db (mysql:5.7)...
5.7: Pulling from library/mysql
....
Pulling wordpress (wordpress:latest)...
latest: Pulling from library/wordpress
....
Creating composewordpress_db_1
Creating composewordpress_wordpress_1

會看到首先 docker 會先建立所需的 volume 與一個全新的 network 讓你的應用服務使用,接著會幫你先確認你定義的 service 所需的 image 是否有抓下來了,最後會建立 container 來執行你定義的 service。接著你就可以在你的瀏覽器上輸入網址,連到 WordPress 的管理頁面了:

http://your-docker-host-ip:8000

當你下 docker-compose down 時,你會看到兩個 container 被停止,然後被刪除,建立的 network 也會被刪除。可以用 docker ps -a 驗證一下。

# docker-compose down
Stopping composewordpress_wordpress_1 ... done
Stopping composewordpress_db_1 ... done
Removing composewordpress_wordpress_1 ... done
Removing composewordpress_db_1 ... done
Removing network composewordpress_default

如果你再一次使用 docker-compose up 命令,Docker 又會把所需的 network 以及兩個 container 建立起來並執行,由於 MySQL 有使用 volume 的關係,因此你可以打開 browser 確認一下,資料都還在,不需在重新設定。

# docker-compose up -d
Creating network "composewordpress_default" with the default driver
Creating composewordpress_db_1
Creating composewordpress_wordpress_1

這兩個範例可以改成手動用 docker run 執行,docker-compose up -d 會變成:

# docker network create wordpress
# docker run -d -v db_data:/var/lib/mysql --name sample_mysql --network wordpress --restart always -e MYSQL_ROOT_PASSWORD=wordpress -e MYSQL_DATABASE=wordpress -e MYSQL_USER=wordpress -e MYSQL_PASSWORD=wordpress mysql:5.7
# docker run -d -p 8000:80 --name sample_wordpress --network wordpress --restart always -e WORDPRESS_DB_HOST=sample_mysql:3306 -e WORDPRESS_DB_PASSWORD=wordpress wordpress:latest

要注意要先建立一個 network,讓兩個 container 在同一個 network 下執行,wordpress 才能使用 mysql 的 container name 找到 DB。

而 docker-compose down 會變成:

# docker stop sample_wordpress
# docker stop sample_mysql
# docker rm sample_wordpress
# docker rm sample_mysql
# docker network rm wordpress

小結

從上述自己手動啟動與停止應用服務的多行命令就可以看出 Docker Compose 的強大與方便了。使用 Docker Compose 可以透過組態設定幫你處理好自己下 docker run 時要下的多個參數,而且能將多個 container 組織成一個應用服務,並管理 container 的相依性;另外透過閱讀 docker-compose.yml 檔案,也可以很容易的了解到整個應用服務的組成與架構。是個軟體開發與測試時能好好運用的工具。

ref

Install Docker Compose

Get started with Docker for Mac

github - docker/compose

Docker Compose

Overview of Docker Compose

wikipedia - YAML

YAML 语言教程

Compose file version 3 reference

Quickstart: Compose and WordPress

談談 Docker network-alias

在使用 docker run 指令啟動 container 時,有一個與 network 有關的 flag,為 network-alias,可以為 container 在網路拓墣取一個別名,讓其他 container 透過這個別名找到該 container。一開始看到時,不知道能拿來做什麼,想說我透過 container name 就能直接找到我要的 container 了,為何還要在另外命名?但是最近在弄新的產品需求時,卻剛好有需要用到 network-alias 的應用場景出現,因此想做個記錄。

network-alias 效果

container 在自定義的 network 底下時,可以使用 container name 找到目標對象,若對象有定義 network-alias 時,則也可以使用 network-alias 找到目標。比如說以下範例,建立兩個 container,名為 foo1 與 foo2,將他們加到 foonet 這個自定義的 network 底下,然後 foo2 取 network-alias 為 bar2。使用 docer exec 指令進入到 foo1 的 bash 後,試著執行 ping foo2 與 ping bar2,都是能找到相同的 container。

docker network create --driver bridge foonet
docker run -d --name foo1 --network foonet tomcat:8.5.11-jre8
docker run -d --name foo2 --network foonet --network-alias bar2 tomcat:8.5.11-jre8
docker exec -it foo1 /bin/bash

root@3769d24e6700:/usr/local/tomcat# ping foo2 -c 3
PING foo2 (172.18.0.3): 56 data bytes
64 bytes from 172.18.0.3: icmp_seq=0 ttl=64 time=0.172 ms
64 bytes from 172.18.0.3: icmp_seq=1 ttl=64 time=0.106 ms
64 bytes from 172.18.0.3: icmp_seq=2 ttl=64 time=0.141 ms
--- foo2 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.106/0.140/0.172/0.027 ms

root@3769d24e6700:/usr/local/tomcat# ping bar2 -c 3
PING bar2 (172.18.0.3): 56 data bytes
64 bytes from 172.18.0.3: icmp_seq=0 ttl=64 time=0.093 ms
64 bytes from 172.18.0.3: icmp_seq=1 ttl=64 time=0.097 ms
64 bytes from 172.18.0.3: icmp_seq=2 ttl=64 time=0.099 ms
--- bar2 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.093/0.096/0.099/0.000 ms
root@3769d24e6700:/usr/local/tomcat#

兩個 container 擁有相同 network-alias

接下來是在官方文件看到的,可以多個 container 使用相同的 network-alias。對這真實的運作情況有點好奇,因此建立了模擬狀況來測試。

底下範例建立了三個 container,分別為 foobar1、foobar2 與 foobar3。其中 foobar1 要做為 ping 的發起端,而 foobar2 與 foobar3 為測試對象,他們使用相同的 network-alias,名為 app。

首先依序把三個 container 建立起來,然後進入 foobar1 的 bash,接著開始 ping network-alias 的測試。在測試的時候發現在 ping network-alias 時,不管 ping 多少次,只會找到 foobar2,也就是第一個使用該 network-alias 的 container,當第一個 foobar2 停掉時,才能 ping 到第二個使用該 network-alias 的 container,也就是 foobar3。

docker run -d --name foobar1 --network foonet tomcat:8.5.11-jre8
docker run -d --name foobar2 --network foonet --network-alias app tomcat:8.5.11-jre8
docker run -d --name foobar3 --network foonet --network-alias app tomcat:8.5.11-jre8
docker exec -it foobar1 /bin/bash
root@05c0ad2a04b2:/usr/local/tomcat# ping app
PING app (172.18.0.3): 56 data bytes
64 bytes from 172.18.0.3: icmp_seq=0 ttl=64 time=0.117 ms
64 bytes from 172.18.0.3: icmp_seq=1 ttl=64 time=0.126 ms
64 bytes from 172.18.0.3: icmp_seq=2 ttl=64 time=0.129 ms
root@05c0ad2a04b2:/usr/local/tomcat# exit
exit
mayertekiMacBook-Air:~ mayer$ docker stop foobar2
foobar2
mayertekiMacBook-Air:~ mayer$ docker exec -it foobar1 /bin/bash
root@05c0ad2a04b2:/usr/local/tomcat# ping app
PING app (172.18.0.4): 56 data bytes
64 bytes from 172.18.0.4: icmp_seq=0 ttl=64 time=0.126 ms
64 bytes from 172.18.0.4: icmp_seq=1 ttl=64 time=0.138 ms

也就是說可以使用 network-alias 建立一個至多個備援的 container,提供服務的 container 掛了後相同 network-alias 的 container 會繼續提供服務,不過實際應用上並不建議這樣使用,若是需要備援服務應該使用如 Docker Swarm 或是 kubernetes 之類的叢集管理工具比較恰當。

應用場景

知道了 network-alias 的效果之後,就會開始思考,這要應用在哪?這可能可以從一個 App Server 與 DB container 的互動來切入,首先我們建立一個 Web 應用程式,要將他放在 tomcat 上,這個 Web 應用程式需要與 DB 互動,因此我們需要一個 MySQL,此時用 Docker 來建立整個環境會如下圖所示:

在 Web 應用程式內,要連到 DB,通常會把他寫成屬性檔,用來描述 DB 的位址以及相關登入資訊,有可能如下:

miniweb.driverClassName = com.mysql.jdbc.Driver
miniweb.url = jdbc:mysql://db1:3306/miniweb?useUnicode=true&characterEncoding=utf-8&autoReconnect=true&useSSL=false
miniweb.username = foo
miniweb.password = bar
miniweb.initialSize = 1
miniweb.maxActive = 50
miniweb.maxIdle = 10
miniweb.maxWait = 180000
miniweb.validationQuery = SELECT CURRENT_TIMESTAMP

在開發與測試階段時,這樣是不會有問題的,但是在程式要上線時,這樣做就會有問題了。通常上線前會先把程式放到 staging 環境先經過測試後,才會上到 production 環境。此時依然可以使用 docker 來為我們建立這兩個環境所需要使用的 container,但是會有個問題,由於 container name 不能重複,因此應用程式內的屬性檔,必須要有兩份才能正常運作,比如說目前環境如下圖:

則你在 ap1 上應用程式的設定檔,DB 的 url 要如下:

miniweb.url = jdbc:mysql://db1:3306/miniweb?useUnicode=true&characterEncoding=utf-8&autoReconnect=true&useSSL=false

而在 ap2 上應用程式的 DB url 要如下:

miniweb.url = jdbc:mysql://db2:3306/miniweb?useUnicode=true&characterEncoding=utf-8&autoReconnect=true&useSSL=false

維護兩份設定檔文件會是一件很麻煩的事情,你可以在打包程式時,手動改這屬性檔,或是使用打包工具搭配一些指令來處理這問題,不管怎樣都需要額外花費一些功夫來處理這狀況。

此時就可以使用 docker network 搭配 network-alias 來處理這問題了,我們使用 docker network 建立兩個獨立的 network 環境,然後各跑各的 Web 應用環境,在 DB containers 上使用 network-alias 把他們都命名為 db,這樣一來,我們的應用程式只要透過 db 這個 network-alias,就能正確的找到相對應的 DB containers。整個環境的概念圖如下:

在這個環境下,屬性檔只要填上 network-alias 即可,如下:

miniweb.url = jdbc:mysql://db:3306/miniweb?useUnicode=true&characterEncoding=utf-8&autoReconnect=true&useSSL=false

Docker Network Bridge

本篇是閱讀 docker network 官方相關文件後一些自己覺得重要的內容而做的筆記,主要是以 bridge 為主,其他詳細的內容可以參考 Docker container networking

network 簡介

network 為 docker 的一項功能,讓你可以建立虛擬網路,將 container 加到網路內,建立起屬於你自己應用程式的網路拓墣。另外 network 也可以橫跨多台主機,讓你實現分散式應用服務。

安裝 Docker 後,預設會有三個 network,分別為 bridge、none 以及 host, 你可以用以下指令查看相關資訊:

# docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
1623cc36aeae        bridge              bridge              local
7039bb756843        host                host                local
564a81a95b49        none                null                local

bridge 是預設的 network,在你使用 docker run 指令啟動 container 時,如果你沒有下 --network 參數指定,則預設都會是 bridge。 你可以透過以下指令來觀察 bridge 的詳細參數:

[root@lzstg ~]# docker network inspect bridge
[
    {
        "Name": "bridge",
        "Id": "1623cc36aeae8538c05780f86517dc07d6dc613c4b9d13ad6f949e33fced8f1e",
        "Created": "2017-03-06T11:35:35.006599348+08:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16",
                    "Gateway": "172.17.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Containers": {
            "5b7be3dc833e3b2a46d9f6510af70e507f0cb068653fca024da7df8d5c86276b": {
                "Name": "miniweb_lzstg",
                "EndpointID": "bd107fcd1fbe5fe269665797b4fbd6728bfd8df06afdf5871ca10d8d426d4888",
                "MacAddress": "02:42:ac:11:00:03",
                "IPv4Address": "172.17.0.3/16",
                "IPv6Address": ""
            },
            "f3aabba6c0a2bc1fac6a262854b0fb1980e5c3329552f63d1fbc61c1eb45ab7d": {
                "Name": "miniweb",
                "EndpointID": "7c8cd504316cd351784a6afee89a4eea48f079e522883322d3f7a253207acf55",
                "MacAddress": "02:42:ac:11:00:02",
                "IPv4Address": "172.17.0.2/16",
                "IPv6Address": ""
            }
        },
        "Options": {
            "com.docker.network.bridge.default_bridge": "true",
            "com.docker.network.bridge.enable_icc": "true",
            "com.docker.network.bridge.enable_ip_masquerade": "true",
            "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
            "com.docker.network.bridge.name": "docker0",
            "com.docker.network.driver.mtu": "1500"
        },
        "Labels": {}
    }
]

在 IPAM -> Config 底下可以看見,Subnet 設定為 172.17.0.0/16,而 Gateway 為 172.17.0.1。

在 Containers 設定內,如果你已經有啟動 container 且它的 network 為 bridge,這邊就會有資料,否則會是 {};另外也可以看到這邊啟動了兩個 container,IP 分別為 172.17.0.3 與 172.17.0.2。

其他兩個預設建立的 network 分別為 host 與 none,設為 host 則 container 的網路會與你 docker host 主機的網路設定相同。舉例來說,如果你啟動一個 tomcat container,並且把它的 network 設為 host,當 container 啟動時,你就可以直接在瀏覽器上輸入你的機器 ip,port 為 8080,就能開啟 tomcat 管理頁面了。設定為 none 顧名思義就是 container 不使用任何網路介面卡。

預設的 bridge

上面有提到,在啟動 container 時如果沒有加上 --network 指令來指定網路設定,則都會使用預設的 bridge。在預設的 bridge 底下的 container,是可以透過 ip 互相 ping 對方的。

比如說我先啟動兩個 container,命名為 foo1 與 foo2,然後在使用 docker network inspect 指令觀察他們配到的 ip,如下:

$ docker run --name foo1 -d tomcat:8.5.11-jre8
$ docker run --name foo2 -d tomcat:8.5.11-jre8
$ docker network inspect bridge
[
    {
        "Name": "bridge",
        "Id": "f2a2d7dd6e4ab4edd774ef0d80f8b91f5b80a8f08b0f5162a35c02abf41ef4e1",
        "Created": "2017-03-31T11:30:40.628367278Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16",
                    "Gateway": "172.17.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Containers": {
            "70cdec387fc68128b9968689b9bf883150b180ff37134756a97fec2c881b62a4": {
                "Name": "foo2",
                "EndpointID": "3ed1dcee3d7805fd61cf5436b53d18563314c9abdb617f9d0665b19ff986fa6a",
                "MacAddress": "02:42:ac:11:00:03",
                "IPv4Address": "172.17.0.3/16",
                "IPv6Address": ""
            },
            "c52be8382496c30d735620a601167c1de527c47cbead841e747be2b3bec6843d": {
                "Name": "foo1",
                "EndpointID": "3601fcf1ea3b94d1150925e10238795eab2d7165394024006542516619a105fb",
                "MacAddress": "02:42:ac:11:00:02",
                "IPv4Address": "172.17.0.2/16",
                "IPv6Address": ""
            }
        },
        "Options": {
            "com.docker.network.bridge.default_bridge": "true",
            "com.docker.network.bridge.enable_icc": "true",
            "com.docker.network.bridge.enable_ip_masquerade": "true",
            "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
            "com.docker.network.bridge.name": "docker0",
            "com.docker.network.driver.mtu": "1500"
        },
        "Labels": {}
    }
]

可以在 Containers 的設定內看到,foo1 配到的 ip 為 172.17.0.2,而 foo2 配到的 ip 為 172.17.0.3,接著我們使用 docker exec 指令進入到 foo1 的 bash,然後看看能不能 ping 到 foo2(172.17.0.3)

$ docker exec -it foo1 /bin/bash
root@c52be8382496:/usr/local/tomcat# ping 172.17.0.3 -c 4
PING 172.17.0.3 (172.17.0.3): 56 data bytes
64 bytes from 172.17.0.3: icmp_seq=0 ttl=64 time=0.171 ms
64 bytes from 172.17.0.3: icmp_seq=1 ttl=64 time=0.094 ms
64 bytes from 172.17.0.3: icmp_seq=2 ttl=64 time=0.094 ms
64 bytes from 172.17.0.3: icmp_seq=3 ttl=64 time=0.115 ms
--- 172.17.0.3 ping statistics ---
4 packets transmitted, 4 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.094/0.118/0.171/0.032 ms

透過 ip ping 的結果是能找到對方的,但是若是透過 container name 則無法找到對方,如下:

root@c52be8382496:/usr/local/tomcat# ping foo2
ping: unknown host

要能透過 container name 直接找到其他 container,有兩種方式,一種是 docker 比較早期的方法,在 docker run 時加上 --link,讓 container 連接在一起,此方法可以參考官網的這篇文件 Legacy container links,官方有說明 --link 最終可能會被棄用,有興趣的可以閱讀此篇文章,這邊不多做說明。另一種方式就是使用自己定義的 network 來達成。

自定義的 bridge network

我們可以使用 docker network create 指令來建立一個新的網路拓墣。

docker network create foonet

接著在使用 docker run 啟動 container 時,使用 --network flag 將 container 指定到此網路拓墣:

docker run -d --network foonet --name foobar1 tomcat:8.5.11-jre8
docker run -d --network foonet --name foobar2 tomcat:8.5.11-jre8

我們可以透過 docker network inspect 指令觀察目前網路狀態,可以在印出來的 Containers 設定內找到兩個加入到此網路拓墣的 container 網路資訊。

docker network inspect foonet
[
    {
        "Name": "foonet",
        "Id": "c15a5f50792fe854f7722d266ca4cc7e57ccf9ac3dfe479dd711959de55deb0d",
        "Created": "2017-03-25T02:37:24.164070946Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.18.0.0/16",
                    "Gateway": "172.18.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Containers": {
            "8c5956967f3e9c0701a84aa868f08053a8f03d691c509c37c462f045dedaa827": {
                "Name": "foobar2",
                "EndpointID": "576f9ee3f923af8838d4ce126de16b0c576dcb2aa814f642a482b1b54dd6f01d",
                "MacAddress": "02:42:ac:12:00:03",
                "IPv4Address": "172.18.0.3/16",
                "IPv6Address": ""
            },
            "b219692bba64311507c23bf4ee69ff33f415ebfcffe3d412b28fa7a44b942356": {
                "Name": "foobar1",
                "EndpointID": "37138005c073250f90f94cad523fbab656155a68f9a238f7a529d116a1bf0833",
                "MacAddress": "02:42:ac:12:00:02",
                "IPv4Address": "172.18.0.2/16",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {}
    }
]

最後我們進入到其中一個 container 內,然後使用 container name 來 ping 對方,看看找不找的到對方。可以從結果看出,若是在自己定義的網路拓墣內,是可以直接用 container name 來找到對方機器的。

docker exec -it foobar1 /bin/bash
root@b219692bba64:/usr/local/tomcat# ping foobar2
PING foobar2 (172.18.0.3): 56 data bytes
64 bytes from 172.18.0.3: icmp_seq=0 ttl=64 time=0.151 ms
64 bytes from 172.18.0.3: icmp_seq=1 ttl=64 time=0.141 ms
64 bytes from 172.18.0.3: icmp_seq=2 ttl=64 time=0.285 ms

參考

Docker container networking

Work with network commands

Docker Volume 初步閱讀與學習紀錄

Docker Volume 是個讓 Docker Container 保存與共用資料的機制。原本 Docker Container 的資料只會存在該 container 內,並不會與外部或是其他 container 共享,如果在 container 運作時新增了一些資料,而在 container 移除時又沒有將該 container commit 成 image,則當 container 被刪除時,資料就會遺失。可以參考官方文件 Understand images, containers, and storage drivers 了解 image 與 container 的運作方式。

要避免資料遺失,就可以使用 Docker Volume。你可以建立一個 volume,將它指定到 container 內的某個目錄,這樣該目錄的資料就可以永續保存著,不會因為 container 移除而被移除。你也可以為 volume 命名,這樣其他 container 就能透過這個名稱來使用相同的 volume,另外 volume 也可以與本機的目錄上連結,這樣你就可以讓你本機上的目錄和 container 上的目錄共享資料。

建立 volume

你可以使用許多方式建立 volume,比如說在建立 container 時使用 -v flag 一併建立:

docker run -d -v /var/lib/mysql mysql:5.7.17

你也可以在使用 -v 時一併指定 volume 名稱,讓資料保存下來,當下次要用別的 container 時,指定相同 volume 名稱就能使用相同資料:

docker run -d -v db_data:/var/lib/mysql mysql:5.7.17

或是先使用 docker volume 指令建立好 volume,在 run 時指定你建立的 volume 名稱即可使用:

docker volume create db_data2
docker run -d -v db_data2:/var/lib/mysql mysql:5.7.17

你也可以使用 volume 來實現本機目錄與 container 目錄資料共享:

docker run -d -v /Users/mayer/Documents/dockerspace/mysqldata:/var/lib/mysql mysql:5.7.17

volume 掛載測試

官方有說明,如果你是建立一個 volume 然後掛載到 container 的目錄,則原本 container 的目錄內所有的資料會複製到 volume 內。但是看文件沒有說明如果掛載的目錄有 a 檔案,而 volume 內也一樣有 a 檔案,會有什麼狀況發生,因此就自己想了個例子測試。

首先建立一個全新的 tomcat container,命名為 foo1,然後使用 -v 指令掛載一個名為 foovolume 的 VOLUME 到 tomcat container 的 /usr/local/tomcat 目錄,接著進入到該 tomcat 的 /bin/bash:

$ docker run -d --name foo1 -v foovolume:/usr/local/tomcat/conf tomcat:8.5.11-jre8
$ docker exec -it foo1 /bin/bash

使用 ls 觀察 server.xml 檔案修改時間,接著在使用 echo 指令在 server.xml 底下加上一些註解,用 tail 指令觀察該檔案是否已經加入資料。然後退出 bash 並停止 foo1 container,讓他不影響接下來的測試。

root@a6e3c1a143f5:/usr/local/tomcat# ls -al conf/server.xml
-rw-------  1 root root 7511 Jan 10 21:05 server.xml
root@a6e3c1a143f5:/usr/local/tomcat# echo '<!-- foobar -->' >> conf/server.xml
root@a6e3c1a143f5:/usr/local/tomcat# tail -n 3 conf/server.xml
  </Service>
</Server>
<!-- foobar -->
root@a6e3c1a143f5:/usr/local/tomcat# ls -al conf/server.xml
-rw------- 1 root root 7527 Mar 16 09:10 conf/server.xml
root@a6e3c1a143f5:/usr/local/tomcat# exit

$ docker stop foo1

接著建立另一個全新的 tomcat container,命名為 foo2,一樣使用 -v 指令掛載剛剛建立好的 foovolume VOLUME 到 /usr/local/tomcat 目錄,接著進入到該 tomcat 的 /bin/bash:

$ docker run -d --name foo2 -v foovolume:/usr/local/tomcat/conf tomcat:8.5.11-jre8
$ docker exec -it foo2 /bin/bash

然後使用 ls 與 tail 指令觀察 server.xml 檔案修改日期與檔案的內容,可以發現到與剛剛 foo1 修改後的檔案日期與內容是一致的。

root@a564777a0059:/usr/local/tomcat# ls -al conf/server.xml
-rw------- 1 root root 7527 Mar 16 09:10 conf/server.xml
root@a564777a0059:/usr/local/tomcat# tail -n 3 conf/server.xml
  </Service>
</Server>
<!-- foobar -->
root@a564777a0059:/usr/local/tomcat# exit

$ docker stop foo2

從這個測試可以觀察出,如果你將一個剛建立的 volume 掛載到某個 container 的目錄,若該目錄裡面已經有資料了,則 Docker 會將資料複製到這個剛建立的 volume,然後把這個 volume 覆蓋在你指定的目錄上,原本目錄的東西將會變成不可視。若你掛載的是一個經過初始化的 volume,則他會直接把 volume 內的資料覆蓋在你指定的目錄。

可以用另一個簡單的測試來驗證上面的說法是否正確。新建立一個 volume,掛載到 tomcat 的 /usr/local/tomcat/conf,然後在將這個 volume 掛載到另一個 tomcat 的 /usr/local/tomcat/webapps,就可以看到,原本 webapps 底下的 ROOT 等等的 Web App 都變成了設定檔了,也就是說,初次建立 volume 時,Docker 把 設定檔複製到 volume 上,volume 再次被掛載時,Docker 把 volume 蓋到 webapps 目錄之上。

$ docker run -d --name foo3 -v foovolume2:/usr/local/tomcat/conf tomcat:8.5.11-jre8
$ docker stop foo3
$ docker run -d --name foo4 -v foovolume2:/usr/local/tomcat/webapps tomcat:8.5.11-jre8
$ docker exec -it foo4 /bin/bash
root@028b5095b04a:/usr/local/tomcat# ls -al webapps/
total 236
drwxr-x---  3 root root    4096 Mar 16 09:50 .
drwxr-sr-x 14 root staff   4096 Mar 16 09:51 ..
drwxr-x---  3 root root    4096 Mar 16 09:50 Catalina
-rw-------  1 root root   12895 Jan 10 21:05 catalina.policy
-rw-------  1 root root    7202 Jan 10 21:05 catalina.properties
-rw-------  1 root root    1338 Jan 10 21:05 context.xml
-rw-------  1 root root    1149 Jan 10 21:05 jaspic-providers.xml
-rw-------  1 root root    2358 Jan 10 21:05 jaspic-providers.xsd
-rw-------  1 root root    3622 Jan 10 21:05 logging.properties
-rw-------  1 root root    7511 Jan 10 21:05 server.xml
-rw-------  1 root root    2164 Jan 10 21:05 tomcat-users.xml
-rw-------  1 root root    2633 Jan 10 21:05 tomcat-users.xsd
-rw-------  1 root root  168133 Jan 10 21:05 web.xml

$ docker stop foo4

volume 與本機目錄掛載測試:

接下來是一些 volume 與本機目錄的掛載測試,主要是要看確認資料是否真的共享,與掛載本機目錄之後的 container 其目錄變化,測試將會使用到 tomcat container,還會用到自己打包的 war 檔。

首先在測試前先準備好 war 檔,放在本機目錄,

$ ls /Users/mayer/Documents/dockerspace/volumetest/
miniweb.war

接著啟動一個沒有掛載 volume 的 tomcat container,然後觀察其 webapps 目錄,可以看到 webapps 底下會有一些預設安裝 tomcat 時的應用程式:

$ docker run -d  --name foo tomcat:8.5.11-jre8
$ docker exec -it foo /bin/bash

$ root@635985e5b693:/usr/local/tomcat# ls webapps
ROOT  docs  examples  host-manager  manager

接著使用 -v flag 啟動一個有掛載的 container,將本機目錄掛載到 webapps 目錄:

$ docker run -d -v
/Users/mayer/Documents/dockerspace/volumetest:/usr/local/tomcat/webapps --name bar tomcat:8.5.11-jre8

然後觀察 tomcat webapps 目錄,可以跟上面沒掛載的對照看,發現原本的 ROOT 等等目錄都不見了,只有你本機目錄上的 war 檔與 tomcat 自動幫你解開的目錄。也就是說原本 webapps 的目錄被隱藏了,現在只會顯示 volume 掛載目錄的內容。

$ docker exec -it bar /bin/bash
root@3eca0381a364:/usr/local/tomcat# ls webapps
miniweb  miniweb.war

然後接著測試是否真的目錄共享,先在 container 內新增一個 bar.txt,然後退出 container bash,觀察本機目錄,可以看到本機目錄上也新增了一個 bar.txt 檔案:

root@3eca0381a364:/usr/local/tomcat# touch webapps/bar.txt
root@3eca0381a364:/usr/local/tomcat# ls webapps
bar.txt  miniweb  miniweb.war
root@3eca0381a364:/usr/local/tomcat# exit
$ ls /Users/mayer/Documents/dockerspace/volumetest
bar.txt     miniweb     miniweb.war

接著反向測試,看看在本機目錄新增 foo.txt,再回到 container 內觀察 webapps 目錄是否也會新增一個 foo.txt。從結果可以看出,container 內也新增 foo.txt 檔案了:

$ touch /Users/mayer/Documents/dockerspace/volumetest/foo.txt
$ docker exec -it bar /bin/bash
root@3eca0381a364:/usr/local/tomcat# ls webapps
bar.txt  foo.txt  miniweb  miniweb.war

透過上面的幾項簡單測試,可以看出當 volume 掛載本機目錄時,container 的目錄與本機的目錄資料會完全共用。

需要注意這個測試和上一個測試的不同之處,若是透過 volume 把本機目錄掛載到 container 的目錄,則 container 目錄裡面原本的內容會被完全隱藏,只是出現你本機目錄的檔案;如果是單純的建立 volume 然後掛載到 container 的目錄,則對於初次建立的 volume,Docker 會將 container 目錄裡面原本的內容全部都複製到 volume 上。

使用其他 container 當成 data volume

官方有提到另一個用法,就是在啟動 container 時使用 volumes-from 指令,來分享 container 之間的資料,對於此用法我個人感受不深,覺得很不直覺,因此這邊只會紀錄一下官方的範例。

首先先建立一個有名稱的 container。該 container 不執行任何的應用,他只重新利用 training/postgres image,使所有容器都使用共同的層,節省磁盤空間。

$ docker create -v /dbdata --name dbstore training/postgres /bin/true

接著你就可以在其他 container 上使用 --volumes-form 來掛載 /dbdata volumes。

$ docker run -d --volumes-from dbstore --name db1 training/postgres
$ docker run -d --volumes-from dbstore --name db2 training/postgres

這範例,如果 training/postgres images 原本 /dbdata 目錄內有資料,則會被隱藏,只有從 dbstore container 掛載過來的資料可以被看見。

另外也可以用在備份上,可以透過以下方式進行備份:

$ docker run --rm --volumes-from dbstore -v $(pwd):/backup ubuntu tar cvf /backup/backup.tar /dbdata

然後將備份還原到其他 container:

$ docker run -v /dbdata --name dbstore2 ubuntu /bin/bash
$ docker run --rm --volumes-from dbstore2 -v $(pwd):/backup ubuntu bash -c "cd /dbdata && tar xvf /backup/backup.tar --strip 1"

移除 volumes

使用 volumes 時要注意到,docker 並不會自動幫你移除你沒用到的 volumes。例如你建立 foo container 並掛載一個 foovolume,當你的 foo container 移除時,foovolume 會繼續存在。

在 docker run 時,可以加上 --rm 指令,讓 container 移除時一併把匿名 volume 移除,比如說以下指令:

$ docker run --rm -v /foo -v awesome:/bar busybox top

此命令會建立一個匿名的 /foo volume。當 container 被移除時,Docker Engine 會移除 /foo volume,而不會移除 awesome volume。

你可以用以下指令查看看哪些 volumes 目前沒被任何 container 關聯,然後在下指令移除他們:

docker volume ls -qf dangling=true
docker volume rm $(docker volume ls -qf dangling=true)

若你也像我一樣,目前正在測試階段,則可以直接使用以下指令,清除沒被用到的 volumes:

docker volume prune

參考

Manage data in containers

Docker volume 簡單用法

深入理解Docker Volume(一)