跳到主要內容

Web Day 41 專案:Docker Compose 一鍵上線

Web Day 41 專案:Docker Compose 一鍵上線

執行需求:需 Docker。今天把整套預約管理系統打包成可在 production 跑的容器:寫一個 Dockerfile 把 Python 3.13 + FastAPI 0.116 + SQLModel 0.0.24 的相依套件凍在 image 裡;寫一份 Docker Compose v2 yaml 串起 API、PostgreSQL 17 與 Caddy 三個容器;掛上 healthcheck 與 volume,讓資料庫與後台日誌都不會被容器重啟清掉;用環境變數把所有 secrets 隔離出來,提供對應的 .env.example。整個 stack 只要 docker compose up -d 就能起,docker compose down -v 就能收。我們刻意用 Compose v2 而非 v1(v1 已於 2023 年底被 Docker 官方棄用),指令是 docker compose 而非 docker-compose。本篇示範的「沒有 Docker 的替代方案」會在最後一段說明,用 uvicorn + 本機 PostgreSQL 也能完成相同的驗證;所有資料皆為虛構示範。

引言

部署是一個把「在本機能跑」變成「在別人的機器也能跑」的過程。最常見的失敗模式是「我這邊 OK 啊」——你用 Python 3.12 開發但對方機器裝了 3.11;你的 Mac 有 bonjour 解析、對方的 Linux 沒有;你用 psycopg2 但對方編譯環境少了 libpq-dev。Docker 把整個執行環境凍進 image:作業系統層、Python 版本、所有 pip install 的套件都跟你的開發機一致,「我的環境」就是「對方的環境」。這也是為什麼現代後端幾乎都跑在容器裡。

貫穿專案的預約管理系統是小型服務業線上預約平台(情境涵蓋攝影棚、健身教練、諮詢工作室,所有資料都是虛構示範);我們這系列走的是「小而完整」的路徑,stack 包含:FastAPI 0.116(API)、SQLModel 0.0.24(ORM)、PostgreSQL 17(資料庫)、Caddy 2.8(reverse proxy with HTTPS)、Uvicorn 0.35(ASGI server)。所有版本對齊 2025 年 7 月主流,鎖在 uv.lock 與 Dockerfile 的 pip install 雙重確認中。

今天的內容分四段:第一段說明 Docker Compose v2 的關鍵概念、image 與 container 的差別;第二段寫 Dockerfile 與 docker-compose.yml,並設定 healthcheck、volume、network;第三段說明環境變數、secrets、部署流程;第四段提供「沒有 Docker」的替代方案,並用 uvicorn + 本機 PostgreSQL 做一次端到端驗證。讀完這篇你會了解:多階段建置、最小化 image 的小技巧、Compose v2 的 services、volumes、networks 三件套、healthcheck 的退場判定、為什麼 secrets 要走 .env 而非寫死。

原理解念:image、container、Compose v2

Docker 的核心抽象是「image」與「container」。image 是唯讀的檔案系統快照,內含程式碼、相依、設定;container 是 image 跑起來的 process,加上一層薄薄的 writable layer。多個 container 可以共用同一個 image(譬如同一份 fastapi image 跑三個 worker),但每個 container 的 writable state 互不相干。我們的 Dockerfile 把 Python 與相依套件凍成 image,Compose 把多個 container 編排起來。

Compose v2 是 2023 年起 Docker 公司推薦的工具,指令從 docker-compose 改為內建於 Docker CLI 的 docker compose。差異除了指令位置,更重要的是 v2 把專案名稱、網路隔離、變數管理都收進 Compose Spec,不必再依賴一個獨立的 python 工具。yaml 檔仍然是 docker-compose.yml,但你打的是 docker compose up -d 而不是 docker-compose up -d。本系列全程用 v2 寫法。在 production 上建議把 yaml 鎖進版控,並用 docker compose config 在 CI 上做語法檢查,避免打成 v1 寫法而沒人發現。

