顯示具有 Docker 標籤的文章。 顯示所有文章
顯示具有 Docker 標籤的文章。 顯示所有文章

2017/08/14

以 docker 安裝一個可以遠端 ssh 登入的 centos 7 image

以下紀錄如何產生一個基本的 docker image,安裝了 openssh-server 可以用 ssh 遠端登入。

設定 docker image 以及 openssh-server

docker run -it --name c1 centos:latest /bin/bash

安裝一些基本工具,以及 openssh-server

#yum provides ifconfig

yum install -y net-tools telnet iptables sudo initscripts
yum install -y passwd openssl openssh-server

測試 sshd

/usr/sbin/sshd -D
Could not load host key: /etc/ssh/ssh_host_rsa_key
Could not load host key: /etc/ssh/ssh_host_ecdsa_key
Could not load host key: /etc/ssh/ssh_host_ed25519_key

缺少了一些 key

ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key
#直接 enter 即可

ssh-keygen -t dsa -f /etc/ssh/ssh_host_dsa_key
#直接 enter 即可

ssh-keygen -t ecdsa -f /etc/ssh/ssh_host_ecdsa_key -N ""

ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N ""

修改 UsePAM 設定

vi /etc/ssh/sshd_config
# UsePAM yes 改成 UsePAM no
UsePAM no

再測試看看 sshd

/usr/sbin/sshd -D

修改 root 密碼

passwd root

離開 docker

exit

以 docker ps -l 找到剛剛那個 container 的 id

$ docker ps -l
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                     PORTS               NAMES
107fb9c3fc0d        centos:latest       "/bin/bash"         7 minutes ago       Exited (0) 2 seconds ago                       c1

將 container 存成另一個新的 image

docker commit 107fb9c3fc0d centosssh

以新的 image 啟動另一個 docker instance

docker run -d -p 10022:22 centosssh /usr/sbin/sshd -D

現在可以直接 ssh 登入新的 docker machine

ssh root@localhost -p 10022

如果遇到 Failed to get D-Bus connection: Operation not permitted 的問題:ref [原创] 解决 CentOS7 容器 Failed to get D-Bus connection: Operation not permitted

docker run -d -p 10022:22 -e "container=docker" --privileged=true -v /sys/fs/cgroup:/sys/fs/cgroup --name centos7test centosssh /usr/sbin/init

docker exec -it centos7test /bin/bash

gitolite 測試

在新的 docker 機器上安裝 gitolite 測試

yum install -y autoconf git

useradd git
passwd git

產生管理員的 key

ssh-keygen

Generating public/private rsa key pair.
Enter file in which to save the key (/root/.ssh/id_rsa):
Created directory '/root/.ssh'.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /root/.ssh/id_rsa.
Your public key has been saved in /root/.ssh/id_rsa.pub.
The key fingerprint is:
01:93:46:03:17:6e:e2:06:ec:d6:07:db:2e:13:a3:92 root@1f01b0c5ad69
The key's randomart image is:
+--[ RSA 2048]----+
|    .oBo         |
| .   oo+         |
|  o o.o .        |
| . + *   .       |
|  o B o S        |
| o o =           |
|E . o .          |
| .   o           |
|                 |
+-----------------+
cp /root/.ssh/id_rsa.pub /home/git/admin.pub

以 scp 遠端測試 key

sshpass -p "password" scp -p -P 10022 git@localhost:/home/git/admin.pub .

在本機上安裝 gitolite

su - git

mkdir ~/bin

git clone git://github.com/sitaramc/gitolite

gitolite/install -ln ~/bin

把 admin.pub 放入 gitolite

gitolite setup -pk admin.pub

Initialized empty Git repository in /home/git/repositories/gitolite-admin.git/
Initialized empty Git repository in /home/git/repositories/testing.git/
WARNING: /home/git/.ssh missing; creating a new one
    (this is normal on a brand new install)
WARNING: /home/git/.ssh/authorized_keys missing; creating a new one
    (this is normal on a brand new install)

回到 root 身份

exit

以 git clone gitolite-admin 進行 local git 測試

mkdir test
cd test
git config --global user.email "charley@maxkit.com.tw"
git config --global user.name "charley"

git clone ssh://git@localhost/gitolite-admin

現在就可以利用 gitolite-admin 進行 git 帳號及 repo 維護

放入新的 user key: test.pub 放到 keydir 目錄中

git add keydir/test.pub

修改 conf/gitolite.conf

repo gitolite-admin
    RW+     =   admin
    RW+     =   test

repo testing
    RW+     =   admin
    RW+     =   test

將新的 test 增加到 gitolite-admin 裡面

git add keydir/test.pub
git add conf/gitolite.conf
git commit -m 'add test key'
git push origin master

也可以用遠端的方式存取 git

git clone ssh://git@localhost:10022/gitolite-admin

How to install Gitolite in CentOS 7

Linux 使用 Gitolite 架設 Git Server

使用Gitolite搭建Git服務器

gitolite basic administration

References

centos7中安裝一個可以ssh登陸的docker容器

Docker安裝SSH【Ubuntu、CentOS】

2017/05/15

以 docker 測試網站

docker container 本身並沒有 persistence 的機制,但可以透過共享 valume 的方式,將本地機器的某個實體的路徑,綁定到 container 中,由於 container 疊加式的文件檔案系統,我們還是會覺得只有一個檔案系統。

靜態網站

在 sample 目錄中,製作一個 Dockerfile 檔案

FROM ubuntu:latest
MAINTAINER yaocl
ENV REFRESHED_AT 2016-12-23

# 安裝 ngnix
RUN apt-get -yqq update && apt-get -yqq install nginx

# 建立 website 目錄
RUN mkdir -p /var/www/html/website

# 調整 nginx 設定
ADD nginx/global.conf /etc/nginx/conf.d/
ADD nginx/nginx.conf /etc/nginx/

# TCP Port 80
EXPOSE 80

另外準備兩個 nginx 設定檔:

nginx/global.conf

server {
        listen          0.0.0.0:80;
        server_name     _;

        root            /var/www/html/website;
        index           index.html index.htm;

        access_log      /var/log/nginx/default_access.log;
        error_log       /var/log/nginx/default_error.log;
}

nginx/nginx.conf

user www-data;
# process 數量
worker_processes 4;
# 紀錄 process id
pid /run/nginx.pid;

# 不讓 nginx 進入 daemon 狀態,以前景執行,否則會讓 container 啟動後直接停掉
daemon off;

events {  }

