Docker tutorial -2
===

**📌 本次內容:**
- 端口映射(Port mapping)
- 建立 Dockerfile,打包自己的image
- Docker compose
- 實作一個本地的LLM(Ollama+Open WebUI)
---

## 端口映射(Port mapping)
在 Docker 中,容器內部運行的服務(例如 Web 伺服器、資料庫等)通常會綁定到**容器的內部端口**,但這個內部端口對外部世界是不可見的。
端口映射(Port mapping)是讓**容器內的端口**與**本機的端口**對接,這樣才能夠從主機或其他容器訪問容器內運行的服務。
:::info
端口映射(Port mapping)就像是把辦公室內線電話(容器端口)轉接到手機(本機端口),這樣外部才能撥打這支電話。
:::
### `-p` 參數
#### 語法格式
```
-p <host_port>:<container_port>
```
- `host_port`:宿主機上的端口
- `container_port`:容器內部的端口
#### 範例
1. **Web 伺服器運行**
假設要啟動一個運行 Web 伺服器的容器,並希望讓本機或其他設備能夠通過瀏覽器訪問它。
```
$ docker run -d -p 8080:80 my_web_image
```
- `-p 8080:80`:將宿主機的端口 `8080` 映射到容器的端口 `80`
- 可以通過 `http://localhost:8080` 來訪問容器內部的 Web 伺服器
2. **Web + Database 運行**
也可以在啟動容器時,進行**多端口映射**,這樣就能同時訪問容器內部的多個服務
```
$ docker run -d -p 8080:80 -p 3306:3306 my_database_web_image
```
- `8080` 端口訪問容器內部的 Web 伺服器
- `3306` 端口訪問容器內部的 MySQL 資料庫
### 端口數字
#### 容器內端口
1. 查看 image 官方說明
2. 自己創建 Dockerfile 時定義
#### 宿主機端口
可以自訂數字,但有幾個小規則:
1. `8080`, `8888`, `3000`, `5000` 等是開發常用的 port 👍🏻
2. 不要重複選到正在運行的 port
3. 避開常見系統服務的 port(如:`80`, `443`, `3306`...)
4. `1024` 以下的 port 會需要 sudo 權限
5. 可使用 `docker container ls` 檢查正在運行的容器端口映射情況
## Dockerfile
Dockerfile 是一個用 YAML 語法格式撰寫的純文字檔。裡面會透過多種「指令」來一步一步描述如何建置出一個 Image。
通常會創建在你本機中,專案的資料夾底下,如下結構圖。
```
myProject/
├── app.py
├── requirements.txt
└── Dockerfile
```
:::info
Docker 預設會找尋目錄中的 "Dockerfile" 來執行,所以這個檔案名稱是約定成俗的,通常不建議自行改名~
:::
### Dockerfile 常用創建指令
| 指令 | 說明 | 格式 |範例 |
|----------------|-------|-------|-------|
| `FROM` | 設定要使用哪個基底 Image,所有 Dockerfile 都要有這個指令。 | `FROM <image>`, `FROM <image>:<tag>` |`FROM python:3.10` |
| `WORKDIR` | 設定容器的工作目錄,後續的指令都會在這個目錄下執行。 | `WORKDIR <path>` |`WORKDIR /app` |
| `COPY` | 複製本地檔案到容器內。 | `COPY <source path> <dist path>`|`COPY requirements.txt .`(`.`的意思是當前目錄) |
| `RUN` | 在建構 Image 時執行的命令,常用來安裝軟體包或執行其他初始化操作。 | `RUN <command> && <command>`(一個`RUN` 就是開啟一個新的映像層,所以只會寫一個`RUN`,用`&&`來連接不同執行步驟)|`RUN apt-get update && apt-get install -y python3` |
| `CMD` | 設定容器啟動後執行的命令,通常只會有一個 `CMD` 指令。 | `CMD [“command name”, argv1, argv2, …]` |`CMD ["python", "app.py"]` |
| `EXPOSE` | 指定容器內(服務器)的端口。 | `EXPOSE <port>` |`EXPOSE 8080` |
| `LABEL` | 添加一些資訊,如:版本號、作者、描述等。 | `LABEL <key1>=<value1> <key2>=<value2> ` |`LABEL version="1.0" maintainer="Michael"` |
#### Dockerfile 範例
```
FROM python:3.10
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "app.py"]
```
### 創建 Image
```
docker build -t <name>:<tag> <Dockerfile path>
```
- `-t`, `--tag`:指定 Image 的名稱與版本標籤。
- `<name>`:看自己想要為這個 Image 取什麼名字。
- `<tag>`:預設是`:latest`,開發測試中可以用 `:dev` 或 `:test`,穩定版本可以用 `:v1.0` 或`:stable` 等等。
- `<Dockerfile path>`:Dockerfile所在的目錄。通常會先進到該專案資料夾中,這時可以用 `.` 表示當前工作目錄。
#### 實作
```
$ docker build -t 0509_demo:test .
$ docker run -d -p 7860:7860 0509_demo:test
# 瀏覽器:http://localhost:7860
```
### 推上 Docker Hub
```
$ docker login
# 格式
$ docker tag <local-image-name> <dockerhub-username>/<image-name>:<tag>
$ docker tag 0509_demo jack555/0509_demo:dev
# 格式
$ docker push <dockerhub-username>/<image-name>:<tag>
$ docker push jack555/0509_demo:dev
```
## Docker compose
Docker Compose 是一個工具,它可以用一個 YAML 檔案,通常叫做 docker-compose.yml,來定義和管理多個 Container 的服務。
例如下圖:在專案資料夾中建立 docker-compose.yml,即可用一行指令,根據 docker-compose.yml 去同時啟動 Web 伺服器 + 後端 API + 資料庫。
```
myProject/
├── docker-compose.yml
├── frontend/
│ ├── Dockerfile
│ └── ...
│
├── backend/
│ ├── Dockerfile
│ ├── requirements.txt
│ └── ...
│
└── db/
├── init.sql
└── ...
```
### docker-compose.yml 寫法
```
services:
服務名稱:
image: 映像檔名稱
container_name: 自定義容器名稱
ports:
- <主機 port>:<容器 port>
volumes:
- <主機 path>:<容器 path>
environment:
- 環境變數=值
depends_on:
- 其他服務名稱
networks:
- 自定義網路名稱
restart: always / unless-stopped / no / on-failure
command: 覆蓋預設執行指令
volumes:
volume名稱:
networks:
網路名稱:
```
### 實作:Ollama + Open WebUI
```
services:
ollama: # 服務 1
image: ollama/ollama
container_name: ollama
ports:
- "11434:11434" # Ollama API port
volumes:
- ollama-data:/root/.ollama # 儲存模型資料
restart: unless-stopped
open-webui: # 服務 2
image: ghcr.io/open-webui/open-webui:main
container_name: open-webui
ports:
- "3000:8080" # 網頁前端 http://localhost:3000
environment:
- OLLAMA_API_BASE_URL=http://ollama:11434
depends_on:
- ollama
restart: unless-stopped
volumes:
- openwebui-data:/app/backend/data # 儲存 WebUI 設定資料
volumes:
ollama-data:
openwebui-data:
```
#### 運行
根據 docker-compose.yml 啟動所有服務。
```
$ docker compose up -d # -d 不佔用終端機的方式在背景運行
# 瀏覽器:http://localhost:3000
```
#### 關閉
停掉所有服務,並刪除 Container(但不會刪掉 Image 和 Volume)。
```
$ docker compose down
```
暫時停掉,但不刪除 Container。
```
$ docker compose stop
```
#### 重新啟動
啟動已存在的 Container,不會重新建構。
```
$ docker compose start
```
#### 選擇模型
```
$ docker exec -it ollama ollama pull <LLM>:<tag>
$ docker exec -it ollama ollama pull gemma3:1b
```
[官方 Ollama 模型 library](https://ollama.com/library):去選擇你想要跑什麼模型
## References
1. [Docker 進階實戰:Volume、Port Mapping 與 Docker Compose 完整指南](https://realnewbie.com/basic-concent/architecture/docker-advanced-hands-on-guide-volume-port-mapping-and-docker-compose/)
2. [Docker 學習筆記 (四) — 如何撰寫Dockerfile](https://medium.com/%E4%B8%80%E5%80%8B%E5%B0%8F%E5%B0%8F%E5%B7%A5%E7%A8%8B%E5%B8%AB%E7%9A%84%E9%9A%A8%E6%89%8B%E7%AD%86%E8%A8%98/docker-%E5%AD%B8%E7%BF%92%E7%AD%86%E8%A8%98-%E5%9B%9B-%E5%A6%82%E4%BD%95%E6%92%B0%E5%AF%ABdockerfile-2a209b485530)
3. [使用 Docker Compose 摻在一起做懶人包 ](https://ithelp.ithome.com.tw/articles/10243618)