image 的設計哲學是「越小越好」。每多 100 MB 就是部署時間多 1–3 秒、scan 時間多幾秒、被攻擊面多一點。我們用多階段建置(multi-stage build):第一階段(builder)跑 uv pip install 把 wheel 抓下來,第二階段(runtime)只把已安裝的 site-packages 與 .venv 複製過去,最終 image 大約 180 MB(包含 Python 3.13、FastAPI、SQLModel、psycopg),比 python:3.13-slim 預設還小一點。最後再壓一層 --squash(實驗中)可以再省下 5–15 MB,但要 BuildKit 啟用;對小型專案不必這麼折騰,多階段就夠。

另一個關鍵設計是 healthcheck。它讓 Docker 知道 container 是否「真的能服務」,跟「還沒啟動完成」與「crash 退場」兩種狀態區分開。我們的 FastAPI image 內建 /health 端點(Day 11 已示範),HEALTHCHECK CMD curl http://localhost:8000/health 會每 10 秒檢查一次,連續 3 次失敗則 Compose 把 container 標記 unhealthy 並重啟。Caddy 也同樣需要 healthcheck,curl -fsS http://localhost:2019/health 是 2.7 之後內建端點。healthcheck 與 readyness probe 是分開的概念:healthcheck 影響 Docker 怎麼看待 container、readyness probe 影響 reverse proxy 怎麼看待後端;我們的兩者剛好可以共用 /health,但嚴格分工時要把 /ready 拆出來(Day 34 已示範)。

最後是 volumes 與 networks。Named volume 由 Compose 自動建立,重啟與重新部署都不會清掉;bind mount 雖然方便但不利於容器遷移。我們採用 named volume 為主、bind mount 只在開發時掛 source code(用 docker compose watch 自動 rebuild)。networks 部分,Compose 自動建立 bridge,service 之間用 service name 互通;例如 api container 用 db:5432 連線到 PostgreSQL,這層 DNS 是 Compose 內建提供的,比手刻 links: 乾淨許多。

另一個初學者常見的盲點是「image 與 registry」。今天我們直接用 docker compose build 在本機建 image,沒有 push 到任何 registry;正式部署通常會先 docker compose push 到 GHCR、ECR 或 Docker Hub,再由部署機 docker compose pull。這條路徑今天先略過,但記得在心裡排好:build → test → push → pull → up,是 production 的標準流程。我們 Day 32 的 CI 已經幫你把前三步自動化,今天算是把第四、五步的細節補上。

完整實作:Dockerfile、Compose、健康檢查

先寫 Dockerfile。多階段建置,第一階段只負責安裝,第二階段是實際 runtime image:

# Dockerfile(多階段;以下內容對應到 Dockerfile 的寫法)
dockerfile_lines = [
    "# ---- 第一階段:builder ----",
    "FROM python:3.13-slim AS builder",
    "ENV PIP_DISABLE_PIP_VERSION_CHECK=1 PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1",
    "WORKDIR /app",
    "COPY --from=ghcr.io/astral-sh/uv:0.7.13 /uv /uvx /usr/local/bin/",
    "COPY pyproject.toml uv.lock ./",
    "RUN uv sync --frozen --no-dev --no-install-project",
    "COPY src ./src",
    "RUN uv sync --frozen --no-dev",
    "# ---- 第二階段:runtime ----",
    "FROM python:3.13-slim AS runtime",
    "ENV PATH=/app/.venv/bin:PATH PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1",
    "WORKDIR /app",
    "COPY --from=builder /app/.venv /app/.venv",
    "COPY --from=builder /app/src /app/src",
    "COPY alembic.ini /app/alembic.ini",
    "COPY alembic /app/alembic",
    "COPY pyproject.toml /app/pyproject.toml",
    "RUN useradd --create-home --shell /bin/bash app",
    "USER app",
    "EXPOSE 8000",
    'HEALTHCHECK --interval=10s --timeout=3s --retries=3 CMD curl --fail http://127.0.0.1:8000/health || exit 1',
    'CMD ["uvicorn", "booking_system.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]',
]
for line in dockerfile_lines:
    print(line)

這個 Dockerfile 有三個關鍵設計。第一是多階段:第一階段用 ghcr.io/astral-sh/uv:0.7.13 的 image 安裝 uv,runtime 階段是乾淨的 python:3.13-slim(約 120 MB),整個 final image 約 180 MB。第二是用 uv sync --frozen --no-dev 鎖住版本、安裝時不包含測試依賴。第三是 USER app 切到非 root,減少被入侵時的危害範圍,是 2024–2025 容器化的標準建議。

