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

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

4-1 撰寫 Dockerfile 與映像檔優化(約 1.5 小時)

為什麼需要自製映像檔?

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

回想一下之前 Flask 的例子,我們的流程是:

  1. 啟動 Python 容器
  2. 手動安裝 Flask
  3. 手動啟動程式

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

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

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

Dockerfile、Image、Container 的關係

用遊戲光碟來比喻的話:

Dockerfile(程式碼)   →  Image(遊戲光碟)   →  Container(遊戲機)
    撰寫程式碼              docker build            docker run
                           燒成光碟片               放進遊戲機跑起來
  • Dockerfile 就像遊戲的原始程式碼,記錄了要怎麼打包
  • Image 就像燒好的光碟片,可以複製很多份、分送給別人
  • Container 就像遊戲機,把光碟放進去就能跑,而且可以同時開好幾台遊戲機跑同一張光碟

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

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 ["python", "app.py"]

逐行解析:

指令用途比喻
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(沒有副檔名):

# 使用 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 ["python", "app.py"]

步驟 3:建構映像檔

cd C:\docker-lab\flask-app
docker build -t my-flask-app .

參數說明:

  • -t my-flask-app:給映像檔取名字(tag)
  • .:指定 Dockerfile 所在的目錄(Build Context)

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

步驟 4:啟動容器

docker run -d --name flask-web -p 5000:5000 my-flask-app

打開瀏覽器 http://localhost:5000,看到 Hello Docker!

比較一下:

  • 之前的方式:docker run + 手動安裝 + 手動啟動(每次都要重複)
  • 現在的方式:docker build 一次,之後只需要 docker run 就搞定

理解 Build Cache

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

快取規則:

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

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

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

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

更多 Dockerfile 指令

ENV:設定環境變數

ENV FLASK_ENV=production
ENV APP_PORT=5000

ARG:建構時的參數

ARG PYTHON_VERSION=3.11
FROM python:${PYTHON_VERSION}-slim

使用方式:

docker build --build-arg PYTHON_VERSION=3.12 -t my-app .

LABEL:加上標籤資訊

LABEL maintainer="your-email@example.com"
LABEL version="1.0"
LABEL description="My Flask web application"

RUN 的兩種寫法:

# Shell 形式(會經過 shell 處理)
RUN pip install flask

# Exec 形式(直接執行)
RUN ["pip", "install", "flask"]

CMD vs ENTRYPOINT:

# CMD:可以被 docker run 的參數覆蓋
CMD ["python", "app.py"]

# ENTRYPOINT:不會被覆蓋,而是把 docker run 的參數附加在後面
ENTRYPOINT ["python"]
CMD ["app.py"]    # 這是預設參數,可以被覆蓋
# 使用 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)

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

就像千層蛋糕一樣,一層一層疊上去:

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

為什麼這很重要?

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

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

docker history my-flask-app

實作:親眼看到分層的差異

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

步驟 1:建立測試專案

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

建立一個簡單的 app.py

print("Hello from layer test!")

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

建立 Dockerfile.many

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 ["python", "app.py"]

注意:4 個 RUN 就是 4 層。

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

建立 Dockerfile.few

