前面提到,如果 Backend 是 Stateless,就可以很容易地增加或減少 Server:
Backend 1
Backend 2
Backend 3
但這時會遇到另一個問題:
怎麼確保每一台 Backend 的執行環境完全一樣?
假設 Backend 1 安裝:
Python 3.12
FastAPI
SQLAlchemy
MariaDB Client
Redis Client
Backend 2 卻是:
Python 3.11
不同版本 FastAPI
不同版本套件
那麼即使程式碼完全一樣,也可能出現:
Backend 1 → 正常
Backend 2 → 發生錯誤
因此部署時,不只是要部署「程式碼」,還要確保:
作業系統環境
Runtime
Python 版本
Package 版本
系統套件
設定
啟動方式
都一致。
Docker 就是為了解決這種「環境一致性」問題而非常常被使用。
如果沒有 Docker,部署 FastAPI 常見流程可能是:
SSH 進入 Server
│
▼
安裝 Python
│
▼
建立 virtualenv
│
▼
pip install requirements.txt
│
▼
設定環境變數
│
▼
啟動 Gunicorn / Uvicorn
例如:
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
gunicorn main:app
如果只有一台 Server,這樣通常沒什麼問題。
但如果現在有:
Backend 1
Backend 2
Backend 3
Backend 4
Backend 5
就必須確保五台機器全部安裝相同環境。
這會開始變得麻煩。
例如 Backend 4:
少裝了一個 library
Backend 5:
Python 版本不同
Backend 3:
OS Package 版本不同
於是就可能出現很經典的問題:
在我的機器上可以跑
為什麼 Server 上不行?
Docker 的目標之一,就是把:
Application
+
Runtime
+
Dependencies
+
Environment
包在一起。
Docker Image 可以理解成:
建立執行環境的模板。
例如 FastAPI 專案:
app/
├── main.py
├── requirements.txt
└── Dockerfile
Dockerfile 可能寫成:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
這份 Dockerfile 描述:
我要使用 Python 3.12
工作目錄是 /app
安裝 requirements.txt
把程式碼放進去
最後啟動 Uvicorn
接著執行:
docker build -t my-fastapi:1.0 .
Docker 就會建立:
my-fastapi:1.0
這就是 Docker Image。
可以把 Image 想成:
FastAPI Backend Template
這是 Docker 最重要的概念之一。
可以簡單理解成:
Image
= 模板
Container
= 根據模板實際跑起來的 Instance
例如:
my-fastapi:1.0
是一個 Image。
可以從這個 Image 啟動:
Container 1
Container 2
Container 3
例如:
docker run my-fastapi:1.0
就是建立一個 Container。
因此:
Image
my-fastapi:1.0
│
┌─────────┼─────────┐
▼ ▼ ▼
Container 1 Container 2 Container 3
三個 Container 使用的是同一個 Image。
所以它們的:
Python Version
Package Version
Application Code
Startup Command
理論上都是一樣的。
前面提到 Horizontal Scaling:
Backend 1
Backend 2
Backend 3
如果使用 Docker,就可以變成:
Docker Image
backend:v1.0
│
┌──────────┼──────────┐
▼ ▼ ▼
Container 1 Container 2 Container 3
每個 Container 都運行相同的 FastAPI。
Load Balancer 可以分配:
Request 1 → Container 1
Request 2 → Container 2
Request 3 → Container 3
如果流量增加:
Container 4
Container 5
只需要繼續使用同一個 Image 啟動新的 Container。
因此:
Stateless Backend
+
Docker Image
│
▼
Backend 可以快速複製
這就是 Docker 和 Auto Scaling 之間很重要的關係。
Docker Container 不是完整的 Virtual Machine。
傳統 VM:
Physical Server
│
Hypervisor
│
┌───┴────────────┐
│ │
VM 1 VM 2
│ │
Guest OS Guest OS
Python Python
FastAPI FastAPI
每一個 VM 通常都有自己的完整 Guest OS。
Container 則是:
Physical Server
│
Host OS
│
Docker Engine
│
┌───┼───┐
▼ ▼ ▼
C1 C2 C3
Container 共享 Host 的 Kernel,但各自擁有隔離的:
Filesystem
Process
Network
Environment
因此 Container 通常比 VM 更輕量,也能更快建立。
假設開發環境:
Developer Mac
測試環境:
Staging Linux Server
Production:
Production Linux Server
如果都是直接安裝環境:
Developer
Python 3.12.1
Staging
Python 3.12.3
Production
Python 3.11
可能產生差異。
如果改成:
backend:v1.0
同一個 Image 部署到:
Developer
Staging
Production
就能大幅降低環境差異。
概念變成:
Code
│
▼
Docker Build
│
▼
backend:v1.0
│
├── Development
├── Staging
└── Production
而不是每個環境都重新安裝一遍。
如果 Image 只存在自己的電腦,就無法讓其他 Server 使用。
因此通常需要一個地方保存 Image。
這就是:
Container Registry
常見服務包括:
Docker Hub
GitHub Container Registry
Amazon ECR
Google Artifact Registry
Azure Container Registry
整個流程可以變成:
Developer
│
▼
Git Push
│
▼
CI/CD
│
▼
Docker Build
│
▼
backend:v1.0
│
▼
Container Registry
接著 Production Server:
Production Server
│
▼
docker pull backend:v1.0
│
▼
docker run
因此 Registry 可以理解成:
Docker Image 的倉庫
加入 Docker 之後,部署流程可以變得更一致。
例如:
Developer
│
│ git push
▼
Git Repository
│
▼
CI Pipeline
│
├── Run Tests
├── Build Docker Image
└── Push Image
│
▼
Container Registry
│
▼
Production Server
│
▼
Pull New Image
│
▼
Start Container
例如版本:
backend:v1.0
backend:v1.1
backend:v1.2
如果新版出問題,也比較容易回到:
backend:v1.1
這就是 Image Versioning 帶來的另一個好處。
雖然程式碼與 Runtime 可以包進 Image,但不同環境的設定通常不應直接寫死。
例如:
Database Password
Redis Host
JWT Secret
API Key
Environment
Development:
DB_HOST=dev-db
Staging:
DB_HOST=staging-db
Production:
DB_HOST=prod-db
但是三個環境仍然可以使用:
backend:v1.0
也就是:
相同 Image
+
不同 Configuration
例如:
docker run \
-e DB_HOST=prod-db \
-e REDIS_HOST=prod-redis \
backend:v1.0
這樣 Image 本身不需要為每個環境重新修改程式碼。
Container 還有一個很重要的特性:
Container 本身應該被視為可以被刪除的。
例如:
Container 1
│
X
刪除
如果重要資料只存在 Container 內部,也可能一起消失。
因此像 Database:
MySQL
MariaDB
通常需要使用 Volume 或外部 Storage。
例如:
MariaDB Container
│
▼
Volume
│
▼
/data/mysql
Container 即使被刪除:
MariaDB Container X
資料還可以保留在 Volume。
這與前面 Stateless Backend 的概念非常相似。
Backend Container:
可以被刪除
可以被重建
重要資料則放在外部:
Database
Redis
Object Storage
Volume
如果開發環境裡不只有 FastAPI,而是:
FastAPI
Redis
MariaDB
可以使用 Docker Compose 管理多個 Container。
例如:
services:
backend:
build: .
ports:
- "8000:8000"
depends_on:
- redis
- db
redis:
image: redis:7
db:
image: mariadb:11
然後:
docker compose up
就可以一次啟動:
Backend Container
Redis Container
MariaDB Container
架構:
Docker Compose
┌─────────┼─────────┐
▼ ▼ ▼
FastAPI Redis MariaDB
Docker Compose 對:
Local Development
POC
測試環境
小型部署
非常方便。
Docker 並不是單純:
把程式放進 Container
它真正解決的是:
如何把 Application 與執行環境一起標準化。
從:
我要 SSH 到每台 Server
然後安裝 Python
然後安裝 Package
然後設定服務
變成:
Build 一次 Image
然後:
Run Everywhere
因此部署單位從:
Source Code
逐漸變成:
Container Image
這是一個非常重要的改變。
現在前面的架構可以變成:
Internet
│
▼
Load Balancer
│
┌────────────┼────────────┐
▼ ▼ ▼
Container 1 Container 2 Container 3
FastAPI FastAPI FastAPI
│ │ │
└──────┬─────┴─────┬──────┘
│ │
▼ ▼
Redis Database
而三個 Container 都來自:
backend:v1.0
因此:
backend:v1.0
│
┌─────────┼─────────┐
▼ ▼ ▼
API 1 API 2 API 3
這讓部署、擴充與替換 Backend 都更加一致。
Docker 解決了:
如何建立一致的 Container
但如果系統有:
100 個 Container
新的問題就會出現:
哪台 Server 要跑哪些 Container?
Container 掛掉誰負責重啟?
Load Balancer 要怎麼知道新 Container?
CPU 太高怎麼自動增加 Container?
部署新版時怎麼逐步替換?
某台 Server 掛掉怎麼把 Container 搬走?
例如:
Server A
├── Container 1
├── Container 2
└── Container 3
Server B
├── Container 4
├── Container 5
└── Container 6
Server C
├── Container 7
├── Container 8
└── Container 9
如果全部靠人工:
SSH Server A
docker run ...
SSH Server B
docker run ...
SSH Server C
docker run ...
系統規模一大又會變得難以管理。
因此下一個問題就變成:
誰來自動管理大量 Container?
這類系統稱為:
Container Orchestration
而目前最常見的代表之一,就是:
Kubernetes。
到目前為止,可以整理成:
單機部署
│
▼
前後端分離
│
▼
Load Balancer
│
▼
多台 Backend
│
▼
Redis / Shared State
│
▼
Stateless Backend
│
▼
Auto Scaling
│
▼
Docker Image
│
▼
Container
│
▼
Container Registry
│
▼
大量 Container 管理
│
▼
Kubernetes
因此 Docker 並不是獨立存在的一個工具。
放在整個部署架構中來看,它解決的是:
如何把 Stateless Backend 標準化成一個可以快速建立、複製、部署與替換的執行單位。
而 Kubernetes 接下來要解決的,就是:
當這些 Container 數量變多之後,如何自動管理它們的生命週期。