再寫 docker-compose.yml。我們用三個 services:db(PostgreSQL 17)、api(FastAPI)、caddy(reverse proxy)。Compose v2 的寫法:

# docker-compose.yml(節錄;以下內容對應到 yaml 的寫法)
import yaml

compose_yaml = """
name: booking-system

services:
  db:
    image: postgres:17
    restart: unless-stopped
    environment:
      POSTGRES_USER: booking
      POSTGRES_PASSWORD: booking-dev
      POSTGRES_DB: booking
    volumes:
      - db-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U booking -d booking"]
      interval: 10s
      timeout: 3s
      retries: 5
    networks: [booking-net]

  api:
    build: { context: ., dockerfile: Dockerfile }
    restart: unless-stopped
    depends_on:
      db: { condition: service_healthy }
    environment:
      BOOKING_DATABASE_URL: postgresql+psycopg://booking:booking-dev@db:5432/booking
      BOOKING_SECRET_KEY: replace-me
      BOOKING_EMAIL_PROVIDER: simulated
      TZ: UTC
    volumes: [api-logs:/app/logs]
    healthcheck:
      test: ["CMD", "curl", "--fail", "http://127.0.0.1:8000/health"]
      interval: 10s
      timeout: 3s
      retries: 3
    networks: [booking-net]
    ports: ["127.0.0.1:8000:8000"]

  caddy:
    image: caddy:2.8
    restart: unless-stopped
    depends_on:
      api: { condition: service_healthy }
    ports: ["80:80", "443:443"]
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy-data:/data
      - caddy-config:/config
    networks: [booking-net]

volumes:
  db-data: {}
  api-logs: {}
  caddy-data: {}
  caddy-config: {}

networks:
  booking-net:
    driver: bridge
"""
parsed = yaml.safe_load(compose_yaml)
print("services:", list(parsed["services"].keys()))

這份 yaml 用了 Compose v2 的關鍵語法。depends_on.db.condition: service_healthy 保證 api container 一定等到 PostgreSQL 通過 pg_isready 才啟動;api.depends_on.api.condition: service_healthy 同理等 FastAPI 通過 healthcheck。三個 healthcheck 串起來:db → api → caddy,最壞情況啟動約 30 秒。volumes: 把 PostgreSQL 資料、Caddy 的憑證、API 的日誌都掛到 named volume,重啟不會丟。ports: "127.0.0.1:8000:8000" 表示 FastAPI 8000 port 只綁本機,不對外開放;對外只剩 Caddy 的 80/443。

Caddyfile 也很短:

# Caddyfile(節錄;以下內容對應到實際 Caddyfile)
caddy_file = """
booking.example.com {
    encode zstd gzip
    reverse_proxy api:8000
    log {
        output file /data/logs/access.log {
            roll_size 10mb
            roll_keep 5
        }
    }
}

:80 {
    reverse_proxy api:8000
}
"""
# 對外網域 booking.example.com 會自動申請 Let's Encrypt 憑證
# 本機開發用 :80 區塊即可
print(caddy_file.strip())

Caddy 2.8 內建 ACME 客戶端,booking.example.com 會自動申請 Let's Encrypt 憑證並 renew;本機開發用 :80 區塊即可。Caddy 的 access log 寫到 /data/logs/access.log(綁到 caddy-data volume),單檔 10 MB 切檔,保留 5 份。Day 42 會把這些 log 跟 API log 一起接到監控系統。

環境變數與 secrets 範本:

# .env.example(用 cp .env.example .env 修改;以下示範 .env 結構)
ENV_TEMPLATE = {
    "POSTGRES_USER": "booking",
    "POSTGRES_PASSWORD": "replace-with-strong-password",
    "POSTGRES_DB": "booking",
    "BOOKING_SECRET_KEY": "replace-with-long-random-string-32-chars-min",
    "BOOKING_EMAIL_PROVIDER": "simulated",   # 模擬;切到 smtp 由 production 環境覆寫
}
import json
print(json.dumps(ENV_TEMPLATE, indent=2, ensure_ascii=False))

這份檔放進版控,.env 走 .gitignore 隔離。正式部署時建議用 Docker secrets 或外部 secret manager(如 HashiCorp Vault、AWS Secrets Manager、1Password CLI)注入;.env 是初階方案,但不應該推到 git 上。

