# Chapter 4. Working with storage and volumes
###### tags: `Docker`, `docker in action 2ed`
## 範圍
- Introducing mount points
- How to share data between the host and a container
- How to share data between containers
- Using temporary, in-memory filesystems
- Managing data with volumes
- Advanced storage with volume plugins
## 前言
### 使用情境
- Application with database
- 資料會存在哪?
- 當 Container 關掉的時候,資料的走向會是如何?
- 要如何保留在 Container 上的資料?
- Log files
- 要如何去寫 log file?
- 要如何 access log file for trouble shooting
## 4.1 File trees and mount points
- Linux 的整個 storage 就是一個 single file tree `/`
- Mount point
- Storage devices (disk partitions or USB disk partitions) are attached to specific locations in that tree.
- 
- 透過 mount point 可以掛載我們需要儲存的位置,而 Container 同時也是這種 file tree,因此類似的概念也可以套用在 Container 上來儲存所需的資料
- common types of storage mounted into containers
- Bind mounts
- In memory storage
- Docker volumes

using the `--mount` flag on the `docker run` and `docker create` sub-commands
## 4.2 Bind mounts
- 在 `docker run` 的同時,告訴 container 某些檔案的路徑要 bind 到 host 上的哪裡
- 範例
- 假設我們起一組 Nginx container,並將 Nginx 的 default.conf bind 到 host 端上的設定檔,同時將 Nginx 的 access log 也 bind 到 host 端上的 log file
- 
```bash=
# 起一個 Nginx 並將設定檔與 log file 用 bind mount 的方式掛載
$ CONF_SRC=~/example.conf; \
CONF_DST=/etc/nginx/conf.d/default.conf; \
LOG_SRC=~/example.log; \
LOG_DST=/var/log/nginx/custom.host.access.log;
$ docker run -d --name diaweb \
--mount type=bind,src=${CONF_SRC},dst=${CONF_DST} \
--mount type=bind,src=${LOG_SRC},dst=${LOG_DST} \
-p 8889:80 \
nginx:latest
$ curl localhost:8889,Nginx 會把 request 寫到 access log 裡
# 確認掛載在 host 上的 example.log 有沒有資料
```
- 可以加上 `readonly=true`,以防 container 修改到 host 上的設定檔
- bind mount 的缺點
- 如果使用 bind mount,會讓 container 不這麼 portable(要確保每台 host 都要有這些路徑)
- 容易與其他 container 造成 conflict(都設定相同的路徑)
- 盡量避免使用 bind mount
## 4.3 In memory storage
- 當 container 有某些較為敏感的資料需要被儲存時(不需要被寫入 Container 內),通常會選用此方法
- `--mount type=tmpfs,dst=/tmp`
- 當 container 停止時,`tmpfs` 也會被移除
- 範例
```bash=
$ docker run --rm \
--mount type=tmpfs,dst=/tmp \
--entrypoint mount \
alpine:latest -v
```
- output: `tmpfs on /tmp type tmpfs (rw,nosuid,nodev,noexec,relatime)`
- read write privilege
- ignore suid(suid 為僅檔案擁有者在執行檔案時才有權限,只適合 binary file)
- http://linux.vbird.org/linux_basic_train/unit05.php#5.3
- no device can interrupt
- no device can execute
- File access times will be updated
- 也可以加入 `tmpfs-size=16k`, `tmpfs-mode=1770` 來限制 tmpfs 的 size or tmpfs 在 container 內的權限(default 是 world-writable 1777)
```bash=
$ docker run -d \
-it \
--name tmptest \
--mount type=tmpfs,destination=/app,tmpfs-mode=1770,tmpfs-size=16k \
nginx:latest
```
- 透過 `docker container inspect tmptest` 來查看 mount 狀況
```jsonc=
"Mounts": [
{
"Type": "tmpfs",
"Target": "/app",
"TmpfsOptions": {
"SizeBytes": 16384,
"Mode": 1016
}
}
]
// By 逸🐟大大
// 1770
// -> 1 111 111 000
// -> 1016
```
## 4.4 Docker volumes
- Volume 是由 Docker 建立與管理的
- 可用 `docker volume create` 指令直接建立,或可以在建立 Container 時一併建立
- Volume 建立時存放在主機的目錄中
- 範例
- 建立一個名為 location-example 的 volume
```bash=
$ docker volume create \
--driver local \
--label example=location \
location-example
```
- 透過 inspect 可以查看 volume 的資訊
```bash=
$ docker volume inspect location-example
```
### 4.4.1 Volumes provide container-independent data management
- 適合使用 Volumes 的情境
- Database software versus database data
- Web application versus log data
- Data processing application versus input and output data
- Web server versus static content
- Products versus support tools
- >images are appropriate for packaging and distributing relatively static files such as programs; volumes hold dynamic data or specializations
- polymorphic(多型態的) and composable(可組成的) tools 都很適合使用 Volumes
### 4.4.2 Using volumes with a NoSQL database
- 以 Apache Cassandra project 為範例,測試 Volume 可以被重複利用,掛載到另一個 Container