http {
  sendfile on;
  tcp_nopush on;
  tcp_nodelay on;
  keepalive_timeout 65;
  types_hash_max_size 2048;
  include /etc/nginx/mime.types;
  default_type application/octet-stream;
  access_log /var/log/nginx/access.log;
  error_log /var/log/nginx/error.log;
  gzip on;
  gzip_disable "msie6";
  include /etc/nginx/conf.d/*.conf;
}

在 website 目錄,放一個 index.html 網頁。

所有檔案的目錄結構為

sample\
    Dockerfile
    nginx\
        global.conf
        nginx.conf
    website\
        index.html

以 build 指令產生 docker image

docker build -t yaocl/nginx .

可透過 history 查看 image 的過程

docker history yaocl/nginx

啟動 container,以 -v 將 website 目錄綁定為 container 的 /var/www/html/website 目錄,覆蓋掉原本在 image 中的那個目錄,我們就可以修改 website 的網頁,並即時由 browser 看到網頁的結果。

$ docker run -d -p 80:80 --name website \
    -v $PWD/website:/var/www/html/website \
    yaocl/nginx nginx
b64855fbfc6bb313c9190eeead2f1d433d28b9d759dba85d06399841e0ef9f78

如果加上 :ro ,就變成 readonly,rw 則是可讀寫

docker run -d -p 80:80 --name website \
    -v $PWD/website:/var/www/html/website:ro \
    yaocl/nginx nginx

連結兩個 container

如果我們需要用到兩個 containers,其中一個用來執行 application server,另一個執行 redis或 db,這時候有兩種方式,可以讓兩個 containers 可以互相連結使用服務。

我們建立一個 redis image 跟 container,另外產生一個只有 os 的 container,讓測試讓後面那個 container 可以用 redis-cli 連結到 redis server。

產生 redis db server 的 Dockerfile

FROM ubuntu:latest
MAINTAINER yaocl
ENV REFRESHED_AT 2016-12-23

RUN apt-get -yqq update && apt-get -yqq install redis-server redis-tools

EXPOSE 6379

ENTRYPOINT ["/usr/bin/redis-server"]
CMD []

產生 redis image,並啟動 container

docker build -t yaocl/redis .

docker run -d -p 6379:6379 --name redis yaocl/redis

如果剛剛沒有指定主機的 port 可以用 port 指令查詢 port

$ docker port redis 6379
0.0.0.0:6379

以本機的 redis-cli 測試 redis:6379

$ redis-cli -h 127.0.0.1 -p 6379
127.0.0.1:6379>

要從 container: redis-cli 連接到 redis,有兩種方式,一種是使用 docker 的內部網路 network stack,安裝 docker 時就會建立一個 docker0 的網路介面,每一個 docker container 都會在這個網路上分配到獨立的 172.16~172.30 這個範圍的 ip。

預設這些網路之間不能互相連接,如果搭配修改 iptables(DNAT),就可以讓 container 互相溝通,但 docker for mac 找不到 docker0 這個 network,如果要測試,可以參考 Docker container networking 的說明。

我們用另一個比較常見的方式,直接讓 container 互相連接。

在 mac 先將 lo0 綁定一個新的 ip

sudo ifconfig lo0 alias 10.200.10.1/24

首先刪除掉剛剛的 redis container

docker stop redis

docker rm redis

重新執行一個 redis container,但不指定 -p 6379

docker run -d --name redis yaocl/redis

redis-cli 的 Dockerfile

FROM ubuntu:latest
MAINTAINER yaocl
ENV REFRESHED_AT 2016-12-23

RUN apt-get update
RUN apt-get -y install inetutils-ping redis-tools

ENTRYPOINT ["/bin/bash"]
CMD []

產生 redis-cli image

docker build -t yaocl/redis-cli .
docker run --name webapp -t -i yaocl/redis-cli

如果不是用 docker for mac,可以用 link 的方式直接將 container 連接起來。

docker run --name webapp --link redis:db -t -i yaocl/redis-cli

啟動 redis-cli container webapp 後,就能直接用 db 這個 hostname 連接到 redis server

root@79bb961c2b78:/# redis-cli -h db
db:6379>

root@79bb961c2b78:/# env
HOSTNAME=79bb961c2b78
DB_NAME=/webapp/db
DB_PORT_6379_TCP_PORT=6379
TERM=xterm
DB_PORT=tcp://172.17.0.2:6379
DB_PORT_6379_TCP=tcp://172.17.0.2:6379
LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.jpg=01;35:*.jpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.m4a=00;36:*.mid=00;36:*.midi=00;36:*.mka=00;36:*.mp3=00;36:*.mpc=00;36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.oga=00;36:*.opus=00;36:*.spx=00;36:*.xspf=00;36:
DB_ENV_REFRESHED_AT=2016-12-23
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
REFRESHED_AT=2016-12-23
PWD=/
DB_PORT_6379_TCP_ADDR=172.17.0.2
DB_PORT_6379_TCP_PROTO=tcp
SHLVL=1
HOME=/root
no_proxy=*.local, 169.254/16
DB_ENV_no_proxy=*.local, 169.254/16
_=/usr/bin/env

root@79bb961c2b78:/# ping db
PING db (172.17.0.2): 56 data bytes
64 bytes from 172.17.0.2: icmp_seq=0 ttl=64 time=0.223 ms
64 bytes from 172.17.0.2: icmp_seq=1 ttl=64 time=0.139 ms

References

The Docker Book

Networking your docker containers using docker0 bridge

How to create a bidirectional link between containers?

2017/05/08

docker for mac

docker 在 mac 上原本是用 boot2docker,但現在已經有了原生的 Docker for mac,Docker for mac 不再是使用 VirtualBox,而是改用比較輕量化的 macOS 虛擬機 HyperKit。

Docker for mac 需要 OS X El Capitan 10.11 以上的作業系統,以及 2010 或更新 Mac 硬體,以支援 MMU virtualization。

在 Get started with Docker for Mac 這個網頁可以下載 docker,目前分為 Stable 及 Beta 兩個版本,我們是安裝 Stable 版本,但還是隨時可以切換到 Beta 版。

安裝完成並執行後,可以在狀態列看到 docker 那隻鯨魚。

docker 的元件

  1. 客戶端及伺服器: client 發送 request 給 docker daemon,由 damone 代理工作並回傳結果

  1. image: 由指令一步一步建構出來的 image,類似 container 的 source code

  2. registry: docker 以 registry 保存 image,分為 public 及 private 兩種,公用的 registry 稱為 docker hub,可以自己架設 private registry。

  3. container: image 是 docker 的建構及打包對象,而 container 是啟動跟執行的目標。

docker 的技術元件

  1. libcontainer: 就是原生的 Linux 容器
  2. 獨立的 kernel namespace 用來隔離文件、process、網路
  3. 每個 container 都有自己的 /root,獨立的文件系統
  4. 獨立的 process
  5. 獨立的網路
  6. 資源隔離及分組: 使用 cgroups 將 CPU 及記憶體獨立分配給每個 container
  7. copy-on-write: 分層的文件系統,但 user 還是只能看到所有文件疊加後的結果,會覺得只有一個文件系統

  8. log: container 的 STDIN, STDOUT, STDERR 都會記錄到 log中

  9. 互動式 shell: 可建立虛擬的 tty terminal,連接到 STDIN

測試 docker

我們可以利用 ubuntu 的 image,執行一個 echo 指令進行 docker 的測試,在安裝了 docker for mac 之後,就可以直接在 terminal 使用 docker 的 command line tool。

$ docker run -it ubuntu echo "Hello World"
Unable to find image 'ubuntu:latest' locally
latest: Pulling from library/ubuntu
b3e1c725a85f: Pull complete
4daad8bdde31: Pull complete
63fe8c0068a8: Pull complete
4a70713c436f: Pull complete
bd842a2105a8: Pull complete
Digest: sha256:7a64bc9c8843b0a8c8b8a7e4715b7615e4e1b0d8ca3c7e7a76ec8250899c397a
Status: Downloaded newer image for ubuntu:latest
Hello World

我們也可以直接執行一個 nginx web server

$ docker run -d -p 80:80 --name webserver nginx
nable to find image 'nginx:latest' locally
latest: Pulling from library/nginx
75a822cd7888: Already exists
64f0219ba3ea: Pull complete
325b624bee1c: Pull complete
Digest: sha256:2a07a07e5bbf62e7b583cbb5257357c7e0ba1a8e9650e8fa76d999a60968530f
Status: Downloaded newer image for nginx:latest
50f78171d0e26922a0b01025125a231b33615086119ebf3e8a0846b2b297760b

啟動了 web server,就可以用 browser 連結到 http://localhost/ 瀏覽網頁。

使用 Kitematic 管理 docker container

docker for mac 並沒有將 kitematic 放進去,而是讓去下載 Kitematic ,下載後會取得 Kitematic-Mac.zip,直接用以下的指令將 kitematic 解壓縮到 Applications 目錄中。

sudo unzip ~/Downloads/Kitematic-Mac.zip -d /Applications

我們可以看到剛剛執行的那個 ubuntu 的 container。

一些 docker 的基本指令

docker 容器有兩種,一種是 interactive container, 可以用互動的方式進行操作,一種是 daemonized container,可以執行應用服務 server。

執行一個名稱為 u1 的容器,-i 是保證容器有打開 STDIN,-t 是產生一個虛擬的 tty 終端,此容器使用 ubuntu 這個 image,然後執行 bash,在 bash shell 中以 exit 離開時,這個容器就會停止。可以在 bash 中執行 ls, ps -aux 等等指令。

docker run --name u1 -i -t ubuntu /bin/bash

啟動剛剛建立的 u1 container

docker start u1

重新附著到 u1,也就是進入 u1 的 bash

docker attach u1

利用 while,產生一個一直列印 hello world 的 daemon server

docker run --name d1 -d ubuntu /bin/sh -c "while true; do echo hello world; sleep 1; done"

查看正在執行的 container

$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS              PORTS               NAMES
a0d673b448a8        ubuntu              "/bin/sh -c 'while tr"   About a minute ago   Up About a minute                       d1

列出所有的 containers,包含執行跟停止中的

docker ps -a

查看 d1 的 log

docker logs d1
# 類似 tail -f
docker logs -f d1

停止 d1

docker stop d1

產生一個新的 d2 container(新的容器名稱不能跟舊的一樣),不管遇到容器的(bash)退出碼是多少,都會自動重新啟動,也可以用 --restart=on-failure:5 來限制退出碼非0時,重新啟動,且只會重啟動 5 次。

docker run --restart=always --name d2 -d ubuntu /bin/sh -c "while true; do echo hello world; sleep 1; done"

可用 inspect 查看 d2 的資訊

$ docker inspect d2
[
    {
        "Id": "8dba5cf2a6ef94143f460bb6be37342d0e14bdae3d3d02645736ab1c9f343621",
        "Created": "2016-12-22T01:57:38.837925059Z",
        "Path": "/bin/sh",
        "Args": [
            "-c",
            "while true; do echo hello world; sleep 1; done"
        ],
        "State": {
            "Status": "running",
            "Running": true,
            "Paused": false,
            "Restarting": false,
            "OOMKilled": false,
            "Dead": false,
            "Pid": 2799,
            "ExitCode": 0,
            "Error": "",
            "StartedAt": "2016-12-22T01:57:39.765031056Z",
            "FinishedAt": "0001-01-01T00:00:00Z"
        },
        "Image": "sha256:104bec311bcdfc882ea08fdd4f5417ecfb1976adea5a0c237e129c728cb7eada",
        "ResolvConfPath": "/var/lib/docker/containers/8dba5cf2a6ef94143f460bb6be37342d0e14bdae3d3d02645736ab1c9f343621/resolv.conf",
        "HostnamePath": "/var/lib/docker/containers/8dba5cf2a6ef94143f460bb6be37342d0e14bdae3d3d02645736ab1c9f343621/hostname",
        "HostsPath": "/var/lib/docker/containers/8dba5cf2a6ef94143f460bb6be37342d0e14bdae3d3d02645736ab1c9f343621/hosts",
        "LogPath": "/var/lib/docker/containers/8dba5cf2a6ef94143f460bb6be37342d0e14bdae3d3d02645736ab1c9f343621/8dba5cf2a6ef94143f460bb6be37342d0e14bdae3d3d02645736ab1c9f343621-json.log",
        "Name": "/d2",
        "RestartCount": 0,
        "Driver": "aufs",
        "MountLabel": "",
        "ProcessLabel": "",
        "AppArmorProfile": "",
        "ExecIDs": null,
        "HostConfig": {
            "Binds": null,
            "ContainerIDFile": "",
            "LogConfig": {
                "Type": "json-file",
                "Config": {}
            },
            "NetworkMode": "default",
            "PortBindings": {},
            "RestartPolicy": {
                "Name": "always",
                "MaximumRetryCount": 0
            },
            "AutoRemove": false,
            "VolumeDriver": "",
            "VolumesFrom": null,
            "CapAdd": null,
            "CapDrop": null,
            "Dns": [],
            "DnsOptions": [],
            "DnsSearch": [],
            "ExtraHosts": null,
            "GroupAdd": null,
            "IpcMode": "",
            "Cgroup": "",
            "Links": null,
            "OomScoreAdj": 0,
            "PidMode": "",
            "Privileged": false,
            "PublishAllPorts": false,
            "ReadonlyRootfs": false,
            "SecurityOpt": null,
            "UTSMode": "",
            "UsernsMode": "",
            "ShmSize": 67108864,
            "Runtime": "runc",
            "ConsoleSize": [
                0,
                0
            ],
            "Isolation": "",
            "CpuShares": 0,
            "Memory": 0,
            "CgroupParent": "",
            "BlkioWeight": 0,
            "BlkioWeightDevice": null,
            "BlkioDeviceReadBps": null,
            "BlkioDeviceWriteBps": null,
            "BlkioDeviceReadIOps": null,
            "BlkioDeviceWriteIOps": null,
            "CpuPeriod": 0,
            "CpuQuota": 0,
            "CpusetCpus": "",
            "CpusetMems": "",
            "Devices": [],
            "DiskQuota": 0,
            "KernelMemory": 0,
            "MemoryReservation": 0,
            "MemorySwap": 0,
            "MemorySwappiness": -1,
            "OomKillDisable": false,
            "PidsLimit": 0,
            "Ulimits": null,
            "CpuCount": 0,
            "CpuPercent": 0,
            "IOMaximumIOps": 0,
            "IOMaximumBandwidth": 0
        },
        "GraphDriver": {
            "Name": "aufs",
            "Data": null
        },
        "Mounts": [],
        "Config": {
            "Hostname": "8dba5cf2a6ef",
            "Domainname": "",
            "User": "",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": [
                "no_proxy=*.local, 169.254/16",
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
            ],
            "Cmd": [
                "/bin/sh",
                "-c",
                "while true; do echo hello world; sleep 1; done"
            ],
            "Image": "ubuntu",
            "Volumes": null,
            "WorkingDir": "",
            "Entrypoint": null,
            "OnBuild": null,
            "Labels": {}
        },
        "NetworkSettings": {
            "Bridge": "",
            "SandboxID": "ea7a45d1dcfb681283e18aad9feaaedd98da8cc8cd4155ca2284e4aeb6db640f",
            "HairpinMode": false,
            "LinkLocalIPv6Address": "",
            "LinkLocalIPv6PrefixLen": 0,
            "Ports": {},
            "SandboxKey": "/var/run/docker/netns/ea7a45d1dcfb",
            "SecondaryIPAddresses": null,
            "SecondaryIPv6Addresses": null,
            "EndpointID": "48480731d5cb9ab9e268e341c65fd924671222660ffa58a6adc61734811bae83",
            "Gateway": "172.17.0.1",
            "GlobalIPv6Address": "",
            "GlobalIPv6PrefixLen": 0,
            "IPAddress": "172.17.0.2",
            "IPPrefixLen": 16,
            "IPv6Gateway": "",
            "MacAddress": "02:42:ac:11:00:02",
            "Networks": {
                "bridge": {
                    "IPAMConfig": null,
                    "Links": null,
                    "Aliases": null,
                    "NetworkID": "b7f0ca41f518b121beb774aac41e709b5032e7cff96939e43e3ddac1ed16cf98",
                    "EndpointID": "48480731d5cb9ab9e268e341c65fd924671222660ffa58a6adc61734811bae83",
                    "Gateway": "172.17.0.1",
                    "IPAddress": "172.17.0.2",
                    "IPPrefixLen": 16,
                    "IPv6Gateway": "",
                    "GlobalIPv6Address": "",
                    "GlobalIPv6PrefixLen": 0,
                    "MacAddress": "02:42:ac:11:00:02"
                }
            }
        }
    }
]

配合 format 查看特定的資訊

$ docker inspect --format='{{ .State.Running }}' d2
true
$ docker inspect --format='{{ .NetworkSettings.IPAddress }}' d2
172.17.0.2

$ docker inspect --format='{{.Name}} {{ .State.Running }}' d1 d2
/d1 false
/d2 true

先停止 d2 後,才能用 rm 刪除 d2

docker stop d2
docker rm d2

列出所有 images

docker images

取得 ubuntu 的 image

docker pull ubuntu

以 TAG 指定 ubuntu 的版本

docker run --name t1 -t -i ubuntu:12.04 /bin/bash

列出 ubuntu 的 images

$ docker images ubuntu
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu              latest              104bec311bcd        6 days ago          129 MB
ubuntu              12.04               f0d07a756afd        6 days ago          103.6 MB

要先移除跟 image 相關的 containers,才能移除 image

docker rm t1

docker rmi ubuntu:12.04

搜尋所有 docker hub 的公用可用 image

docker search puppet

建立 docker image

有兩種方式:

  1. 使用 docker commit: 不建議用這種方式

    登入 docker hub

    $docker login
    Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
    Username: yaocl
    Password:
    Login Succeeded

    執行一個 ubuntu 的 container

    docker run -i -t ubuntu /bin/bash

    在容器中安裝 apache

    apt-get -yqq update
    apt-get -y install apache
    exit

    將 container 以 commit 存成另一個 image

    $ docker commit fd915f899feb yaocl/apache2
    sha256:32421625c13d3076c90a18d3146dab180a9ed5c33e8439b5572acdcf7a328e02
    
    $ docker images yaocl/apache2
    REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
    yaocl/apache2       latest              32421625c13d        31 seconds ago      267.6 MB

    自訂一些資訊欄位,產生新的 image tag

    $ docker commit -m="custom image" --author="yaocl" fd915f899feb yaocl/apache2:webserver
    sha256:b2e0f5faed623a8f62bdad08223773ae85274c9fc40e12753ac1df1258152f2f
    
    $ docker images yaocl/apache2
    REPOSITORY          TAG                 IMAGE ID            CREATED              SIZE
    yaocl/apache2       webserver           b2e0f5faed62        About a minute ago   267.6 MB
    yaocl/apache2       latest              32421625c13d        2 minutes ago        267.6 MB

    使用新的 yaocl/apache2:webserver image

    docker run -t -i yaocl/apache2:webserver /bin/bash
  2. 使用 dokcer build 及 Dockerfile 文件

    static_web 目錄就是 dokcer 的 build environment,此環境就稱為 context 或 build context,先在這個 context 中建立一個 Dockerfile

    mkdir static_web
    cd static_web/
    touch Dockerfile

    編輯 Dockerfile 內容

    # Version 0.0.1
    FROM ubuntu:latest
    MAINTAINER yaocl
    ENV REFRESHED_AT 2016/12/22
    RUN apt-get update
    RUN apt-get install -y nginx
    RUN echo "hi from container" > /usr/share/ngnix/html/index.html
    EXPOSE 80

    以 build 指令建立 image,如果沒有寫 :v1,就是預設的 latest 這個 tag

    cd static_web/
    #docker build -t="yaocl/static_web" .
    
    docker build -t="yaocl/static_web:v1" .

    建立 images 時發生錯誤

    Setting up nginx (1.4.6-1ubuntu3.7) ...
    Processing triggers for libc-bin (2.19-0ubuntu6.9) ...
    Processing triggers for sgml-base (1.26+nmu4ubuntu1) ...
     ---> 83f7d5f1ad20
    Removing intermediate container 2681f1e39e14
    Step 5 : RUN echo "hi from container"   > /usr/shre/ngnix/html/index.html
     ---> Running in f1bd373dc64e
    /bin/sh: 1: cannot create /usr/shre/ngnix/html/index.html: Directory nonexistent
    The command '/bin/sh -c echo "hi from container"    > /usr/shre/ngnix/html/index.html' returned a non-zero code: 2

    以 docker images 可以看到錯誤步驟前那一個 image id: 83f7d5f1ad20

    $ docker images
    REPOSITORY                      TAG                 IMAGE ID            CREATED             SIZE
    <none>                          <none>              83f7d5f1ad20        39 seconds ago      231.1 MB

    以該 image id 執行一個 container

    $ docker run --name test -i -t 83f7d5f1ad20 /bin/bash

    發現是路徑寫錯了 /usr/share/nginx/html

    修改 Dockerfile 內容

    # Version 0.0.1
    FROM ubuntu:latest
    MAINTAINER yaocl
    ENV REFRESHED_AT 2016/12/22
    RUN apt-get update
    RUN apt-get install -y nginx
    RUN echo "hi from container" > /usr/share/nginx/html/index.html
    EXPOSE 80

    再 build 一次,就成功了

    $ docker build -t="yaocl/static_web:v1" .
    Sending build context to Docker daemon 2.048 kB
    Step 1 : FROM ubuntu:latest
     ---> 3f755ca42730
    Step 2 : MAINTAINER yaocl
     ---> Using cache
     ---> 062ac75f50aa
    Step 3 : RUN apt-get update
     ---> Using cache
     ---> 43e1cecf45be
    Step 4 : RUN apt-get install -y nginx
     ---> Using cache
     ---> 83f7d5f1ad20
    Step 5 : RUN echo "hi from container" > /usr/share/nginx/html/index.html
     ---> Running in 9a1dcbadd174
     ---> 11fa4a76019d
    Removing intermediate container 9a1dcbadd174
    Step 6 : EXPOSE 80
     ---> Running in 2d0db9f90d57
     ---> 683486e4c00a
    Removing intermediate container 2d0db9f90d57
    Successfully built 683486e4c00a
    [14:10]charley@cmbp ~/Downloads/static_web$ docker images
    REPOSITORY                      TAG                 IMAGE ID            CREATED             SIZE
    yaocl/static_web                v1                  683486e4c00a        4 seconds ago       231.1 MB

    由於 docker 會在每一個步驟都產生 image,可以加上 --no-cache 避免產生這個問題

    docker build --no-cache -t="yaocl/static_web:v1" .

    產生 yaocl/static_web:v1 的 container

    $ docker run --name t1 -d -p 80 yaocl/static_web:v1 nginx -g "daemon off;"
    4dbe731715cb046c6c12bb23264e9b4add0631948b3dfa15cb4e740a586eed68

    以 ps 指令查看主機隨機以 49153 ~ 65535 任意一個 port 對應到 80 的資訊

    $ docker ps
    CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS              PORTS                   NAMES
    4dbe731715cb        yaocl/static_web:v1   "nginx -g 'daemon off"   29 seconds ago      Up 28 seconds       0.0.0.0:32768->80/tcp   t1

    瀏覽器可以連結到 http://localhost:32768 查看網頁

    如果是 -p 80:80 就是使用主機的 80

    $ docker run --name t1 -d -p 80:80 yaocl/static_web:v1 nginx -g "daemon off;"

    綁定主機的 127.0.0.1:80

    $ docker run --name t1 -d -p 127.0.0.1:80:80 yaocl/static_web:v1 nginx -g "daemon off;"

Dockerfile 指令

Dockerfile reference

  1. CMD

    指定 container 最後要執行的命令,也可以用 docker run 覆蓋 CMD 的指令

    CMD ["nginx", "-g", "daemon off;"]

    可以省略最後用 /bin/sh -c 執行的指令

    $ docker run --name t2 -d -p 127.0.0.1:80:80 yaocl/static_web:v2
  2. ENTRYPOINT

    跟 CMD 類似但不一樣,使用 ENTRYPOINT 可以接受 run 後面增加的參數

    ENTRYPOINT ["/usr/sbin/nginx"]

    執行時會增加後面 -g "daemon off;" 的參數給 nginx

    $ docker run --name t2 -d -p 127.0.0.1:80:80 yaocl/static_web:v2 -g "daemon off;"

    所以實際上使用會是

    ENTRYPOINT ["/usr/sbin/nginx"]
    CMD ["-g", "daemon off;"]

    或是在沒有參數時,列印 help 資訊

    ENTRYPOINT ["/usr/sbin/nginx"]
    CMD ["-h"]
  3. WORKDIR

    在 container 內設定工作目錄,執行 ENTRYPOINT 或 CMD

    使用時可以在過程中隨時切換目錄

    WORKDIR /opt/webapp/db
    RUN bundle install
    WORKDIR /opt/webapp
    ENTRYPOINT ["rackup"]
  4. ENV

    設定環境變數,可以在 docker run 以 -e 參數指定環境變數

    ENV RVM_PATH /home/rvm/
    
    ENV TARGET_DIR /opt/app
    WORKDIR $TARGET_DIR
    docker run -i -t -e "WEB_PORT=8080" ubuntu env
  5. USER

    指定用什麼帳號身份執行指令,可以用 docker run 的 -u 參數覆蓋這個設定,如果沒有指定,預設為 root

    USER user
    USER user:group
    USER uid
    USER uid:gid
    USER uid:group
  6. VOLUME

    對 container 增加 volume,一個 volume 可同時存在於一個或多個 container 的特定目錄,該目錄可繞過聯合文件系統,進行資料共享或持久化的工作

    • volume 可在 container 之間共享, 重用
    • container 不一定要跟其他 container 共享 volume
    • 對 volume 修改資料會立即生效
    • 修改 volume 裡面的資料,不會對 image 產生影響
    • volume 會一直存在,直到沒有任何 container 使用它
    VOLUME ["/opt/project", "/data"]
  7. ADD

    將建構環境中的檔案複製到 image 中,後面的目的地檔名不能省略,新目錄或檔案權限都是 0755,UID及 GID 都是 0

    ADD sw.lic /opt/application/sw.lic
    ADD http://wordpress.org/latest.zip /root/wordpress.zip

    這會自動將 latest.tar.gz 解壓縮到目的地的目錄中

    ADD latest.tar.gz /var/www/wordpress/
  8. COPY

    類似 ADD,但沒有解壓縮的功能

    COPY conf.d/ /etc/apache2/
  9. ONBUILD

    為 image 增加 trigger 的功能,當 image 被使用時,就會觸發 trigger

    ONBUILD ADD . /app/src
    ONBUILD RUN cd /app/src && make

Docket hub 與私有的 Docker Registry

push image to Docker hub

docker push static_web

也可以在 docket hub 增加 github/BitBucket 帳號連結,然後製作 Automated Build 自動產生 image


用這個指令可以產生一個 private docker registry

docker run -p 5000:5000 registry
$ docker images yaocl/static_web
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
yaocl/static_web    v2                  1d48b7f306d8        2 hours ago         231.1 MB
yaocl/static_web    v1                  ce19993fc181        3 hours ago         231.1 MB 

將 image push 到 docker.example.com:5000 這個 private registry

docker tag 1d48b7f306d8 docker.example.com:5000/yaocl/static_web
docker run -t -i docket.example.com:5000/yaocl/static_web /bin/bash

scripts

移除所有 stopped containers

docker rm $(docker ps -a -q)

移除所有 UNTAG images

docker rmi $(docker images | grep "<none>" | awk '{print $3}')

References

[Docker] 在 Mac 上使用原生的 Docker for Mac 操作 container

撰寫一份符合需求的 Dockerfile

2017/04/30

談談 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&amp;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&amp;characterEncoding=utf-8&autoReconnect=true&useSSL=false

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

miniweb.url = jdbc:mysql://db2:3306/miniweb?useUnicode=true&amp;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&amp;characterEncoding=utf-8&autoReconnect=true&useSSL=false

2017/04/08

Dcoker 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

2017/03/19

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(一)

2017/03/04

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

2017/02/15

Dockerfile 初步閱讀與學習紀錄

最近花了點時間閱讀 Docker 官網的上 Dockerfile reference 這篇教學文件。閱讀後在加上一些自己實作測試,稍微有點心得,因此就想說再寫篇文章當作學習筆記吧。

Dockerfile 簡介

在 docker 要自己建置 image,你可以使用比較麻煩點的方法,就是自己先把自己想要的 base image 從 docker hub 拉下來,然後用 container 執行它,接著在 container 內執行你想做的命令,然後在 commit container 讓他變成 image,這方式就會變成是一直在重複用 container run,然後 commit 成 image 的動作。

docker 提供了另一種方式來建置 image,也就是本篇要提到的 Dockerfile。docker 可以透過 Dockerfile 讀指令的方式自動建立 image,也就是說你可以把你要 build image 的步驟透過 Dockerfile 的指令一步一步的寫出來,這樣就能使用 docker build 指令來幫你自動建置 image。至於中間過程需要用 container 執行你的命令、再將 container commit 回 image 這些瑣碎的細節,你可以不需要太在意他。

Context:

在開始寫 Dockerfile 之前,要先知道 build context 的概念,context 可以是本地端的目錄或是某個 URL(Git repository),context 不論是本地端目錄或是 URL,其底下的所有子目錄或是檔案都是屬於 context 內。

當你在 command line 下 docker build 指令要透過 Dockerfile 來 build image 時,docker 並不會在 command line 直接幫你 build,而是要透過 docker daemon 來執行。它會把 build context 傳送給 docker daemon,之後 docker daemon 在幫你 build。

所以 build 的一切過程,是在 docker daemon 上執行的,你在 build 過程中如果要複製檔案到 image 內,檔案也必須要先放在 context 內,這樣 docker daemon 才能在 build 過程中找到檔案。

另外你也可以把 build image 相關的說明文件放在 context 目錄內,然後在搭配 .dockerignore 檔案讓說明文件在 docker build 時不會被傳送到 docker daemon 中。

這邊的 context 有點像是在寫 project 時,一個 project 會放在某個目錄底下,該目錄可以視為一個 context,然後該 project 要執行所需的原始碼、lib、以及相關的說明文件都會放在此目錄底下的。

Dockerfile command:

Dockerfile command 說多不多,以下列出並加上簡短說明。

  • FROM:base image 設定,你在 build image 時,一定需要指定一個 image 來源。
  • RUN:build 時使用。會執行你的命令,然後 commit 結果,被 commit 的結果會被 Dockerfile 下一步驟所使用。
  • CMD:run container 時要執行的命令,要注意這個跟 RUN 的區別,他是指當你使用 docker run 時,container 建立起來的時候要跑什麼樣的命令。另外一個 Dockerfile 內要有一個 CMD 或是等等會提到的 ENTRYPOINT。
  • LABEL:標籤,會是以 key=value 的形式來定義。你可以用 LABEL 記錄一些 image 資訊,如版號、作者等等的訊息。官方建議用一個 LABEL 指令來定義多組值會比較有效率。
  • EXPOSE:告訴 container 在執行時需要監聽哪個 port。注意,EXPOSE 並不是直接開放 port 給外界訪問,要讓外界訪問,你要在下 docker run 指令時加上 -p flag 才能讓外界訪問。
  • ENV:環境變數設定。
  • ADD:將檔案加到 images 內,檔案來源可以是 URL 或是 context 內的檔案。
  • COPY:將檔案加到 images 內,檔案來源是 context 內的檔案,注意這個跟 ADD 的區別,上網查有人是說這個語意清楚,行為單純,就是複製檔案的概念,建議用這個。
  • ENTRYPOINT:run container 時要執行的命令,可以和 CMD 混搭用,此時 CMD 會被視為初始參數來使用。
  • VOLUME:掛載目錄用。
  • USER:可以指令使用者為何。在此指令過後的 Dockerfile 指令,都會用這個使用者來執行。
  • WORKDIR:工作目錄設定,在執行此指令後,在 Dockerfile 內後續的 RUN、CMD、ENTRYPOINT、COPY
  • ARG:buid 時可以設定參數,讓 docker build 指令可以下參數來處理。
  • ONBUILD:提供一個 trigger 機制,當別人要使用你 build 好的 image 當成 base 時,如果你當初在 build 時有寫 ONBUILD 指令,則別人在用這個 image 當成 base image 時,當跑完他 Dockerfile 的 FROM 指令之後會先跑你寫的 ONBUILD,接著才會開始跑它寫的其他指令。
  • STOPSIGNAL:要不要在結束時收到系統發的信號。
  • HEALTHCHECK:提供一個檢查的機制,你可以透過 HEALTHCHECK 來讓 docker 知道什麼樣的狀況對你的 container 來說才算是正常的。
  • SHELL:改變預設的 shell。Dockerfile command 會有 shell 格式的命令,預設是用 /bin/sh -c 來執行命令,你可以透過 SHELL 強制修改掉。

一個簡單的 Dockerfile 範例

接下來示範怎麼寫一個簡單的 Dockerfile:

  1. 首先先在本機上決定你的 context 要在哪,比如說我用以下路徑的 foobar 目錄,foobar 底下的所有檔案都會視為 context。

    /Users/mayer/Documents/dockerspace/foobar
  2. 由於我是使用 Mac 系統,所以我要用 docker 啟動時一併開啟的終端機來 build,先將終端機切換到該目錄:

    cd /Users/mayer/Documents/dockerspace/foobar
  3. 之後,透過 vi 新增 Dockerfile

    vi Dockerfile
  4. 然後開始撰寫 Dockerfile,這邊 FROM image 我們使用 alpine linux,這是個容量只有 3.9 MB 左右的超輕量 Linux,然後預設他並不會執行任何的命令,所以我們會讓他預設起來就執行 top 指令,Dockerfile 內容如下:

    FROM alpine:3.5
    CMD ["top"]
  5. 之後使用 :wq 離開 vi,這樣 Dockerfile 檔案就準備好了。

  6. 接著要使用此 Dockerfile 來 build 我們自己的 image,我們可以使用 docker build 指令來處理,以下是 build 的資訊:

    $ docker build -t mayer/foo:1.0.0 .
    Sending build context to Docker daemon 2.048 kB
    Step 1 : FROM alpine:3.5
    3.5: Pulling from library/alpine
    0a8490d0dfd3: Pull complete 
    Digest: sha256:dfbd4a3a8ebca874ebd2474f044a0b33600d4523d03b0df76e5c5986cb02d7e8
    Status: Downloaded newer image for alpine:3.5
     ---> 88e169ea8f46
    Step 2 : CMD top
     ---> Running in 5f41dcb5519c
     ---> 7265d7f11cd1
    Removing intermediate container 5f41dcb5519c
    Successfully built 7265d7f11cd1

    上述的 docker build 指令有用上 -t flag,用來指定 image 名稱與 tag,另外後面的 . 代表 context 的路徑為當下目錄。

    下完指令後的第一個訊息,可以看到 docker 先把 build context 傳送給 Docker daemon,然後才開始 build 步驟,首先 docker daemon 在執行 FROM 指令時發現到本地端沒有 alpine:3.5 這個 image,所以他先幫你 pull 下來,然後結束第一步驟。接著執行第二步驟 CMD top,指定這個 image 在透過 container 啟動時預設要執行的命令為 top,這樣就完成了 build image 步驟。

  7. 可以透過 docker images 查看是否 build 成功:

    $ docker images
    REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
    mayer/foo           1.0.0               7265d7f11cd1        8 minutes ago       3.984 MB
  8. 接著使用 container 來執行這個 image,這邊用 -d flag 讓他在背景執行:

    docker run -d mayer/foo:1.0.0
  9. 你可以透過 docker ps 來查詢 container 運行狀態:

    $ docker ps
    CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
    8332ee2f38a6        mayer/foo:1.0.0     "top"               30 seconds ago      Up 29 seconds                           nauseous_albattani      
  10. 另外除了 docker ps 指令之外,你也可以 Kilematic 工具來直接觀看 container 運作的內容,看畫面在跑感受會比較深一點。

透過以上步驟,我們已經完成了透過 Dockerfile 來 build image,然後使用 container 來執行 image 的流程了。

接著下面要講記錄一些讓人有些困惑的指令。

ENTRYPOINT 與 CMD

假設有個 Dockerfile 長這樣:

FROM centos:7.2.1511
ENTRYPOINT ["top", "-b"]
CMD ["-c"]

我們透過此 Dockerfile 來 build images:

docker build -t mayer/foo .

接著執行兩個 container:

docker run -d mayer/foo
docker run -d mayer/foo -H

使用 docker ps -a 查詢:

CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
83b6433d168b        mayer/foo           "top -b -H"         20 minutes ago      Up 4 minutes                            modest_hodgkin
20ba3187d8e3        mayer/foo           "top -b -c"         21 minutes ago      Up 9 seconds                            angry_bardeen

可以看到 Docker 會執行 ENTRYPOINT,若有 CMD,他會將它當成預設參數,你可以在 run 時改變它。

另外可以下指令查 images 相關資訊:

docker inspect mayer/foo:latest

他會印出此 image 預設的 Entrypoint 與 Cmd 為何:

    ...
    "Cmd": [
        "-c"
    ],
    "Image": "sha256:97aa54a9a5ebfeff5a2b1992a1e0cb0721162fc15061da2c4192205ba93f8554",
    "Volumes": null,
    "WorkingDir": "",
    "Entrypoint": [
        "top",
        "-b"
    ],
    ...

WORKDIR:

image 的 workdir 可以用 docker inspect 來觀察,比如說 tomcat:8.5.11-jre8 的:

$ docker inspect tomcat:8.5.11-jre8

...
"WorkingDir": "/usr/local/tomcat",
...

可以看到此 image,他把 WorkingDir 設定在 /usr/local/tomcat。然後我們想從這個 image 加上自己的 webapp,我們該如何做?

首先,在本機上建立一個目錄,把這個目錄視為 context,這邊先用以下路徑的 sample 目錄作為 context:

/Users/mayer/Documents/dockerspace/sample

然後用終端機切換到那個目錄內:

cd /Users/mayer/Documents/dockerspace/sample

然後我們要將 Web Application 放到這目錄,

mv /somepath/sample.war ./

接著使用 vi 來編輯檔案

vi Dockerfile

Dockerfile,內容如下:

FROM tomcat:8.5.11-jre8
COPY sample.war webapps/

可以注意到,這邊 COPY 指令的 dest,是直接寫 webapps/,這邊能這樣寫,是因為他有設定了 WORKDIR 的緣故,其實真正的 dest 會是:

/usr/local/tomcat/webapps

所以上面的 Dockerfile 是說,我要 build 一個 images,來源是 tomcat:8.5.11-jre8,然後幫我把檔案 sample.war 複製到 /usr/local/tomcat/webapps 目錄底下,這樣一來我就可以直接使用 docker build 來建立一個裡面包含我 webapp 的 tomcat image 了:

docker build -t mayer/sample .

注意 docker build 最後那個 . 代表 context 為當下目錄,不能省略。

CMD 與 EXPOSE

接著要如何執行呢?在執行之前,讓我們再看一下 tomcat:8.5.11-jre8 的詳細資訊

$ docker inspect tomcat:8.5.11-jre8

...
"ExposedPorts": {
            "8080/tcp": {}
        },
...
"Cmd": [
    "catalina.sh",
    "run"
],
...
"Entrypoint": null,
...

這邊要注意的是這三個資訊,一個是預設執行的 container 會監聽 8080,一個是他的初始 Cmd 就是讓 tomcat 啟動,然後他沒有寫 Entrypoint,所以可以不用擔心會影響到 Cmd,也就是說如果我們要使用我們 build 的 image 的話,我們只需要使用 docker run 指令來啟動 container 並執行,如下:

docker run -d -p 8080:8080 mayer/foo

注意 port 要透過 -p flag 處理,冒號前面的 8080 代表本地端機器的 port,冒號後面的 8080 代表 container 的 port。

更多實際應用範例

如果你需要更多範例,請參考官網 Docker Documentation - Dockerization examples 這個網頁,裡面包含像是 MongoDB、PostgreSQL 之類的應用使用 Dockerfile 來 build image 的過程。

Reference:

Docker Documentation - Dockerfile reference

Stackoverflow - What is the difference between the COPY and ADD commands in a Dockerfile?

Stackoverflow - What is the difference between CMD and ENTRYPOINT in a Dockerfile?

segmentfault - Dockerfile里指定执行命令用ENTRYPOING和用CMD有何不同?

2015/01/27

Mac OS X 裡安裝 Docker

最近 Docker 成了當紅炸子雞,公司內部也將開發的測試環境,開始逐漸移轉到 Docker 上,在此介紹一下 Mac OS X 環境安裝 Docker 的程序

Step 1.
首先安裝 Vitual box https://www.virtualbox.org/wiki/Downloads
在下載的網頁裡面可以選擇下載 OS X hosts ,目前是 4.3.20 版
下載後請直接安裝。

Step 2.
用Homebrew 來安裝 docker

$ brew install docker

==> Downloading https://downloads.sf.net/project/machomebrew/Bottles/docker-1.4.1.yosemite.bottle.tar.gz
Already downloaded: /Library/Caches/Homebrew/docker-1.4.1.yosemite.bottle.tar.gz
==> Pouring docker-1.4.1.yosemite.bottle.tar.gz
==> Caveats
Bash completion has been installed to:
  /usr/local/etc/bash_completion.d

zsh completion has been installed to:
  /usr/local/share/zsh/site-functions


Step 3.
安裝這次的重點 boot2docker,boot2docker 會用 Virtualbox 建立 Linux VM,在 Linux VM 上開啟 docker daemon 後,就可以直接對 VM 裡的 docker 下控制命令。
我們來繼續安裝 boot2docker

$ brew install boot2docker

==> Downloading https://downloads.sf.net/project/machomebrew/Bottles/boot2docker-1.4.1.yosemite.bottle.tar.gz
######################################################################## 100.0%
==> Pouring boot2docker-1.4.1.yosemite.bottle.tar.gz
==> Caveats
To have launchd start boot2docker at login:
    ln -sfv /usr/local/opt/boot2docker/*.plist ~/Library/LaunchAgents
Then to load boot2docker now:
    launchctl load ~/Library/LaunchAgents/homebrew.mxcl.boot2docker.plist

Step 4.
使用 boot2docker init 指令,建立起 VM 的環境
    
$ boot2docker init

Latest release for boot2docker/boot2docker is v1.4.1
Downloading boot2docker ISO image...
Success: downloaded https://github.com/boot2docker/boot2docker/releases/download/v1.4.1/boot2docker.iso
    to /Users/martin/.boot2docker/boot2docker.iso
Generating public/private rsa key pair.
Your identification has been saved in /Users/martin/.ssh/id_boot2docker.
Your public key has been saved in /Users/martin/.ssh/id_boot2docker.pub.
The key fingerprint is:
4e:1d:89:54:78:95:e7:f2:10:8c:11:90:a0:02:f1:a6 martin@Martin-MacBook-Pro-2.local
The key's randomart image is:
+--[ RSA 2048]----+
|o.    ..o=+*..   |
| o   . .o.o.+ .  |
|  + .   ..o  +   |
| o .     . .o .  |
|E       S .  +   |
|       o      .  |
|        .        |
|                 |
|                 |
+-----------------+

Step 5.
啟動 boot2docker

$ boot2docker start

Waiting for VM and Docker daemon to start...
..........................................oooooooooooooooooooooooooooooooooo
Started.
Writing /Users/martin/.boot2docker/certs/boot2docker-vm/ca.pem
Writing /Users/martin/.boot2docker/certs/boot2docker-vm/cert.pem
Writing /Users/martin/.boot2docker/certs/boot2docker-vm/key.pem

To connect the Docker client to the Docker daemon, please set:
    export DOCKER_HOST=tcp://192.168.59.103:2376
    export DOCKER_CERT_PATH=/Users/martin/.boot2docker/certs/boot2docker-vm
    export DOCKER_TLS_VERIFY=1

Step 6.
記得一一使用 export 來將這些環境變數設定在Mac OS X 裡
PS: 這裡的環境變數請參考 Step 5 啟動 boot2docker 後的顯示說明

$export DOCKER_HOST=tcp://192.168.59.103:2376
$export DOCKER_CERT_PATH=/Users/martin/.boot2docker/certs/boot2docker-vm
$export DOCKER_TLS_VERIFY=1

Step 7.
使用 docker version 查看一下是否已經安裝成功並且成功啟用,如果有出現以下訊息,那就確認安裝沒問題。
    
$ docker version

Client version: 1.4.1
Client API version: 1.16
Go version (client): go1.4
Git commit (client): 5bc2ff8
OS/Arch (client): darwin/amd64
Server version: 1.4.1
Server API version: 1.16
Go version (server): go1.3.3
Git commit (server): 5bc2ff8

您現在已經可以開始使用 docker 指令,下載 Image ,操作docker container 。

2014/12/06

在Docker上部署Java Web Application

Docker很友善,它提供一個Docker Hub平台,在上面你可以找到很多由許多官方提供的Image檔,因此像我這種對於Linux目錄結構以及軟體相依性之類的事情不太熟悉的人,也能夠輕鬆的直接透過Docker Hub抓取Tomcat image,獲得一個官方建制好的Image,直接使用它來將自己開發的Web Application丟上去執行,實在是太方便了!

執行流程

首先先將tomcat官方提供的images抓回來,以下為官方images檔的網址:
https://registry.hub.docker.com/_/tomcat/

進去之後,你可以看到它有很多的Tags,分別代表Tomcat的版本以及它所使用的Java版本,這次我選用tomcat7配上jre8的版本,因此透過以下指令抓取images回來:
docker pull tomcat:7.0.57-jre8
接著我要啟動Container,並且我將它命名為mayer_tomcat,然後將設定其port綁定,將實體機器上18080與Container的8080port做綁定,接著使用-d標籤讓它在背景執行:
docker run --name mayer_tomcat -p 18080:8080 -d tomcat:7.0.57-jre8
這樣一來,Tomcat Container就已經啟動了,由於我是在Mac上測試的關係,因此我必須先下指令查詢boot2docker的ip為何:
bash-3.2$ boot2docker ip

The VM's Host only interface IP address is: 192.168.59.103  
得到ip之後,就可以直接開啓瀏覽器,輸入192.168.59.103:18080,就能看到Tomcat畫面:

接著使用Eclipse開啓一個新的Dynamic Web Project,將之命名為dockerap,這邊我只弄了一個index.html的檔案,內容只印出Hello Docker,然後我將它打包成war檔,並且上傳至網路空間。
接著使用exec指令,執行該Container的bash:
docker exec -it mayer_tomcat /bin/bash
進去之後,預設路徑為/usr/local/tomcat,我先切換至webapps目錄,接著在將該war檔下載回來,最後離開bash:
cd webapps
wget https://dl.dropboxusercontent.com/u/xxxxxxxx/dockerap.war
exit
最後重新啟動Tomcat Container:
docker restart mayer_tomcat
開啓瀏覽器,輸入192.168.59.103:18080/dockerap/,就能看到Web Application已經成功部署在Container上了,大功告成!