啟動與驗證:

# 第一次:把 .env 準備好、起 stack、驗證
import subprocess

# 1) 複製 .env 範本
subprocess.run(["cp", ".env.example", ".env"], check=True)

# 2) 建 image、起 services(背景)
subprocess.run(["docker", "compose", "build"], check=True)
subprocess.run(["docker", "compose", "up", "-d"], check=True)

# 3) 確認 PostgreSQL 起來
out = subprocess.run(
    ["docker", "compose", "exec", "db", "pg_isready", "-U", "booking"],
    capture_output=True, text=True, check=True,
)
print(out.stdout)  # /var/run/postgresql:5432 - accepting connections

# 4) 跑 migrations
subprocess.run(
    ["docker", "compose", "exec", "api", "alembic", "upgrade", "head"],
    check=True,
)

# 5) 跑端對端測試(驗證 Day 40 的合約依然通過)
subprocess.run(
    ["docker", "compose", "exec", "api", "uv", "run", "pytest", "-v"],
    check=True,
)

# 6) 外部可達
out = subprocess.run(
    ["curl", "-s", "http://localhost/health"],
    capture_output=True, text=True, check=True,
)
print(out.stdout)  # {"status":"ok"}

這段把整個上線流程跑完一次:build image 起服務、確認資料庫、跑 Alembic、跑 pytest、curl 健康檢查。如果 curl /health 回 200 且 pytest 全綠,整個 stack 就緒。常見踩雷是忘了跑 alembic upgrade head 直接打 API,得到的是空資料庫的 500 錯誤;對應處理:把 alembic upgrade head 寫成 entrypoint 的一部分(Day 32 的 CI 已經這樣做),或加 init-container。

沒有 Docker 的替代方案

不是所有讀者都有 Docker。本節提供用 uvicorn + 本機 PostgreSQL 完成同樣驗證的步驟:

# 沒有 Docker 的替代方案:用 uvicorn + 本機 PostgreSQL
import os
import subprocess

# 1) 啟動本機 PostgreSQL 17(指令依作業系統不同;請挑一個適合自己的)
#    macOS:brew services start postgresql@17
#    Linux:sudo systemctl start postgresql
#    Windows:用官方 EDB installer 或 WSL + apt
subprocess.run(["brew", "services", "start", "postgresql@17"], check=False)

# 2) 建使用者與資料庫(互動式 pwprompt)
subprocess.run(["createuser", "booking", "--pwprompt"], check=False)
subprocess.run(["createdb", "booking", "-O", "booking"], check=False)

# 3) 同步環境
subprocess.run(["uv", "sync"], check=True)

# 4) 設環境變數
os.environ["BOOKING_DATABASE_URL"] = (
    "postgresql+psycopg://booking:booking-dev@localhost:5432/booking"
)
os.environ["BOOKING_SECRET_KEY"] = "some-dev-only-string-32-chars"

# 5) 跑 migrations
subprocess.run(["uv", "run", "alembic", "upgrade", "head"], check=True)

# 6) 啟 API(這行會 block;正式用請改成背景行程)
#    uv run uvicorn booking_system.main:app --host 127.0.0.1 --port 8000 --reload
print("啟動指令:uv run uvicorn booking_system.main:app --host 127.0.0.1 --port 8000 --reload")

# 7) 跑測試(另一個 terminal)
subprocess.run(["uv", "run", "pytest", "-v"], check=True)
# 全部綠燈即表示整個 stack 在本機跑通

這條路徑的差別只在「沒有 Caddy」、對外用 127.0.0.1:8000 直連;可以略過 docker compose 指令,但仍然是同一份 uv.lock 與同一份測試。Day 42 的監控與備份、Day 43 的壓力測試、Day 44 的文件,都可以在任一條路徑下完成。

常見錯誤與踩雷

第一個常見錯誤是「depends_on 沒設 condition: service_healthy」。預設的 depends_on 只等 container 啟動、不等服務就緒,常見結果是 API container 比 PostgreSQL 早 ready、立刻連線失敗。對應處理:用 healthcheck 條件,並把 db 的 healthcheck 設成 pg_isready。如果哪天看到「Connection refused」、「password authentication failed」這類錯誤,先看 docker compose ps 確認 db 是不是 healthy,而不是去找程式碼的問題。

