---
# System prepended metadata

title: Docker
tags: [Container, LSA, NCNU, DevOps]

---

Docker
===
###### tags: `NCNU` `LSA` `Container` `DevOps`

## First
### Install
``sudo apt install docker.io``

### Check status
``service docker status``

### Check version
``docker version``

正常會顯示這樣
```
Client:
 Version:           18.09.2
 API version:       1.39
 Go version:        go1.10.4
 Git commit:        6247962
 Built:             Tue Feb 26 23:52:23 2019
 OS/Arch:           linux/amd64
 Experimental:      false

Server:
 Engine:
  Version:          18.09.2
  API version:      1.39 (minimum version 1.12)
  Go version:       go1.10.4
  Git commit:       6247962
  Built:            Wed Feb 13 00:24:14 2019
  OS/Arch:          linux/amd64
  Experimental:     false
```

若有出現什麼錯誤或是權限問題
需將帳號加入 docker 群組並登出/重起

``sudo usermod -aG docker yourAccount``

### Search Image
舉例搜尋 hello-world

``docker search hello-world``

```
NAME                                       DESCRIPTION                                     STARS               OFFICIAL            AUTOMATED
hello-world                                Hello World! (an example of minimal Dockeriz…   871                 [OK]                
kitematic/hello-world-nginx                A light-weight nginx container that demonstr…   124                                     
tutum/hello-world                          Image to test docker deployments. Has Apache…   59                                      [OK]
dockercloud/hello-world                    Hello World!                                    15                                      [OK]
hypriot/armhf-hello-world                  Hello World! (an example of minimal Dockeriz…   6                                       
...
```

選好之後下載

``docker image pull hello-world``
:::info
可以不加 image
但現在版本對指令有規範 故養成好習慣
以下 image 指令皆不再贅述
:::

### View all downloaded Images
``docker image ls``
:::info
也可以用 ``docker images``
:::

```
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
hello-world         latest              fce289e99eb9        3 months ago        1.84kB
```

### Run Image
``docker container run hello-world``
:::info
可以不加 container
但現在版本對指令有規範 故養成好習慣
以下 container 指令皆不再贅述
:::

### Delete Image
- ``docker image rm {Image Name}:{Tag}``
- ``docker image rm {Image ID}``

:::info
image rm 可寫成 rmi
:::
ex:
``docker rmi hello-world:latest``
``docker rmi fce289e99eb9``

刪除所有 image
``docker image prune``

#### 如果 image 已經在 container 中被使用
1. 查看所有 container
``docker container ls [-a]``(預設只顯示執行中的)
也可以用 ``docker ps [-a]``
```
CONTAINER ID        IMAGE                    COMMAND             CREATED             STATUS                     PORTS               NAMES
4fc65c601e5a        hbdoy/node-web-app:1.0   "npm start"         6 minutes ago       Exited (0) 3 minutes ago                       zealous_feistel
4a7891a432a2        hello-world              "/hello"            5 hours ago         Exited (0) 5 hours ago                         sad_beaver
```

2. 終止該 container
``docker container stop {CONTAINER ID}``

3. 移除該 container
``docker container rm {CONTAINER ID}``

4. 刪除 image
``docker rmi {Image ID}``

### Other Container Command
#### create
``docker container create {Image} [--name xxx]``

``docker container create {Image} sh -c "while true; do sleep 3600; done"``

#### start
``docker container start {Container ID}``

#### create & start
``docker container run {Image} [-d]``
:::info
``-d`` 讓 container 在背景執行
:::

#### filter
``docker container ls [-q] -f status=created``
:::info
``-q`` 只會列出 Container ID
:::

批次啟動
``docker container start $(docker container ls -q -f status=created)``

批次刪除
``docker container rm $(docker container ls -a -q)``
``docker container prune``

查看 container IP
``docker inspect <container ID> | grep IPAddress``

### Commit
若是 image 內容有更新，可以重新打包成一個新的 image
``docker commit {container ID} {newName}[:tag] ``

ex: 在 ubuntu image 安裝 nginx
```
docker run -d -p 80:80 -it ubuntu /bin/bash
apt-get update
apt-get install nginx
service nginx start
ps aux

exit
```
```
docker commit {container id} ubuntu_nginx
```

## Docker Machine
方便快速的建立 docker host (擁有 docker 環境)
若有多台虛擬機則不需要手動逐一安裝 docker

