一. 前言:本地 AI 也需要「可重現」 #
本地 LLM 很迷人:安裝 Ollama、拉一個模型、打開 terminal,幾分鐘內就能開始聊天。
但只要你把它放進真正的開發流程,問題就會慢慢冒出來。
今天在筆電可以跑,明天換到工作站就忘記裝哪幾個服務。
API、UI、Redis、向量資料庫各自啟動,terminal 分頁開到像章魚。
demo 時才發現環境變數少一個,模型也還沒 pull。
這不是 Ollama 的問題。
這是本地 AI stack 開始長大後,一定會遇到的工程問題。
拍拍君今天要做的事很單純:用 Docker Compose 把一組本地 AI 開發環境整理起來。
我們會把這些東西放進同一份 compose.yaml:
- Ollama:提供本地 LLM API
- Open WebUI:瀏覽器聊天介面
- FastAPI app:自己的 AI 應用服務
- Redis:快取或背景任務狀態
- volumes:保存模型與應用資料 最後你可以用一行指令啟動:
docker compose up -d
不是雲端平台,不是企業級 MLOps。 就是一個乾淨、可重現、適合個人與小團隊的本地 AI 開發底座。 如果你已經看過 Docker Compose 入門 與 Ollama + Python 小助手,這篇就是把兩條線接起來。
二. 專案結構與模型選擇 #
這篇假設你已經裝好 Docker。 macOS 或 Windows 最簡單的方式是 Docker Desktop;Linux 則可以安裝 Docker Engine 加上 Compose plugin。 先確認版本:
docker --version
docker compose version
你不一定要先在 host 安裝 Ollama,今天的主角就是把 Ollama 放進容器裡跑。 不過 LLM 模型很吃資源,如果機器記憶體比較小,先用輕量模型,例如:
llama3.2:3b
qwen2.5:3b
gemma3:4b
環境先跑順,比一開始追最大模型更重要。 先建立資料夾:
mkdir local-ai-stack
cd local-ai-stack
mkdir app
最後會長這樣:
local-ai-stack/
├── compose.yaml
├── .env
└── app/
├── Dockerfile
├── requirements.txt
└── main.py
角色分工可以想成:Open WebUI 給你瀏覽器聊天介面,FastAPI app 是自己的應用服務,Ollama 負責模型 API,Redis 則負責快取或任務狀態。 這個組合很適合 prototype、內部工具、教學 demo,也很適合把「我電腦上可以跑」整理成「別人也能重現」。
三. 第一版 compose.yaml:Ollama + Open WebUI #
先放最小可用版本。
建立 compose.yaml:
services:
ollama:
image: ollama/ollama:latest
container_name: local-ollama
ports:
- "11434:11434"
volumes:
- ollama-data:/root/.ollama
restart: unless-stopped
open-webui:
image: ghcr.io/open-webui/open-webui:main
container_name: local-open-webui
ports:
- "3000:8080"
environment:
OLLAMA_BASE_URL: http://ollama:11434
volumes:
- open-webui-data:/app/backend/data
depends_on:
- ollama
restart: unless-stopped
volumes:
ollama-data:
open-webui-data:
啟動:
docker compose up -d
docker compose ps
打開瀏覽器:
http://localhost:3000
Open WebUI 會連到 Compose network 裡的 ollama:11434。
注意這裡不是 localhost:11434。
在容器裡,localhost 指的是容器自己,不是 host,也不是隔壁服務。
Compose 會幫每個 service 建立 DNS 名稱,所以服務之間用 service name 溝通。
這是 Compose 最值得記住的概念之一。
四. 下載模型:不要每次重來 #
Ollama 的模型會放在 /root/.ollama。
我們把它掛成 named volume:
volumes:
- ollama-data:/root/.ollama
所以容器重開時模型不會消失。 進容器下載模型:
docker compose exec ollama ollama pull llama3.2
docker compose exec ollama ollama list
測試 API:
curl http://localhost:11434/api/generate \
-H 'Content-Type: application/json' \
-d '{"model":"llama3.2","prompt":"用一句話解釋 Docker Compose。","stream":false}'
如果有 JSON 回應,就代表 Ollama API 正常。
這裡刻意把 11434 暴露到 host,是為了方便本機工具測試。
如果你的 stack 只給容器內部使用,也可以不要 publish port。
五. 加入自己的 FastAPI app #
接著做一個很小的 API,示範如何從自己的程式呼叫 Ollama。
建立 app/requirements.txt:
fastapi==0.111.0
uvicorn[standard]==0.30.1
httpx==0.27.0
redis==5.0.4
建立 app/main.py:
import os
import httpx
from fastapi import FastAPI
from pydantic import BaseModel
from redis import Redis
OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://ollama:11434")
MODEL_NAME = os.getenv("MODEL_NAME", "llama3.2")
REDIS_URL = os.getenv("REDIS_URL", "redis://redis:6379/0")
app = FastAPI(title="Local AI Stack")
redis = Redis.from_url(REDIS_URL, decode_responses=True)
class ChatRequest(BaseModel):
prompt: str
@app.get("/health")
def health():
redis.ping()
return {"status": "ok", "model": MODEL_NAME}
@app.post("/chat")
async def chat(req: ChatRequest):
cache_key = f"chat:{MODEL_NAME}:{req.prompt}"
if cached := redis.get(cache_key):
return {"cached": True, "response": cached}
payload = {"model": MODEL_NAME, "prompt": req.prompt, "stream": False}
async with httpx.AsyncClient(timeout=120) as client:
r = await client.post(f"{OLLAMA_BASE_URL}/api/generate", json=payload)
r.raise_for_status()
answer = r.json()["response"]
redis.setex(cache_key, 3600, answer)
return {"cached": False, "response": answer}
這個範例故意保持簡單。
/chat 會把 prompt 送給 Ollama,回應放進 Redis 快取一小時。
真實產品不一定會直接用完整 prompt 當 cache key,但教學範例這樣最容易看懂。
再建立 app/Dockerfile:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY main.py .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
把 api 和 redis 合併進前面的 Compose:
redis:
image: redis:7-alpine
volumes:
- redis-data:/data
restart: unless-stopped
api:
build: ./app
ports:
- "8000:8000"
environment:
OLLAMA_BASE_URL: http://ollama:11434
MODEL_NAME: ${MODEL_NAME:-llama3.2}
REDIS_URL: redis://redis:6379/0
depends_on:
- ollama
- redis
restart: unless-stopped
也別忘了在最下面多加一個 volume:
volumes:
redis-data:
重新啟動並測試:
docker compose up -d --build
curl http://localhost:8000/health
curl http://localhost:8000/chat \
-H 'Content-Type: application/json' \
-d '{"prompt": "用三點說明本地 LLM stack 的優點。"}'
第一次會比較慢,因為模型需要載入。 第二次如果 prompt 一樣,Redis cache 會直接回應。
六. 用 .env 管理模型名稱 #
Compose 會自動讀取同層的 .env。
建立 .env:
MODEL_NAME=llama3.2
之後想換模型,只要改這裡:
MODEL_NAME=qwen2.5:3b
然後:
docker compose exec ollama ollama pull qwen2.5:3b
docker compose up -d
.env 很適合放「開發環境可調的設定」。
但不要把秘密直接 commit 進 repo。
如果未來接外部 API key,請改用 .env.example 示範欄位,真正的 .env 加進 .gitignore。
七. 加上 healthcheck,讓等待更可靠 #
前一篇 Docker Compose 進階 提過,depends_on 只代表啟動順序,不代表服務 ready。
對本地 AI stack 來說,Ollama 容器啟動後,API 可能還沒準備好;Redis 也可能還在初始化。
可以加 healthcheck:
services:
ollama:
healthcheck:
test: ["CMD", "ollama", "list"]
interval: 10s
timeout: 5s
retries: 10
redis:
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 10
api:
depends_on:
ollama:
condition: service_healthy
redis:
condition: service_healthy
這樣 API 會等 Ollama 與 Redis 通過 healthcheck 後再啟動。 它不是萬靈丹;模型是否已經下載、第一次載入會不會很慢,仍然要在應用層處理。 但它可以避免很多「容器起來了但服務還不能用」的小坑。
八. GPU 與平台注意事項 #
Ollama 在不同平台的容器支援狀況會不太一樣。 Linux + NVIDIA GPU 通常需要 NVIDIA Container Toolkit,Compose 可能會長這樣:
services:
ollama:
image: ollama/ollama:latest
gpus: all
macOS 的情況又不同。 Docker Desktop 跑 Linux VM,容器不一定能像原生 Ollama app 那樣完整吃到 Apple Silicon 的加速能力。 所以拍拍君會這樣選:
- 只是要可重現 demo:Ollama in Docker 很方便
- 要最大化 Mac 本機效能:host 跑 Ollama,Compose 裡的 app 連 host
- 要在 Linux GPU server 上部署:Ollama container + GPU 設定比較合理 如果選擇 host 跑 Ollama,API service 可以這樣設定:
services:
api:
environment:
OLLAMA_BASE_URL: http://host.docker.internal:11434
在 macOS / Windows Docker Desktop,host.docker.internal 通常可用。
Linux 則可能要額外設定 extra_hosts: ["host.docker.internal:host-gateway"]。
本地 AI 的「本地」不是一種單一部署方式,而是一組取捨。
九. profiles:把可選工具收起來 #
開發 AI app 時,很容易想加一堆輔助服務:RedisInsight、Qdrant、LiteLLM、Prometheus、Grafana。 但不是每天都要開全部。 Compose profiles 很適合管理可選工具。 例如加一個 RedisInsight:
services:
redisinsight:
image: redis/redisinsight:latest
ports:
- "5540:5540"
profiles:
- tools
depends_on:
- redis
平常啟動不會開它:
docker compose up -d
需要工具時才開:
docker compose --profile tools up -d
這比把 YAML 註解來註解去乾淨多了。
十. 常見踩雷清單 #
1. 容器裡不要用 localhost 找其他 service #
在 api 裡要用 http://ollama:11434,不是 http://localhost:11434。
除非 Ollama 跟 API 在同一個容器裡,否則 localhost 幾乎一定錯。
2. 模型資料要放 volume #
如果沒有 ollama-data:/root/.ollama,容器刪掉後,模型可能就跟著不見。
下次又要重新下載,很浪費時間。
3. 第一次請先 pull 小模型 #
不要一開始就拉超大模型,然後怪 Compose 不好用。 先用 3B 或 4B 驗證流程。
4. timeout 要設長一點 #
LLM 第一次載入可能超過一般 HTTP client 的預設 timeout。
範例裡用 httpx.AsyncClient(timeout=120),實務上也可以拆成 connect/read/write/pool。
5. 不要把聊天資料亂放進 image #
image 應該是可重建的程式與依賴。 資料則交給 volume、資料庫或外部儲存。
十一. 什麼時候不要這樣做? #
Compose 很好用,但不是所有情境都該硬塞進 Compose。
如果你只是在週末試一個 prompt,ollama run llama3.2 就夠了。
如果你要長期服務多個使用者、有權限控管、監控、滾動部署、資源排程,Compose 可能很快不夠。
那時候才考慮 Kubernetes、Nomad,或雲端模型平台。
拍拍君很喜歡 Compose 的原因,是它剛好站在中間:比一堆手動指令可靠,比 Kubernetes 輕很多,比口頭環境說明可重現,也很適合 prototype、內部工具、教學 demo。
本地 AI 開發最怕的不是模型不夠聰明,而是環境太亂,讓你每次想實驗都要先修水管。
結語 #
今天我們把 Ollama、Open WebUI、FastAPI 與 Redis 串成一個本地 AI 開發環境。 你現在有一個可以一鍵啟動的 stack:
docker compose up -d --build
也可以一鍵收掉:
docker compose down
如果想連資料一起清掉,才使用:
docker compose down -v
小心,-v 會刪掉 volumes,模型也可能一起被刪掉。
拍拍君的建議是:先把這份 stack 跑起來,確認 Open WebUI 可以聊天,FastAPI 可以呼叫 /chat。
然後再加你真正需要的東西:文件 loader、向量搜尋、背景 worker、簡單 auth、metrics。
本地 AI 不必一開始就很壯觀;先讓它可重現、可啟動、可 debug,才會從玩具慢慢變成工具。🐳