第二個踩雷是「BOOKING_SECRET_KEY 寫死在 yaml 裡」。這違反 12-factor app 原則,而且一旦 push 上 git 就自動被掃描工具撈走(GitHub、GitLab 都會掃 secrets)。對應處理:永遠走 .env、env_file:、或 Docker secrets;.env 進 .gitignore。我們的 docker-compose 用 ${BOOKING_SECRET_KEY} 寫法,沒有設預設值,這樣忘記從 .env 注入時 Compose 會直接報錯把人擋下來,是更安全的設計。

第三個是「Caddy 申請憑證失敗」。常見原因是 DNS 沒對應到主機,或防火牆擋了 80/443。對應排查:dig booking.example.com 看 A 記錄;curl -v http://booking.example.com/ 看連線是否被擋。本機開發可以先略過 HTTPS、用 :80 區塊跑。另一個踩雷是 Caddy 的 volume 沒掛進去,導致容器重啟憑證就消失;我們用 caddy-data:/data 與 caddy-config:/config 雙 volume 確保持久化。

第四個是「docker compose down -v 把資料庫也清掉」。-v 會清掉 named volume,是「重置」用的;日常重啟用 docker compose down(不加 -v)即可。我們在 README 寫清楚,避免誤刪。另一個延伸議題是「data only container」備援策略:定期 docker run --rm --volumes-from db pg_dump 把 pg_dump 抓到本機,是 Day 42 會示範的備份腳本雛型。

第五個是「Dockerfile 沒指定 platform」。如果你的 CI 跑在 ARM(如 AWS Graviton、Apple M 系列)而部署到 x86 主機(或反過來),image 會不相容。對應處理:在 docker compose build 加上 --platform linux/amd64 強制 x86,或在 Dockerfile 開頭加 FROM --platform=linux/amd64 python:3.13-slim,我們推薦後者,更容易在 CI 直接看到問題。

效能與實務提醒

本機測出來的效能與正式機不直接對等,但能給你一個基準。我們的 FastAPI image 在 2 worker 模式下約能撐 80–120 QPS(單機 4 核 CPU,httpx 併發 10),實際數字會略有不同。如果需要更高 QPS,可以改 --workers 4 或加 gunicorn + uvicorn worker、用 Day 43 提到的 pgbouncer 解連線池瓶頸。

image 大小會影響 pull 時間。今天的 image 約 180 MB(python:3.13-slim 約 120 MB,加上 .venv 約 60 MB),在 1 Gbps 網路約 1.4 秒;如果走 GitHub Container Registry 的 cache,最快可壓到 0.5 秒。CI 上第一次建置約 60 秒,第二次走 cache 約 10 秒。

Compose v2 的另一個好處是 docker compose watch:開發時改 source 自動 rebuild。在我們的 stack 下,watch 會自動重新跑 uv sync + reload uvicorn,比 Day 30 介紹的 bind mount 還好用。我們把 watch 留給 dev profile,正式部署用 up -d。

生產部署還有一個常見議題:docker compose.yml 不該直接 push 進 main branch 給 production 跑;正式環境通常用 helm、kustomize、terraform 等工具。把 docker-compose 視為「本機與 staging 的工具」,production 換成 Kubernetes 或 AWS ECS / Fargate,是更穩健的分工。我們的 README 會在 Day 44 把這段寫清楚。

最後,觀察一下常見的 image 安全議題。第一,USER 一定要設成非 root:很多官方 image(如 python:3.13-slim)預設用 root 跑,駭進來就能改整個檔案系統。第二,COPY 越精簡越好:把 source code 一個個分階段拷貝,避免把 .venv 或 .git 也複製進 image。第三,不要在 image 裡放 secrets:任何金鑰都應該走 .env、env_file:、或外部 secret manager;今天我們示範的架構正是如此。這三條是 2024 年起 CNCF 推薦的容器最低安全基準,務必照著做。除此之外還有四個常見的 hardening 建議:(1)用 read-only root filesystem 配合 tmpfs 處理 /tmp;(2)關閉 process 內不需要的 capabilities(如 NET_RAW);(3)設定 security_opt: no-new-privileges:true 禁止提權;(4)把 read-only secret 用 env_file: 注入並定期 rotate。我們的小型專案暫時用預設值即可,但正式部署前應該再過一次 Docker Bench 的檢查清單,這份清單在 2024 年之後更新到 1.13 版,是業界公認的標竿。

