# Container 概念與實作
## container 基礎概念
### 什麼是 container ?
容器 (container) 是一項能將所有檔案打包,並提供獨立的運行環境來執行所有檔案的虛擬化技術。這使應用程式在環境(開發、測試、生產等)之間移動變得容易,且同時保留完整功能。
### VM vs Container

> 個人開發環境的虛擬機架構會是下列這種
> 
>- Bins:通常指二進制文件(Binaries)的存放目錄,這些文件是可執行程式或命令行工具。
>- Lib:Library的簡稱,包含了一些常用的功能函數。
>- Container Engine:根據映像建立容器的軟體程式。如 Docker、Podman
### container 的優點
#### 1.輕量化
- 資源利用效率高
>所有 container 皆共用主機的 OS kernel,而不是像虛擬機(VM)那樣,每一個都運行一個完整的OS。這使得 container 更加輕量,佔用的系統資源更少。
- 啟動速度快
>由於容器不需要啟動一個完整的 OS,它們的啟動時間比虛擬機要快得多,通常可以在幾秒鐘內啟動。
#### 2.可攜性
- 跨平台一致性
>容器打包了應用程式及其所須的依賴項,使得應用程式可以在不同的環境中運行。這樣可以確保在開發機器上運行良好的應用程式在部屬環境中也能正常運行。
- 易於移轉
>容器映像檔(image)可以藉由container engine,輕鬆地從一個環境移轉到另一個環境。
#### 3.隔離性
- 環境隔離
>每個container都運行在自己的獨立環境中,這確保了不同容器中的應用程式互不干擾。
## Docker 簡介
- Docker是一種虛擬化技術,能將應用程式連同其執行環境一起打包成映像檔( image ),使得在部屬時無需再花費時間處理環境設定的問題。只要機器上安裝了Docker,就能執行任何映像檔。

- 映像檔( image )是一組打包好的檔案,概念上類似程式中的類別(class),包含許多函數(functions)和變數(variables);而當映像檔被執行後,便會生成容器(container),這類似於程式中被實際執行的實例(instance)。

