Docker 教學 第 4 堂:Dockerfile、映像檔優化與資源配置
本系列為 18 小時 Docker 基礎教學講義,適合初學者,使用 Windows 電腦。 本篇為第 4 堂課,約 3 小時。
4-1 撰寫 Dockerfile 與映像檔優化(約 1.5 小時)
為什麼需要自製映像檔?
到目前為止,我們都是用別人做好的映像檔。但在實際工作中,你需要把自己的程式打包成映像檔。
回想一下之前 Flask 的例子,我們的流程是:
- 啟動 Python 容器
- 手動安裝 Flask
- 手動啟動程式
每次都要重複這些步驟很麻煩。如果能把這些步驟「寫下來」,讓 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=5000ARG:建構時的參數
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」。
如果你的專案目錄裡有很多不需要的檔案(例如 .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 很像:
#開頭是註解*匹配任意字元!開頭表示例外(不要排除)
任何列出的檔案或資料夾在建構過程中都不會被包含,這樣可以幫助減少最終映像檔的大小。
實作:比較有 vs 沒有 .dockerignore 的差異
步驟 1:準備測試專案
用之前的 Flask 專案 C:\docker-lab\flask-app\,先確認裡面有 app.py、requirements.txt、Dockerfile。
步驟 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 bashls
# 只會看到 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→ 進入容器時就在/appCOPY . .→ 本機的檔案都複製到/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-imagesave/load vs export/import 的差別:
save/load:保留完整的映像檔層結構和 metadataexport/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-network4-3 容器資源配置(約 1 小時)
為什麼需要限制資源?
預設情況下,一個容器可以使用主機上所有可用的 CPU 和記憶體資源。這會造成問題:
- 一個失控的容器可能吃掉所有 CPU,拖慢其他容器
- 記憶體用完可能導致系統崩潰
- 多個容器搶資源導致效能不穩定
在生產環境中,合理分配資源是非常重要的。
查看系統資源
# 查看 Docker 可用的資源
docker info | findstr -i "cpu\|memory"
# 查看執行中容器的即時資源使用狀況
docker statsdocker 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-softdocker 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可以動態修改的參數:
| 參數 | 用途 |
|---|---|
--cpus | CPU 核心數 |
--cpu-shares | CPU 權重 |
--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 全面清理