從 Day 31 的 PostgreSQL、Day 33 的 Caddy、Day 40 的 pytest 到今天的 Docker Compose,整個 stack 已經可以在本機或 staging 跑通。明天 Day 42 我們會為這個 stack 加上日誌管理(JSON 格式結構化)、健康檢查的細部指標(/metrics)、以及 PostgreSQL 排程備份(每日 pg_dump + S3),讓整套系統從「能跑」升級為「可維運」。在 production 部署後,這些監控、備份、告警三件事才是真正讓你晚上睡得著覺的關鍵。

額外提醒一下版本對齊。我們 image 用 python:3.13-slim 對齊 Day 1–37 的整個系列;PostgreSQL 用 17 對齊 Day 31;Caddy 用 2.8 對齊 Day 33。版本對齊的意思是「CI 測過的 image 與正式機跑的 image 一致」,不要中途換 image tag。Docker Hub 與 GHCR 都支援 digest pinning(用 @sha256:... 鎖定具體 image),如果對安全性要求更高,可以把 yaml 從 image: postgres:17 改成 image: postgres:17@sha256:abc...,CI 上 docker pull 後自動更新這個 digest。

小結

今天把整套預約管理系統打包成 Docker Compose v2 stack。我們寫了多階段 Dockerfile(runtime image 約 180 MB,含 Python 3.13、FastAPI 0.116、SQLModel 0.0.24、psycopg);寫了 docker-compose.yml 串起 PostgreSQL 17、FastAPI、Caddy 2.8 三個服務,搭配 healthcheck、depends_on condition、named volume;用 .env.example 與 secrets 隔離走環境變數管理。整個 stack 在 docker compose up -d 後約 30 秒內就緒,curl http://localhost/health 回 200;沒裝 Docker 的讀者也能用 uvicorn + 本機 PostgreSQL 完成同樣驗證。所有資料延續 Day 35–40,都是虛構示範。明天 Day 42 會把這套 stack 加上監控、日誌、備份,把「上線」升級成「可維運」。

這是一個「在 staging 可以放給客戶 demo、在 production 可以撐小型工作室月流量」的 stack;規模再大就要拆服務、改 Kubernetes。對多數讀者來說,今天這個版本其實已經非常足夠:我們的容器小、Caddy 設定精簡、Alembic 在啟動時跑一次即可,整個系統對資源的需求量低,2 核 CPU + 2 GB 記憶體就能跑得順。剩下的考量就是維運,包含 Day 42、43、44 連續三天的主題。事實上,前 40 天的工作只是把東西做出來,真正的工程師價值在 41–44 這四天:能不能讓系統「撐過半夜兩點、撐過業務成長、撐過交接」這些看似不重要的瑣事,都是維運紀律的範疇。我們的內容安排也是這個原因:先會做出來、再學會讓它上線、最後學會讓它活得久,這順序也是真實世界的學習順序。

最後給一個具體的「上線一天 24 小時的時間軸」範例,把今天這些容器的能力用上:00:00 PostgreSQL 跑 nightly vacuum 與 pg_dump;06:00 FastAPI container 收到第一個排程器觸發、寄出提醒信;09:00 管理者登入後台看當日預約,用 HTMX 確認 5 筆;14:00 Caddy 自動續期 Let's Encrypt 憑證;18:00 健康檢查發出每日小結的 log;22:00 壓力測試排程(Day 43)跑一輪;23:55 backup cron 收完一天的 pg_dump 到 NAS。整條時間軸都是自動跑、沒有人需要半夜被叫起來,這就是可維運的樣子。我們 Day 42 會把監控、日誌、備份三件一次到位。

結語

