# NCKU SA/NA Course - 03. Docker
## Outline
- 甚麼是 Docker
- Docker 與 VM 差在哪
- Docker 基本概念
- Docker Image
- 什麼是 docker image
- Docker Container
- 什麼是 docker container
- Docker Image vs Docker Container
- Docker Repository
- Docker Hub
- docker search
- docker push
- privicy place
- docker-registry
- Docker install
- sudo apt update
- sudo apt-get remove docker docker-engine docker.io
- sudo apt install docker.io
- docker --version
- Docker Image
- docker pull
- docker images
- docker commit
- Dockerfile
- save and load image
- remove docker image
- Docker Container
- run
- docker run
- process https://blog.csdn.net/u013190088/article/details/81255804
- docker start
- Daemonized
- docker ps
- docker logs
- stop
- docker stop
- --restart
- entry container
- (*)docker exec
- docker attach
- save and load container
- docker export
- docker import
- remove docker container
- Manage application data
- bind mount
- volumes
- bind mount vs volumes
- Dockerfile
- struct
- command
- build
- port Forwarding
- https://ithelp.ithome.com.tw/articles/10193291
- docker image to Dockerfile
- https://github.com/CenturyLinkLabs/dockerfile-from-image
- Docker compose
- what is docker-compose
- Docker ground knowledge
- https://st.vincent55.tw/DWZ
- https://st.vincent55.tw/5Gn
- https://kknews.cc/zh-tw/code/gp9lzve.html
- implement tech
- Linux Kernel namespace cgroup
- namespace
- cgroup
- Archtech
- docker-server: Docker Daemon
- docker-client: Docker Client
practice lab : https://github.com/Vincent550102/nasa-docker-lab
## 甚麼是 Docker
> Docker 是一個開放原始碼軟體,是一個開放平台,用於開發應用、交付應用、執行應用。 Docker允許使用者將基礎設施中的應用單獨分割出來,形成更小的顆粒,從而提高交付軟體的速度。 Docker容器與虛擬機器類似,但二者在原理上不同。[Reference]([/vXSvgBwET4aX26eD8b6M_g](https://zh.wikipedia.org/wiki/Docker))
- 一個開源並讓你能快速建立、測試、部署應用程式的軟體平台
- 一種使用 Golang 實作的輕量級的作業系統虛擬化解決方案
- 在 LXC(Linux Container) 的基礎上 Docker 進行了進一步的封裝
- 可將應用程式與其環境打包,使其移植十分方便

### 若遇到了以下的情境

#### 解決方案
1. Virtual environment
2. Virtual machine
3. Docker
#### Virtual environment
> 有些情況不能純使用虛擬環境,此方法不通用(跨平台)

#### Virtual machine、Docker 是較為實用的方法
## Docker Container 與 VM 差在哪
### 虛擬化技術(以抽象程度)
- 硬體層級虛擬化(Hardware Abstraction Level)
- 透過 Hypervisor,在主體機器之上運行多台完整客體機器
- ex. VirtualBox、Hyper-V、VMware
- 作業系統層級虛擬化(Operating System Level)
- 又稱容器化,將作業系統核心虛擬化,可使容器共用同個 Host OS
- ex. Docker、LXC、KVM
- 程式語言虛擬化(Programming Language Level)
- 高階語言轉譯成bytecode,實現跨作業系統、跨語言
- ec. Microsoft NET、Oracle Java、Parrot

虛擬機需要虛擬出完整的機器,包括 OS、硬體環境、Memory 等等,並將其包裝於 VM images,而 Docker 則是透過 Docker Engine 來調度 images 與使其產生 Container。
值得一提的是,在非 Linux 作業系統下,為何仍需要一個 Hypervisor,並虛擬出一台 Guest Linux。因為 Docker 在底層設計時,使用到了 Linux Kernel 當中,用來虛擬化的技術 namespace 與 Cgroup。
有關底層設計,在 # 會更進一步提到。
以下表格將比較兩者異同
| 比較 | VM | Docker |
|-------------------- |---------------------- |------------------------------ |
| 啟動時間 | 分鐘級 | 秒級 |
| Memory 效率 | 低,需要加載整個系統 | 高,只需加載滿足運行必要配置 |
| 存儲使用 | GB | MB |
| 一般系統最大承載量 | 十幾個 | 上千個 |
## Docker Components基本概念

### Docker images
Docker 映像檔為一個唯獨的模板,包含了構建 Docker Container 的必要資訊,Docker image 可以是多個 Docker image 堆疊而成,如下圖。

構建出 Docker image 大致有三種方式
1. 從 Docker Repository 拉取下來,例如 Docker Hub
2. 在其他環境使用匯出 Docker image 並在自己電腦上匯入
3. 使用 Dockerfile 自行配置 image
### Docker Container
Docker Container 是一個透過 Docker image 執行起來的 Process,可以想像為 Docker images 所建立的一個實例,每個 images 可建立多個互相隔離的 Docker Container。
Docker Container 支援開始、停止、修改、刪除的操作。上面有提到 Docker image 是個唯獨(read-only) 的檔案,Docker container 在 Docker image 最上層加上一層可讀可寫層。
Docker Container 之間是隔離的,正常使用下不會發生 port 衝突的問題。

### Docker images vs Container

### Docker registry
registry 是集中存放映像檔的地方,中文稱作倉庫,倉庫又區分為公有倉庫與私有倉庫。
- 公有倉庫
其中公有倉庫最常使用的為 Docker 官方維護的 [Docker Hub](https://hub.docker.com/),大部分我們需要的現成映像檔都會從這去拉取,我們可以透過 `docker search [OPTIONS] TERM`去搜尋官方倉庫中的映像檔。

可以使用 `--filter=stars=[startnumber]` 來限定只會出現幾顆星星以上的映像檔。

登入後可用 `docker push` 將映像檔放上 Docker Hub
- 私有倉庫
在某些情況下,不方便使用像 Docker Hub 的公開倉庫,就會需要使用私有倉庫來儲存映像檔,有關私有倉庫的架設可以使用[docker-registry](https://github.com/docker/docker-registry)。
officeal
github
dockerhub demo
## Docker 下載
1. 將系統更新
`sudo apt update`
2. 將舊版本 docker 移除
`sudo apt-get remove docker docker-engine docker.io`
3. 安裝 docker
`sudo apt install docker.io`
4. 檢查 docker 是否安裝成功
`docker --version`

## Docker Image
### 拉取現有映像檔(docker pull)
從公開或私有的倉庫拉取 image,預設是 Docker Hub 公共註冊伺服器中的 Repository。

### 檢視當前映像檔(docker images)
檢視當前存在的 docker image,是 `docker image list` 的別名

### 修改已有映像檔(docker commit)
若想修改已有映像檔,則需使用`docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]`
我們可以先製作一個映像檔實例,也就是容器,並在該目錄創建一個隨意檔案。

完成修改後,我們退出 Docker 介面,之後在 Host 下 `sudo docker commit -m "change file" 4ea57b5196be ubuntu:v2` ,其中 f4fd69e6adad 為經修改之 Container ID,ubuntu 及 v2 分別為 REPOSITORY 與 TAG。之後確認 `sudo docker images`,發現新出現了一個 docker image(畫面中的第一個)。

並用該 docker image 重新創建一個容器實例,發現是經過修改後的結果。

### 自行建立並設計映像檔(Dockerfile/docker build)
對於小型的個人專案,透過 `docker commit` 來擴展一個 Docker Image 還算是一個方法,但若場景換到多人合作與大型專案需要進行分享就不適合。
Docker 提供了簡單的方式來自定義一個 image,我們只要寫好 Dockerfile 後,執行 `docker build` 便可自動化的配置好我們的 Docker image。Dockerfile 內每一行皆是用來建立映像檔的指令(Instruction)。
我們準備好一個空目錄,並在裡面放入一個包含 `print('Hello Docker!')` 名為 `hello.py`的 python 文件。

再來,我們在該路徑下新增包含以下內容的 Dockerfile

```dockerfile
# My First Dockerfile
FROM python:3.8
RUN apt-get -qq update
COPY hello.py /app/
CMD ["python3", "/app/hello.py"]
```
- 其中 # 為註解
- FROM:使用到的基底映像名稱
- RUN:後面擺放 Linux 指令用來執行設定 Image 需要的環境
- COPY:將本地的檔案複製到 Docker 中,類似的指令有 `ADD`
- CMD:在執行完 docker run 後,會自動執行的指令
之後,我們在與 Dockerfile 同層的資料夾運行 `docker build` 指令
```shell
sudo docker build -t mydockerfile .
```
可使用 -t 來指定 image 的 tag,後方的 . 代表 Dockerfile 位於當前目錄下。
::: info
在 docker build 的過程中 Dockerfile 中遇到 docker images 中沒有的 Image,將會自動 docker pull
:::
執行結果如下。

我們檢查 `docker images`,並用 docker run 創建容器實例,看是否會印出東西。

### 移除映像檔(docker rm)
若要移除某個 images 可以使用以下指令。若要強制刪除 image 可以加上 `-f` 來 force remove。
```shell
docker rmi [OPTION] [IMAGE ID]
```

以下指令能一次強制刪除所有的 images
```shell
docker rmi -f $(docker images -aq)
```
### 匯出與匯入映像檔(docker save/ docker load)
#### docker save
```shell
sudo docker save -o FILENAME IMAGE [IMAGE...]
```

#### docker load
載入匯出後的 images 檔案。
```shell
sudo docker load -i [FILENAME]
```

## Docker Container
### 檢視當前容器(docker ps)
檢視當前存活的容器之相關資訊,指令格式如下
```shell
docker ps [OPTIONS]
```
可以使用 `-a` 檢視所有容器(包含已終止的容器)
```shell
docker ps -a
```
### 啟動一個容器(docker run/ docker start)
#### 建立容器實例並執行(docker run)
指令格式如下
```shell
docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
```
```shell
sudo docker run ubuntu:v2 echo 'Hello! container'
```
* **ubuntu:v2**:指定需要建立容器的 docker images
* **echo 'Hello! container'**:指定啟動後需要運行什麼指令

以下簡列 docker run 後的步驟
1. 檢查本地是否有該映像檔,否則自動使用 `git pull`
2. 利用該映像檔創建並啟動一個容器
3. 分配一個 file system,並在映像檔外掛載一個可讀寫層
4. 從宿主主機上網路橋接口橋接一個到該容器內
5. 分配一個 ip 給該容器
6. 執行指定的應用程式
7. 執行完畢後容器終止(若沒特別指定 Daemonized)
在很多時候,我們會想要進入容器互動,因此我們可以為容器分配一個虛擬終端(`-t`)並讓標準輸入保持打開(`-i`),這兩個參數可擺在同個 - 後面(`-it`)。

若想在容器主程序一結束時,立刻移除容器可使用 `--rm`。
### docker logs
取得容器的 logs,指令格式如下
```shell
docker logs [OPTIONS] CONTAINER
```
### 守護態執行(Daemonized -d)
有時候我們會想要一些服務是在後台運行的,我們就可以在啟動時使用 `-d` 將該容器以守護態執行,這時就不會將結果輸出(`STDOUT`)在宿主機上。
```shell
sudo docker run -dit ubuntu:v2 bash -c "while true; do echo hello; sleep 1; done"
```

:::info
可以比較看看有加 -d 與沒加的差別
:::
### 終止一個容器(docker stop)
終止一個正在運行的(`Up`)容器變為終止的(`Exit`),指令格式如下
```shell
docker stop [OPTIONS] CONTAINER [CONTAINER...]
```
:::warning
若要終止一個容器也可使用 docker kill。兩者的區別在於 docker stop 會向容器發送 `SIGTERM`,而 docker kill 會向容器發送 `SIGKILL`
:::
#### 啟動已終止容器(docker start)
可啟動一個狀態為終止(`Exited`)的容器,指令格式如下
```shell=
docker start [OPTIONS] CONTAINER [CONTAINER...]
```
#### 重新啟動一個容器(docker restart)
docker restart 會將容器先 `docker stop` 後再自動執行 `docker start`,指令格式如下。
```shell=
docker restart [OPTIONS] CONTAINER [CONTAINER...]
```
### 進入調適容器(docker exec/docker attach)
要進入一個正在運行的容器有許多方法,這邊簡介兩種方法 `docker attach` 、`docker exec`,但個人較推薦使用 `docker exec`,下面會解釋原因。
#### docker attach
docker attach 指令會開啟一個此容器正在運行的終端,這會有兩個問題。
第一個,若有多個使用者同時 attach 上同一個容器會發現操作會衝突。第二個,由於使用上是與容器同個終端,因此若操作不當可能會不小心將容器關閉,若要離開該終端需要 Ctrl+P 加 Ctrl+Q 。
```shell
docker attach [OPTIONS] CONTAINER
```
#### (*)docker exec
在運行中的容器執行指令可以使用 `docker exec`,後面可以使用多個參數,一樣地,我們可以像在 `docker run` 時為他`-t`分配一個虛擬終端以及將`-i`標準輸出(`STDIN`)保持開啟,指令格式如下。
```shell
sudo docker exec [OPTIONS] CONTAINER COMMAND [ARG...]
```
若要開啟一個可互動的終端只要在 COMMAND 的部分放上要開啟的 shell 就可以了,以下範例。
```shell
sudo docker exec -it CONTAINER /bin/bash
```

### 導出與導入容器(docker export/ docker import)
#### 導出容器(docker export)
若要將容器導出為某個容器快照文件,可以使用 `docker export`,指令格式如下。
```shell
docker export [OPTIONS] CONTAINER
```
舉個範例。
```shell
sudo docker export CONTAINER > ubuntuv3.tar
```

#### 導入容器為鏡像(docker import)
若要將由 `docker export` 產生的容器快照文件導入為映像檔,可以使用 `docker import`,指令格式如下
```shell
docker import [OPTIONS] file|URL|- [REPOSITORY[:TAG]]
```
以下範例
```shell
cat [FILENAME] | sudo docker import - [REPOSITORY]
```

:::info
由指令格式可以看到,容器快照文件也支援從網址去導入映像檔
:::
:::warning
在 Image 介紹中也提到了另一個可以導入映像檔的方式 `docker load`。
`docker load` 與 `docker import ` 的區別在於, `docker load` 的鏡像存儲文件會保存完整紀錄, `docker import` 則會丟棄所有歷史紀錄與 metadata。
https://stackoverflow.com/questions/36925261/what-is-the-difference-between-import-and-load-in-docker
:::
### 刪除容器(docker rm)
與刪除映像檔類似,要刪除容器的指令格式如下。
```shell
docker rm [OPTIONS] CONTAINER [CONTAINER...]
```
相同地,若要強制刪除一個容器可以加上 `-f`,會發送 `SIGKILL` 給該容器。
```shell
docker rm -f CONTAINER [CONTAINER...]
```
docker 提供了一個指令可以清除所有終止狀態(`exited`)的容器,指令格式如下。
```
docker container prune
```
### 容器數據管理
容器實例是由映像檔加上一層可讀可寫層構成,文件的新增與修改都在這上面。這會有以下問題。
1. 當該容器不再存在時,數據不會持久存在,並且如果另一個進程需要數據,則很難將數據從容器中取出。
2. 容器的可寫層與運行容器的主機緊密耦合。您無法輕鬆地將數據移動到其他地方。
3. 寫入容器的可寫層需要存儲驅動程序來管理文件系統。存儲驅動程序提供了一個聯合文件系統,使用 Linux 內核。與使用直接寫入主機文件系統的數據卷相比,這種額外的抽象會降低性能。
from https://docs.docker.com/storage/
解決這個數據管理的問題,有三種解決方法,`bind mount`、`volumes`、`tmpfs mount`,這邊會簡介前兩個方法,`tmpfs mount` 會將檔案儲存於 Memory 當中,因此並不持久化,常用在 Linux 系統上的 docker 與不希望數據在主機或容器內持久存在的情況,有興趣的人能到這裡[查看](https://docs.docker.com/storage/tmpfs/)相關資訊。

#### bind mount
bind mount 是一個 docker 早期便可使用的功能,使用 bind mount 會將位於本機上的指定檔案或目錄讓容器掛載。
bind mount a,但因可以通過容器中運行的進程來更改主機文件系統,所以不當使用時可能帶來資安問題,若要指定一個容器 `bind monut` ,需要在其實例化的時候 `docker run` 給予參數 `-v`,指令格式如下。
```shell
sudo docker run -dit -v host/path/to/mount:container/path/to/mount [CONTAINER]
```
**host/path/to/mount**:宿主機上的路徑
**container/path/to/mount**:容器上的路徑
#### volumes
在使用 volume 時,docker 會將 volume 存放在 Docker area,以 Linux 來說預設是 `/var/lib/docker/volumes`,**非 Docker 的行程不應該修改檔案系統中的這一部分**,可使用 `docker volume ls` 來確認當前有哪些 Volume。
首先,我們需要創建一個 Volume,我們需要使用 `docker volume create` 來創建一個 Volume,指令格式如下。
`docker volume create [OPTIONS] [VOLUME]`
:::info
若在創建容器實例(`docker run`)的過程中,指定了尚未創建(`docker volume create`) 的容器,將會自動創建一個該 Volume。
:::
再來,與 `bind mount` 類似地,我們將容器使用 Volume 需要在創建容器的過程中給定 `-v` 參數,與 `bind mount` 相同,不過這次冒號左方不是放路徑,而是 Volume 的名稱(可以透過 `docker volume ls` 來獲得),指令格式如下
```shell
sudo docker run -dit -v volume_name:container/path/to/mount [CONTAINER]
```
創建完成後,我們可以透過 `docker inspect [CONTAINER] | grep volume` 來查看放置 Volume 的路徑。
在一般情況下,我們刪除容器不會連同 Volume 一同刪除,但若要連同 Volume 一併刪除,可以在 `docker rm` 時,給定 `-v` 的參數。
若要刪除 Volume ,可以使用 `docker volume rm [VOLUME]`。使用 `docker volume prune` 可刪除無主的 Volume。
#### bind mount vs volumes
以下分別條列使用 bind mount 及 volume 的時機,若沒有特別需求,較推薦使用 volume
- bind mount
1. 將 config 同時分享給容器與宿主機時。容器預設的 DNS 解析處理便是透過掛載 `/etc/resolv.conf` 分享設定檔到各容器下。
2. 在宿主機以及容器這兩個開發環境分享原始碼或檔案時。一般情況下 docker 內沒有編輯器。
3. 確保掛載路徑的子路徑與檔案穩定。
- volume
1. 在多個容器中共用資料時。創建容器實例時可使用 `--volumes-from` 複製另一容器的 volume 設定。
2. 需要備份與遷移數據的時候。docker 有一許多指定能做到這件事,只需對 docker area 進行操作即可,預設為 ` /var/lib/docker/volumes/`
::: success
bind mount 與 volumes 的另一點不同在於 volumes 是由 Docker 創建的,和宿主機的核心(core functionality)隔離。
掛載 bind mount 與 volume 除了 `-v` 也可以透過 `--mount` ,`-v` 較為直觀,相對的 `--mount` 能指定更多東西,有興趣的人能到[這裡](https://docs.docker.com/storage/volumes/#choose-the--v-or---mount-flag)研究。
:::
## Dockerfile
### .dockerignore
與 Git 中的 `.gitignore` 類似,可將規則寫入 `.gitignore` 中,就會以黑名單的方式防止檔案複製入容器中。
例如我們在 Dockerfile 中撰寫 `COPY . .`,我們可以寫以下 `.gitignore` 將不想複製入容器的檔案隔離在外。
```
Dockerfile*
.git
**/.git
**/node_modules
```
`**` 任意數量路徑匹配(包含 0 路徑)
### 其餘指令
#### WORKDIR
可設置在 `docker build` 時的工作目錄,之後的指令(`RUN`、`CMD`、`ENTRYPOINT`、`COPY`、`ADD`)皆會在該目錄下進行。 指令格式如下。
```
WORKDIR /path/to/workdir
```
#### ENV
可設置環境變數。指定格式如下。
```
ENV <key>=<value> ...
```
#### ARG
與 `ENV` 類似可設置環境變數,不過只能作用於 `docker build` 的過程中。指定格式如下。
```
ARG <name>[=<default value>]
```
#### VOLUME
在 Dockerfile 中,可以透過`VOLUME`將容器內路徑掛載至自動生成的 Volume,在創建容器實例時,可以透過 `docker run -v` 將其掛載點覆蓋。指令格式如下。
```
VOLUME ["</data1>", "</data2>"...]
VOLUME </data1>
```
:::info
透過 Dockerfile 指定的 VOLUME 使透過此方式產生的映像檔創建的容器實例都有了掛載點。
與在 `docker run -v` 不同在於,Dockerfile 指定的 VOLUME 不能指定宿主機掛載點與 Volume name。
:::
#### USER
可設置在運行映像時的使用者名稱或 UID ,為之後的指令操作(`RUN`、`CMD`、`ENTRYPOINT`)設置權限。指令格式如下。
```
USER <user>[:<group>]
USER <UID>[:<GID>]
```
#### ENTRYPOINT
設置容器啟動後執行的命令,不會被 `docker run` 提供的參數覆蓋,如果在 Dockerfile 中有多個 `ENTRYPOINT`,則只會認最後一個。指令格式如下
```
ENTRYPOINT ["executable", "param1", "param2"]
ENTRYPOINT command param1 param2
```
:::info
CMD 與 ENTRYPOINT 的差異簡而言之就是, CMD 會被 `docker run` 提供的參數覆蓋, ENTRYPOINT 不會被覆蓋。
兩者同時使用時,結果會如在 shell 中執行 <ENTRYPOINT> "<CMD>"
:::
### 網路調適(EXPOSE)
若有一個客戶端想要請求容器內的一個服務,如下圖。

在沒有設定網路的狀況下,我們無法存取容器內的服務,因為該服務被保護於 Server 中,若想要開放外部存取該服務,常見可使用端口映射。將容器中的 Port 對應到 Server 中的 Port,這樣外部只要存取 Server 宿主機上的 Port ,便可連線到容器中的服務。
在 Dockerfile 中,`EXPOSE` 可說明容器打算將什么端口向外暴露,可使此映像的使用者理解此映像的服務端口,方便後續配置映射;另外也可在使用 `docker run -P` 時能預設指定該 `EXPOSE` 的端口映射隨機宿主機端口。
若想將宿主機端口映射至容器中除了上方提到的 `docker run -P` 外,還有許多做法,以下簡介 `docker run -P` 用法與 `docker run -p HostPort:ContainerPort`。
可使用 `docker port [CONTAINER]` 來檢視容器端口映射狀況
#### docker run -P
可以在 `docker run` 時使用 `-P` 或 `--publish-all=true`,會根據容器指定的 `EXPORT` 端口(除了在 Dockerfile 中指定也可透過 `--expose [PORT]` 指定) 隨機分配一個尚未使用的端口映射至容器服務端口,可至 `/proc/sys/net/ipv4/ip_local_port_range` 查看分配範圍。
指令格式如下。
```shell
sudo docker run -P IMAGE [COMMAND] [ARG...]
```
#### docker run -p
除了將所有 EXPOSE 的端口映射至隨機宿主機端口,也可透過 `docker run -p` 來選擇性的設置宿主機與容器內端口的映射。如果要將容器內的 `80` 端口映射至宿主機的 `8080` 端口,可在 `docker run` 時加入 `-p 80:8080` 或 `--publish=80:8080` 來指定端口映射。
指令格式如下
```shell
sudo docker run -p HOST_PORT:CONTAINER_PORT [IMAGE]
```
docker 的 port forwarding 預設會將 0.0.0.0、::: 上的端口映射出去,也就是所有符合的 ipv4、ipv6。docker 也支援綁定宿主機上的 IP。(可綁定 127.0.0.1 使該服務僅供本機存取)
指定格式如下
```shell
sudo docker run -p IP:HOST_PORT:CONTAINER_PORT [IMAGE]
```
此外,該指令若不給予 `HOST_PORT` ,則會與 `docker run -P` 類似,隨機分配一個宿主機端口映射服務。
指令格式如下。
```shell
sudo docker run -p CONTAINER_PORT [IMAGE]
```
:::info
docker run -p 有許多映射方式,可自行嘗試。
`docker run -p [[[IP:]HOST_PORT:]CONTAINER_PORT]`
:::
:::warning
若端口映射關係複雜,建議使用 docker-compose 此工具處理。
:::
### 映像檔轉換為 Dockerfile
若有將映像檔轉換為 Dockerfile,可使用[這個](https://github.com/CenturyLinkLabs/dockerfile-from-image)逆向工程工具。
### Docker layer
```
FROM ubuntu:15.04
MAINTAINER Vincent
WORKDIR /app/
RUN echo "echo 'hello'" > sayhello.sh
ENTRYPOINT ["sh", "sayhello.sh"]
```

```
docker history
```
docker 預設最多支援 127 層,層與層之間透過[聯合檔案系統](https://en.wikipedia.org/wiki/UnionFS)支援把多個目錄內容聯合掛載到同個虛擬檔案系統下,若遇到了有同個路徑,則上層路徑會覆蓋下層路徑。

docker 中,映像檔使用了分層的結構,透過這樣的結構,能很好的進行**共享資源**,例如在構建另一個時,若發現該基底映像存在於本機上,則 docker host 只需在本機保存一份基底映像。
這時可能會出現一個疑問,若有許多映像檔都使用了同一個基底映像檔文件,那當某個容器實例修改了某個基底映像的內容,是否會影響到其他映像檔?
答案是不會的,先前有提到在創建容器實例的時候,會在 Read-only 的 Image layers 上加上一層可讀可寫的 Container layer,所有在容器上的修改皆會被限制在這一層,這運用到了有名的 `copy on write(CoW)` 技巧。
### copy on write
透過聯合檔案系統技術,在一個多層的映像檔中,每一層的映象層都會一起組成一個完整的文件系統,上層的路徑會覆蓋下層的路徑。
那上面提到,當創建容器實例時,會在只讀的映象層加上可讀可寫的容器層,且所有的修改都在上面進行,那 docker host 是如何透過 `copy on write(CoW)` 將這個讀寫的過程實現呢。以下簡介 CRUD。
1. **新增** : 直接新增在容器層。
2. **讀取** : 由上往下在映像層查找該檔案。
3. **修改** : 由上往下在映像層查找該檔案,找到後複製到容器層,並進行修改。
4. **刪除** : 由上往下在映像層查找該檔案,找到後在容器層記錄刪除的操作。
`copy on write(CoW)` 的主要精神為,只在需要修改時複製該檔案,想知道更細節實作可前往[這裡](https://en.wikipedia.org/wiki/Copy-on-write)。
## Docker compose
docker-compose 是一個能夠透過撰寫 `docker-compose.yml` 來調適多個容器上的應用的工具。
使用 docker-compose 主要有三個步驟。
1. 設定你的應用們的 `Dockerfile`,並確保它們能成功在各處構建服務。
2. 撰寫 `docker-compose.yml`,確保各個服務能在獨立環境一起運行。
3. 使用 `docker-compose up`,開始運行整個服務。
一個 docker-compose.yml 看起來像是。
```yaml=
version: '3.1'
services:
db:
image: mysql
command: --default-authentication-plugin=mysql_native_password
restart: always
environment:
MYSQL_ROOT_PASSWORD: example
adminer:
image: adminer
restart: always
ports:
- 8080:8080
```
**line 1** Compose file 的版本
**line 2** 開始設置各個服務
**line 3、9** 各個服務的名稱
**line 4、10** 此服務使用的現有映像檔
**line 5** 容器啟動後要執行甚麼指令
**line 6、11** 容器重新啟動規範,可使用no、always、on-failure、unless-stopped,設定 always 表示容器停止或關機都會自動啟動容器
**line 7** 定義容器內的環境變數
**line 12**與 `docker run -p` 類似,冒號左方為宿主機端口,右方為容器內端口
https://hub.docker.com/_/mysql/
### docker-compose 常用指令
- docker-compose ps
- 查看容器狀態
- docker-compose stop
- 停止容器,可使用 `docker-compose start` 重新啟動
- docker-compose start
- 啟動容器
- docker-compose down
- 停止並移除Network與容器
- `-v` 移除 Volume
- docker-compose up
- 建立並執行Network與容器
- `-d` 背景執行
- `--build` 會再 build 一次映像
- docker-compose logs
- 查看服務輸出的日誌
- `-f` 持續追蹤日誌
- docker-compose rm
- 刪除已停止的容器
- `-v` 連同 volume 一起刪除
- docker-compose exec
- `docker-compose exec [options] [-e KEY=VAL...] SERVICE COMMAND [ARGS...]`
- 進入容器內執行相關指令與操作
### docker-compose.yml 架構
#### links
- 將兩個服務連結起來,可直接使用 services 作為 hostname。
- 範例
```yaml
...
web:
links:
- "db"
- "db:database"
- "redis"
...
```
可使用 docker-compose exec 進入容器並試試看是否能 ping 到該服務
```
sudo apt-get update
sudo apt-get install iputils-ping
```
#### depends_on
- 設定該服務會在給定的服務照順序開始配置,依照以下情況。
- `docker-compose up` 給定的服務(db、redis)會先建置完之後再建置該服務(web)。
- `docker-compose up SERVICE` 會自動先將給定的服務(db、redis)建置完,再建構該服務(web)。
- `docker-compose stop` 會先將該服務(web)停止,之後再將給定的服務(db、redis)停止。
```yaml
version: "3.9"
services:
web:
build: .
depends_on:
- db
- redis
redis:
image: redis
db:
image: postgres
```
### cheatsheet
https://devhints.io/docker-compose
## 結尾