---
# System prepended metadata

title: Docker學習筆記
tags: [BruceYang, Docker/K8S]

---

# Docker學習筆記
</br>

# 目錄
> [TOC]

---
# 事前準備
- ## 實作環境說明
  - 私有網路環境內的 VM。
  - 作業系統為 Linux CentOS 7.9。
  - VM 無綁訂公網 IP，但可訪問公網。
- ## 學習資料來源
  - [尚硅谷-陽哥Docker實戰教程](https://www.bilibili.com/video/BV1gr4y1U7CY/?share_source=copy_web&vd_source=fa18f4026f6b6ce32447e8fda25e0a1f)
  - [Docker Engine 官方文檔](https://docs.docker.com/manuals/)
  - [Portainer 官方文檔](https://docs.portainer.io/)
  - [Redis 官方文檔](https://redis.io/)
</br>

---
![Docker-Logo](https://hackmd.io/_uploads/SkdkHdaH0.jpg)
# 一、Docker是什麼
## 1.簡介
  為解決運行環境和配置問題的容器應用程式，方便做持續集成並且有助於發布的虛擬化技術。
</br>

## 2.Docker vs VM
在作業系統上面實現虛擬化，直接使用本地主機的作業系統上面部屬 Docker 引擎供多個應用程式使用，不需要加載整個作業系統的內核，因此在 CPU、記憶體在使用效率上有優勢。
- **虛擬主機**
  ```
  Infra（個人電腦）> Hypervisor（VMWare）> Guest OS(s)映像檔 > APP(s) > VM(s)
  ```
- **Docker**
  ```
  Infra（個人電腦）> Host Operating System > Docker > APP(s)
  ```
- **2 者比較表格**
    |               |**Docker**                |**虛擬主機（VM）**          |
    | ------------- | ------------------------ | ------------------------ |
    |**作業系統**    |與本地主機共享               |在本地主機OS上運行VM的OS     |
    |**儲存空間**    |鏡像較小                    |ISO檔較龐大                |
    |**性能使用**    |本地主機資源使用較有效率       |需占用額外CPU、記憶體        |
    |**遷移/部屬**   |較輕便，Linux環境友善         |較笨重、不易部屬            |
</br>

## 3.Docker 的 Client／Server 架構圖
  ![Docker底層架構2](https://hackmd.io/_uploads/H1JoMdxip.png =70%x)

  - Docker Client 發送請求到 Docker Daemon 提供的 Docker Server 來接收請求。
  - Docker Engine 負責執行工作，每項工作以 Job 的形式存在。
  - Docker Engine 運行 Job 的流程如下：
    - 若需要容器的鏡像時，會前往 Registry 下載鏡像，並透過鏡像管理驅動（Graph Driver）將下載的鏡像以 Graph 的形式保存。
    - 若需要為 Docker 創建網路環境時，會透過網路管理驅動（NetworkDriver）創建並配置容器的網路環境。
    - 若需要限制容器運行資源、執行 User 指令操作時，則透過執行驅動（ExecDriver）來完成。
  - Libcontainer 是一個獨立的容器管理包，NetworkDriver、ExecDriver 皆是透過 Libcontainer 來執行針對容器的操作。

## 4.安裝Docker Engine
  [Docker Engine 官方安裝文檔](https://docs.docker.com/engine/install/)

---

# 二、鏡像(Image)
## 1.簡介
- 輕量、可執行的獨立應用程式包，包含應用程式所需要的環境、文件系統
- Docker 容器僅能透過 Docker 鏡像生成。
</br>

## 2.鏡像的基礎文件系統
![鏡像內容](https://hackmd.io/_uploads/SJGJ7ueja.png =35%x)
當 Boot 加載完後，整個內核就會在本地主機內存中（此時內存的使用權已由 BootFS 轉交給內核，接著系統也會卸載 BootFS。）

- **引導文件系統（Boot File System）**
  Docker 鏡像最底層的是引導文件系統，主要包含根加載器（Bootloader）和作業系統內核（Kernal），所有鏡像的 BootFS 基本上是一致的。
- **根文件系統（Root File System）**
  Linux 系統中的 /dev、/proc、/bin、/etc 等核心目錄和文件等最基本的指令、工具、程序庫；不同版本的 Linux 作業系統，RootFS 包含的目錄可能有差異。 
</br>

## 3.聯合文件系統（UnionFS）
- 是一種分層、輕量級的高性能文件系統，它會透過修改的方式，一層層堆疊不同的文件系統、目錄到最終文件系統下。
- Docker 鏡像便是以 UnionFS 的方式進行構建，在宿主機只保留一份 base 鏡像，運行容器時，內存也只需要加載一份 base，雖然每個容器的 RootFS 層不一定相同，但鏡像的每一層都可以共享，以分層的方式進行鏡像間的繼承、下載，如下圖所示：
![聯合文件分層](https://hackmd.io/_uploads/rkmaf_goa.png =80%x)
</br>

## 4.結論
![鏡像分層](https://hackmd.io/_uploads/rkS07ueo6.png =45%x)
  | Docker 分層 | 說明 |
  | -------- | -------- |
  | 鏡像層 | 只讀，在容器層底下的都叫做鏡像層。 |
  | 容器層 | 可寫；當一個 Docker 容器啟動時，一個可寫的容器層會被加載到鏡像的頂部；對容器任何添加、刪除、修改文件等操作，都只會發生在容器層。 |

---

# 三、鏡像常用指令
- **列出本地鏡像**
  ```
  docker images
  ```
  - `-a`：列出本地所有的鏡像（含歷史映像層）。
  - `-q`：只顯示鏡像ID。
  - `-qa`：顯示本地所有的鏡像ID。
</br>

- **到docker hub查找某個鏡像**
  ```
  docker search 鏡像名稱
  ```
  - `--limit N 鏡像名稱`：只列出N個鏡像。
</br>

- **下載鏡像(沒有添加TAG，默認拉取最新版本（latest）**
  ```
  docker pull 鏡像名稱
  ```
  - `鏡像名稱[:TAG]`：指定某個鏡像的某個版本。
</br>

- **查看鏡像/容器/數據卷所占的本地空間**
  ```
  docker system df
  ```
</br>

- **刪除單一鏡像（需要先暫停容器）**
  ```
  docker rmi 鏡像名稱
  ```
  - `-f 鏡像ID`：不必暫停容器，強制刪除單一鏡像。
  - `-f 鏡像名稱1:TAG 鏡像名稱2:TAG`：強制刪除多個鏡像。
  - `-f $(docker images -qa )`：強制刪除所有鏡像。
</br>

- **特別場景**
  - 虛懸鏡像（Repository、TAG都顯示 `<none>` 的鏡像，此種鏡像無法使用，可以刪除）。

---

# 四、容器基本指令
- **執行鏡像建立容器**
  ```
  docker run [Options] 鏡像名稱 [Command] [ARG…]
  ```
  :::info
  :::spoiler **[Options]說明**
  - **容器名稱**
    `-name="容器名稱"`：為容器命名。
    ![容器run--name](https://hackmd.io/_uploads/Hyr74desT.png)

  - **守護式容器**
    `-d`：在本地後台運行容器並返回容器 ID（運行後不會進入容器進行交互）。

  - **交互式容器**
    `-i`：interactive，以交互式運行容器，通常與 -t 同時使用（-it）。
    `-ｔ`：tty，為容器重新分配一個偽輸入終端，與 -i 同時使用（-it）。
    ![容器run-it](https://hackmd.io/_uploads/rJfSNuljT.png)

  - **端口（HostPort:ContainerPort）**
    `-P`：隨機端口（大寫P）。
    `-p`：指定端口（小寫p）。

  - **映射容器的檔案目錄**
    `-v`：[【七、容器的檔案系統目錄】](#七、容器的檔案系統目錄)
  :::
</br>

- **列出所有正在運行的容器**
  ```
  docker ps [Options] 
  ```
  - `-a`：列出所有歷史運行過的容器。
  - `-l`：列出最近創建的容器。
  - `-變數n`：列出最近n個創建的容器。
  - `-q`：列出所有正在運行的容器編號。
</br>

- **退出容器**
  - `exit`：退出容器後，容器停止運行。
  - `ctrl+p+q`：退出容器後，容器持續運行。
</br>

- **啟動已停止的容器**
  ```
  docker start 容器名稱/ID
  ```
</br>

- **重啟容器**
  ```
  docker restart 容器名稱/ID
  ```
</br>

- **停止容器**
  ```
  docker stop 容器名稱/ID
  ```
</br>

- **強制停止容器**
  ```
  docker kill 容器名稱/ID
  ```
</br>

- **刪除已停止的容器**
  ```
  docker rm 容器名稱/ID
  ```
  - `-f 容器名稱/ID`：強制刪除容器(不論容器是否運行中)。

---

# 五、容器重要指令
- **查看容器日誌**
  ```
  docker log 容器名稱/ID
  ```
</br>

- **查看容器進程（top）**
  ```
  docker top 容器名稱/ID
  ```
</br>

- **查看容器內部細節（網橋Bridge、容器的鏡像等）**
  ```
  docker inspect 容器名稱/ID
  ```
</br>

- **進入運行中的容器並進行交互（A）**
  ```
  docker exec -it 容器名稱/ID [Command]
  ```
  ![容器exec](https://hackmd.io/_uploads/rJS_N_gsa.png)
  - 在容器中打開新的終端，並且可以啟動新的進程，用 exit 退出，不會導致容器的停止。
</br>

- **進入運行中的容器並進行交互（B）**
  ```
  docker attach 容器名稱/ID 
  ```
  ![容器attach](https://hackmd.io/_uploads/H1QtV_ejp.png)
  - `docker cp 容器名稱/ID:容器路徑 主機路徑`：將容器文件拷貝至主機。
  - 直接進入容器啟動命令的終端，不會啟動新的進程，用 exit 退出會導致容器的停止。
</br>

- **Export導出容器**
  將容器內容打包成一個 tar 壓縮檔至主機當前路徑
  ```
  docker export 容器名稱/ID > 文件名稱.tar
  ```
  ![容器導出export](https://hackmd.io/_uploads/rkWqVuxoa.png)
</br>

- **Import導入容器**
  用 tar 壓縮檔創建新的文件系統，再導入為鏡像
  ```
  cat 文件名稱.tar | docker import - 鏡像用戶/鏡像名稱:鏡像版本號
  ```
  ![容器導入import](https://hackmd.io/_uploads/rkHo4dxsp.png)
</br>

- **提交容器副本使其成為新鏡像**
  ```
  docker commit -m "鏡像描述訊息" -a="作者" 容器名稱/ID  新的鏡像名稱:[TAG]
  ```
  - 因為 Docker 鏡像的根文件系統僅包含最基本的指令、工具、程序庫，並不具備某些指令，例如：`vim`，因此必須在現有容器安裝需求指令後，重新創建新鏡像（背後原理便是在原有的 Base 鏡像添加 vim Image 上去）。
</br>

- **實作範例**
  :::info
  :::spoiler **Ubuntu 鏡像安裝vim指令**
  </br>

  **1.更新管理工具包**
  ```
  apt-get update
  ```
  </br>

  **2.安裝`vim`指令**
  ```
  apt-get -y install vim
  ```
  </br>

  **3.透過當前容器創建新鏡像**
  ```
  docker commit -m "鏡像描述訊息" -a="作者" 容器名稱/ID 新鏡像名稱:[版本標籤]
  ```
  ![鏡像commit](https://hackmd.io/_uploads/r1wW4dgsT.png =80%x)
  </br>

  **4.重啟一個容器**
  ```
  docker run -it --name=新容器名稱 鏡像ID
  ```
  </br>

  **5.創建新的 ubuntu 鏡像，並將該鏡像打包上傳至本地宿主機的 Registry**
  - **5.1 下載 Registry 鏡像**
    ```
    docker pull registry
    ```
  - **5.2 運行 Registry 容器** 
    ```
    docker run -d -p 5000:5000 -v 宿主機目錄路徑:Registry容器的目錄路徑 --privileged=true registry
    ```
    ![docker run registry](https://hackmd.io/_uploads/BJ69P53ip.png)
    - **`-v`：**
      為將該容器的目錄路徑掛載到本地宿主機的根目錄中（俗稱映射），以方便後續使用。
    - **`--privileged`：**
      允許容器有權限將容器的目錄掛載到宿主機的目錄，否則可能會跳出 `Cannot open directory:Permission denied.` 的錯誤碼，詳細說明可以參考[【七、容器的檔案系統目錄】](#七、容器的檔案系統目錄)。
  :::

---

# 六、鏡像倉庫
## 1.Docker Hub是什麼？
Docker 官方提供的私有鏡像倉庫。
</br>

## 2.Registry是什麼？
透過運行 Registry 容器的方式，在本地宿主機環境中，建立一個類似於 Docker Hub 的私有的鏡像倉庫(Registry)，供組織內部人員使用。
</br>

## 3.實作範例
  :::info
  :::spoiler **創建新的 ubuntu 鏡像，且該 ubuntu 鏡像已安裝 `ifconfig`、`vim` 指令**
  </br>

  **1.下載 ubuntu 鏡像**
  ```
  docker pull ubuntu
  ```
  </br>

  **2.運行 ubuntu 容器**
  ```
  docker run -it --name=容器名稱 /bin/bash ubuntu
  ```
  </br>

  **3.為 ubuntu 容器安裝`ifconfig`、`vim`，並且退出 ubuntu 容器**
  ```
  apt-get install net-tools
  ```
  ```
  apt-get install -y vim
  ```
  ```
  ctrl+p+q
  ```
  </br>

  **4.提交 ubuntu 容器副本使其成為新鏡像**
  ```
  docker commit -m "鏡像描述訊息" -a="作者" 容器名稱/ID  新的鏡像名稱:[TAG]
  ```
  ![鏡像commit-2](https://hackmd.io/_uploads/H1G2m_ar0.png)
  </br>

  **5.`curl` 查看私有鏡像倉庫 Registry 當前內容**
  ```
  curl -XGET http://宿主機IP:5000/v2/_catalog
  ```
  ![registry內容](https://hackmd.io/_uploads/HJTs5oaiT.png)
  </br>

  **6.使鏡像符合私有鏡像倉庫 Registry 的命名規範**
  私有鏡像倉庫 Registry 對於鏡像名稱有所規範，因此必須透過此步驟來克隆、修改出一個符合規範的鏡像。
  ```
  docker tag 鏡像名稱:[TAG] 宿主機IP:5000/鏡像名稱:[TAG]
  ```
  ![docker修改鏡像](https://hackmd.io/_uploads/HkSqCspsp.png)
  </br>

  **7.修改配置文件使私有鏡像倉庫 Registry 支持 HTTP 協議**
  私有鏡像倉庫 Registry 預設不支持使用 HTTP 協議上傳鏡像，因此必須修改其配置文件來啟用 HTTP 協議。
  - **7.1 創建配置文件**
    ```
    vim /etc/docker/daemon.json
    ```
  - **7.2 新增文件內容**
    ```
    {"insecure-registries":["192.168.0.98:5000"]}
    ```
    ![daemon](https://hackmd.io/_uploads/SyKaN26sp.png =80%x)
  - **7.3 離開並儲存文件**
    ```
    wq!
    ```
  - **7.4 重啟 Docker**
    ```
    sudo systemctl restart docker
    ```
  - **7.5 重啟 Registry 容器**
    ```
    docker restart Registry容器名稱/ID
    ```
  </br>

  **8.上傳符合規範的新鏡像至私有鏡像倉庫 Registry**
  ```
  docker push 鏡像名稱:[TAG]
  ```
  ![docker push](https://hackmd.io/_uploads/S1OmnT6oT.png)
  </br>

  **9.`curl` 重新查看私有鏡像倉庫 Registry 當前內容**
  ```
  curl -XGET http://宿主機IP:5000/v2/_catalog
  ```
  ![registry內容2](https://hackmd.io/_uploads/BJmi26Tja.png)
  </br>

  **10.下載私有鏡像倉庫 Registry 內的鏡像，並運行為容器**
  - **10.1 刪除本地的舊鏡像**
    ```
    docker rmi -f 鏡像名稱:[TAG]
    ```
    ![docker rmi本地舊的鏡像](https://hackmd.io/_uploads/B1tARpTjp.png)
  - **10.2 下載私有鏡像倉庫 Registry 內的鏡像**
    ```
    docker pull 宿主機IP:5000:鏡像名稱:[TAG]
    ```
    ![docker pull新鏡像](https://hackmd.io/_uploads/B1FgeA6sp.png)
  - **10.3 運行為容器**
    ```
    docker run -it --name=容器名稱 鏡像名稱:[TAG] /bin/bash
    ```
    ![docker run上傳鏡像範例](https://hackmd.io/_uploads/BJ84-0TjT.png)
  - **10.4 測試該 ubuntu 容器是否可以執行 `vim`、`ifconfig`指令**
    ![上傳容器範例-容器測試](https://hackmd.io/_uploads/SyTDzRTo6.png)
  :::

---

# 七、容器的檔案系統目錄
- **容器的檔案系統映射至宿主機**
  ```
  docker run -it --privileged=true -v /宿主機絕對路徑目錄:/容器內絕對路徑目錄:rw 鏡像名稱
  ```
  - **`--privileged=true`**
    賦予容器去掛載本地宿主機目錄的權限，也就是使容器內的Root使用者擁有真正本地Root使用者的權限，否則容器內的Root使用者僅僅只是本地宿主機一個普通權限的用戶而已。

  - **`-v /宿主機絕對路徑目錄:/容器內絕對路徑目錄`**
    實現容器的絕對路徑目錄、宿主機的絕對路徑目錄間的資料互聯、同步，目的是將容器數據卷的重要資料備份、持久化至本地宿主機，即便刪除該Docker容器，也不影響其掛載至本地宿主機的數據卷；反之本地宿主機修改目錄內容，同樣會映射到停止運行的容器檔案系統中。
  - `-v` 全名為 volumes，可以映射多個容器檔案系統至宿主機，因此可以有多組 `-v` 指令。

  - **`:rw`／`:ro`**
    - 若沒輸入，默認就是讀寫（RW）權限。
    - `ro` 代表容器的該檔案系統目錄僅提供唯讀權限（本地宿主機的目錄可讀可寫）。
</br>

- **容器間繼承、共享檔案系統目錄**
  ```
  docker run -it --privileged=true --volumes-from 容器1名稱 --name=容器2名稱 鏡像名稱
  ```
  - 將容器1的檔案系統映射至本地宿主機。
  - 容器2繼承容器1的檔案系統規則。
  - 本地宿主機、容器1、容器2的檔案系統目錄內容會永遠一致，且容器1、容器2不受彼此影響，即便刪除或重啟任一容器。
</br>

- **查看容器檔案系統的映射路徑**
  ```
  docker inspect 容器ID/名稱
  ```
  - 以 `JSON` 格式呈現，明確紀錄映射的容器的檔案系統路徑、宿主機檔案系統路徑。
</br>

- **實作範例**
  :::info
  :::spoiler **容器卷映射至宿主機**
  </br>

  **1.容器卷映射**
  ```
  docker run -it --privileged=true -v /tmp/host_data:/tmp/docker_data --name=容器名稱 ubuntu
  ```
  ![25](https://hackmd.io/_uploads/r1KF2Baa6.png =250%x)
  </br>

  **2.確認容器檔案系統成功映射至本地宿主機**
  ```
  docker inspect 容器ID/名稱
  ```
  ![30](https://hackmd.io/_uploads/SJSC88a6a.png)
  </br>

  **3.容器卷內創建測試文件**
  ```
  cd /tmp/docker_data
  ```
  ```
  touch 文件名稱.txt
  ```
  ![26](https://hackmd.io/_uploads/S1dm0raTT.png)
  </br>

  **4.查看測試文件是否映射至宿主機路徑**
  ```
  ctrl+p+q
  ```
  ```
  cd /tmp/host_data
  ```
  ![27](https://hackmd.io/_uploads/SJxUkI6Tp.png)
  </br>

  **5.宿主機建立測試文件2**
  ```
  touch 文件2名稱.txt
  ```
  ![28](https://hackmd.io/_uploads/rkF-xL6T6.png)
  </br>

  **6.查看測試文件2是否映射至容器路徑**
  ```
  docker exec -it 容器ID/名稱 /bin/bash
  ```
  ```
  cd /tmp/docker_data
  ```
  ![29](https://hackmd.io/_uploads/SyhY-IapT.png)
  :::

---

# 八、Dockerfile
## 1.什麼是Dockerfile
是一條條鏡像需要的指令、參數所組成的文本，簡單的來說就是用來構建 Docker 鏡像的文本文件。
</br>

## 2.Dockerfile 構建 3 步驟
![建立Dockerfile](https://hackmd.io/_uploads/By2QcjrHC.png)

- **編寫 Dockerfile 文件：**
  Dockerfile 文件包含程式碼、環境變數、補丁包、作業系統、內核進程等（若程式碼）。
- **構建 Docker 鏡像：**
  透過 `Docker build` 指令，建立 Docker Image。
- **Docker run 依鏡像運行容器：**
  使用 Docker Image 執行 `docker run` 指令建立容器，正是提供服務。
</br>

## 3.Dockerfile 常用指令
  - **`FROM`**
    基礎鏡像，當前的新鏡像底層是哪個鏡像，第一條必須是 `FROM`。

  - **`MAINTAINER`**
    該鏡像維護者的姓名、信箱地址。

  - **`RUN`**
    - RUN是在執行 docker build 時運行的，也就是構建容器時，需要運行的指令。
    - 2種格式：`shell`、`exec`（JSON）。

  - **`EXPOSE`**
    當前容器對外暴露使用的連接埠號。

  - **`WORKDIR`**
    指定在創建容器後，終端默認的工作目錄路徑。
    - 範例：`/bin/bash`、`/`、`/usr/local/tomcat`

  - **`USER`**
    指定該鏡像以什麼用戶去執行，若沒有指定默認是`root`。

  - **`ENV`**
    構建鏡像過程中設置的環境變數。
    - Tomcat容器範例：
  `   ENV CATALINA_HOME /usr/local/tomcat`
  `   WORKDIR $CATALINA_HOME`

  - **`ADD`**
    將宿主機目錄下的文件複製到鏡像（會自動處理URL、tar壓縮檔解壓縮）
    - 適用場景：希望鏡像包含宿主機的應用程式包。

  - **`COPY`**
    類似 `ADD`，複製宿主機的文件、目錄到鏡像中。
    - 範例：
      `COPY src dest`
      `COPY ["src","dest"]`
      `<src源路徑>：源文件或者源目錄`
      `<dest目標路徑>：容器內的指定路徑，該路徑不必事先建立`

  - **`VOLUME`**
    容器的數據卷，用於數據保存與持久化工作。

  - **`CMD`**
    - 啟動容器後，要執行的指令。
    - 注意事項：Dockerfile中有多個CMD指令，但只有最後一個生效，CMD會被Docker run之後的參數替換。
    - `RUN` 差異：CMD在docker run時候運行；RUN是在docker build時候運行。

  - **`ENTRYPOINT`**
    - 類似 `CMD` 指令，但不會被docker run之後的命令覆蓋，這些命令會被當作參數傳送給 ENTRYPOINT 指定的應用程式。
    - 適用場景：若需要修改應用程式參數，可與CMD指令搭配使用（CMD負責傳送參數給ENTRYPOINT）。
    - 範例：`<ENTRYPOINT>"<CMD>"`
</br>

## 4.實作範例
  :::info
  :::spoiler **透過Dockerfile建立自定義鏡像（安裝Java的CentOS鏡像）**
  </br>

  **1.啟動centos容器**
  ![啟動centos容器](https://hackmd.io/_uploads/HkOCpirS0.png)

  </br>

  **2.確認容器沒有安裝JAVA**
  ![啟動centos容器2](https://hackmd.io/_uploads/Sy7kCsSrC.png)
  </br>

  **3.宿主機安裝JAVA**
  ![啟動centos容器3](https://hackmd.io/_uploads/rJleCsHBA.png)

  ![啟動centos容器3-2](https://hackmd.io/_uploads/SJ9eRiBB0.png)
  </br>

  **4.在宿主機撰寫Dockerfile文件**
  ```
  vim Dockerfile
  ```
  ![啟動centos容器4](https://hackmd.io/_uploads/r1VZAsSSR.png)

  ```
  FROM centos
  MAINTAINER bruce<個人信箱>

  # 進入容器的默認目錄路徑
  ENV MYPATH /usr/local
  WORKDIR $MYPATH

  # 使用 Vault 倉庫
  RUN sed -i 's|mirrorlist=|#mirrorlist=|g' /etc/yum.repos.d/CentOS-*.repo && \
    sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-*.repo

  RUN yum -y install vim \
      && yum -y install net-tools \
      && yum -y install glibc.i686

  RUN mkdir /usr/local/java

  # ADD 更改 COPY 並確保 jdk-22_linux-x64_bin.tar.gz 文件路徑正確
  COPY jdk-22_linux-x64_bin.tar.gz /usr/local/java/

  RUN tar -xzvf /usr/local/java/jdk-22_linux-x64_bin.tar.gz -C /usr/local/java/

  ENV JAVA_HOME /usr/local/java/jdk-22
  ENV JRE_HOME $JAVA_HOME/jre
  ENV CLASSPATH $JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JRE_HOME/lib:$CLASSPATH
  ENV PATH $JAVA_HOME/bin:$PATH

  EXPOSE 80

  CMD echo $MYPATH && echo "success--------ok" && /bin/bash
  ```
  ![啟動centos容器4-2](https://hackmd.io/_uploads/HkYMAoHSR.png)
  </br>

  **5.建立Dockerfile**
  ```
  docker build -t dockerfile名稱:tag .
  ```
  - 結尾必須空一格+英文句號
  - 若少了英文句號，鏡像名稱會顯示 `<none>`
  ![啟動centos容器5](https://hackmd.io/_uploads/SJdmAiHHC.jpg)
  ![啟動centos容器6](https://hackmd.io/_uploads/SyGN0iHH0.png)
  </br>

  **6.啟動新CentOS容器並測試**
  ![啟動centos容器7](https://hackmd.io/_uploads/HksVCoSB0.png)
  :::

---

# 九、常見應用程式容器實作範例
## 1.Tomcat
  :::info
  :::spoiler **建立 Tomcat 容器（基礎版）**
  </br>

  **1.抓取 Tomcat 鏡像**
  ```
  docker pull tomcat
  ```
  </br>

  **2.運行 Tomcat 容器**
  ```
  docker run -d -p 8080:8080 --name=容器名稱 tomcat
  ```
  ![31](https://hackmd.io/_uploads/ByNHgs66p.png)
  </br>

  **3.瀏覽器訪問網站**
  ```
  ip:8080
  ```
  ![32](https://hackmd.io/_uploads/SkmolspT6.png)
  </br>

  **4.修正網站404錯誤**
  - **4.1 進入Tomcat容器的bash**
    ```
    docker exec -it 容器ID/名稱 /bin/bash
    ```
  - **4.2 進入Tomcat檔案系統目錄**
    ```
    cd /usr/local/tomcat
    ```
  - **4.3 刪除空的webapps目錄**
    ```
    rm -rf webapps
    ```
  - **4.4 更改webapps.dist為webapps**
    ```
    mv webapps.dist webapps
    ```
    ![33](https://hackmd.io/_uploads/ryrQfTC6a.png)
  </br>

  **5.再次使用瀏覽器訪問網站**
  ```
  ip:8080
  ```
  ![34](https://hackmd.io/_uploads/BJ09fa066.png)
  :::

## 2.MySQL
  :::info
  :::spoiler **建立 MySQL 容器（基礎版）**
  </br>

  **1.抓取 MySQL 鏡像**
  ```
  docker pull mysql
  ```
  </br>

  **2.確認本地宿主機的 3306 port是否被 MySQL 佔用**
  - **查看當前監聽端口**
    ```
    netstat -tunlp
    ```
    ![36](https://hackmd.io/_uploads/Bk5ZKJyCp.png)
  </br>

  **3.建立 MySQL 容器**
  ```
  docker run -d -p 3306:3306 --privileged=true -v 宿主機MySQL日誌路徑:容器內MySQL日誌路徑 -v 宿主機MySQL資料路徑:容器內MySQL資料路徑 -v 宿主機MySQL設定檔路徑:容器內MySQL設定檔路徑 -e MYSQL_ROOT_PASSWORD=MySQL根使用者密碼 --name=容器名稱 mysql
  ```
  - 為備份 MySQL 資料、備份MySQL日誌、修改MySQL設定檔內容，因此將上述容器的檔案系統目錄映射至本地宿主機的檔案系統目錄下，詳細說明請參考[【七、容器的檔案系統目錄】](#七、容器的檔案系統目錄)。
  - 因為映射至本地宿主機，因此若當前MySQL容器刪除，重新啟動一個新MySQL容器，資料仍存在。
  - 必須為 MySQL 資料庫的根使用者設置登入密碼。
  </br>

  **4.使 MySQL 支援使用UTF8中文**
  - **4.1 進入本地宿主機的 MySQL config 設定目錄**
    ```
    cd  宿主機MySQL設定檔路徑
    ```
  - **4.2 編輯 MySQL config 設定目錄**
    ```
    vim my.cnf
    ```
  - **4.3 貼上下列內容並儲存my.cnf文件**
    ```
    [client]
    default_character_set=utf8
    [mysqld]
    collation_server = utf8_general_ci
    character_set_server = utf8
    ```
    ![mysql utf8中文](https://hackmd.io/_uploads/rk38P-0eR.png)
  - **4.4 重新啟動 mysql 容器**
    ```
    docker restart 容器ID/名稱
    ```
  </br>

  **6.進入 docker 容器**
  ```
  docker exec -it 容器ID/名稱 /bin/bash
  ```
  ```
  mysql -u root -p
  ```
  </br>

  **7.查看 MySQL 字符編碼是否已經更改為utf8**
  ```
  SHOW VARIABLES LIKE 'character%';
  ```
  :::

  :::info
  :::spoiler **主從架構 MySQL 容器**
  </br>

  **1.MySQL主從架構說明**
  主要用於實現MySQL資料的複製、高可用性，通常這種資料庫架構包含：
  - **一台主伺服器**：負責處理所有寫入（Write）操作。
  - **一或多台從伺服器**：從主伺服器同步資料，並負責處理讀取（Read）操作。
  </br>

  **2.建立 MySQL 主節點容器**
  ```
  docker run -d -p 3306:3306 --privileged=true -v 宿主機MySQL主節點日誌路徑:容器內MySQL日誌路徑 -v 宿主機MySQL主節點資料路徑:容器內MySQL資料路徑 -v 宿主機MySQL主節點設定檔路徑:容器內MySQL設定檔路徑 -e MYSQL_ROOT_PASSWORD=MySQL根使用者密碼 --name=主節點容器名稱 mysql
  ```
  ![啟動mysql主節點容器](https://hackmd.io/_uploads/SJswyo8BA.png)
  </br>

  **3.編輯本地 MySQL 主節點配置文件**
  ```
  cd 宿主機MySQL主節點設定檔路徑
  ```
  ```
  vim my.cnf
  ```
  ![編輯本地mysql配置文件](https://hackmd.io/_uploads/ry4FkiLrR.png)
  - **MySQL 8.3**
    ```
    [mysqld]
    server_id=101
    ##設置Server ID，同網段中只能唯一

    binlog-ignore-db=mysql
    ##指定不需要同步的數據庫名稱

    log-bin=mall-mysql-bin
    ##開啟二進制日誌功能

    binlog_cache_size=1M
    ##設置二進制日誌使用的內存大小

    binlog_format=mixed
    ##設置使用的二進制日誌格式

    binlog_expire_logs_seconds=604800
    ##二進制日誌過期時間，默認為0(表示不清理)

    replica_skip_errors=1062
    ##忽略主從同步過程中的所有錯誤或特定錯誤，避免Slave端同步錯誤
    ##1062錯誤：主鍵資料重複
    ##1032錯誤：主從資料庫資料不一致
    ```

  - **MySQL 5.7**
    ```
    [mysqld]
    server_id=101
    ##設置Server ID，同網段中只能唯一

    binlog-ignore-db=mysql
    ##指定不需要同步的數據庫名稱

    log-bin=mall-mysql-bin
    ##開啟二進制日誌功能

    binlog_cache_size=1M
    ##設置二進制日誌使用的內存大小

    binlog_format=mixed
    ##設置使用的二進制日誌格式

    expire_logs_days=7
    ##二進制日誌過期時間，默認為0(表示不清理)

    slave_skip_errors=1062
    ##忽略主從同步過程中的所有錯誤或特定錯誤，避免Slave端同步錯誤
    ##1062錯誤：主鍵資料重複
    ##1032錯誤：主從資料庫資料不一致
    ```
    ![主mysql的conf檔](https://hackmd.io/_uploads/S1t9yj8SC.png)
  </br>

  **4.重啟 MySQL 主節點容器**
  ```
  docker restart "MySQL主節點容器名稱/ID"
  ```
  ```
  docker ps
  ```
  ![主mysql重啟](https://hackmd.io/_uploads/ryTi1i8rR.png)
  - 必須確認 MySQL 主節點容器重啟後仍在運行中，若沒有運行可能是 `my.cnf` 內容有誤而無法使用。
  </br>

  **5.進入 MySQL 主節點容器**
  ```
  docker exec -it "MySQL主節點容器名稱/ID" /bin/bash
  ```
  ```
  mysql -u root -p
  ```
  ![進入主mysql](https://hackmd.io/_uploads/Byv6kiIBR.png)
  </br>

  **6.MySQL 主節點容器內創建同步數據的用戶**
  - **6.1創建 MySQL 用戶**
    - MySQL 8.3
      ```
      CREATE USER 'MySQL同步使用者的名稱'@'%' IDENTIFIED　WITH mysql_native_password BY "MySQL同步使用者的密碼";
      ```
    - MySQL 5.7
      ```
      CREATE USER 'MySQL同步使用者的名稱'@'%' IDENTIFIED BY "MySQL同步使用者的密碼";
      ```
      ![主MYSQL建立同步使用者](https://hackmd.io/_uploads/rkCCyjUHC.png)
  - **6.2 賦予 MySQL 用戶權限**
    ```
    GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'MySQL同步使用者的名稱'@'%';
    ```
    ![主MYSQL賦予同步使用者權限](https://hackmd.io/_uploads/rkMeeoLSR.png)
  </br>

  **7.建立 MySQL 從節點容器**
  ```
  docker run -d -p 3307:3306 --privileged=true -v 宿主機MySQL從節點日誌路徑:容器內MySQL日誌路徑 -v 宿主機MySQL從節點資料路徑:容器內MySQL資料路徑 -v 宿主機MySQL從節點設定檔路徑:容器內MySQL設定檔路徑 -e MYSQL_ROOT_PASSWORD=MySQL根使用者密碼 --name=從節點容器名稱 mysql
  ```
  ![建立從MySQL容器](https://hackmd.io/_uploads/Skp-eo8rR.png)
  - 使用本地宿主機的 3307 埠號來運行 MySQL 從節點容器。
  </br>

  **8.編輯本地 MySQL 從節點配置文件**
  ```
  cd 宿主機MySQL從節點設定檔路徑
  ```
  ```
  vim my.cnf
  ```
  ![進入mysql從節點配置目錄](https://hackmd.io/_uploads/H1-7xiUHR.png)
  - **MySQL 8.3**
    ```
    [mysqld]
    server_id=102
    ##設置Server ID，同網段中只能唯一

    binlog-ignore-db=mysql
    ##指定不需要同步的數據庫名稱

    log-bin=mall-mysql-slave1-bin
    ##開啟二進制日誌功能

    binlog_cache_size=1M
    ##設置二進制日誌使用的內存大小

    binlog_format=mixed
    ##設置使用的二進制日誌格式

    binlog_expire_logs_seconds=604800
    ##二進制日誌過期時間，默認為0(表示不清理)

    replica_skip_errors=1062
    ##忽略主從同步過程中的所有錯誤或特定錯誤，避免Slave端同步錯誤
    ##1062錯誤：主鍵資料重複
    ##1032錯誤：主從資料庫資料不一致

    relay_log=mall-mysql-relay-bin
    ##relay_log配置中繼日誌

    log_slave_updates=1
    ##log_slave_updates表示slave將同步事件寫進自己的二進制日誌

    read_only=1
    ##slave設置為只讀(具有super權限的用戶除外)
    ```

  - **MySQL 5.7**
    ```
    [mysqld]
    server_id=102
    ##設置Server ID，同網段中只能唯一

    binlog-ignore-db=mysql
    ##指定不需要同步的數據庫名稱

    log-bin=mall-mysql-slave1-bin
    ##開啟二進制日誌功能

    binlog_cache_size=1M
    ##設置二進制日誌使用的內存大小

    binlog_format=mixed
    ##設置使用的二進制日誌格式

    expire_logs_days=7
    ##二進制日誌過期時間，默認為0(表示不清理)

    slave_skip_errors=1062
    ##忽略主從同步過程中的所有錯誤或特定錯誤，避免Slave端同步錯誤
    ##1062錯誤：主鍵資料重複
    ##1032錯誤：主從資料庫資料不一致

    relay_log=mall-mysql-relay-bin
    ##relay_log配置中繼日誌
  
    log_slave_updates=1
    ##log_slave_updates表示slave將同步事件寫進自己的二進制日誌

    read_only=1
    ##slave設置為只讀(具有super權限的用戶除外)
    ```
    ![從mysql的conf檔](https://hackmd.io/_uploads/BkB4go8H0.png)
  </br>

  **9.重啟 MySQL 從節點容器**
  ```
  docker restart "MySQL從節點容器名稱/ID"
  ```
  ![從mysql重啟](https://hackmd.io/_uploads/rJVvlj8rR.png)
  </br>

  **10.在 MySQL 主節點容器查看主從同步狀態**
  ```
  docker exec -it "MySQL主節點容器名稱/ID" /bin/bash
  ```
  ```
  mysql -u root -p
  ```
  ```
  show master status;
  ```
  ![主mysql查看主從同步](https://hackmd.io/_uploads/HkDulsLHR.png)
  - 記下 `File參數` 、 `Position參數` ，下一步驟會使用到。
  - `File參數`：二進制日誌文件的名稱。
  - `Position參數`：表示下一個事件日誌將會從哪裡開始被寫入。
  </br>

  **11.進入 MySQL 從節點容器並配置主從同步**
  ```
  docker exec -it "MySQL從節點容器名稱/ID" /bin/bash
  ```
  ```
  mysql -u root -p
  ```
  ```
  CHANGE MASTER TO 
  master_host='MySQL主節點容器的宿主機IP', 
  master_user='MySQL同步使用者的名稱', 
  master_password='MySQL同步使用者的密碼', 
  master_port=MySQL主節點容器的宿主機埠號,
  master_log_file='File參數', 
  master_log_pos=Position參數, 
  master_connect_retry=連接失敗重試的時間間隔;
  ```
  ![從mysql配置主從同步](https://hackmd.io/_uploads/B13FlsLB0.png)
  </br>

  **12.在 MySQL 從節點容器查看主從同步狀態**
  ```
  show slave status \G;
  ```
  ![從mysql查看主從狀態](https://hackmd.io/_uploads/SkbsljUrA.png)
  - 上圖可以看到，目前尚未開啟主從同步。
  </br>

  **13.在 MySQL 從節點容器開啟主從同步**
  ```
  start slave;
  ```
  ![從mysql開啟主從同步](https://hackmd.io/_uploads/BymnxiIBC.png)
  </br>

  **14.查看 MySQL 從節點容器狀態確認已經同步**
  ```
  show slave status \G;
  ```
  ![從mysql確認主從同步](https://hackmd.io/_uploads/B1lRloUSR.png)

  - `Slave_IO_Running`、`Slave_SQL_Running`：
    皆必須顯示為 `Yes` 才代表主從同步連線成功，若否則代表同步連線失敗。
  - `Last_IO_Error` ：
    若同步連線失敗，可查看此項目來取得連線失敗的詳細資訊，並以此作為排查依據。
    ![從mysql確認主從同步(錯誤碼)](https://hackmd.io/_uploads/HyEJWj8BA.png)
  </br>

  **15.MySQL主從同步測試**
  - **15.1 MySQL 主節點容器建立新database並插入數據**
    ```
    create database 資料庫名稱;
    ```
    ```
    use 資料庫名稱;
    ```
    ```
    create table 資料表名稱(資料表schema);
    ```
    ```
    insert into 資料表表名稱(資料、數據); 
    ```
    ![主mysql插入資料](https://hackmd.io/_uploads/ryUg-oLrA.png)
  - **15.2 MySQL 從節點容器查看記錄**
    ```
    use 資料庫名稱;
    ```
    ```
    select * from 資料表名稱;
    ```
    ![從mysql確認插入資料](https://hackmd.io/_uploads/B1eM-oUrC.png)
  :::

## 3.Redis
  :::info
  :::spoiler **建立 Redis 容器（單節點基礎版）**
  </br>

  **1.抓取 Redis 鏡像**
  ```
  docker pull redis
  ```
  </br>

  **2.在宿主機建立 Redis 目錄**
  ```
  mkdir -p /app/redis
  ```
  ![建立redis文件](https://hackmd.io/_uploads/HJSwQzAeC.png)
  </br>

  **3.在 Redis 目錄建立 Redis 配置文件( redis.conf )**
  - **3.1 建立 Redis 配置文件**
    ```
    vim redis.conf
    ```
  - **3.2 貼上官方 Redis 配置文件範本**
    [Redis 配置文件範本](https://redis.io/docs/latest/operate/oss_and_stack/management/config-file/)
  </br>

  **4.修改 Redis 配置文件內容**
  - **4.1 允許外部連線進入 Redis**
    ```
    # 127.0.0.1-::1
    ```
    ![redis配置文件註釋本地](https://hackmd.io/_uploads/SJHWUqkbR.png)
  - **4.2 取消 Redis 作為常駐程式(否則後續容器會啟動失敗)**
    ```
    daemonize no
    ```
    ![redis配置文件daemon](https://hackmd.io/_uploads/HybNDq1bR.png)
  </br>

  **5.建立 Redis 容器**
  ```
  docker run -p 6379:6379 --name=容器名稱 --privileged=true -v 宿主機Redis的配置文件路徑:容器內Redis配置文件路徑 -v 宿主機Redis數據路徑:容器內Redis數據路徑 -d redis鏡像名稱 redis-server 容器內Redis的配置文件路徑
  ```
  - `redis-server`：若本地宿主機本來就有 Redis 配置文件（redis.conf），可以添此參數內容，使得該 Redis 容器在啟動時便套用指定的 Redis 配置文件。
    ![redis啟動](https://hackmd.io/_uploads/B1hPWdJb0.png)
  </br>

  **6.測試 Redis 容器是否套用本地宿主機的 Redis 配置文件**
  - **6.1 修改 redis.conf 文件中的 database 數量**
    ![redis配置文件](https://hackmd.io/_uploads/Sy2AV_kWA.png)
  - **6.2 重新啟動 Redis 容器**
    ```
    docker exec -it Redis容器ID/名稱
    ```
    ![redis測試0](https://hackmd.io/_uploads/SyXtKqkb0.png)
  - **6.3 切換使用不同的 Redis database**
    ```
    select database數字
    ```
    下圖顯示超出 Redis 配置文件所設置的 database 數量：
    ![redis測試1](https://hackmd.io/_uploads/BylhKq1WR.png)
  :::

  :::info
  :::spoiler **建立 Redis 集群容器（3主3從）**
  </br>
  
  **1.架構圖**
  ![Redis-Docker架構圖](https://hackmd.io/_uploads/B1kNbo8B0.png)
  </br>

  **2.啟動6個 Redis 容器實例**
  ```
  docker run -d --name 容器名稱 --net host --privileged=true -v /本地映射目錄路徑:容器映射目錄路徑 redis鏡像名稱 --cluster-enabled yes --appendonly yes --port 容器port號
  ```
  - `--net host`：使用本地主機IP、Port埠號。
  - `--cluster-enabled`：是否開啟 Redis 集群。
  - `--appendonly`：是否開啟持久化。
    ![1建立多個redis容器](https://hackmd.io/_uploads/S1lrQu6SR.png)
  </br>

  **3.進入任ㄧ Redis 容器**
  ```
  docker exec -it 任一Redis容器名稱 /bin/bash
  ```
  ![進入redis容器節點](https://hackmd.io/_uploads/Hkk8biLSC.png)
  </br>

  **4.設定6個容器集群關係**
  ```
  redis-cli --cluster create 本地主機IP:REDIS埠號 本地主機IP:REDIS埠號2 本地主機IP:REDIS埠號3 本地主機IP:REDIS埠號4 本地主機IP:REDIS埠號5 本地主機IP:REDIS埠號6 --cluster-replicas 1
  ```
  - `--cluster-replicas 1`：代表每１個 Master 匹配 1 個從節點。
  - 本地主機 IP 可以是內網 IP。
  ```
  yes
  ```
  ![建立redis容器節點集群](https://hackmd.io/_uploads/BkFPWjIS0.png)
  </br>

  **5.查看 Redis 集群狀態**
  - **5.1 進入 Redis** 
    ```
    redis-cli -p 要查看的容器port
    ```
  - **5.2 查看當前 Redis 容器的集群資料**
    ```
    cluster info
    ```
    ![查看redis容器集群狀態](https://hackmd.io/_uploads/r11t-jUBA.png)
  - **5.3 確認 Redis 集群中，主、從節點間的配對**
    ```
    cluster nodes
    ```
    ![查看redis容器集群狀態2](https://hackmd.io/_uploads/SkRtZiLB0.png)
  - **5.4 查看主、從節點包含的[Slots 槽位](https://medium.com/jerrynotes/redis-hash-tag-%E5%A6%82%E4%BD%95%E9%81%8B%E4%BD%9C-2b99a190c664)**
    ```
    cluster slots
    ```
    ![查看redis容器集群狀態3](https://hackmd.io/_uploads/HkRiboLrA.png)
  - **5.5 本文範例主、從節點間的掛載關係如下（每個人實際操作時，主、從的掛載關係可能不盡相同）：**
      | Master主節點 | Slave從節點 |  Slots槽位  |
      |:------------:|:-----------:|:-----------:|
      |     6381     |    6384     |   0-5460    |
      |     6382     |    6385     | 5461-10922  |
      |     6383     |    6386     | 10923-16383 |
  :::

  :::info
  :::spoiler **Redis 集群容器的資料儲存**
  </br>

  **1.進入任一 Redis 主節點容器**
  ```
  docker exec -it 任一Redis主節點容器名稱 /bin/bash
  ```
  </br>

  **2.以集群環境啟動主節點容器的 Redis 應用程式**
  ```
  redis-cli -p Redis主節點埠號 -c
  ``` 
  - `-c`：以 Redis 的集群環境去啟動 Redis，若沒有加入此參數，便會以單機模式啟動 Redis 容器。
    ![進入redis容器節點2](https://hackmd.io/_uploads/BkeC-s8BA.png)
  </br>

  **3.測試 Redis 集群的插槽路由是否有效**
  ```
  set k1 v1 
  ```
  ![redis集群路由測試1](https://hackmd.io/_uploads/S1nlzsUSC.png)
  ```
  set k2 v2
  ```
  ![redis集群路由測試2](https://hackmd.io/_uploads/HJwZGo8SC.png)
  - 上圖可以看到插入的 `Key-Value` 資料，分別儲存在不同 Redis 主節點的插槽（Slots）範圍內，但只要是以集群環境啟動主節點容器的 Redis 應用程式，便仍會自動路由至正確的 Redis 主節點進行資料插入。
  - 若沒有以集群環境啟動 Redis（參數 `-c` ），在插入`Key-Value`資料時，若該資料儲存在不同的 Redis 主節點插槽範圍內，便會出現下圖 `Moved ERROR` 的路由錯誤提示：
    ![redis集群路由測試3](https://hackmd.io/_uploads/S1jMfs8BR.png)
  </br>

  **4.查看 Redis 集群容器的資料訊息**
  ```
  exit
  ```
  ```
  redis-cli --cluster check 本地主機IP:Redis主節點埠號
  ```
  ![redis集群容器的資料訊息](https://hackmd.io/_uploads/B1qmzo8r0.png)
  - 上圖可以看到各個 Redis 主節點容器儲存了多少 `Key-Value` 資料，下方顯示 Redis 從節點容器複製主節點容器內的 `Key-Value` 資料。
  :::


  :::info
  :::spoiler **Redis 集群容器的主從切換**
  </br>

  **1.停用任一 Redis 集群的主節點容器**
  ```
  docker stop 任一Redis主節點容器名稱
  ```
  ![redis集群停用主節點容器](https://hackmd.io/_uploads/BkHHfsUBC.png)
  ![Redis集群停用節點架構圖](https://hackmd.io/_uploads/B18UMj8rA.png)
  </br>

  **2.進入其他 Redis 主節點容器，並以集群環境啟動 Redis 應用程式**
  ```
  docker exec -it 其他Redis主節點容器名稱 /bin/bash
  ```
  ```
  redis-cli -p Redis主節點埠號 -c
  ``` 
  ![redis集群進入其他主節點容器](https://hackmd.io/_uploads/ryL1XoIrC.png)
  </br>

  **3.查看目前主、從節點間的配對**
  ```
  cluster nodes
  ```
  ![redis集群停用主節點查看節點配對關係](https://hackmd.io/_uploads/H1sxXoIBR.png)
  ![Redis集群停用節點架構圖2](https://hackmd.io/_uploads/HJa-7iUrA.png)
  - 原先的主節點 Redis 容器（Port:6381）停用後，原先的從節點 Redis 容器（Port:6384）會變更為新的 Redis 主節點。
  </br>

  **4.確認 Redis 集群內的 `Key-Value` 資料**
  ```
  get Key名稱
  ```
  ![redis集群停用主節點確認資料](https://hackmd.io/_uploads/HJW77oUSA.png)
  </br>

  **5.還原 Redis 集群主節點容器**
  ```
  ctrl+p+q
  ```
  ```
  docker start 已停用的Redis主節點容器名稱 
  ```
  ![redis集群重啟主節點](https://hackmd.io/_uploads/H17NmiIB0.png)
  </br>

  **6.進入其他 Redis 主節點容器，並以集群環境啟動 Redis 應用程式**
  ```
  docker exec -it 其他Redis主節點容器名稱 /bin/bash
  ```
  ```
  redis-cli -p Redis主節點埠號 -c
  ``` 
  ![redis集群進入其他主節點容器](https://hackmd.io/_uploads/Skzr7o8rA.png)
  </br>

  **7.查看目前主、從節點間的配對**
  ```
  cluster nodes
  ```
  ![redis集群重啟主節點查看節點關係](https://hackmd.io/_uploads/Hk_Umo8BA.png)
  ![redis集群重啟容器架構圖](https://hackmd.io/_uploads/HyawmiIrR.png)
  - 上圖可以看到原先的主節點容器（Port:6381）已經轉變為 Redis 從節點容器；原先的從節點容器（Port:6384）已經轉變為 Redis 主節點容器。
  - 若希望原先的主節點容器（Port:6381）重新設置為 Redis 主節點，可透過停用、啟動原先的從節點容器（Port:6384）來達成。
  :::

  :::info
  :::spoiler **Redis 集群容器的擴容**
  </br>

  **1.流程說明**
  ![redis集群擴容](https://hackmd.io/_uploads/ByutXsUHC.png)
  為現有的 Redis 集群再加入一組 Redis 主、從節點容器，大致操作步驟如下：
  - 運行2個新的 Redis 容器（1主1從）。
  - 將其中一個新的 Redis 容器以主節點的身分，加入 Redis 集群。
  - 重新分配 Redis 集群的主節點插槽範圍。
  - 將剩下新的 Redis 容器設定為新 Redis 主節點的從節點。
  </br>

  **2.啟動2個 Redis 容器實例**
  ```
  docker run -d --name 容器名稱 --net host --privileged=true -v /本地映射目錄路徑:容器映射目錄路徑 redis鏡像名稱 --cluster-enabled yes --appendonly yes --port 容器port號
  ```
  ![redis集群擴容1](https://hackmd.io/_uploads/SyY5miIrR.png)
  </br>

  **3.進入6387 Redis容器**
  ```
  docker exec -it redis-node-7 /bin/bash
  ```
  </br>

  **4.將6387 Redis容器加入6381 Redis集群中**
  ```
  redis-cli --cluster add-node 主機IP:6387 主機IP:6381
  ```
  ![redis集群擴容2](https://hackmd.io/_uploads/ryPs7iLH0.png)
  </br>

  **5.重新分配插槽範圍**
  ```
  redis-cli --cluster reshard 主機IP:6381
  ```
  ![redis集群擴容3](https://hackmd.io/_uploads/HJ8h7iIHR.png)
  - **5.1 要添加給新節點（Port6387）的槽位數**
    ```
    How many slots do you want to move(from 1 to 16384)? 要分配的slot槽數
    ```
    ![redis集群擴容4](https://hackmd.io/_uploads/SJtp7oIHA.png)
  - **5.2 輸入新節點ID**
    ```
    What is the receiving node ID? 新節點ID
    ```
    ![redis集群擴容5-2](https://hackmd.io/_uploads/rJrb4iUBA.png)
    ![redis集群擴容5-1](https://hackmd.io/_uploads/B1mzEjUB0.png)
  - **5.3 拿哪些節點的插槽來做分配**
    ```
    Source node #1:all
    ```
    ![redis集群擴容6](https://hackmd.io/_uploads/rJhsNoUrA.png)
    - `all`：代表選取原先所有節點的槽位來進行分配。
  </br>

  **6.查看當前Redis叢集的槽位分配（4主3從）**
  ```
  redis-cli --cluster check 主機IP:6381
  ```
  ![redis集群擴容7](https://hackmd.io/_uploads/r1ahNi8BR.png)
  ![redis集群擴容架構圖2](https://hackmd.io/_uploads/r1CpEoLrR.png)
  - Redis為避免影響已插入資料的儲存位置(分配成本太高)，因此從每個主節點插槽平均拿取相同槽位來分配給新的主節點。
  </br>

  **7.為新主節點添加從節點**
  ```
  redis-cli --cluster add-node 主機IP:新的從節點容器埠號 主機IP:主節點容器埠號 --cluster-slave --cluster-master-id 主節點ID
  ```
  ![redis集群擴容8](https://hackmd.io/_uploads/B1RCEi8rA.png)
  </br>

  **8.查看當前Redis集群的節點分布（4主4從）**
  ```
  redis-cli --cluster check 主機IP:6381
  ```
  ![redis集群擴容9](https://hackmd.io/_uploads/Bka1BsUr0.png)
  ![redis集群擴容](https://hackmd.io/_uploads/B1oxBiLHA.png)
  :::

  :::info
  :::spoiler **Redis 集群容器的縮容**
  </br>

  **1.流程說明**
  ![redis集群縮容架構圖1](https://hackmd.io/_uploads/H1ymSoUBC.png)
  從現有的 Redis 集群，刪除一組 Redis 主、從節點容器，大致操作步驟如下：
  - 將目標從節點從 Redis 集群中刪除。
  - 將目標主節點清空並重新分配槽位給剩餘主節點。
  - 將目標主節點從 Redis 集群中刪除。
  - 將剩下新的 Redis 容器設定為新 Redis 主節點的從節點。
  </br>

  **2.將目標從節點從 Redis 集群中刪除**
  ```
  redis-cli --cluster del-node 主機IP:從節點埠號 從節點ID
  ```
  ![redis集群縮容1](https://hackmd.io/_uploads/By67SoLrC.png)
  </br>

  **3.將目標主節點的槽位清空，並將槽位分配給其他主節點**
  ```
  redis-cli --cluster reshard 主機IP:其他主節點埠號
  ```
  ![redis集群縮容2](https://hackmd.io/_uploads/H1i4rjUrA.png)
  ```
  How many slots do you want to move(from 1 to 16384)? 輸入要清空的槽位數
  What is the receiving node ID? 輸入要接收槽位的主節點ID

  Source node #1:輸入要清空槽位的主節點ID
  :
  :
  Source node #n:完成後輸入done
  ```
  ![redis集群縮容3](https://hackmd.io/_uploads/BJ5BSiUrC.png)
  </br>

  **4.將目標主節點從 Redis 集群中刪除**
  ```
  redis-cli --cluster del-node 主機IP:主節點埠號 主節點ID
  ```
  ![redis集群縮容4](https://hackmd.io/_uploads/BysLSjIrA.png)
  </br>

  **5.檢查 Redis 集群當前的狀況**
  ```
  redis-cli --cluster check 主機IP:Redis節點埠號
  ```
  ![redis集群縮容5](https://hackmd.io/_uploads/H1vvBo8SR.png)
  :::

---

# 十、Docker 網路
## 1.Docker 網路的功能
- 使容器間進行交互通信、端口映射。
- 容器變更IP後，可以通過容器名稱直接進行交互，而不受影響。
</br>

## 2.啟動 Docker 前、後的宿主機網路環境比較
- **Docker啟動前的主機網路**
    | 網路設置名稱 | 說明 |
    | -------- | -------- |
    | `eth/ens` | 主機 bios 內建的網路卡（不同網路接口命名規則，名稱可能不同）。 |
    | `lo` | 回環接口（Loopback Interface），用於將網路流量發送回本地計算機，IP 地址通常是 127.0.0.1。 |
    | `virbr` | 虛擬網橋接口（Virtual Bridge Interface），在虛擬化環境中，將虛擬機連接到宿主機的網路上。 |

- **Docker啟動後的主機網路**
    | 網路設置名稱 | 說明 |
    | -------- | -------- |
    | `docker0` | 主機會自動生成名為 `docker` 的類虛擬網橋接口，如下圖所示 |

    ![網卡2](https://hackmd.io/_uploads/SyPwajBr0.png)
</br>

## 3.Docker網路模式說明：
- **查看當前 docker 當前所有的網路模式：**
    ```
    docker network ls
    ```
    ![網卡3](https://hackmd.io/_uploads/HJn_TiBS0.png)

- **Docker 網路模式介紹：**
    | 網路模式名稱 | 簡易說明 | 使用指令 |
    | -------- | -------- | -------- |
    | `bridge`（默認使用） | 為容器分配、設置IP等工作，並將容器連接至該 `docker0` 虛擬網橋接口。 | `--network bridge` |
    | `host`（默認使用） | 容器使用本地宿主機的 IP、Port 號。 | `--network host` |
    | `none`（默認使用） | 容器不進行任何網路設置（網路接口、IP、Port 等），但有自己獨立的  Network namespace。 | `--network none` |
    | `container`（需自行手動新增） | 該容器與一指定容器共享 IP、Port 號範圍等。 | `--network container:Name/ID` |
</br>

## 4.實作範例
  :::info
  :::spoiler **Docker 網路底層IP、容器映射的變化對比**
  </br>

  **1.查看宿主機的 `docker0` 虛擬網橋接口**
  ```
  ifconfig
  ```
  ```
  inet 127.17.0.1
  ```
  ![網卡6](https://hackmd.io/_uploads/HJu9ZIeHR.png)
  </br>

  **2.創建2個任意容器**
  ![網卡5](https://hackmd.io/_uploads/SJiXZIeBR.png)
  </br>

  **3.查看2個容器的網路設置**
  ```
  docker inspect 容器名稱/ID
  ```
  ![網卡7](https://hackmd.io/_uploads/HJBaGLxBR.png)
  ![網卡7-2](https://hackmd.io/_uploads/Hyapf8gS0.png)
  - 可以看到 2 個容器的網路環境都是使用宿主機內相同的 `docker0` 虛擬網橋接口，而 2 個容器也在該 `docker0` 網橋接口的 CIDR 網段中（bridge, 127.17.0.1/16）各自創建自己的容器 IP 地址（172.17.0.2、172.17.0.3）。
  </br>

  **4.刪除其中1個容器並運行新的容器**
  ![網卡8](https://hackmd.io/_uploads/H1GzPIlHC.png)
  </br>

  **5.查看新容器的網路設置**
  ```
  docker inspect 容器名稱/ID
  ```
  ![網卡9](https://hackmd.io/_uploads/S1tUdUlH0.png)
  - 與刪除的容器 2 相同 IP 地址。
  </br>

  **6.結論**
  - Docker 容器的 IP 地址有可能改變，因此必須確保容器不會隨著 IP 地址的改變而無法運行。
  - 容器最好運行在同一 IP 網段內（自行手動創建 Docker 網路模式）。
  :::

---

# 十一、Docker 網路（進階）
## 1.Bridge
- **說明：**
  - Docker 服務默認會創建一個虛擬網橋接口，該虛擬網橋接口名稱為 `docker0` ，它在宿主機 kernel 內核層連通其他物理或虛擬網卡，使得容器會和本地宿主機使用相同物理網路。
  - Docker 服務默認指定 `docker0 網橋接口` 的 IP 地址和子網路遮罩，使得宿主機和容器可以透過 `docker0 網橋接口` 互相通信。
- **架構圖：**
  ![網卡12](https://hackmd.io/_uploads/H1G5b5HSC.png)
  - 宿主機的每個虛擬網橋接口叫 `veth`。
  - 每個容器的網橋接口叫 `eth0`。
  - `docker0` 虛擬網橋接口上的每個 `veth` 匹配容器內的網橋接口 `eth0`，兩兩配對一組（一對接口叫 `veth pair`）。
  - 使用 `docker0` 虛擬網橋接口的容器，彼此間的網路是互通的。
- **實作範例**
    :::info
    :::spoiler **查看 Bridge 網路模式的容器**
    </br>
    
    **1.創建 2 個使用 Bridge 網路模式的容器**
    ```
    docker run -d --network bridge --name "容器名稱" 鏡像名稱／ID
    ```
    ![網卡10-0](https://hackmd.io/_uploads/Skgls9BrA.png)
    </br>

    **2.本地宿主機查看當前網路設置**
    ```
    ip addr
    ```
    ![網卡10](https://hackmd.io/_uploads/SkH4i9BSC.png)
    </br>

    **3.分別進入容器查看當前網路設置**
    ```
    ip addr
    ```
    ![網卡11](https://hackmd.io/_uploads/BJG4n5rBC.png)
    
    ![網卡11-2](https://hackmd.io/_uploads/S1nN2crSA.png)
    </br>

    **4.Bridge 網路模式的 `veth pair` 配對如下**
    - 本地宿主機：veth63  <--->  eth64：Docker容器1。
    - 本地宿主機：veth65  <--->  eth66：Docker容器2。
    :::
  </br>

## 2.Host
- **說明：**
  容器將不會虛擬出自己的網卡，而是和宿主機的 eth0 網卡共用 IP 和 Port，如下圖所示。
  ![網卡13](https://hackmd.io/_uploads/r1ElbjSBA.png)
- **實作範例**
    :::info
    :::spoiler **啟動使用 Host 網路模式的容器**
    </br>

    **1.啟動使用 Host 網路模式的容器**
    ```
    docker run -d --network host --name "容器名稱" 鏡像名稱／ID
    ```
    ![網卡14](https://hackmd.io/_uploads/HkFjmjSr0.png)
    - 啟動容器時不必添加 `-p` 的 Port 號參數，因為與本地主機共用 Port。
    </br>

    **2.查看本地宿主機 IP 地址**
    ```
    ip addr
    ```
    ![網卡15-2](https://hackmd.io/_uploads/H1zqHoHHR.png)
    </br>

    **3.訪問主機IP：容器 Port 號**
    ```
    curl 主機IP：容器Port號
    ```
    ![網卡15-3](https://hackmd.io/_uploads/HkH-LiBr0.png)
    ```
    http://主機IP:容器Port號
    ```
    ![網卡15](https://hackmd.io/_uploads/HJmzLoBHR.png)
    :::
  </br>

## 3.none
- **說明：**
  使用 `none` 網路模式下，該容器禁用網路功能，沒有網卡、IP、路由、端口等網路訊息，僅有宿主機的本地網卡 `lo`，需要自行手動再另外添加網卡、IP 等網路訊息。
- **實作範例**
    ::: info
    ::: spoiler **啟動使用 none 網路模式的容器**
    </br>

    **1.啟動使用 none 網路模式的容器**
    ```
    docker run -it -p 宿主機Port號:容器Port號 --network none --name 容器名稱 鏡像名稱／ID
    ```
    ```
    ip addr
    ```
    ![網卡16](https://hackmd.io/_uploads/S1ESR1DrC.png)
    </br>
    
    **2.在本地宿主機查看該容器的網路設置**
    ```
    docker inspect 容器名稱／ID| tail -n 20
    ```
    ![網卡17](https://hackmd.io/_uploads/H17d0JDHA.png)
    - 確認 `Gateway`、`IPAddress` 皆為空白。
    :::
  </br>

## 4.container
- **說明：**
  ![網卡18](https://hackmd.io/_uploads/BkjbY1PSC.png)
  - 新建的容器與一已存在的指定容器共享一個 IP 地址、Port 端口，而不是和宿主機共享、自行創建網卡。
  - 若啟動的容器需要在不同的端口運行，則不適合使用 Container 網路模式，否則會出現端口與網路模式衝突的錯誤：
   ![網卡18-2](https://hackmd.io/_uploads/Sk-y7_arC.png)
- **實作範例**
    :::info
    :::spoiler **啟動使用Container網路模式的容器**
    </br>

    **1.抓取Alpine鏡像**
    ```
    docker pull alpine
    ```
    - Alpine 是一輕量型的 Linux 作業系統鏡像，非常適合容器打包，詳細介紹可參考[Alpine官方文檔](https://alpinelinux.org/about/)
    </br>

    **2.啟動使用Bridge網路模式的Alpine容器1**
    ```
    docker run -it --name 容器名稱 alpine /bin/sh
    ```
    ![網卡19](https://hackmd.io/_uploads/H1lDnQFSA.png)
    </br>
    
    **3.查看Alpine容器1的網路環境**
    ```
    ip addr
    ```
    ![網卡19-2](https://hackmd.io/_uploads/ByWOa4KS0.png)
    </br>
    
    **4.啟動使用Container網路模式的Alpine容器2**
    ```
    docker run -it --network container:Alpine容器1名稱/ID --name 容器名稱 alpine /bin/sh
    ```
    ![網卡20](https://hackmd.io/_uploads/Bkoks4YHA.png)
    </br>
    
    **5.查看Apline容器2的網路環境**
    ```
    ip addr
    ```
    ![網卡20-2](https://hackmd.io/_uploads/H1sopVtrC.png)
    - 驗證 2 個 Alpine 容器共用了同一網段、IP 地址。
    :::
  </br>

## 5.自定義網路
- **說明：**
  - 為避免 Docker 容器的 IP 變更，因此創建自定義網路來提供容器在同一虛擬網橋接口上運行。
  - Docker 19.03 後的版本已不支援使用 `--link` 的作法。
- **比較『預設Bridge網路模式』、『自定義網路模式』運行多個容器**
    :::info
    :::spoiler **預設Bridge網路模式**
    </br>

    **1.使用預設Bridge網路模式運行2個Alpine容器**
    ![網卡21](https://hackmd.io/_uploads/rJALxttHA.png)
    </br>

    **2.分別進入使用預設Bridge網路模式的Alpine容器查看容器IP地址**
    ```
    ip addr
    ```
    ![網卡24](https://hackmd.io/_uploads/Syct7KKSC.png)
    ![網卡25](https://hackmd.io/_uploads/rJL5mYFBC.png)
    </br>

    **3.預設Bridge網路模式的2容器互PING IP地址、容器名稱/ID**
    ```
    ping 容器IP地址
    ```
    ```
    ping 容器名稱/ID
    ```
    ![網卡27](https://hackmd.io/_uploads/HyDErtKr0.png)
    ![網卡26](https://hackmd.io/_uploads/SJ7HrFYHC.png)
    </br>
    
    **4.總結**
    - 使用『預設 Bridge 網路模式』運行多個容器，容器間可以透過 IP 地址互相通信，但無法透過容器名稱互相通信。
    - 若某一容器的 IP 地址變更，則會造成容器間的通信問題。
    :::

    :::info
    :::spoiler **自定義網路模式**
    </br>
    
    **1.創建自定義網路**
    ```
    docker network create 自定義網路名稱
    ```
    ```
    docker network ls
    ```
    ![網卡22](https://hackmd.io/_uploads/SynVWYFSC.png)
    </br>
    
    **2.使用『自定義網路模式』運行2個Alpine容器**
    ![網卡23](https://hackmd.io/_uploads/Syr0ZKtSC.png)
    </br>
    
    **3.分別進入使用自定義網路模式的Alpine容器查看容器IP地址**
    ```
    ip addr
    ```
    ![網卡28](https://hackmd.io/_uploads/rJtYDFFrA.png)
    ![網卡29](https://hackmd.io/_uploads/rkZ5PKKBA.png)
    </br>

    **4.自定義網路模式的2容器互PING IP地址、容器名稱/ID**
    ```
    ping 容器IP地址
    ```
    ```
    ping 容器名稱/ID
    ```
    ![網卡30](https://hackmd.io/_uploads/HJpF_FYHC.png)
    ![網卡31](https://hackmd.io/_uploads/BJdquYFB0.png)
    </br>
    
    **5.總結**
    使用『自定義網路模式』運行多個容器，容器間可以透過 IP 地址、容器名稱互相通信，因此即便容器的 IP 地址有所變更，也不影響容器間的通信。
    :::

---

# 十二、Compose容器編排
## 1.什麼是Docker Compose
- 由 Docker 官方推出應用程式工具，目的是為了管理多個 Docker 容器來組成一個應用程式／專案項目（Poroject）。
- 需要一個 YAML 格式文件（`docker-compose.yml`）做為 Compose 的配置文件，撰寫內容為多個容器間的調用關係。後續僅需透過運行 Compose 命令，便可同時啟動／關閉這些容器。
</br>

## 2.組成要素
- **文件**：`docker-compose.yml` 文件。
- **服務**：指多個應用程式容器，如 Mysql、Redis、Nginx 等。
- **專案項目**：關連多個服務容器組成的單位，並透過 `docker-compose.yml` 定義服務容器間的交互。
</br>

## 3.安裝Docker Compose
- [Docker Compose 官方安裝文檔](https://docs.docker.com/compose/install/)
- [CentOS 7 安裝 Docker-Compose 文檔](https://www.hostinger.com/tutorials/how-to-install-docker-compose-centos-7/)
</br>

## 4.Docker Compose使用3步驟
- 撰寫 Dockerfile 去定義各個容器內容，並建立鏡像。
- 撰寫 docker-compose.yml 去定義各個容器間的調用關係，安排好完整專案項目。
- 執行 `Docker-compose up` 指令來啟動整個應用程式，完成布署上線。
</br>

## 5.Docker Compose常用指令
  - **查看docker-compose幫助說明**
    ```
    docker-compose -h
    ```

  - **啟動所有docker-compose內所有容器**
    ```
    docker-compose up
    ```

  - **啟動所有docker-compose內所有容器，並在後台運行**
    ```
    docker-compose up -d
    ```

  - **停止並刪除容器、網路、映射卷、鏡像**
    ```
    docker-compose down
    ```

  - **進入 `docker-compose.yml` 文件中的容器內**
    ```
    docker-compose exec 容器名稱/ID /bin/bash
    ```

  - **展示 `docker-compose.yml` 文件中運行的所有容器**
    ```
    docker-compose ps
    ```

  - **展示 `docker-compose.yml` 文件中所有容器的進程**
    ```
    docker-compose top
    ```

  - **查看容器輸出的日誌內容**
    ```
    docker-compose logs 容器名稱/ID
    ```

  - **檢查docker-compose.yml配置**
    ```
    docker-compose config
    ```

  - **檢查配置，有錯誤才有輸出內容**
    ```
    docker-compose config -q
    ```

  - **重啟專案應用程式**
    ```
    docker-compose restart
    ```

  - **啟動專案應用程式**
    ```
    docker-compose start
    ```

  - **停止專案應用程式**
    ```
    docker-compose stop
    ```
</br>

## 6.實作範例
  :::info
  :::spoiler **透過Docker-Compose運行專案容器**
  </br>

  **1.docker-compose.yml撰寫範例**
  ```
  vim docker-compose.yml
  ```
  ```
  version:"3"
    
  services:
    microService:
      image: 專案應用程式的鏡像名稱
      container_name: 專案應用程式名稱
      port:
        - "本地宿主機Port號:專案應用程式Port號"
      volumes:
        - "本地宿主機檔案路徑:專案應用程式檔案路徑"
      networks:
        - docker自定義網路模式名稱
      depends_on:
        - redis  ##包含容器1
        - mysql  ##包含容器2

  redis:
    images: redis鏡像名稱
    ports:
      - "本地宿主機Port號:redis容器Port號"
    volumes:
        - /app/redis/redis.conf:/etc/redis/redis.conf
          ##本地宿主機redis設定檔的檔案路徑:redis容器設定檔的檔案路徑
        - /app/redis/data:/data
          ##本地宿主機redis日誌檔的檔案路徑:redis容器日誌檔的檔案路徑
    networks:
        - docker自定義網路模式名稱
    command: redis-server /etc/redis/redis.conf

  mysql:
    images: mysql鏡像名稱
    environment:  ##Mysql資料庫的環境參數設定
      MYSQL_ROOT_PASSWORD: '123456'
      MYSQL_ALLOW_EMPTY_PASSWORD: 'no'
      MYSQL_DATABASE: 'db2011'
      MYSQL_USER: 'bruce'
      MYSQL_PASSWORD: 'bruce123456'        
    ports:
      - "本地宿主機Port號:mysql容器Port號"
    volumes:
        - /app/mysql/db:/var/lib/mysql
          ##本地宿主機mysql資料的檔案路徑:mysql容器資料的檔案路徑
        - /app/mysql/conf/my.cnf:/etc/my.cnf
          ##本地宿主機mysql日誌檔的檔案路徑:mysql容器日誌檔的檔案路徑
        - /app/mysql/init:/docker-entrypoint-initdb.d
          ##本地宿主機連接文件的檔案路徑:mysql容器連接文件的檔案路徑
    networks:
        - docker自定義網路模式名稱
    command: --default-authentication-plugin=mysql_native_password
    ##解決外部無法訪問

  network:
  docker自定義網路模式名稱
  ```
  </br>

  **2.打包專案應用程式的鏡像**
  - 將應用程式那包丟到與 `docker-compose.yml` 相同路徑下。
  - 編寫應用程式打包成 `.jar` 的 Dockerfile。
  - 創建應用程式鏡像（docker build）。
  </br>

  **3.確認docker-compose.yaml文件內容是否正確**
  - 必須在 `docker-compose.yaml` 檔案路徑下執行指令。
    ```
    docker-compose config -q
    ```
  </br>

  **4.docker-compose運行專案應用程式容器**
  - 必須在 `docker-compose.yaml` 檔案路徑下執行指令。
    ```
    docker-compose up -d
    ```
  </br>

  **5.確認專案應用程式容器的相關資源是否成功創建**
  - 檢查容器使用的自定義網路。
    ```
    docker network ls
    ```
  - 檢查建立的應用程式鏡像。
    ```
    docker ps
    ```
  </br>

  **6.進入MySQL容器，在目標資料庫創建關連的Table表**
  ```
  CREATE TABLE '表名稱' ...
  ```
  </br>

  **7.確認專案應用程式運行狀況**
  - 瀏覽器訪問應用程式。
  - 在應用程式插入資料。
  - 在應用程式讀取資料。
  :::
</br>

  :::info
  :::spoiler **透過Docker-Compose部屬LAMP靜態網站**
  </br>

  **1.建立專案目錄**
  ```
  mkdir -p 專案名稱 && cd 專案名稱
  ```
  ```
  mkdir -p app && cd app
  ```

  **2.docker-compose.yml撰寫範例**
  ```
  vim docker-compose.yml
  ```
  ```
  version: '3'

  services:
    phpapache:
      build: ./php
      ports:
        - "80:80"
      depends_on:
        - mysql
      volumes:
        - ./php:/var/www/html

    mysql:
      image: mysql
      environment:
        MYSQL_ROOT_PASSWORD: 'root使用者密碼'
        MYSQL_ALLOW_EMPTY_PASSWORD: 'no'
        MYSQL_DATABASE: '資料庫名稱'
        MYSQL_USER: '使用者名稱'
        MYSQL_PASSWORD: '使用者的密碼'
      ports:
        - "3306:3306"
      volumes:
        - ./mysql/db:/var/lib/mysql
        # 資料路徑
        - ./mysql/conf/my.cnf:/etc/my.cnf
        # 設定檔路徑
        - ./mysql/init:/docker-entrypoint-initdb.d
  ```
  </br>

  **3.在『./專案名稱/app』路徑下建立mysql、php相關目錄**
  ```
  mkdir -p ./php
  mkdir -p ./mysql/db
  mkdir -p ./mysql/conf
  mkdir -p ./mysql/init
  touch ./mysql/conf/my.cnf
  ```
  </br>

  **4.在『./專案名稱/app/php』目錄下，建立php dockerfile文件**
  ```
  vim Dockerfile
  ```
  ```
  FROM php:7.4-apache

  # Install the pdo_mysql extension
  RUN docker-php-ext-install pdo pdo_mysql

  # Copy your application to the container
  COPY . /var/www/html/
  ```
  </br>

  **5.docker-compose運行容器**
  ```
  docker-compose up --build -d
  ```
  </br>

  **6.進入mysql容器插入資料**
    ```
    docker exec -it mysql容器ID /bin/bash
    ```
  - 進入mysql資料庫並建立資料表。
    ```
    mysql -u root -p
    ```
    ```
    use 資料庫名稱;
    ```
    ```
    CREATE TABLE 資料表名稱 (item_id INT AUTO_INCREMENT, content VARCHAR(255), PRIMARY KEY(item_id));
    ```
    ```
    INSERT INTO 資料表名稱(content) VALUES ("My first important item");
    ```
    ```
    SELECT * FROM 資料表名稱;
    ```
  - 退出資料庫
    ```
    exit;
    ```
  - 退出容器
    ```
    ctrl+p+q
    ```
  </br>

  **8.上傳圖片至『./專案名稱/app/php/image/』目錄路徑**
  - 在『./專案名稱/app/php』目錄建立圖片目錄
    ```
    mkdir -p image
    ```
  - 使用FTP軟體(FileZilla)，將本地圖片上傳至宿主主機的『./專案名稱/app/php/image/』目錄路徑
  </br>

  **9.在『./專案名稱/app/php』目錄下，建立index.php首頁文件**
  - 建立index.php文件
    ```
    vim index.php 
    ```
  - 編輯index.php文件
    ```
    <html>
    <head>
      <title>Homepage</title>
    </head>
    <body>
      <h1>Welcome to Homepage!</h1>

      <!-- Insert a static image -->
      <img src ="image/Butler-ot.jpg" alt="Jimmy Butler" width="500">
      <img src ="image/X.jpg" alt="My Favorite Singer:XXXtentacion" width="500">

      <!-- Dynamic content: PHP code to display the todolist -->
      <?php
      $user = "root";
      $password = "Zhixian14512!";
      $database = "bruce";
      $table = "todolist";

      try {
          $db = new PDO("mysql:host=mysql;dbname=$database", $user, $password);
          echo "<h2>Bruce's important things</h2><ol>";
          foreach($db->query("SELECT content FROM $table") as $row)
          {
          echo "<li>" . $row['content'] . "</li>";
          }
          echo "</ol>";
          }
          catch (PDOException $e)
          {
          print "Error!: " . $e->getMessage() . "<br/>";
          die();
          }
      ?>

      <footer>
        <h2>Thanks for visiting!</h2>
      </footer>

    </body>
    </html>
    ```
  </br>

  **10.透過docker compose重新佈署專案容器**
  - docker compose停止專案容器
  ```
  docker compose down
  ```
  - docker compose部屬專案容器
  ```
  docker compose up --build -d
  ```
  :::
</br>

---

# 十三、Portainer可視化工具
## 1.Portainer是什麼
一個輕量級應用程式，提供圖形化介面來管理Docker環境（包括單機環境、集群環境）。
</br>

## 2.使用Portainer
- **1.安裝Portainer**
  [Portainer 官方安裝文檔](https://docs.portainer.io/start/install/server/docker)

- **2.瀏覽器訪問**
  ```
  https://主機IP:9000
  ```
  或是
  ```
  https://主機IP:9443
  ```
  ![Portainer0](https://hackmd.io/_uploads/SyYlPU6HR.png)

- **3.設置Portainer**
  [Portainer 官方安裝文檔](https://docs.portainer.io/start/install/server/setup)
  ![Portainer2](https://hackmd.io/_uploads/r1VcYI6HA.png)

- **4.使用Portainer**
  [Portainer 官方安裝文檔](https://docs.portainer.io/user/home)

---