iT邦幫忙

2025 iThome 鐵人賽

DAY 27
0
Rust

Rust 後端入門系列 第 27

Day 27 Axum 專案使用 Docker 打包

  • 分享至 

  • xImage
  •  

接下來把開發的 Rust/Axum API 打包成可重現的容器影像,搭配 docker-compose 啟動 Postgres、Redis 以及 API,方便在開發/測試或小型生產環境快速部署。

為什麼要用 Docker

  • 可重現環境:容器把執行環境與相依打包起來,確保不同主機上行為一致,減少「在我機器可以但在伺服器不行」的問題。
  • 部署自動化:容器影像可在 CI 中建置並推到 registry,再在目標環境拉取並執行,容易整合 CI/CD。
  • 環境隔離:把 DB、cache、API 各自放在不同容器,便於擴展與管理。
  • 快速啟動測試環境:使用 docker-compose 可在本機或 CI 上快速啟動整組依賴(Postgres、Redis、API)。
  • 規模與可移植性:用同樣的影像可在本機、雲端虛機或雲端容器服務(ECS、GKE、Azure Container Instances)執行。

架構設計考量

  • 使用 multi-stage build:在第一階段用 Rust 工具鏈編譯 release binary;在第二階段把編譯出的 binary 與必要資源(例如 migrations、templates)複製進較小的基底映像。
  • 避免在 runtime 映像安裝 Rust toolchain(減少影像大小、攻擊面)。
  • 把敏感設定(DATABASE_URL、JWT_SECRET)透過環境變數或 secret 管理,不要寫死在映像。
  • 如果使用 sqlx::migrate! 在程式啟動時自動執行 migrations,確保 container 啟動順序(DB ready)或先做 healthcheck/retry。
  • 為生產環境考慮:使用非 root 執行、限制資源、做好日誌/監控與備份策略。

範例:Dockerfile

這個範例假設你的 Cargo.toml、src/、migrations/ 在專案目錄。程式名稱以 cargo 的 package.name(例如 sqlx_connect_demo)為準。

Dockerfile

# ---- Build stage ----
FROM rust:1.88 as builder
WORKDIR /usr/src/app