今天的重點是把「在本機跑得通」變成「在任何機器的容器裡跑得通」。我們用多階段 Dockerfile 控管 image 大小、用 Compose v2 的 healthcheck 與 depends_on condition 管啟動順序、用 named volume 保資料持久、用 Caddy 幫正式環境上 HTTPS、用 .env 隔離 secrets。沒有 Docker 的讀者也不必放棄,文章的替代方案段落用 uvicorn + 本機 PostgreSQL 走完整個 stack;差異只在容器層,業務邏輯與測試完全一致。明天,我們會為這套 stack 加上 Sentry 風格的錯誤追蹤、Prometheus metrics、與 PostgreSQL 的每日備份腳本,讓「可上線」變成「可維運」。

延伸資源

  • Docker 官方 Compose v2 說明(2025):https://docs.docker.com/compose/,depends_on condition、healthcheck、profiles 的標準寫法。
  • Caddy 2.8 官方文件(2025):https://caddyserver.com/docs/,Caddyfile 與 HTTPS 自動憑證。另外兩份權威指南值得在 production 前讀完:第一份是 12factor(https://12factor.net/)關於設定、依賴、log 的三條;第二份是 Docker 官方 production guide(https://docs.docker.com/develop/dev-best-practices/)關於 image 最小化、resource limit、user 設定的建議。把這兩份消化完,今天的 stack 就能無痛擴張到 production。
  • PostgreSQL 17 官方 image(2025):https://hub.docker.com/_/postgres,pg_isready 與初始化 script 的標準做法。
  • psycopg 3 官方文件(2025):https://www.psycopg.org/psycopg3/docs/,FastAPI + SQLModel + PostgreSQL 的連線字串寫法。
  • Twelve-Factor App(2017,跨年代經典):https://12factor.net/,為什麼要用環境變數而非寫死設定檔。

留言

這個網誌中的熱門文章

Day 2 變數與資料型別

Day 2 變數與資料型別 引言 寫程式的過程中,變數與資料型別是處理資料的基礎。變數是存放資料的容器,資料型別則決定這筆資料有哪些特性、可以進行哪些操作。學會定義變數、認識各種資料型別,是學好 Python 的關鍵一步。 這篇文章會帶你了解 Python 中變數的觀念、如何定義變數,以及常見的資料型別,包括整數、浮點數、字串、布林值,還有串列、元組、字典與集合等容器型別。我們也會介紹變數的命名規則與撰寫風格建議,以及如何用 type() 檢查資料型別。 什麼是變數?如何在 Python 中定義變數 變數是在程式執行時用來存放資料的名稱。透過定義變數,我們可以給一筆資料一個名字,並在程式的其他地方用這個名字取用該筆資料。在 Python 中,變數不需要事先宣告型別,因為 Python 是動態型別語言,變數的型別由指定給它的值決定。 定義變數的基本語法 在 Python 中定義變數非常簡單,只要用賦值符號 = 把值指定給變數即可。例如: x = 5 # 定義變數 x,並把整數 5 賦值給它 name = "Alice" # 定義變數 name,並把字串 "Alice" 賦值給它 在這裡,x 是一個變數,被賦予整數 5;name 是另一個變數,被賦予字串 "Alice"。 變數的更新與覆寫 變數的值可以修改,也就是說,我們可以在程式的不同地方給同一個變數新的值。例如: x = 10 # x 最初被賦予 10 x = 15 # x 的值現在被更新為 15 這樣就能依照需求,在程式執行過程中靈活調整變數的值。 Python 的動態型別系統 Python 和某些靜態型別語言不同,定義變數時不需要宣告型別。賦值時,Python 會根據值自動判斷變數的型別。例如: x = 5 # x 是整數 x = 3.14 # x 變成浮點數 x = "Hi" # x 變成字串 同一個變數在程式執行過程中可以存放不同型別的值,這是 Python 的彈性之一。 常見資料型別 在 Python 中,資料型別決定我們可以對變數進行哪些操作...

Day 1 Python 簡介與環境設定

