---
# System prepended metadata

title: NCKU SA/NA Course - 03. Docker

---

# 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 進行了進一步的封裝
- 可將應用程式與其環境打包，使其移植十分方便

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




### 若遇到了以下的情境
![](https://i.imgur.com/3sRaoje.png)

#### 解決方案
1. Virtual environment
2. Virtual machine
3. Docker

#### Virtual environment
> 有些情況不能純使用虛擬環境，此方法不通用(跨平台)

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

#### 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

![](https://i.imgur.com/rnh9Hw6.png)
虛擬機需要虛擬出完整的機器，包括 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基本概念
![](https://i.imgur.com/Fe97hJn.png)
### Docker images
Docker 映像檔為一個唯獨的模板，包含了構建 Docker Container 的必要資訊，Docker image 可以是多個 Docker image 堆疊而成，如下圖。
![](https://imgur.com/QIp9gOX.png)

構建出 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 衝突的問題。
![](https://i.imgur.com/QqwxwZJ.png)

### Docker images vs Container
![](https://i.imgur.com/AEtRqIi.png)





### Docker registry 
registry 是集中存放映像檔的地方，中文稱作倉庫，倉庫又區分為公有倉庫與私有倉庫。

- 公有倉庫
其中公有倉庫最常使用的為 Docker 官方維護的 [Docker Hub](https://hub.docker.com/)，大部分我們需要的現成映像檔都會從這去拉取，我們可以透過 `docker search [OPTIONS] TERM`去搜尋官方倉庫中的映像檔。
![](https://imgur.com/XvCgAd3.png)
可以使用 `--filter=stars=[startnumber]` 來限定只會出現幾顆星星以上的映像檔。
![](https://imgur.com/QwkiUUM.png)
登入後可用 `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`

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


## Docker Image
### 拉取現有映像檔(docker pull)
從公開或私有的倉庫拉取 image，預設是 Docker Hub 公共註冊伺服器中的 Repository。
![](https://imgur.com/B7Cbq5K.png)
### 檢視當前映像檔(docker images)
檢視當前存在的 docker image，是 `docker image list` 的別名
![](https://imgur.com/DGceXjX.png)
### 修改已有映像檔(docker commit)
若想修改已有映像檔，則需使用`docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]`

我們可以先製作一個映像檔實例，也就是容器，並在該目錄創建一個隨意檔案。
![](https://i.imgur.com/irhZpLO.png)

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

並用該 docker image 重新創建一個容器實例，發現是經過修改後的結果。
![](https://i.imgur.com/DmLK0Zi.png)

### 自行建立並設計映像檔(Dockerfile/docker build) 
對於小型的個人專案，透過 `docker commit` 來擴展一個 Docker Image 還算是一個方法，但若場景換到多人合作與大型專案需要進行分享就不適合。

Docker 提供了簡單的方式來自定義一個 image，我們只要寫好 Dockerfile 後，執行 `docker build` 便可自動化的配置好我們的 Docker image。Dockerfile 內每一行皆是用來建立映像檔的指令(Instruction)。

我們準備好一個空目錄，並在裡面放入一個包含 `print('Hello Docker!')` 名為 `hello.py`的 python 文件。
![](https://i.imgur.com/G3izqDu.png)

再來，我們在該路徑下新增包含以下內容的 Dockerfile
![](https://i.imgur.com/4aCLWbH.png)
```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
:::

執行結果如下。
![](https://i.imgur.com/UWWq1pP.png)

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

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

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

以下指令能一次強制刪除所有的 images

```shell
docker rmi -f $(docker images -aq)
```


### 匯出與匯入映像檔(docker save/ docker load)
#### docker save 
```shell
sudo docker save -o FILENAME IMAGE [IMAGE...]
```
![](https://i.imgur.com/B3FUdEx.png)


#### docker load
載入匯出後的 images 檔案。
```shell
sudo docker load -i [FILENAME]
```
![](https://i.imgur.com/GGSXt0P.png)




## 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'**：指定啟動後需要運行什麼指令

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

以下簡列 docker run 後的步驟
1. 檢查本地是否有該映像檔，否則自動使用 `git pull`
2. 利用該映像檔創建並啟動一個容器
3. 分配一個 file system，並在映像檔外掛載一個可讀寫層
4. 從宿主主機上網路橋接口橋接一個到該容器內
5. 分配一個 ip 給該容器
6. 執行指定的應用程式
7. 執行完畢後容器終止(若沒特別指定 Daemonized)




在很多時候，我們會想要進入容器互動，因此我們可以為容器分配一個虛擬終端(`-t`)並讓標準輸入保持打開(`-i`)，這兩個參數可擺在同個 - 後面(`-it`)。


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

若想在容器主程序一結束時，立刻移除容器可使用 `--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"
```
![](https://i.imgur.com/HIWllOc.png)

:::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
```
![](https://i.imgur.com/1b21Cfq.png)


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

舉個範例。
```shell
sudo docker export CONTAINER > ubuntuv3.tar
```
![](https://i.imgur.com/X7iYdYo.png)

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

以下範例
```shell
cat [FILENAME] | sudo docker import - [REPOSITORY]
```
![](https://i.imgur.com/nPIzNSW.png)


:::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/)相關資訊。 

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

#### 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)
若有一個客戶端想要請求容器內的一個服務，如下圖。

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

在沒有設定網路的狀況下，我們無法存取容器內的服務，因為該服務被保護於 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"]
```
![](https://i.imgur.com/QXRMDTB.png)

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

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

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
## 結尾