### Dockerfile
關於環境設定以及程式的運行方式,我們會建立一個名為Dockerfile的檔案(無需副檔名),通常放置於與程式碼相同的目錄下。Dockerfile的每一行指令都構成了一層映像層(image layer),這些層會疊加在基礎映像(base image)之上。其內容通常包括:
- 執行環境設定
- 執行目錄
- 需要安裝的套件等
詳細語法請見本文[Dockerfile指令](#Dockerfile-指令)
### Docker Hub
當我們建立了自己的映像檔後,可以將其上傳(push)到Docker Hub上,這是一個Docker Registry,使用方式類似於GitHub。我們可以將自己的映像檔上傳,也可以下載(pull)其他人的映像檔。平台上的映像檔分為官方映像和非官方映像。如果是官方映像,通常可以直接使用映像名稱來下載,例如:
`docker pull python`
若是非官方映像檔,則需要使用以下格式:
`docker pull docker.io/<username>/<image name>`
<!-- Docker是一種虛擬化技術,將應用程式連同環境一起build成image,部屬時就不用花時間處理環境的問題。只要機器有裝docker,就可以執行任何image。image是一堆打包好的檔案,概念上就像程式中的class包含許多functions跟variables,而當image被run之後,就會成為container,概念上就像是程式中被實際執行的instance。關於環境設定以及程式運行方式,會建立Dockerfile(不用副檔名),寫在跟程式同一目錄下,Dockerfile的每一列都是一層image layer,疊加在base image之上,內容會包含執行環境、執行目錄、需安裝的套件等。
當我們建立了自己的image後,可以將其push到[Docker Hub](https://hub.docker.com/explore/)上,這是個Docker Registry,使用上跟github類似,可以push自己的image跟pull別人的image,在這個平台上的image分成官方跟非官方,如果是官方通常可以直接打image名稱,例如`docker pull python`,如果是私人的則是`docker pull docker.io/<username>/<image name>` -->
### Docker 與 Podman 的差異
- Docker 和 Podman 都是用於容器管理的工具。
- Docker採用客戶端-伺服器架構,包括Docker Daemon和Docker CLI。Docker Daemon負責管理容器,並需要以root身份運行。
- Podman:是無Daemon的容器引擎,不需要持續運行的後台服務。Podman能以非root身份運行容器,提高安全性。它跟Docker相容,可以使用相同的Dockerfile來建立映像檔,也支援大部分Docker的命令。
- 
## Podman 教學
### 安裝Podman
這裡預設的作業系統為RHEL(Red Hat Enterprise Linux),所以本文將用`dnf`作為安裝所使用的套件。若使用的是Ubuntu,將`dnf`改為`apt`即可。
- 安裝 podman 指令
```
sudo dnf update #更新軟體包索引
sudo dnf upgrade #升級系統上已安裝的軟體包
sudo dnf install podman #安裝podman
```
- 查看 podman 版本
`podman --version`
> 出現此畫面即表示安裝成功

### 建立 image
- `podman build -t repo_name(:tag_name) .`
>`-t` 這個參數表示 tag , `.` 表示Dockerfile的路徑為預設。
>
> 例如 `podman build -t test:1.0 .` 表示Dockerfile在預設路徑,建立一叫做 名稱為 test 、標籤(tag)為 1.0 的 image。
>
>也可以選擇不要設定tag,那麼 tag 將被系統預設為 latest。
- 若出現以下畫面,表示忘記輸入 Dockerfile 路徑

### 建立 container
podman 將透過已經建立好的 image 來建構一個新的 container,並開始運行。
- `podman run {指令參數} {image ID / image_name:tag}`這段指令會為image建立新的container。
舉例:`podman run --name first -p 8080:3333 test:1.0`
> - `--name` 表示容器名稱
>- `-p 主機port:容器port` 表示指定端口映射
>- `run` 這個指令預設在退出 container 後,並不會刪除 container,而是會將其設定為退出狀態(Exited)。 假如希望運行結束時便刪除container,則須加上參數 `--rm`
>- `-d`是在背景執行container
### pull
pull 的指令:
- `podman pull repo_name`
pull Docker Hub 中的 image 指令:
- `podman pull docker.io/library/repo_name:tag`
>- 假如想要pull Docker Hub上的image,可以把Docker指令中的 `docker` 改為 `podman`。
>- 若出現以下畫面,表示那個image在podman中沒有對應的別名,只能打完整的路徑。
>
### push
>本文以 Docker Hub 作為使用範例
#### step 1
登入 Docker Hub 帳號
- `podman login docker.io`
- 成功畫面

- 帳號密碼輸入錯誤畫面

- 已登入後重複登錄

- 如果遇到 `x509: certificate signed by unknown authority`,就改成
`podman login --tls-verify=false docker.io`
- 若非以上幾種畫面,則表示有其他問題待解決
#### step 2
將已經建立好的 image 推送至 Docker Hub
- `podman push image:tag docker.io/username/repo_name:tag_name(自訂)
`
- 成功畫面:

- 如果遇到 `x509: certificate signed by unknown authority`,就改成`podman push --tls-verify=false docker.io/oliver0029/first_podman_app:latest`
##### 登出帳號
- `podman logout docker.io`
### 列出 image
可列出 image 的相關資訊(名稱、標籤、ID、創立時間、檔案大小)
- `podman images`

### 列出 container
- `podman ps` 會列出目前正在運行的container,並顯示其資訊。
- `podman ps -a` 會列出所有container(包含已退出、已停用的),並顯示其相關資訊。

- `podamn stats` 可以列出運行中的container所佔用的電腦資源。
按`Ctrl` + `z`可以結束此界面,回到原本的終端機。

### 清除 image
image 的清除指令如下:
- `podam rmi {image id/name}`
>* 假如失敗的話,可以加上參數 `-f` 表示強制(force)
>`podman rmi -f {image id/name}`
>
>* 假如想要刪掉全部的話,可加參數 `-a` 表示全部(all)
>`podman rmi -a {image id/name}`
>
>* 假如想要強制刪掉全部的話,則加上`-a` 、`-f`
>`podman rmi -a -f {image id/name}`
### 清除 container
透過`podman ps -a`查詢Container ID後即可透過指令刪除
- `podman rm {container ID}`
### 停止 Container 運行
可以透過以下指令停用運行中的container,而停用(stopping)達一定時長後,會自動變為退出(Exited)。
- `podman container stop {container ID}`
### 啟動 Container
啟動一個現有的container
- `podman container start {container ID}`
> 跟 `run` 的差異在於, `start` 並不會產生新的container,它只能啟動已退出(Exited)或是暫停中(Stopping)的 container。
### 重啟 Container
將運行中的 container 強制停止停止並重新啟動,或是將非運行中的 container 啟動。
- `podman container restart {container ID}`
### 進入容器
我們可以透過下述指令進入容器內部,本文將以Ubuntu為例。
#### step1
- 下載建立Ubuntu環境container所需的image
`podman pull ubuntu`
#### step2
- 建立container,並開啟container環境的終端
`podman run -it ubuntu`

- 若要離開並關閉container,可在終端打`exit`或按 `Ctrl`+ `d`
- 若要離開但不關閉container,可直接關閉本機的終端機
#### 離開後再進入
- 如果想進入正在運行的container,可以使用下列指令
`podman exec -it {name/id} /bin/bash `
> `-it`:交互模式並分配一個偽終端(pseudo-TTY)。
> `/bin/bash`:要在容器內部執行的命令,這裡是啟動 Bash shell。
#### container所佔資源

### 錯誤排除
透過 Dockerfile 執行 image 時可能會遇到憑證(Certificate)問題,導致無法成功運行。關於 SSL Certificate Error,可以嘗試將 Dockerfile 中下載檔案的指令改為以下:
- 將 `podman pull <package name>`
改為`podman pull --tls-verify=false <package name>`
- 將`pip install <package name>`
改為`pip install --trusted-host pypi.org --trusted-host files.pythonhosted.org <package name>`
透過以上指令,可以避開缺乏憑證的問題。
>上述指令中的 tls 是指 transport layer security,是種用來確保網路溝通隱私權的協定。trusted host 則是告訴 proxy (代理伺服器) 跳過對指定套件的 SSL 憑證驗證。
## Container 實作
### "Hello World!" 映像檔實作
[Demo 影片連結: ](https://youtu.be/4Nlz-iM8tyc)
{%youtube 4Nlz-iM8tyc %}
本次實作內容為一個會印出 "Hello World!" 字樣的基本映像檔,映像檔中包含Dockerfile跟python檔案。
影片中依序會建置映像檔、執行容器、標記後push到docker.io平台;並且也實作pull他人的映像檔、執行pull下來的映像檔、客戶端發送請求給伺服器端並獲得"HTTP 200 OK"回應等內容。
### Dockerfile 指令
- `FROM` 指定基礎映像檔。
範例:`FROM python:3`
說明:所有 Dockerfile 必須以 `FROM` 指令開始,指定要使用的基礎映像檔。
- `ADD` 從構建環境複製文件到映像檔中,可以自動處理 URL 和解壓縮 tar 文件。
範例:`ADD example.tar.gz /app`
說明:除了複製文件外,還能下載 URL 或解壓縮 tar 文件到指定路徑。像是範例中的指定目標路徑是根目錄底下的app資料夾。
> 源路徑:可以是相對路徑、絕對路徑、URL。
> 目標路徑:映像檔建立在容器內的路徑。
> 舉例:ADD file.tar.gz /app
> 如果源路徑是 tar.gz 文件,Docker 會自動解壓縮到目標路徑 /app。
> 如果源路徑是 URL,Docker 會自動下載並儲存到目標路徑 /app。
- `RUN` 在映像檔建構時執行命令。
範例:`RUN apt-get update && apt-get install -y python3`
說明:用來執行安裝軟體包或其他需要在image建構階段完成的操作。每執行一次 RUN 指令,會創建一層新的映像層。
- `CMD`
指定容器啟動時執行的命令。
範例:CMD ["python3", "./helloworld.py"]
說明:用來設置容器啟動時要運行的默認命令。如果 Dockerfile 中有多個 CMD 指令,只有最後一個會被執行。
- `EXPOSE`
聲明容器在特定端口上提供服務。
範例:`EXPOSE 8080`
說明:告訴 Docker 容器啟動後會在哪些端口提供服務,但不會自動映射這些端口到主機。
### 進入 container
#### 查看OS版本
這邊以python:3的image為例,在啟動container之後,進入bash可以看見它的整個安裝包有包含哪些資料,進入etc資料夾內,開啟'os-release'的檔案,可以看到這個Python image所使用的OS系統是Debian GNU/Linux第12版。


### Image 輕量化
1. 使用特定版本的基礎映像:使用輕量版本的基礎映像。
範例:以`python-slim`或`python-alpine`取代`python`
1. 清理暫時文件:在 RUN 指令中清理安裝過程中產生的暫時文件。
範例:`apt-get clean` 和 `rm -rf /var/lib/apt/lists/*`
1. 合併 `RUN` 指令:將多個 `RUN` 指令合併成一個,減少映像層的數量。
範例:將原本二行指令
```
RUN pip install flask
RUN pip install flask_restful
```
改為
```
RUN pip install flask && \ pip install flask_restful
```
3. 最小化安裝的軟體包:只安裝必需的軟體包,避免不必要的依賴。
範例:`RUN apt-get update && apt-get install -y --no-install-recommends python3`
說明:使用 `--no-install-recommends` 選項來避免安裝推薦的軟體包。
## 建立 local registry
由於 docker.io 限制了免費版帳號 private 的 image 總量,因此為了存放機密的 images,我們在本地端建立 local registry,便可以將建置好的images推送到此存放。
在 windows 建立本地端registry的步驟與指令如下:
1. 安裝 podman 並開啟 wsl
2. 使用以下指令創建 local registry
``` bash
mkdir -p ~/local_registry/data
podman run -d \
-p 5000:5000 \
--name registry \
-v ~/local_registry/data:/var/lib/registry \
registry:2
```
3. 建立後就可以嘗試拉取image並於標記後推送映像到local registry,此例以拉取alpine呈現
``` bash
podman pull docker.io/library/alpine
podman tag docker.io/library/alpine localhost:5000/my-alpine
podman push localhost:5000/my-alpine
```
4. 要執行映像時,即可從本地註冊表拉取並執行映像:
``` bash
podman pull localhost:5000/my-alpine
podman run -it localhost:5000/my-alpine sh
```
## Reference
https://hackmd.io/@YuiciANnRnGAAeNEzFkXqg/r1nh8hMtn#Podman-Registry