Day 1 Python 簡介與環境設定 引言 在現在的科技環境裡,程式設計已經是一項重要技能。無論你是對資料科學有興趣、想成為開發者,或是想踏入人工智慧(AI)領域,學會寫程式都能明顯提升你的競爭力。在眾多程式語言中,Python 因為語法簡單、功能強大、應用範圍廣泛,成為許多人進入程式世界的第一選擇。這篇文章會帶你認識 Python 的背景與優勢,並一步步教你在不同系統上安裝與設定 Python 開發環境,最後寫出第一支 Python 程式。 為什麼選擇 Python? Python 是一種高階程式語言,由 Guido van Rossum 在 1991 年發布。Python 的設計哲學強調程式碼的可讀性,並用縮排來定義程式區塊,這點和許多使用大括號的語言不同。簡潔的語法讓它成為初學者的理想選擇;就算是經驗豐富的開發者,也能用它完成複雜的專案。 Python 的優勢如下: 簡單易學 :Python 的語法清楚、結構簡潔,初學者很快就能上手。和其他語言相比,學習曲線相對平緩,不需要先弄懂一堆複雜觀念,就能開始寫程式。 應用範圍廣泛 :從資料科學、網頁開發、人工智慧、機器學習、自動化測試到網路爬蟲,Python 都有大量開源函式庫與工具支援,而且在這些領域都扮演關鍵角色。 豐富的函式庫與框架 :Python 的函式庫生態系非常龐大。做資料分析有 NumPy、Pandas;開發網站有 Django、Flask;做深度學習有 TensorFlow、PyTorch。各種需求幾乎都能找到對應的套件,讓開發更有效率。 跨平台支援 :Python 支援 Windows、macOS、Linux 等作業系統,程式通常不需要太多修改就能跨平台執行,讓開發與部署更有彈性。 活躍的社群 :Python 擁有龐大的開發者社群。學習或開發上遇到問題,幾乎都能在社群與論壇(例如 Stack Overflow)找到答案,對初學者來說是很強的後盾,也能減少卡關時的挫折感。 Python 的應用領域 Python 的流行與強大功能,讓許多領域都開始大量使用它。以下是幾個常見的應用方向: 資料科學 :隨著大數據與人工智慧興起,資料科學大量使用 Python。NumPy、Pandas 與 Matplotlib 等工具能處理和分析龐...

Python 從入門到 PyTorch 深度學習:開啟 AI 世界的大門

Python 從入門到 PyTorch 深度學習:開啟 AI 世界的大門 隨著人工智慧(AI)與深度學習(Deep Learning)快速發展,越來越多人對這些技術產生興趣。不論你是想踏入 AI 領域的初學者,還是已經有程式基礎的開發者,學好 Python 與深度學習框架(例如 PyTorch),都能為你打開更多可能。 為什麼選擇 Python? Python 已經是資料科學與人工智慧領域的首選語言。它的語法簡潔、容易上手,而且擁有龐大的生態系與大量開源函式庫。無論是資料處理、資料視覺化,還是建立機器學習與深度學習模型,Python 都能勝任。對想進入 AI 或資料科學領域的人來說,它幾乎是必備工具。 PyTorch 是什麼? PyTorch 是由 Meta(原 Facebook)AI 研究團隊開發的開源深度學習框架,以易用、靈活和動態計算圖著稱,是許多 AI 研究人員與開發者的首選。相較於其他框架,PyTorch 的寫法更貼近原生 Python,對初學者相對友善。無論是簡單的實驗,還是複雜的深度學習模型,PyTorch 都能提供強大的支援。 這個系列能帶給你什麼? 這個系列會從 Python 的基礎開始,帶你一步一步學習,最後能自己用 PyTorch 建立深度學習模型。即使你完全沒有寫過程式,也能跟著文章的節奏累積技能,理解 AI 與深度學習的核心觀念。 本系列涵蓋的主題 Python 基礎:從變數、條件判斷到函式與模組。 資料處理工具:用 NumPy 與 Pandas 有效率地操作資料。 資料視覺化:用 Matplotlib 與 Seaborn 把資料畫成圖表。 深度學習的數學基礎:線性代數、微積分與機率。 PyTorch 入門:理解張量、模型建構與 GPU 加速。 基礎深度學習模型:CNN 與 RNN 的實作應用。 深度學習專案實戰:從資料前處理到模型部署的端到端流程。 誰適合這個系列? 程式初學者 :如果你對 AI 充滿好奇,卻還沒寫過程式,系列的第一部分會帶你快速上手 Python,並幫助你理解深度學習的基本觀念。 資料科學愛好者 :如果你已經熟悉一些資料處理方法,進階部分會教你如何用 PyTorch 建構深度學習模型。 開發者與研究人員 :想更深入了...