FROM python:3.11-slim
WORKDIR /app
RUN apt-get update && \
    apt-get install -y curl wget && \
    rm -rf /var/lib/apt/lists/*
COPY app.py .
CMD ["python", "app.py"]

&& 把 4 行合併成 1 行,只有 1 層。

步驟 4:分別 build 並比較

# 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 看每一層

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

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

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

步驟 6:清理

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

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

映像檔優化技巧

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

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

技巧 2:合併 RUN 指令,減少層數(剛才已經實作過了)

# 不好 ✗(三層,中間的暫存檔殘留在前面的層裡)
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*

# 好 ✓(一層,暫存檔在同一層就被清掉了)
RUN apt-get update && \
    apt-get install -y curl && \
    rm -rf /var/lib/apt/lists/*

技巧 3:善用 .dockerignore(下一節會詳細講)

技巧 4:使用非 root 使用者

# 建立一個非 root 使用者
RUN useradd -m appuser
USER appuser

實作:優化我們的 Flask 映像檔

改良版 Dockerfile:

FROM python:3.11-slim

# 加上標籤
LABEL maintainer="student@example.com"
LABEL version="1.0"

# 設定環境變數
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 && chown -R appuser:appuser /app
USER appuser

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

EXPOSE 5000

CMD ["python", "app.py"]
# 建構並比較大小
docker build -t my-flask-app:v2 .
docker images | findstr my-flask

使用 .dockerignore 排除不必要檔案

什麼是 Build Context?

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

如果你的專案目錄裡有很多不需要的檔案(例如 .gitnode_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 很像:

  • # 開頭是註解
  • * 匹配任意字元
  • ! 開頭表示例外(不要排除)

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

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

步驟 1:準備測試專案

用之前的 Flask 專案 C:\docker-lab\flask-app\,先確認裡面有 app.pyrequirements.txtDockerfile

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

cd C:\docker-lab\flask-app

# 模擬 Python 快取
mkdir __pycache__
echo "cache" > __pycache__\test.pyc

# 模擬虛擬環境(通常幾百 MB)
mkdir venv
echo "fake venv" > venv\fake.txt

# 模擬 IDE 設定
mkdir .vscode
echo "{}" > .vscode\settings.json

# 模擬 git 目錄
mkdir .git
echo "git data" > .git\config

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

步驟 3:先不用 .dockerignore 建構

確認目前沒有 .dockerignore 檔案,然後:

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 重新建構

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

再看第一行的 Context 大小,應該小非常多!

步驟 6:比較兩個映像檔

docker images | findstr flask
沒有 .dockerignore有 .dockerignore
Build Context 大小大(包含所有垃圾檔案)小(只有需要的檔案)
Build 速度
映像檔大小可能更大(垃圾檔案被 COPY 進去)更小更乾淨

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

docker exec -it flask-with-ignore bash
ls
# 只會看到 app.py 和 requirements.txt
# 不會看到 __pycache__、venv、.vscode、bigfile.dat
exit

步驟 8:清理

docker rmi flask-no-ignore flask-with-ignore

重點.dockerignore 就是映像檔的減肥清單。養成好習慣——每個有 Dockerfile 的專案都應該要有 .dockerignore

小技巧docker build --no-cache 可以強制不使用快取重新建構,適合在測試 .dockerignore 效果時使用。

測試映像檔

啟動容器並測試:

# 啟動
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 的每個指令是否生效:

docker exec -it test-flask 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

透過這個驗證流程,你可以親眼確認 Dockerfile 裡的每個指令都做了什麼:

  • WORKDIR /app → 進入容器時就在 /app
  • COPY . . → 本機的檔案都複製到 /app 裡了
  • RUN pip install → 套件都裝好了
  • USER appuser → 不是用 root 在跑

映像檔的匯出與匯入

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

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

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

也可以匯出/匯入容器:

# 匯出容器的檔案系統
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

docker login

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

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

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

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

通常會同時推一個具體版本(v3)和 latest,這樣別人 pull 的時候不指定版本就會拿到最新的。

步驟 4:推上 Docker Hub

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

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

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

# 任何人只要打這行就能用你的映像檔
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(啟動)

注意:推上 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 每次啟動可能不同)。

解決方案:建立自訂網路

# 先看看目前有哪些網路
docker network ls

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

# 確認建立成功
docker network ls

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

實作:Flask + MySQL 完整應用

步驟 1:建立自訂網路

docker network create flask-network

步驟 2:啟動 MySQL 容器(加入網路)

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

from flask import Flask, jsonify
import mysql.connector
import time

app = Flask(__name__)

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

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

@app.route('/')
def hello():
    try:
        conn = get_db_connection()
        cursor = conn.cursor()

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

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

        cursor.close()
        conn.close()

        return f'<h1>Hello Docker!</h1><p>You are visitor #{count}</p>'
    except Exception as e:
        return f'<h1>Error</h1><p>{str(e)}</p>'

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

if __name__ == '__main__':
    init_db()
    app.run(host='0.0.0.0', port=5000)

步驟 4:重新建構 Flask 映像檔

cd C:\docker-lab\flask-app
docker build -t my-flask-app:v4 .

步驟 5:啟動 Flask 容器(加入同一個網路)

docker run -d ^
  --name flask-web ^
  --network flask-network ^
  -p 5000:5000 ^
  my-flask-app:v4

步驟 6:測試

  • 前往 http://localhost:5000,每次重新整理,訪問次數會增加
  • 前往 http://localhost:5000/visitors 查看最近的訪問紀錄

關鍵觀念host='mysql-db' 這行——在同一個自訂網路中,容器名稱就是主機名稱,Docker 會自動幫你做 DNS 解析。

網路管理指令

# 查看所有網路
docker network ls

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

# 把容器連到網路
docker network connect flask-network <容器名稱>

# 把容器從網路斷開
docker network disconnect flask-network <容器名稱>

# 刪除網路
docker network rm flask-network

4-3 容器資源配置(約 1 小時)

為什麼需要限制資源?

預設情況下,一個容器可以使用主機上所有可用的 CPU 和記憶體資源。這會造成問題:

  • 一個失控的容器可能吃掉所有 CPU,拖慢其他容器
  • 記憶體用完可能導致系統崩潰
  • 多個容器搶資源導致效能不穩定

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

查看系統資源

# 查看 Docker 可用的資源
docker info | findstr -i "cpu\|memory"

# 查看執行中容器的即時資源使用狀況
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 + C 離開。

CPU 限制參數

--cpus:限制可使用的 CPU 核心數

# 最多使用 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)

# 容器 A 的權重是容器 B 的兩倍
docker run -d --cpu-shares=2048 --name high-priority nginx
docker run -d --cpu-shares=1024 --name low-priority nginx

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

--cpuset-cpus:指定只能使用哪幾個 CPU 核心

# 只使用第 0 和第 1 個 CPU 核心
docker run -d --cpuset-cpus="0,1" --name pinned-cpu nginx

實作:CPU 壓力測試

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

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

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

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

你會看到:

  • stress-no-limit:CPU 使用率接近 100%(一個核心的 100%)
  • stress-limited:CPU 使用率被限制在約 50%
# 測試完記得清理
docker rm -f stress-no-limit stress-limited

記憶體限制參數

--memory(或 -m):設定記憶體上限

# 限制最多使用 256 MB 記憶體
docker run -d -m 256m --name mem-limited nginx

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

--memory-swap:設定記憶體 + swap 的總上限

# 記憶體 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:軟限制(建議值)

# 軟限制 128 MB,硬限制 256 MB
docker run -d -m 256m --memory-reservation 128m --name mem-soft nginx
  • 硬限制(-m):超過就會被殺掉(OOM Kill)
  • 軟限制(--memory-reservation):Docker 會嘗試讓容器維持在這個值以下,但不會強制

記憶體超限會怎樣?

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

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

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

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

docker inspect oom-test --format='{{.State.OOMKilled}}'

實作:觀察記憶體使用

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

# 即時觀察
docker stats mem-monitor

# 查看 log
docker logs -f mem-monitor

練習:設定不同的記憶體限制,觀察容器在接近限制時的行為。

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

docker update 動態修改資源

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

# 先啟動一個容器
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

可以動態修改的參數:

參數用途
--cpusCPU 核心數
--cpu-sharesCPU 權重
--cpuset-cpus指定 CPU 核心
--memory記憶體上限
--memory-swap記憶體 + swap 上限
--memory-reservation記憶體軟限制

注意事項:

  • 只能增加記憶體限制,不能減少(減少可能導致 OOM Kill)
  • CPU 相關的限制可以自由增減
  • 在 Windows Docker Desktop 上,某些參數可能不支援動態修改
# 實作練習
# 啟動一個低資源容器
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:建立網路

docker network create production-net

步驟 2:啟動 Redis

docker run -d ^
  --name prod-redis ^
  --network production-net ^
  --cpus=0.5 ^
  -m 256m ^
  redis:7-alpine

步驟 3:啟動 MySQL

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

docker run -d ^
  --name prod-flask ^
  --network production-net ^
  --cpus=1 ^
  -m 512m ^
  -p 5000:5000 ^
  my-flask-app:v4

步驟 5:觀察所有容器的資源使用

docker stats

資源監控與調整

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

模擬流量增加,動態調整資源:

# Web 容器需要更多資源
docker update --cpus=2 --memory=1g prod-flask

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

清理

# 停止並刪除所有容器
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 <映像檔>            拉取映像檔
docker images                   列出本機映像檔
docker rmi <映像檔>             刪除映像檔
docker build -t <名稱> .        建構映像檔

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

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

# 網路
docker network create <名稱>     建立網路
docker network ls                列出網路
docker network rm <名稱>         刪除網路

# 資源
docker stats                     即時資源監控
docker update [選項] <容器>       動態修改資源限制

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