```bash=
# 先建立需要的 volume
$ docker volume create \
--driver local \
--label example=cassandra \
cass-shared
# 建立 cass1
$ docker run -d \
--volume cass-shared:/var/lib/cassandra/data \
--name cass1 \
cassandra:2.2
# 建立 CQLSH(Cassandra Client)
$ docker run -it --rm \
--link cass1:cass \
cassandra:2.2 cqlsh cass
## 新增一筆資料
$ create keyspace docker_hello_world
with replication = {
'class' : 'SimpleStrategy',
'replication_factor': 1
};
## 確定資料存在
$ select *
from system.schema_keyspaces
where keyspace_name = 'docker_hello_world';
## 離開
$ quit
# 刪除 cass1 並建立 cass2 將同樣的 Volume 也 mount 進去,同時使用同樣的方式去檢查資料是否存在
$ docker stop cass1
$ docker rm -vf cass1
$ docker run -d \
--volume cass-shared:/var/lib/cassandra/data \
--name cass2 \
cassandra:2.2
$ docker run -it --rm \
--link cass2:cass \
cassandra:2.2 \
cqlsh cass
$ select *
from system.schema_keyspaces
where keyspace_name = 'docker_hello_world';
$ quit
$ docker rm -vf cass2 cass-shared
```
## 4.5 Shared mount point and sharing files
- 比較 bind mount 和 Volume
```bash=
# Bind Mount
## 建立 host 上要 mount 進去 Container 的資料夾
$ LOG_SRC=~/web-logs-example
$ mkdir ${LOG_SRC}
## 起一個 Container 可以寫入 logA
$ docker run --name plath -d \
--mount type=bind,src=${LOG_SRC},dst=/data \
dockerinaction/ch4_writer_a
## 透過 Container 查看該 log
$ docker run --rm \
--mount type=bind,src=${LOG_SRC},dst=/data \
alpine:latest \
head /data/logA
## 透過 host 端查看該 log
$ cat ${LOG_SRC}/logA
$ docker rm -f plath
# Volume
## 建立 Volume named logging-example
$ docker volume create \
--driver local \
logging-example
## 與上面步驟一樣,換成 Volume mount 方式
$ docker run --name plath -d \
--mount type=volume,src=logging-example,dst=/data \
dockerinaction/ch4_writer_a
$ docker run --rm \
--mount type=volume,src=logging-example,dst=/data \
alpine:latest \
head /data/logA
## 透過 host 查看
$ cat /var/lib/docker/volumes/logging-example/_data/logA
$ docker rm -f plath
```
- 差別在於 Volume 不需要知道任何 host 端上的路徑
### 4.5.1 Anonymous volumes and the volumes-from flag
- Volume 可以是匿名的
- it is assigned a unique identifier
- 在 `docker run` 同時,會自動幫我們建立起 Volume
```bash=
$ docker run --name fowler \
--mount type=volume,dst=/library/PoEAA \
--mount type=bind,src=/tmp,dst=/library/DSL \
alpine:latest \
echo "Fowler collection created."
$ docker volume ls
```
- 也可以透過 `--volumes-from` 來複製 mount destinations
```bash=
$ docker run --name knuth \
--mount type=volume,dst=/library/TAoCP.vol1 \
--mount type=volume,dst=/library/TAoCP.vol2 \
--mount type=volume,dst=/library/TAoCP.vol3 \
--mount type=volume,dst=/library/TAoCP.vol4.a \
alpine:latest \
echo "Knuth collection created"
$ docker run --name reader \
--volumes-from fowler \
--volumes-from knuth \
alpine:latest ls -l /library/
docker inspect --format "{{json .Mounts}}" reader
```
- 三種情境下,不能使用 `--volumes-form` tag
- a shared volume mounted to a different location
- the volume sources conflict with each other
```bash=
$ docker run --name chomsky --volume /library/ss \
alpine:latest echo "Chomsky collection created."
$ docker run --name lamport --volume /library/ss \
alpine:latest echo "Lamport collection created."
$ docker run --name student \
--volumes-from chomsky --volumes-from lamport \
alpine:latest ls -l /library/
$ docker inspect -f "{{json .Mounts}}" student
# => 發現只會有一個被 mount 進去
```
- need to change the write permission of a volume
## 4.6 Cleaning up volumes
- Anonymous volumes can be deleted via the `docker run --rm` or `docker rm -v` flags
- or `docker volume remove f9db93b59ceb01927ed5f954f9170c3aa2699b4975a0bbfb0a7803df6f2c302c`
- `docker rm -v` Remove the volumes associated with the container
- named volumes must always be deleted manually
- This behavior can be helpful when containers are running jobs that collect partitioned or periodic data
```bash=
$ for i in amazon google microsoft; \
do \
docker run --rm \
--mount type=volume,src=$i,dst=/tmp \
--entrypoint /bin/sh \
alpine:latest -c "nslookup $i.com > /tmp/results.txt"; \
done
$ docker volume list
$ docker volume remove amazon google microsoft
```
- 唯一的限制:沒在被使用的 Volume 才能被移除
- 也可以透過 `docker volume prune` 來清除沒在使用的 Volume
## 4.7 Advanced storage with volume plugins
- 可以透過一些 plugin 來 enhance docker volume
- REX-Ray (https://github.com/rexray/rexray)
- provides volumes on several cloud and on-premises storage platforms
## Summary

- Bind mount 可存放在 host 上的任何地方,任何人都可以隨時修改。
- tmpfs mount 只存放在 memory 中,不會寫入 host 的 fs。
- Volume 存放在 host 中由 Docker 管理的地方(`/var/lib/docker/volumes/`)。非 Docker 者不應該修改檔案系統中的這一部分。
## Docker Layers
- 使用 storage drivers 儲存及管理 layers,如:
- overlay2
- overlay
- aufs
- 一個 image 由一至多個 layer 組成。
- Every instruction that modify the filesystem in Dockerfile adds up a layer.
```dockerfile=
# syntax=docker/dockerfile:1
FROM ubuntu:18.04
LABEL org.opencontainers.image.authors="org@example.com"
COPY . /app
RUN make /app
RUN rm -r $HOME/.cache
CMD python /app/app.py
```
- 每個 Layer 紀錄 data 差異變化 (like Git commit)
- `docker pull` 時,各 layer 各自獨立拉取

- 每個 container 都基於 image 原有之唯讀 layers ,建立一層「薄薄的」可寫 layer:
- Container size on disk (`docker ps -s`)

- 圖示:


- 某個 container 需讀寫 image 內有之檔案時:
- 需讀取,會從最上層往下尋找,並讀取有檔案最新版本的 layer 裡面的檔案,各 container 共同讀取同一檔案。
- 需寫入,會採用 copy-on-write (CoW) strategy,各 container 找到檔案後複製一份來當前 layer 使用。
### Summary
- image 由 Dockerfile 生成,每行有 I/O 指令的結果儲存於一個 layer。
- layer 可達到孤立環境效果,image 內原有的檔案不會有任何改變,各 container 間亦互不影響,若 container 消滅,則其自有之 layer 亦消滅,其改動亦不復存在。
- 使用 stroage driver 使 I/O 效率比原生差,但 CoW 使唯讀檔案時能較有效使用。
- 若有大量寫入需求,應考慮使用 volume 。
- 同樣的 layer 在不同 image 之間可共用,善用其 cache 特性復用 layer 可增進 pull 效率。
- 各 layer 紀錄的 filesystem 變化都會保留,有存在過就會影響 image 大小。