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

### CNM Drivers

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/)