# 安裝 musl 工具鏈與必要套件
RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential musl-tools musl-dev pkg-config libssl-dev ca-certificates \
 && rm -rf /var/lib/apt/lists/*

RUN rustup target add x86_64-unknown-linux-musl

# 複製 Cargo 檔以利用 Docker cache
COPY Cargo.toml Cargo.lock ./

# 建一個暫時的 main.rs 以便下載依賴(cache trick)
RUN mkdir -p src
RUN echo 'fn main() { println!("dummy"); }' > src/main.rs

# 預先抓取依賴
RUN cargo fetch

# 複製專案檔案(不複製 target,因為 target 的檔案大,複製又慢又久,再加上內容都是可下載的依賴套件)
COPY src ./src
COPY migrations ./migrations
COPY tests ./tests
COPY .env .env

# 以 musl target 建置 release binary
RUN cargo build --release --target x86_64-unknown-linux-musl

# ---- Runtime stage(使用 scratch) ----
FROM scratch
# 請把 sqlx_connect_demo 換成你 Cargo.toml 裡的 package.name
COPY --from=builder /usr/src/app/target/x86_64-unknown-linux-musl/release/sqlx_connect_demo /sqlx_connect_demo

# 複製 CA certs(如果程式需要 TLS)
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt

EXPOSE 3000
ENTRYPOINT ["/sqlx_connect_demo"]

說明:

  • 在 builder 階段用 musl 產生一個靜態的 Linux ELF 二進位(x86_64-unknown-linux-musl),然後把該二進位複製到最小的 runtime 映像(scratch)以減少映像大小與攻擊面。
  • 產生可以在大部分 Linux 發行版上直接執行的靜態 binary、不依賴 runtime image 的系統套件。

範例:docker-compose.yml(包含 Postgres 與 Redis)

以下範例在同一個 compose network 內啟動三個服務:db、redis、api。檔名 docker-compose.yml:

services:
  db:
    image: postgres:18
    restart: unless-stopped
    environment:
      POSTGRES_USER: myapp
      POSTGRES_PASSWORD: myapp_pass
      POSTGRES_DB: myapp_db
    volumes:
      - db_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 5s
      timeout: 5s
      retries: 10
    networks:
      - mynet

  redis:
    image: redis:8
    restart: unless-stopped
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - mynet

  api:
    build:
      context: .
      dockerfile: Dockerfile
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped
    environment:
      DATABASE_URL: postgresql://myapp:myapp_pass@db:5432/myapp_db
      REDIS_URL: redis://redis:6379/
      JWT_SECRET: change_me_in_production
      RUST_LOG: info
    ports:
      - "3000:3000"
    networks:
      - mynet

volumes:
  db_data:
  redis_data:

networks:
  mynet:
    driver: bridge

說明重點:

  • db 與 redis 都有 healthcheck,api 的 depends_on 使用 service_healthy 確保 API 啟動前依賴已 ready。
  • DATABASE_URL、REDIS_URL、JWT_SECRET 請以實際生產設定替換,生產環境應使用 docker secrets 或外部 secret manager。
  • ports 對應 host:container(3000:3000)使外部可透過 localhost:3000 存取 API。

修復先前程式錯誤

我們先前的程式碼誤用了 macro! 這將會造成 build 錯誤,請把 handles delete_user 的 macro! 修改成:

let res = sqlx::query(
		r#"
		DELETE FROM users
		WHERE id = $1 AND caller_id = $2
		"#,
	)
	.bind(id)
	.bind(caller_id)
	.execute(&pool)
	.await
	.map_err(|e| internal_err(e))?;

另外,main.rs 中的 127.0.0.1 讓我們只能在 docker 容器內通訊,必須修改成 0.0.0.0,這樣暴露接口才能從外部連接。

let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
.await
.unwrap();

測試與常用指令

把 Dockerfile 和 docker-compose.yml 放在跟 Cargo.toml 同一個資料夾中,就能開始了。

建立與執行:

  1. local build 並啟動:

    • docker compose build

      [+] Building 116.7s (22/22) FINISHED
       => [internal] load local bake definitions                                                                         0.0s
       => => reading from stdin 540B                                                                                     0.0s
       => [internal] load build definition from Dockerfile                                                               0.1s
       => => transferring dockerfile: 1.49kB                                                                             0.0s
       => WARN: FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 2)                                     0.1s
       => [internal] load metadata for docker.io/library/rust:1.88                                                       1.5s
       => [internal] load .dockerignore                                                                                  0.0s
       => => transferring context: 2B                                                                                    0.0s
       => [builder  1/13] FROM docker.io/library/rust:1.88@sha256:af306cfa71d987911a781c37b59d7d67d934f49684058f96cf720  0.0s
       => [internal] load build context                                                                                  0.0s
       => => transferring context: 13.59kB                                                                               0.0s
       => CACHED [builder  2/13] WORKDIR /usr/src/app                                                                    0.0s
       => CACHED [builder  3/13] RUN apt-get update && apt-get install -y --no-install-recommends     build-essential m  0.0s
       => CACHED [builder  4/13] RUN rustup target add x86_64-unknown-linux-musl                                         0.0s
       => CACHED [builder  5/13] COPY Cargo.toml Cargo.lock ./                                                           0.0s
       => CACHED [builder  6/13] RUN mkdir -p src                                                                        0.0s
       => CACHED [builder  7/13] RUN echo 'fn main() { println!("dummy"); }' > src/main.rs                               0.0s
       => CACHED [builder  8/13] RUN cargo fetch                                                                         0.0s
       => [builder  9/13] COPY src ./src                                                                                 0.1s
       => [builder 10/13] COPY migrations ./migrations                                                                   0.0s
       => [builder 11/13] COPY tests ./tests                                                                             0.0s
       => [builder 12/13] COPY .env .env                                                                                 0.0s
       => [builder 13/13] RUN cargo build --release --target x86_64-unknown-linux-musl                                 113.1s
       => [stage-1 1/2] COPY --from=builder /usr/src/app/target/x86_64-unknown-linux-musl/release/sqlx_connect_demo /sq  0.1s
       => [stage-1 2/2] COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt        0.1s
       => exporting to image                                                                                             0.3s
       => => exporting layers                                                                                            0.2s
       => => writing image sha256:4c6bd3f093bbe92b20768bb7a45969d471e83477a715ebdc848b140beb18824f                       0.0s
       => => naming to docker.io/library/sqlx_connect_demo-api                                                           0.0s
       => resolving provenance for metadata file                                                                         0.0s
      [+] Building 1/1
       ✔ sqlx_connect_demo-api  Built                                                                                    0.0s
      
    • docker compose up(關閉視窗或按下 Ctrl + C,程式就會結束)

      [+] Running 2/3
       ✔ Container sqlx_connect_demo-db-1     Running                                                                    0.0s[+] Running 3/3
       ✔ Container sqlx_connect_demo-db-1     Running                                                                    0.0s
       ✔ Container sqlx_connect_demo-redis-1  Running                                                                    0.0s
       ✔ Container sqlx_connect_demo-api-1    Recreated                                                                  0.1s
      Attaching to api-1
      api-1  | 成功建立 PgPool
      api-1  | 2025-10-10T01:47:13.461193Z  INFO relation "_sqlx_migrations" already exists, skipping
      api-1  | 成功完成 migrations
      api-1  | 2025-10-10T01:47:20.711707Z  INFO finished processing request latency=0 ms status=303
      api-1  | 2025-10-10T01:47:20.732604Z  INFO finished processing request latency=0 ms status=200
      api-1  | 2025-10-10T01:47:20.804000Z  INFO finished processing request latency=0 ms status=200
      api-1  | 2025-10-10T01:47:20.806666Z  INFO finished processing request latency=0 ms status=200
      api-1  | 2025-10-10T01:47:20.825082Z  INFO finished processing request latency=0 ms status=200
      api-1  | 2025-10-10T01:47:20.840708Z  INFO finished processing request latency=0 ms status=200
      api-1  | 2025-10-10T01:47:20.863574Z  INFO finished processing request latency=0 ms status=200
      api-1  | 2025-10-10T01:47:20.980924Z  INFO finished processing request latency=0 ms status=200
      api-1  | 2025-10-10T01:47:21.442509Z  INFO finished processing request latency=0 ms status=200
      
  2. 後台模式(不必一直開啟視窗):

    • docker compose up -d
  3. 檢查 log:

    • docker compose logs -f api
    • docker compose logs -f db
  4. 停止:

    • docker compose stop
  5. 停止並移除(-v 會刪除 volumes):

    • docker compose down -v

實務上的注意事項與建議

  1. 管理 secrets:
    • 不要把密碼或 JWT secret 寫在 docker-compose.yml 或 Dockerfile。生產環境用 Docker secrets、或雲端 KMS(AWS Secrets Manager / Azure Key Vault / GCP Secret Manager)。
    • 在 CI 中把敏感參數透過環境變數注入。
  2. 日誌與監控:
    • 容器應把日誌輸出到 stdout/stderr,讓 docker logging driver 或聚合工具(ELK/Prometheus/Grafana)收集。
    • 考慮加入 /metrics endpoint(Prometheus)與追蹤(OpenTelemetry)以便監控。
  3. 安全與精簡映像:
    • 考慮使用 distroless 或 musl static 節省映像大小與攻擊面。
    • 建置時去除不必要的套件與 build 工具,在 runtime 階段只留下必要檔案。
    • 避免以 root 權限執行容器。
  4. 資源限制:
    • 在 production 加上 resource limits(memory、cpu)避免單一容器耗盡主機資源。例:deploy on docker swarm 或 k8s 時設定 resource requests/limits。
  5. 健康檢查 / readiness:
    • 在容器內加入 health endpoint(例如 /healthz)讓 orchestrator(docker swarm / k8s)知道服務是否健康,避免流量導向剛啟動但未準備好的容器。

總結

  • Docker + docker-compose 能快速把你的 Rust/Axum API 與相依服務(Postgres、Redis)打包並一起啟動,帶來可重現的執行環境、方便的測試/開發流程,以及良好的 CI/CD 整合能力。
  • 重點在於使用 multi-stage build 生成 release binary、把敏感資訊交給環境或 secret 管理、在 compose 中定義 healthcheck 與 volumes,以及考慮 production 細節(非 root、資源限制、監控、日誌與安全)。

上一篇
Day 26 Axum 加入 Swagger
下一篇
Day 28 Axum 整合 GitHub Actions 完成 CI
系列文
Rust 後端入門30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言