# Docker 教學 第 4 堂：Dockerfile、映像檔優化與資源配置


&gt; 本系列為 18 小時 Docker 基礎教學講義，適合初學者，使用 Windows 電腦。
&gt; 本篇為第 4 堂課，約 3 小時。

&lt;!--more--&gt;

## 4-1 撰寫 Dockerfile 與映像檔優化（約 1.5 小時）

### 為什麼需要自製映像檔？

到目前為止，我們都是用別人做好的映像檔。但在實際工作中，你需要把自己的程式打包成映像檔。

回想一下之前 Flask 的例子，我們的流程是：
1. 啟動 Python 容器
2. 手動安裝 Flask
3. 手動啟動程式

每次都要重複這些步驟很麻煩。如果能把這些步驟「寫下來」，讓 Docker 自動幫你做呢？

這就是 **Dockerfile** 的用途——它是一個文字檔，包含一系列指令，用來建立自己的映像檔（Image），確保應用能夠在不同環境中一致運行。

&gt; **注意**：Dockerfile 的預設檔名就是 `Dockerfile`，**沒有副檔名**。這樣 `docker build` 指令會自動找到它，不需要額外指定檔名。建立時請確認你的編輯器沒有自動加上 `.txt` 之類的副檔名。

### Dockerfile、Image、Container 的關係

用遊戲光碟來比喻的話：

```
Dockerfile（程式碼）   →  Image（遊戲光碟）   →  Container（遊戲機）
    撰寫程式碼              docker build            docker run
                           燒成光碟片               放進遊戲機跑起來
```

- **Dockerfile** 就像遊戲的原始程式碼，記錄了要怎麼打包
- **Image** 就像燒好的光碟片，可以複製很多份、分送給別人
- **Container** 就像遊戲機，把光碟放進去就能跑，而且可以同時開好幾台遊戲機跑同一張光碟

簡單來說：Dockerfile 是食譜、Image 是做好的料理包、Container 是加熱上桌的那盤菜。

### Dockerfile 基礎語法

```dockerfile
# 每個 Dockerfile 都從 FROM 開始，指定基礎映像檔
FROM python:3.11-slim

# 設定工作目錄
WORKDIR /app

# 複製檔案到映像檔裡
COPY requirements.txt .

# 執行指令（安裝套件）
RUN pip install --no-cache-dir -r requirements.txt

# 複製程式碼
COPY . .

# 宣告容器要使用的端口
EXPOSE 5000

# 容器啟動時要執行的指令
CMD [&#34;python&#34;, &#34;app.py&#34;]
```

**逐行解析：**

| 指令 | 用途 | 比喻 |
|------|------|------|
| `FROM` | 基礎映像檔 | 你的起點，像是選擇一棟毛胚屋 |
| `WORKDIR` | 工作目錄 | 決定在哪個房間工作 |
| `COPY` | 複製檔案 | 把你的東西搬進去 |
| `RUN` | 執行指令 | 裝修房子（安裝、設定） |
| `EXPOSE` | 宣告端口 | 開一扇門讓外面的人可以進來 |
| `CMD` | 啟動指令 | 入住後的第一件事 |

### 實作：建構 Flask 映像檔

**步驟 1：確認專案結構**
```
C:\docker-lab\flask-app\
  ├── app.py
  ├── requirements.txt
  └── Dockerfile          ← 新建這個檔案
```

**步驟 2：建立 Dockerfile**

在 `C:\docker-lab\flask-app\` 目錄下建立 `Dockerfile`（沒有副檔名）：

```dockerfile
# 使用 Python 3.11 精簡版作為基礎
FROM python:3.11-slim

# 設定工作目錄
WORKDIR /app

# 先複製 requirements.txt 並安裝套件
# （這樣做可以利用 Docker 的快取機制，後面會解釋）
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 再複製其餘程式碼
COPY . .

# 宣告使用 5000 port
EXPOSE 5000

