快轉到主要內容
  1. 教學文章/

Docker Compose + Ollama:一鍵啟動本地 AI 開發環境

·8 分鐘· loading · loading · ·
Docker Docker-Compose Ollama LLM Local AI Devops
每日拍拍
作者
每日拍拍
科學家 X 科技宅宅
目錄
DevOps - 本文屬於一個選集。
§ 5: 本文

featured

一. 前言:本地 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"]

apiredis 合併進前面的 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,才會從玩具慢慢變成工具。🐳

延伸閱讀
#

DevOps - 本文屬於一個選集。
§ 5: 本文

相關文章

Docker Compose 進階:healthcheck、profiles 與 dev/prod 設定分離
·6 分鐘· loading · loading
Docker Docker-Compose Devops Container Healthcheck Profiles
Docker Compose:多容器服務編排實戰
·5 分鐘· loading · loading
Docker Docker-Compose Devops Container 部署
Kubernetes 入門:Pod, Service, Deployment 一次搞懂
·10 分鐘· loading · loading
Kubernetes K8s Devops Container Docker Pod Deployment
Streamlit + Ollama:打造本地 LLM Chatbot App
·11 分鐘· loading · loading
LLM Ollama Streamlit Python Local AI Chatbot
Docker for Python:讓你的程式在任何地方都能跑
·6 分鐘· loading · loading
Python Docker Container Devops 部署
在 Mac/iPhone 生態跑本地 AI:Ollama、MLX 與行動端工作流
·9 分鐘· loading · loading
LLM Ollama Mlx Apple-Silicon IPhone Local AI