### Install
```
base=https://github.com/docker/machine/releases/download/v0.16.0 &&
  curl -L $base/docker-machine-$(uname -s)-$(uname -m) >/tmp/docker-machine &&
  sudo install /tmp/docker-machine /usr/local/bin/docker-machine
```

### Check version
``docker-machine version``

### Create
:::info
請事先安裝 virtualbox
:::
:::danger
若是因為 secure boot 導致 virtualbox 無法使用可以參考:
[VirtualBox + Secure Boot + Ubuntu = fail](https://stegard.net/2016/10/virtualbox-secure-boot-ubuntu-fail/)
:::

``docker-machine create demo``

預設的 driver 會是 virtualbox
如果需要更改可以加上參數 ``-d "xxx"``
或是直接輸入 ``docker-machine create`` 查看更多說明

#### 查看建立的 machine
``docker-machine ls``

#### 常用指令
``docker-machine start demo``
``docker-machine stop demo``
``docker-machine rm demo``
``docker-machine scp my-file.txt demo:~``

### SSH
``docker-machine ssh demo``

## Play with Docker
只是想要試試 Docker 但覺得安裝太麻煩了嘛
可以試試 [Play with Docker](https://labs.play-with-docker.com/)
註冊後就可以得到一個臨時的 Docker Host（4小時就會重置一次）
可以透過 web terminal 或 ssh 進去使用

## Docker Hub
[Docker Hub](https://hub.docker.com/) 像是 Github 只是裡面存放 docker images
可以上去找自己需要的 images 也可以把自己的 images 放上去

### Make your own Image
此處以 nodejs 為例
1. 建立最基本的 express 範例
```
$ npm init
$ npm install express --save
```
**index.js**
```javascript=
var express = require("express");

var app = express();

app.get('/', function (req, res) {
    res.send('Hello World!');
});

app.listen(3000, function () {
    console.log('Example app listening on port 3000!');
});
```
**package.json**
```
{
  "name": "demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node index.js"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.16.4"
  }
}
```

2.在相同目錄下建立 ``Dockerfile`` 檔案
```
FROM node:8

# Create app directory
WORKDIR /usr/src/app

# Install app dependencies
COPY package*.json ./

RUN npm install

# Bundle app source
COPY . .

# port
EXPOSE 3000

CMD [ "npm", "start" ]
```

以及 ``.dockerignore``
```
node_modules
npm-debug.log
```

3. Build
```
docker build -t {name}:{tag} .
```

ex: 為了方便推上 Dockerhub，name 可以取 Dockerhub 帳戶名稱即可
```
docker build -t hbdoy/node-web-app:1.0 .
```

4. 查看是否建立完成

``docker images``
```
REPOSITORY           TAG                 IMAGE ID            CREATED             SIZE
hbdoy/node-web-app   1.0                 25960f3be895        7 minutes ago       898MB
node                 8                   8c51cec97ebf        9 days ago          895MB
```

5. 執行 Image

``docker run -p 49160:3000 -d hbdoy/node-web-app:1.0``
:::info
``-p`` 映射 port
:::

6. 查看 console

``docker ps``
```
CONTAINER ID        IMAGE                    COMMAND             CREATED             STATUS              PORTS                     NAMES
f2c21b27a12d        hbdoy/node-web-app:1.0   "npm start"         10 seconds ago      Up 8 seconds        0.0.0.0:49160->3000/tcp   agitated_swirles
```

``docker logs {CONTAINER ID}``
```
> demo@1.0.0 start /usr/src/app
> node index.js
> Example app listening on port 3000!
```

接著查看 ``localhost:49160`` ~

7. 也可以進入 container 中

``docker exec -it {Container ID} /bin/bash``
```
root@05dd08c41e21:/usr/src/app# ls
Dockerfile  index.js  node_modules  package-lock.json  package.json
```

8. 將 Image 上傳
沒有登入過的先執行 ``docker login``
接著 ``docker push {REPOSITORY}:{TAG}``
ex:
``docker push hbdoy/node-web-app:1.0``

:::success
透過 Dockerfile 就能夠方便快速的建立環境，這也是 **Infrastructure as Code** 的理念
:::

### 改裝現有 Image
覺得 Dockerhub 上的 image 功能不夠多嘛
也可以自己包喔

此處以 ubuntu image 為例

**`Dockerfile`**
```
FROM ubuntu:18.04

# Install Linux library dependency
RUN apt-get update
RUN apt-get install xxx

# Display Greeting
CMD [ "echo", "Ubuntu 18.04 LTS with xxx"]
```

只要有這個 Dockerfile 就能夠建立自訂的 ubuntu container 了，還可以把 Dockerfile 列入 git 版控

## Container Network Model(CNM)
### Network Namespace
每個 container 都有自己的 network interface、routing table、Firewall rules

![](https://i.imgur.com/WPYC55F.png)

### CNM Drivers
![](https://i.imgur.com/07EXg7C.jpg)

Container 在單個 Docker Host 的通訊方式
1. Linux Bridge
2. IPtables

Container 在多個 Docker Hosts 的通訊方式
1. Tunnel
    - Docker build-in overlay network: VXLAN
    - OVS: VXLAN or GRE
    - Flannel: VXLAN or UDP
    - Weave: VXLAN or UDP
3. Routing
    - Calico: Layer3 routing based on BGP
    - Contiv: Layer3 routing based on BGP

[更多請查看](https://github.com/docker/libnetwork/blob/master/docs/design.md)


## 單一 host 中的兩個 container
此處範例為一個 python flask 建立的 web server，還有用來儲存訪問次數的 redis 資料庫，並將 web server、redis 各別包成 container。

**`app.py`**

Q: 在沒有 docker 情況下，redis 的位置可以用環境變數來給予
但當 redis 包成 container 時，該如何知道它的 ip 位址呢?

A: 可以透過 bridge 的原理，先啟動 redis 的 container，接著在 build web server 時加入 ``link`` 的參數連結到 redis 的 container。 

```python=
from flask import Flask
from redis import Redis
import os
import socket

app = Flask(__name__)
redis = Redis(host=os.environ.get('REDIS_HOST', '127.0.0.1'), port=6379)

@app.route('/')
def hello():
    redis.incr('hits')
    return 'Hello Container World! I have been seen %s times and my hostname is %s.\n' % (redis.get('hits'),socket.gethostname())

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000, debug=True)
```

**`requirements.txt`**
```
flask
redis
```

**`Dockerfile`**
```
FROM python:2.7
MAINTAINER hbdoy "s104213046@mail1.ncnu.edu.tw"
COPY . /app
WORKDIR /app
RUN pip install -r requirements.txt
EXPOSE 5000
CMD [ "python", "app.py" ]
```

### Step1
建立 redis container，並將 container 名稱設為 redis

``docker run -d --name redis redis``

執行後可以查看是否正確運行

``docker container ls``
```
CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS              PORTS               NAMES
81b566229b77        redis               "docker-entrypoint.s…"   About a minute ago   Up About a minute   6379/tcp            redis
```

### Step2
先將 ``app.py`` 中的 host 改成剛剛建立的 redis container 名稱
```python=
redis = Redis(host='redis', port=6379)
```

接著 build 它

``docker build -t hbdoy/python-web .``

### Step3
接下來就可以執行剛剛 build 好的 python-web 了

``docker images``
```
REPOSITORY           TAG                 IMAGE ID            CREATED             SIZE
hbdoy/python-web     latest              179ea3ad8ed5        2 minutes ago       919MB
```

``docker run -d --name python-web --link redis -p 5000:5000 179ea3ad8ed5``

### Step4
查看運行狀況

``docker container ls``
```
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
7f383159f5c4        179ea3ad8ed5        "python app.py"          2 minutes ago       Up 2 minutes        0.0.0.0:5000->5000/tcp   python-web
81b566229b77        redis               "docker-entrypoint.s…"   13 minutes ago      Up 13 minutes       6379/tcp                 redis
```

沒問題就連接看看吧!
```
hbdoy@hbdoy:~$ curl 127.0.0.1:5000
Hello Container World! I have been seen 4 times and my hostname is 7f383159f5c4.
hbdoy@hbdoy:~$ curl 127.0.0.1:5000
Hello Container World! I have been seen 5 times and my hostname is 7f383159f5c4.
hbdoy@hbdoy:~$ curl 127.0.0.1:5000
Hello Container World! I have been seen 6 times and my hostname is 7f383159f5c4.
hbdoy@hbdoy:~$ curl 127.0.0.1:5000
Hello Container World! I have been seen 7 times and my hostname is 7f383159f5c4.
hbdoy@hbdoy:~$ curl 127.0.0.1:5000
Hello Container World! I have been seen 8 times and my hostname is 7f383159f5c4.
hbdoy@hbdoy:~$ curl 127.0.0.1:5000
Hello Container World! I have been seen 9 times and my hostname is 7f383159f5c4.
hbdoy@hbdoy:~$ curl 127.0.0.1:5000
Hello Container World! I have been seen 10 times and my hostname is 7f383159f5c4.
```

## Docker Volume
當你使用 volume 時，docker 會在你的本機上隨機新增一個資料夾（Local storage area），大部分會在 /var 底下，然後讓這個資料夾跟 container 裡面的某個資料夾互通。
因為他們是互通的，所以當你 container 裡面那個資料夾有任何變更時，本地的資料夾也會跟著變，而且很重要的一點是：container 被刪掉時那個資料夾還會原封不動保留在那邊。

```
docker volume create --name <volume>

# 新增了一個 volume 叫做 db-data
docker volume create --name db-data

docker volume ls
```

在啟動時加一個 -v 參數，就可以指定 volume 要跟容器內哪一個資料夾連通
```
# 查看裡面有什麼檔案
docker run -v db-data:/db/data -it ubuntu ls -l /db/data

# 增加一個檔案 test
docker run -v db-data:/db/data -it ubuntu touch /db/data/test

# 查看裡面有沒有新增檔案
docker run -v db-data:/db/data -it ubuntu ls -l /db/data
```

## Docker Compose
上面的例子中，只是簡單的計數應用，就有兩個 container。

正常的服務更為複雜，可能需要打包好幾個 services，這幾個 images 在 run 時可能又會有相依與先後問題，管理與執行將會變得很複雜，所以可以用 Docker Compose 來管理。

:::info
Docker Compose 為 yaml 格式，定義了 services、network、volumes 等等
:::

service 指的是 container，而它的 image 來源可能是透過 Dockerfile 來 build，或是從 Dockerhub 下載的，或是其他地方。

### Install
指令中的 **1.24.0** 為版本號（目前最新）
```
sudo curl -L "https://github.com/docker/compose/releases/download/1.24.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
```
```
sudo chmod +x /usr/local/bin/docker-compose
```

### DIY
延續上面 python 的例子，在相同目錄下建立

**`docker-compose.yml`**
```
version: "3"

services:
  web:
    build: .
    ports:
      - "80:5000"
    depends_on:
      - redis
    networks:
      - compose-demo-bridge

  redis:
    image: redis
    ports: ["6379"]
    networks:
      - compose-demo-bridge

networks:
  compose-demo-bridge:
```

執行 build 建立 image

``docker-compose build``

建立好後執行

``docker-compose up [-d]``(``-d`` 可以讓 container 在背景執行)

:::info
預設抓的設定檔名稱是 **docker-compose.yml**
如果要選擇不同的檔名，則需改成
``docker-compose -f xxx.yml up [-d]``
:::

接著到 80 port 看看吧(因為剛剛在 docker-compose 文件中設定的本地 port 是 80)

```
hbdoy@hbdoy:~$ curl 127.0.0.1
Hello Container World! I have been seen 1 times and my hostname is 1c4ae1a99ff3.
hbdoy@hbdoy:~$ curl 127.0.0.1
Hello Container World! I have been seen 2 times and my hostname is 1c4ae1a99ff3.
hbdoy@hbdoy:~$ curl 127.0.0.1
Hello Container World! I have been seen 3 times and my hostname is 1c4ae1a99ff3.
hbdoy@hbdoy:~$ curl 127.0.0.1
Hello Container World! I have been seen 4 times and my hostname is 1c4ae1a99ff3.
hbdoy@hbdoy:~$ curl 127.0.0.1
Hello Container World! I have been seen 5 times and my hostname is 1c4ae1a99ff3.
```

可以查看 container 運行的狀況

``docker container ls``
```
CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS              PORTS                     NAMES
1c4ae1a99ff3        flask-redis_web     "python app.py"          About a minute ago   Up About a minute   0.0.0.0:80->5000/tcp      flask-redis_web_1
fb8d9779af6e        redis               "docker-entrypoint.s…"   About a minute ago   Up About a minute   0.0.0.0:32768->6379/tcp   flask-redis_redis_1
```

也可以查看 docker network 狀況

``docker network ls``
```
NETWORK ID          NAME                              DRIVER              SCOPE
cf62ca42e982        bridge                            bridge              local
ef1e2d2f865f        flask-redis_compose-demo-bridge   bridge              local
05b99dbed86c        host                              host                local
2d9478947980        none                              null                local
```

想要停止的話就執行

``docker-compose stop``

:::success
所以我們只需透過 **`docker-compose.yml`** 就省掉了
1.需要先啟動 redis container
2.接著執行 web service 時還需 link 到 redis 、映射 port
:::

### scale
能夠開啟多個 service

``docker-compose scale {Service Name}={數量}``

這邊結合 load balance 來試試看

``dockercloud/haproxy`` 提供簡單的 load balance 服務

**`docker-compose.yml`**
```
version: "3"

services:
  web:
    build: .
    ports: ["5000"]
    links:
      - redis
    networks:
      - compose-demo-bridge

  redis:
    image: redis
    ports: ["6379"]
    networks:
      - compose-demo-bridge

  lb:
    image: dockercloud/haproxy
    ports:
      - 80:80
    links:
      - web
    networks:
      - compose-demo-bridge
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

networks:
  compose-demo-bridge:
```

執行 build

``docker-compose build``

執行 image

``docker-compose up -d``

查看 container

``docker container ls``
```
CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS              PORTS                                   NAMES
3f731c79e055        dockercloud/haproxy   "/sbin/tini -- docke…"   53 seconds ago      Up 51 seconds       443/tcp, 0.0.0.0:80->80/tcp, 1936/tcp   lb-scaling_lb_1
f45f87c70c18        lb-scaling_web        "python app.py"          55 seconds ago      Up 53 seconds       0.0.0.0:32772->5000/tcp                 lb-scaling_web_1
b983a50f865e        redis                 "docker-entrypoint.s…"   59 seconds ago      Up 55 seconds       0.0.0.0:32771->6379/tcp                 lb-scaling_redis_1
```

連接後可以發現 hostname 都是 **f45f87c70c18**，因為也只有開一個 web service
```
hbdoy@hbdoy:~$ curl 127.0.0.1
Hello Container World! I have been seen 1 times and my hostname is f45f87c70c18.
hbdoy@hbdoy:~$ curl 127.0.0.1
Hello Container World! I have been seen 2 times and my hostname is f45f87c70c18.
```

接下來多開幾個 web service

``docker-compose scale web=3``

查看一下 container 可以發現最後的 Names 分別有 ``lb-scaling_web_1~3``，也就是目前總共有 3 個 web service
```
CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS              PORTS                                   NAMES
eece79e4a00f        lb-scaling_web        "python app.py"          11 seconds ago      Up 7 seconds        0.0.0.0:32774->5000/tcp                 lb-scaling_web_2
2ee85ec7cd8d        lb-scaling_web        "python app.py"          11 seconds ago      Up 7 seconds        0.0.0.0:32773->5000/tcp                 lb-scaling_web_3
3f731c79e055        dockercloud/haproxy   "/sbin/tini -- docke…"   5 minutes ago       Up 5 minutes        443/tcp, 0.0.0.0:80->80/tcp, 1936/tcp   lb-scaling_lb_1
f45f87c70c18        lb-scaling_web        "python app.py"          5 minutes ago       Up 5 minutes        0.0.0.0:32772->5000/tcp                 lb-scaling_web_1
b983a50f865e        redis                 "docker-entrypoint.s…"   5 minutes ago       Up 5 minutes        0.0.0.0:32771->6379/tcp                 lb-scaling_redis_1
```

接下來就連接看看吧(注意 hostname)
```
hbdoy@hbdoy:~$ curl 127.0.0.1
Hello Container World! I have been seen 5 times and my hostname is f45f87c70c18.
hbdoy@hbdoy:~$ curl 127.0.0.1
Hello Container World! I have been seen 6 times and my hostname is eece79e4a00f.
hbdoy@hbdoy:~$ curl 127.0.0.1
Hello Container World! I have been seen 7 times and my hostname is 2ee85ec7cd8d.
hbdoy@hbdoy:~$ curl 127.0.0.1
Hello Container World! I have been seen 8 times and my hostname is f45f87c70c18.
hbdoy@hbdoy:~$ curl 127.0.0.1
Hello Container World! I have been seen 9 times and my hostname is eece79e4a00f.
hbdoy@hbdoy:~$ curl 127.0.0.1
Hello Container World! I have been seen 10 times and my hostname is 2ee85ec7cd8d.
hbdoy@hbdoy:~$ curl 127.0.0.1
Hello Container World! I have been seen 11 times and my hostname is f45f87c70c18.
```

## Reference
[Ubuntu Linux 安裝 Docker 步驟與使用教學](https://blog.gtwang.org/virtualization/ubuntu-linux-install-docker-tutorial/)