# 容器啟動時執行 Flask
CMD [&#34;python&#34;, &#34;app.py&#34;]
```

**步驟 3：建構映像檔**
```powershell
cd C:\docker-lab\flask-app
docker build -t my-flask-app .
```

**參數說明：**
- `-t my-flask-app`：給映像檔取名字（tag）
- `.`：指定 Dockerfile 所在的目錄（Build Context）

觀察建構過程，你會看到每一行 Dockerfile 指令都對應一個「Step」。

**步驟 4：啟動容器**
```powershell
docker run -d --name flask-web -p 5000:5000 my-flask-app
```

打開瀏覽器 `http://localhost:5000`，看到 Hello Docker！

**比較一下：**
- 之前的方式：`docker run` &#43; 手動安裝 &#43; 手動啟動（每次都要重複）
- 現在的方式：`docker build` 一次，之後只需要 `docker run` 就搞定

### 理解 Build Cache

第二次執行 `docker build` 時，你會發現速度快很多，因為 Docker 有快取機制。

**快取規則：**
- Docker 會逐層檢查，如果某一層的指令和上次一模一樣，就直接用快取
- 一旦某一層的快取失效，它之後的所有層都會重新建構

**這就是為什麼要先 COPY requirements.txt 再 COPY . .**

```dockerfile
# 好的做法 ✓
COPY requirements.txt .        # 這層只要 requirements.txt 沒變就用快取
RUN pip install -r requirements.txt  # 套件不變就不用重裝
COPY . .                       # 程式碼改了只有這層會重建

# 不好的做法 ✗
COPY . .                       # 任何檔案一改，這層就失效
RUN pip install -r requirements.txt  # 導致每次都要重裝套件
```

### 更多 Dockerfile 指令

**ENV：設定環境變數**
```dockerfile
ENV FLASK_ENV=production
ENV APP_PORT=5000
```

**ARG：建構時的參數**
```dockerfile
ARG PYTHON_VERSION=3.11
FROM python:${PYTHON_VERSION}-slim
```

使用方式：
```powershell
docker build --build-arg PYTHON_VERSION=3.12 -t my-app .
```

**LABEL：加上標籤資訊**
```dockerfile
LABEL maintainer=&#34;your-email@example.com&#34;
LABEL version=&#34;1.0&#34;
LABEL description=&#34;My Flask web application&#34;
```

**RUN 的兩種寫法：**
```dockerfile
# Shell 形式（會經過 shell 處理）
RUN pip install flask

# Exec 形式（直接執行）
RUN [&#34;pip&#34;, &#34;install&#34;, &#34;flask&#34;]
```

**CMD vs ENTRYPOINT：**
```dockerfile
# CMD：可以被 docker run 的參數覆蓋
CMD [&#34;python&#34;, &#34;app.py&#34;]

# ENTRYPOINT：不會被覆蓋，而是把 docker run 的參數附加在後面
ENTRYPOINT [&#34;python&#34;]
CMD [&#34;app.py&#34;]    # 這是預設參數，可以被覆蓋
```

```powershell
# 使用 CMD 的情況
docker run my-app                  # 執行 python app.py
docker run my-app python test.py   # 執行 python test.py（CMD 被覆蓋）

# 使用 ENTRYPOINT 的情況
docker run my-app                  # 執行 python app.py
docker run my-app test.py          # 執行 python test.py（app.py 被覆蓋）
```

### 理解映像檔的分層結構

在學優化技巧之前，先理解一個關鍵概念：**Dockerfile 裡的每一行指令都會產生一層（Layer）**。

```dockerfile
FROM python:3.11-slim    # 第 1 層：基礎映像檔
WORKDIR /app             # 第 2 層：建立工作目錄
COPY requirements.txt .  # 第 3 層：複製檔案
RUN pip install -r requirements.txt  # 第 4 層：安裝套件
COPY . .                 # 第 5 層：複製程式碼
CMD [&#34;python&#34;, &#34;app.py&#34;] # 第 6 層：設定啟動指令
```

就像千層蛋糕一樣，一層一層疊上去：

```
┌─────────────────────────┐
│ CMD [&#34;python&#34;, &#34;app.py&#34;]│  ← 第 6 層
├─────────────────────────┤
│ COPY . .                │  ← 第 5 層
├─────────────────────────┤
│ RUN pip install ...     │  ← 第 4 層（這層通常最大）
├─────────────────────────┤
│ COPY requirements.txt . │  ← 第 3 層
├─────────────────────────┤
│ WORKDIR /app            │  ← 第 2 層
├─────────────────────────┤
│ python:3.11-slim        │  ← 第 1 層（基礎）
└─────────────────────────┘
```

**為什麼這很重要？**
- 每一層都會佔用空間，層數越多映像檔可能越大
- Docker 有快取機制，只有改變的那一層和它之後的層才會重建
- 不必要的層會讓 build 變慢、image 變大

你可以用 `docker history` 來查看映像檔的每一層：

```powershell
docker history my-flask-app
```

### 實作：親眼看到分層的差異

讓我們建立兩個 Dockerfile，一個層數多、一個層數少，比較差異。

**步驟 1：建立測試專案**

```powershell
mkdir C:\docker-lab\layer-test
cd C:\docker-lab\layer-test
```

建立一個簡單的 `app.py`：
```python
print(&#34;Hello from layer test!&#34;)
```

**步驟 2：建立「層數多」的 Dockerfile**

建立 `Dockerfile.many`：
```dockerfile
FROM python:3.11-slim
WORKDIR /app
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y wget
RUN rm -rf /var/lib/apt/lists/*
COPY app.py .
CMD [&#34;python&#34;, &#34;app.py&#34;]
```

注意：4 個 `RUN` 就是 4 層。

**步驟 3：建立「層數少」的 Dockerfile**

建立 `Dockerfile.few`：
```dockerfile
FROM python:3.11-slim
WORKDIR /app
RUN apt-get update &amp;&amp; \
    apt-get install -y curl wget &amp;&amp; \
    rm -rf /var/lib/apt/lists/*
COPY app.py .
CMD [&#34;python&#34;, &#34;app.py&#34;]
```

用 `&amp;&amp;` 把 4 行合併成 1 行，只有 1 層。

**步驟 4：分別 build 並比較**

```powershell
# build 層數多的版本
docker build -f Dockerfile.many -t layer-test:many .

# build 層數少的版本
docker build -f Dockerfile.few -t layer-test:few .

# 比較大小
docker images | findstr layer-test
```

你會發現 `layer-test:few` 的大小比 `layer-test:many` 小，因為合併 RUN 之後，中間產生的暫存檔（apt 快取）在同一層就被刪掉了，不會殘留在前面的層裡。

**步驟 5：用 docker history 看每一層**

```powershell
# 層數多的版本
docker history layer-test:many

# 層數少的版本
docker history layer-test:few
```

比較兩個輸出，你會明顯看到 `many` 版本多出好幾層。

**步驟 6：清理**

```powershell
docker rmi layer-test:many layer-test:few
```

&gt; **重點**：每一行 `RUN` 就是一層。如果這些指令是相關的（例如安裝套件 → 清理快取），就用 `&amp;&amp;` 合併成一行，減少不必要的層。但也不用走極端把所有指令都塞成一行——保持可讀性也很重要。

### 映像檔優化技巧

**技巧 1：選擇適合的基礎映像檔**

| 映像檔 | 大小 | 適合場景 |
|--------|------|---------|
| `python:3.11` | ~1 GB | 需要完整開發工具時 |
| `python:3.11-slim` | ~155 MB | 大多數情況下推薦 |
| `python:3.11-alpine` | ~50 MB | 追求極小體積（但可能有相容問題） |

**技巧 2：合併 RUN 指令，減少層數**（剛才已經實作過了）
```dockerfile
# 不好 ✗（三層，中間的暫存檔殘留在前面的層裡）
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*

# 好 ✓（一層，暫存檔在同一層就被清掉了）
RUN apt-get update &amp;&amp; \
    apt-get install -y curl &amp;&amp; \
    rm -rf /var/lib/apt/lists/*
```

**技巧 3：善用 .dockerignore（下一節會詳細講）**

**技巧 4：使用非 root 使用者**
```dockerfile
# 建立一個非 root 使用者
RUN useradd -m appuser
USER appuser
```

### 實作：優化我們的 Flask 映像檔

改良版 Dockerfile：
```dockerfile
FROM python:3.11-slim

# 加上標籤
LABEL maintainer=&#34;student@example.com&#34;
LABEL version=&#34;1.0&#34;

# 設定環境變數
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

WORKDIR /app

# 安裝依賴
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 建立非 root 使用者
RUN useradd -m appuser &amp;&amp; chown -R appuser:appuser /app
USER appuser

# 複製程式碼
COPY --chown=appuser:appuser . .

EXPOSE 5000

CMD [&#34;python&#34;, &#34;app.py&#34;]
```

```powershell
# 建構並比較大小
docker build -t my-flask-app:v2 .
docker images | findstr my-flask
```

### 使用 .dockerignore 排除不必要檔案

#### 什麼是 Build Context？

當你執行 `docker build .` 時，Docker 會把 `.` 目錄下的所有檔案打包送給 Docker Engine。這個過程叫做「傳送 Build Context」。

如果你的專案目錄裡有很多不需要的檔案（例如 `.git`、`node_modules`、測試資料），它們都會被一起送過去，浪費時間和空間。

`.dockerignore` 就是映像檔的「**減肥清單**」——把不需要進 image 的東西列出來，讓映像檔瘦下來。

#### 建立 .dockerignore

在專案根目錄建立 `.dockerignore` 檔案（跟 Dockerfile 放在同一層）：

```
# 版本控制
.git
.gitignore

# Python
__pycache__
*.pyc
*.pyo
.pytest_cache
venv/
.env

# IDE
.vscode/
.idea/
*.swp

# Docker
Dockerfile
docker-compose.yml

# 其他
*.md
*.log
```

**規則跟 `.gitignore` 很像：**
- `#` 開頭是註解
- `*` 匹配任意字元
- `!` 開頭表示例外（不要排除）

&gt; 任何列出的檔案或資料夾在建構過程中都不會被包含，這樣可以幫助減少最終映像檔的大小。

#### 實作：比較有 vs 沒有 .dockerignore 的差異

**步驟 1：準備測試專案**

用之前的 Flask 專案 `C:\docker-lab\flask-app\`，先確認裡面有 `app.py`、`requirements.txt`、`Dockerfile`。

**步驟 2：製造一些「垃圾檔案」來模擬真實專案**

```powershell
cd C:\docker-lab\flask-app

# 模擬 Python 快取
mkdir __pycache__
echo &#34;cache&#34; &gt; __pycache__\test.pyc

# 模擬虛擬環境（通常幾百 MB）
mkdir venv
echo &#34;fake venv&#34; &gt; venv\fake.txt

# 模擬 IDE 設定
mkdir .vscode
echo &#34;{}&#34; &gt; .vscode\settings.json

# 模擬 git 目錄
mkdir .git
echo &#34;git data&#34; &gt; .git\config

# 模擬一個大檔案
fsutil file createnew bigfile.dat 10485760
```

**步驟 3：先不用 .dockerignore 建構**

確認目前沒有 `.dockerignore` 檔案，然後：

```powershell
docker build --no-cache -t flask-no-ignore .
```

注意 build 輸出第一行的 Context 大小，例如 `Sending build context to Docker daemon  10.5MB`。

**步驟 4：建立 .dockerignore**

在專案根目錄建立 `.dockerignore`：

```
.git
__pycache__
venv/
.vscode/
*.dat
*.pyc
*.md
*.log
Dockerfile
docker-compose.yml
```

**步驟 5：用 .dockerignore 重新建構**

```powershell
docker build --no-cache -t flask-with-ignore .
```

再看第一行的 Context 大小，應該小非常多！

**步驟 6：比較兩個映像檔**

```powershell
docker images | findstr flask
```

| | 沒有 .dockerignore | 有 .dockerignore |
|---|---|---|
| Build Context 大小 | 大（包含所有垃圾檔案） | 小（只有需要的檔案） |
| Build 速度 | 慢 | 快 |
| 映像檔大小 | 可能更大（垃圾檔案被 COPY 進去） | 更小更乾淨 |

**步驟 7：進容器確認垃圾檔案不在裡面**

```powershell
docker exec -it flask-with-ignore bash
```

```bash
ls
# 只會看到 app.py 和 requirements.txt
# 不會看到 __pycache__、venv、.vscode、bigfile.dat
exit
```

**步驟 8：清理**

```powershell
docker rmi flask-no-ignore flask-with-ignore
```

&gt; **重點**：`.dockerignore` 就是映像檔的減肥清單。養成好習慣——每個有 Dockerfile 的專案都應該要有 `.dockerignore`。
&gt;
&gt; **小技巧**：`docker build --no-cache` 可以強制不使用快取重新建構，適合在測試 `.dockerignore` 效果時使用。

### 測試映像檔

**啟動容器並測試：**
```powershell
# 啟動
docker run -d --name test-flask -p 5000:5000 my-flask-app:v3

# 查看 log
docker logs test-flask

# 查看容器詳情
docker inspect test-flask
```

打開瀏覽器 `http://localhost:5000` 確認網頁能正常顯示。

**進入容器，驗證 Dockerfile 的每個指令是否生效：**

```powershell
docker exec -it test-flask bash
```

進入容器後，一步步確認：

```bash
# 1. 確認 WORKDIR 是否正確（應該直接在 /app）
pwd
# 輸出：/app

# 2. 回上一層，看看容器根目錄的結構
cd ..
ls
# 你會看到跟一般 Linux 一樣的目錄結構：bin、etc、usr、app...

# 3. 確認 app 資料夾存在（這是 WORKDIR 建立的）
cd app
ls
# 輸出：app.py  requirements.txt
# 這些檔案就是透過 COPY . . 從本機複製進容器的

# 4. 查看 app.py 內容，確認程式碼在容器裡面
cat app.py

# 5. 確認 RUN pip install 有成功安裝套件
pip list
# 應該看到 flask 在清單裡

# 6. 確認 USER 設定（如果有的話）
whoami
# 如果 Dockerfile 有設定 USER appuser，這裡會顯示 appuser

# 7. 離開容器
exit
```

&gt; 透過這個驗證流程，你可以親眼確認 Dockerfile 裡的每個指令都做了什麼：
&gt; - `WORKDIR /app` → 進入容器時就在 `/app`
&gt; - `COPY . .` → 本機的檔案都複製到 `/app` 裡了
&gt; - `RUN pip install` → 套件都裝好了
&gt; - `USER appuser` → 不是用 root 在跑

### 映像檔的匯出與匯入

如果你要把映像檔帶到沒有網路的環境：

```powershell
# 匯出為 tar 檔案
docker save -o my-flask-app.tar my-flask-app:v3

# 在另一台電腦匯入
docker load -i my-flask-app.tar
```

也可以匯出/匯入容器：
```powershell
# 匯出容器的檔案系統
docker export -o container-backup.tar test-flask

# 匯入為新的映像檔
docker import container-backup.tar my-imported-image
```

**save/load vs export/import 的差別：**
- `save/load`：保留完整的映像檔層結構和 metadata
- `export/import`：只保留檔案系統，會變成單一層的映像檔

### 上傳映像檔到 Docker Hub

除了用 `save/load` 匯出檔案，你也可以把映像檔推上 Docker Hub，讓任何人都能 `docker pull` 下載使用。

**步驟 1：註冊 Docker Hub 帳號**

前往 hub.docker.com 註冊帳號（建議用 Google 登入最方便）。假設你的帳號是 `myname`。

**步驟 2：在終端登入 Docker Hub**

```powershell
docker login
```

輸入你的 Docker Hub 帳號和密碼。登入成功會顯示 `Login Succeeded`。

**步驟 3：幫映像檔加上正確的 tag**

Docker Hub 的命名規則是 `帳號名/映像檔名:版本`，所以要先重新 tag：

```powershell
# 格式：docker tag 本地映像檔 帳號名/映像檔名:版本
docker tag my-flask-app:v3 myname/my-flask-app:v3
docker tag my-flask-app:v3 myname/my-flask-app:latest
```

&gt; 通常會同時推一個具體版本（`v3`）和 `latest`，這樣別人 pull 的時候不指定版本就會拿到最新的。

**步驟 4：推上 Docker Hub**

```powershell
docker push myname/my-flask-app:v3
docker push myname/my-flask-app:latest
```

推送完成後，到 Docker Hub 的你的個人頁面就能看到這個映像檔了。

**步驟 5：別人就能下載使用了**

```powershell
# 任何人只要打這行就能用你的映像檔
docker pull myname/my-flask-app:v3
docker run -d -p 5000:5000 myname/my-flask-app:v3
```

**完整流程回顧：**

```
寫 Dockerfile → docker build（建構）→ docker tag（命名）→ docker push（上傳）
                                                              ↓
別人用的時候：                              docker pull（下載）→ docker run（啟動）
```

&gt; **注意**：推上 Docker Hub 的公開倉庫任何人都能看到和下載。如果映像檔裡有敏感資料（密碼、API Key 等），千萬不要推上去！敏感資料應該用環境變數 `-e` 在啟動時傳入，不要寫死在映像檔裡。

---

## 4-2 Docker Network 與多容器串接（約 0.5 小時）

### 為什麼要把服務拆到不同容器？

在進入多容器串接之前，先來看看：如果在同一台主機上跑多個服務，傳統部署和容器部署有什麼差異？

| 項目 | 傳統部署 | 容器部署 |
|------|---------|---------|
| **Port 衝突** | 四個服務都要聽 80 port → 需要反向代理或改 port | 每個容器內部都可用 80，對外透過 `-p 8081:80`、`-p 8082:80` 映射，互不衝突 |
| **資源競爭** | 容易互相吃掉記憶體/CPU，需要額外設定 | 可限制每個容器的 CPU、記憶體，避免互相拖垮 |
| **升級/回滾** | 要保留舊檔案，回滾流程繁瑣 | 透過舊版本 image tag，`docker run image:old` 即可快速回退 |

這三個問題在後面的單元都會實際操作到：Port 映射（unit2 已學過）、資源限制（unit4 會教）、image tag 版本管理（本單元已學過）。

接下來我們就來實際把 Flask 和 MySQL 拆成兩個獨立的容器，讓它們透過網路互相溝通。

### Docker Network 概念

到目前為止，我們的容器都是各自獨立運行的。但在實際應用中，Web 伺服器需要連線到資料庫。

**用池塘比喻理解 Docker Network：**

一個 Docker Network 就像一個「池塘」，每個放進去的容器都在同一個池塘裡游，所以它們之間可以互相連線、交換資料。但池塘外的世界（宿主機或外部網路）看不到裡面的容器。

```
┌─────────────── Network（池塘）───────────────┐
│                                              │
│   ┌────────┐  ┌────────┐  ┌────────┐        │
│   │ nginx  │  │  app   │  │   db   │        │
│   │        │←→│        │←→│        │        │
│   └────────┘  └────────┘  └────────┘        │
│                                              │
│   容器之間可以用「名稱」互相找到對方           │
└──────────────────────────────────────────────┘
```

**Docker 網路類型：**
- `bridge`（預設）：容器可以透過 IP 互相溝通
- `host`：容器直接使用主機的網路
- `none`：沒有網路

**問題：**
預設的 bridge 網路中，容器之間沒辦法用「名稱」互相找到對方，只能用 IP（但 IP 每次啟動可能不同）。

**解決方案：建立自訂網路**
```powershell
# 先看看目前有哪些網路
docker network ls

# 建立自訂網路
docker network create my-network

# 確認建立成功
docker network ls
```

在自訂網路中，容器可以用「容器名稱」當作主機名稱互相溝通！就像在同一個池塘裡，大家可以直接叫對方的名字。

### 實作：Flask &#43; MySQL 完整應用

**步驟 1：建立自訂網路**
```powershell
docker network create flask-network
```

**步驟 2：啟動 MySQL 容器（加入網路）**
```powershell
docker run -d ^
  --name mysql-db ^
  --network flask-network ^
  -e MYSQL_ROOT_PASSWORD=rootpass ^
  -e MYSQL_DATABASE=flaskdb ^
  -e MYSQL_USER=flaskuser ^
  -e MYSQL_PASSWORD=flaskpass ^
  -v mysql-data:/var/lib/mysql ^
  mysql:8.0
```

**步驟 3：修改 Flask 應用程式**

更新 `C:\docker-lab\flask-app\requirements.txt`：
```
flask
mysql-connector-python
```

更新 `C:\docker-lab\flask-app\app.py`：
```python
from flask import Flask, jsonify
import mysql.connector
import time

app = Flask(__name__)

def get_db_connection():
    &#34;&#34;&#34;建立資料庫連線&#34;&#34;&#34;
    return mysql.connector.connect(
        host=&#39;mysql-db&#39;,          # 用容器名稱當主機名！
        user=&#39;flaskuser&#39;,
        password=&#39;flaskpass&#39;,
        database=&#39;flaskdb&#39;
    )

def init_db():
    &#34;&#34;&#34;初始化資料庫&#34;&#34;&#34;
    # 等待 MySQL 啟動完成
    for i in range(30):
        try:
            conn = get_db_connection()
            cursor = conn.cursor()
            cursor.execute(&#39;&#39;&#39;
                CREATE TABLE IF NOT EXISTS visitors (
                    id INT AUTO_INCREMENT PRIMARY KEY,
                    visit_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
                )
            &#39;&#39;&#39;)
            conn.commit()
            cursor.close()
            conn.close()
            print(&#34;Database initialized!&#34;)
            return
        except Exception as e:
            print(f&#34;Waiting for MySQL... ({i&#43;1}/30)&#34;)
            time.sleep(2)
    print(&#34;Failed to connect to MySQL!&#34;)

@app.route(&#39;/&#39;)
def hello():
    try:
        conn = get_db_connection()
        cursor = conn.cursor()

        # 記錄訪問
        cursor.execute(&#34;INSERT INTO visitors () VALUES ()&#34;)
        conn.commit()

        # 取得總訪問次數
        cursor.execute(&#34;SELECT COUNT(*) FROM visitors&#34;)
        count = cursor.fetchone()[0]

        cursor.close()
        conn.close()

        return f&#39;&lt;h1&gt;Hello Docker!&lt;/h1&gt;&lt;p&gt;You are visitor #{count}&lt;/p&gt;&#39;
    except Exception as e:
        return f&#39;&lt;h1&gt;Error&lt;/h1&gt;&lt;p&gt;{str(e)}&lt;/p&gt;&#39;

@app.route(&#39;/visitors&#39;)
def visitors():
    try:
        conn = get_db_connection()
        cursor = conn.cursor()
        cursor.execute(&#34;SELECT * FROM visitors ORDER BY visit_time DESC LIMIT 10&#34;)
        rows = cursor.fetchall()
        cursor.close()
        conn.close()
        return jsonify([{&#39;id&#39;: r[0], &#39;time&#39;: str(r[1])} for r in rows])
    except Exception as e:
        return jsonify({&#39;error&#39;: str(e)})

if __name__ == &#39;__main__&#39;:
    init_db()
    app.run(host=&#39;0.0.0.0&#39;, port=5000)
```

**步驟 4：重新建構 Flask 映像檔**
```powershell
cd C:\docker-lab\flask-app
docker build -t my-flask-app:v4 .
```

**步驟 5：啟動 Flask 容器（加入同一個網路）**
```powershell
docker run -d ^
  --name flask-web ^
  --network flask-network ^
  -p 5000:5000 ^
  my-flask-app:v4
```

**步驟 6：測試**
- 前往 `http://localhost:5000`，每次重新整理，訪問次數會增加
- 前往 `http://localhost:5000/visitors` 查看最近的訪問紀錄

&gt; **關鍵觀念**：`host=&#39;mysql-db&#39;` 這行——在同一個自訂網路中，容器名稱就是主機名稱，Docker 會自動幫你做 DNS 解析。

### 網路管理指令

```powershell
# 查看所有網路
docker network ls

# 查看網路詳情（包含哪些容器連接在上面）
docker network inspect flask-network

# 把容器連到網路
docker network connect flask-network &lt;容器名稱&gt;

# 把容器從網路斷開
docker network disconnect flask-network &lt;容器名稱&gt;

# 刪除網路
docker network rm flask-network
```

---

## 4-3 容器資源配置（約 1 小時）

### 為什麼需要限制資源？

預設情況下，一個容器可以使用主機上所有可用的 CPU 和記憶體資源。這會造成問題：
- 一個失控的容器可能吃掉所有 CPU，拖慢其他容器
- 記憶體用完可能導致系統崩潰
- 多個容器搶資源導致效能不穩定

在生產環境中，合理分配資源是非常重要的。

### 查看系統資源

```powershell
# 查看 Docker 可用的資源
docker info | findstr -i &#34;cpu\|memory&#34;

# 查看執行中容器的即時資源使用狀況
docker stats
```

`docker stats` 會顯示：
```
CONTAINER ID   NAME        CPU %   MEM USAGE / LIMIT   MEM %   NET I/O   BLOCK I/O
abc123         flask-web   0.50%   50MiB / 7.7GiB      0.63%   1kB/1kB   0B/0B
```

按 `Ctrl &#43; C` 離開。

### CPU 限制參數

**`--cpus`：限制可使用的 CPU 核心數**
```powershell
# 最多使用 1.5 個 CPU 核心
docker run -d --cpus=1.5 --name cpu-test nginx

# 最多使用 0.5 個 CPU 核心（50%）
docker run -d --cpus=0.5 --name cpu-test2 nginx
```

**`--cpu-shares`：設定 CPU 的相對權重（預設 1024）**
```powershell
# 容器 A 的權重是容器 B 的兩倍
docker run -d --cpu-shares=2048 --name high-priority nginx
docker run -d --cpu-shares=1024 --name low-priority nginx
```

&gt; **注意**：`--cpu-shares` 只在 CPU 資源競爭時才有效。如果只有一個容器在跑，它還是可以用到所有 CPU。

**`--cpuset-cpus`：指定只能使用哪幾個 CPU 核心**
```powershell
# 只使用第 0 和第 1 個 CPU 核心
docker run -d --cpuset-cpus=&#34;0,1&#34; --name pinned-cpu nginx
```

### 實作：CPU 壓力測試

讓我們實際看看 CPU 限制的效果：

```powershell
# 不限制 CPU - 啟動壓力測試
docker run -d --name stress-no-limit alpine sh -c &#34;while true; do :; done&#34;

# 限制只能用 0.5 個 CPU
docker run -d --cpus=0.5 --name stress-limited alpine sh -c &#34;while true; do :; done&#34;

# 觀察兩者的 CPU 使用率
docker stats
```

你會看到：
- `stress-no-limit`：CPU 使用率接近 100%（一個核心的 100%）
- `stress-limited`：CPU 使用率被限制在約 50%

```powershell
# 測試完記得清理
docker rm -f stress-no-limit stress-limited
```

### 記憶體限制參數

**`--memory`（或 `-m`）：設定記憶體上限**
```powershell
# 限制最多使用 256 MB 記憶體
docker run -d -m 256m --name mem-limited nginx

# 限制最多使用 1 GB
docker run -d -m 1g --name mem-1gb nginx
```

**`--memory-swap`：設定記憶體 &#43; swap 的總上限**
```powershell
# 記憶體 256 MB，swap 256 MB（總共 512 MB）
docker run -d -m 256m --memory-swap 512m --name mem-swap nginx

# 停用 swap（記憶體和 swap 一樣大）
docker run -d -m 256m --memory-swap 256m --name no-swap nginx
```

**`--memory-reservation`：軟限制（建議值）**
```powershell
# 軟限制 128 MB，硬限制 256 MB
docker run -d -m 256m --memory-reservation 128m --name mem-soft nginx
```

- 硬限制（`-m`）：超過就會被殺掉（OOM Kill）
- 軟限制（`--memory-reservation`）：Docker 會嘗試讓容器維持在這個值以下，但不會強制

### 記憶體超限會怎樣？

```powershell
# 啟動一個只有 100 MB 記憶體的容器
docker run -d -m 100m --name oom-test python:3.11-slim sleep infinity

# 進入容器
docker exec -it oom-test bash

# 在容器裡嘗試吃掉大量記憶體
python -c &#34;
data = []
try:
    while True:
        data.append(&#39;x&#39; * 10**6)  # 每次加 1 MB
except MemoryError:
    print(f&#39;Allocated approximately {len(data)} MB before OOM&#39;)
&#34;
```

容器可能會被 OOM Killer 直接殺掉。用 `docker inspect` 可以看到退出原因：

```powershell
docker inspect oom-test --format=&#39;{{.State.OOMKilled}}&#39;
```

### 實作：觀察記憶體使用

```powershell
# 啟動有記憶體限制的容器
docker run -d -m 512m --name mem-monitor python:3.11-slim python -c &#34;
import time
data = []
while True:
    data.append(&#39;x&#39; * 1024 * 1024)  # 每秒增加 1 MB
    print(f&#39;Memory used: ~{len(data)} MB&#39;)
    time.sleep(1)
&#34;

# 即時觀察
docker stats mem-monitor

# 查看 log
docker logs -f mem-monitor
```

&gt; **練習**：設定不同的記憶體限制，觀察容器在接近限制時的行為。

```powershell
# 清理
docker rm -f mem-monitor oom-test mem-limited mem-1gb mem-swap no-swap mem-soft
```

### docker update 動態修改資源

如果容器已經在執行，你可以用 `docker update` 動態修改資源限制，不需要停止容器！

```powershell
# 先啟動一個容器
docker run -d --name live-update -m 256m --cpus=1 nginx

# 確認目前的限制
docker stats live-update --no-stream

# 修改記憶體限制為 512 MB
docker update --memory 512m --memory-swap 1g live-update

# 修改 CPU 限制
docker update --cpus 2 live-update

# 確認修改後的限制
docker stats live-update --no-stream
```

**可以動態修改的參數：**

| 參數 | 用途 |
|------|------|
| `--cpus` | CPU 核心數 |
| `--cpu-shares` | CPU 權重 |
| `--cpuset-cpus` | 指定 CPU 核心 |
| `--memory` | 記憶體上限 |
| `--memory-swap` | 記憶體 &#43; swap 上限 |
| `--memory-reservation` | 記憶體軟限制 |

**注意事項：**
- 只能增加記憶體限制，不能減少（減少可能導致 OOM Kill）
- CPU 相關的限制可以自由增減
- 在 Windows Docker Desktop 上，某些參數可能不支援動態修改

```powershell
# 實作練習
# 啟動一個低資源容器
docker run -d --name flexible-app -m 128m --cpus=0.5 nginx

# 模擬「哇，流量變大了，要加資源」
docker update --memory 512m --cpus=2 flexible-app

# 確認
docker stats flexible-app --no-stream

# 清理
docker rm -f flexible-app live-update
```

### 多容器資源配置實戰演練

#### 情境設定

我們要模擬一個實際場景：在一台主機上同時運行 Web 伺服器和資料庫，合理分配資源。

假設主機有：
- 4 個 CPU 核心
- 8 GB 記憶體

目標：
- Web 容器（Flask）：最多用 1 CPU、512 MB 記憶體
- 資料庫容器（MySQL）：最多用 2 CPU、2 GB 記憶體
- 快取容器（Redis）：最多用 0.5 CPU、256 MB 記憶體

#### 實作：建構完整環境

**步驟 1：建立網路**
```powershell
docker network create production-net
```

**步驟 2：啟動 Redis**
```powershell
docker run -d ^
  --name prod-redis ^
  --network production-net ^
  --cpus=0.5 ^
  -m 256m ^
  redis:7-alpine
```

**步驟 3：啟動 MySQL**
```powershell
docker run -d ^
  --name prod-mysql ^
  --network production-net ^
  --cpus=2 ^
  -m 2g ^
  -e MYSQL_ROOT_PASSWORD=prodpass ^
  -e MYSQL_DATABASE=proddb ^
  -v prod-mysql-data:/var/lib/mysql ^
  mysql:8.0
```

**步驟 4：啟動 Flask**
```powershell
docker run -d ^
  --name prod-flask ^
  --network production-net ^
  --cpus=1 ^
  -m 512m ^
  -p 5000:5000 ^
  my-flask-app:v4
```

**步驟 5：觀察所有容器的資源使用**
```powershell
docker stats
```

#### 資源監控與調整

```powershell
# 查看每個容器的資源設定
docker inspect prod-flask --format=&#39;CPU: {{.HostConfig.NanoCpus}} Memory: {{.HostConfig.Memory}}&#39;
docker inspect prod-mysql --format=&#39;CPU: {{.HostConfig.NanoCpus}} Memory: {{.HostConfig.Memory}}&#39;
docker inspect prod-redis --format=&#39;CPU: {{.HostConfig.NanoCpus}} Memory: {{.HostConfig.Memory}}&#39;
```

模擬流量增加，動態調整資源：
```powershell
# Web 容器需要更多資源
docker update --cpus=2 --memory=1g prod-flask

# 確認調整後的狀態
docker stats --no-stream
```

#### 清理

```powershell
# 停止並刪除所有容器
docker rm -f prod-flask prod-mysql prod-redis

# 刪除網路
docker network rm production-net

# 刪除 Volume（如果不需要了）
docker volume rm prod-mysql-data

# 查看還有什麼殘留
docker ps -a
docker images
docker volume ls
docker network ls
```

### 指令速查表

```
# 映像檔
docker pull &lt;映像檔&gt;            拉取映像檔
docker images                   列出本機映像檔
docker rmi &lt;映像檔&gt;             刪除映像檔
docker build -t &lt;名稱&gt; .        建構映像檔

# 容器
docker run [選項] &lt;映像檔&gt;       建立並啟動容器
docker ps                       列出執行中的容器
docker ps -a                    列出所有容器
docker stop &lt;容器&gt;              停止容器
docker start &lt;容器&gt;             啟動容器
docker rm &lt;容器&gt;                刪除容器
docker logs &lt;容器&gt;              查看容器 log
docker exec -it &lt;容器&gt; bash     進入容器

# 常用 run 選項
-d                              背景執行
-it                             互動模式
-p 主機:容器                     端口映射
-v 主機路徑:容器路徑              掛載目錄
-v 名稱:容器路徑                  掛載 Volume
-e KEY=VALUE                    設定環境變數
--name 名稱                      指定容器名稱
--network 網路名稱               加入指定網路
--cpus=N                         限制 CPU
-m 大小                          限制記憶體

# 網路
docker network create &lt;名稱&gt;     建立網路
docker network ls                列出網路
docker network rm &lt;名稱&gt;         刪除網路

# 資源
docker stats                     即時資源監控
docker update [選項] &lt;容器&gt;       動態修改資源限制

# 清理
docker container prune            刪除所有已停止的容器
docker image prune                刪除未使用的映像檔
docker system prune -a            全面清理
```


---

> 作者: luk  
> URL: https://yoru-karu-blog-lalaluk-52581ac5e0cef170a3c8922c19182ecb6f7bd604.gitlab.io/posts/tutorial/docker/docker-session4-dockerfile-resources/  

