容器化技術 ch12 實戰:用 Compose 搭一整套開發環境
下一章→
CH 12 Compose 主線

實戰:用 Compose 搭一整套開發環境

Spring Boot + SQL Server + RedisSQL Server 初始化的真實陷阱健康檢查實作熱重載與遠端除錯dev/prod 分離

把 ch09 打包好的 Spring Boot 映像,和你已經在用的 SQL Server 組成一整套環境—— 新同事 clone 專案後只要一句 docker compose up,五分鐘內就有完整可跑的開發環境。

這一章給的是一份可以直接抄回專案用的完整設定,但重點在中間那個很多教學會講錯的地方:SQL Server 的官方映像沒有 docker-entrypoint-initdb.d 這個機制—— 那是 PostgreSQL 和 MySQL 才有的。所以「怎麼自動建資料表」在 SQL Server 上要另外想辦法。

架構

              你的電腦(宿主機)
 ┌──────────────────────────────────────────┐
 │  localhost:8080 ──┐   localhost:1433 ──┐ │
 └───────────────────┼────────────────────┼─┘
                     │  port mapping      │
 ┌───────────────────┼────────────────────┼─┐
 │ myapp_default 網路(Compose 自動建立)   │
 │   ┌──────────┐   ┌────────────┐  ┌──────┐│
 │   │   app    │──→│ sqlserver  │  │ redis││
 │   │ (Spring) │──────────────────→│      ││
 │   └──────────┘   └─────┬──────┘  └───┬──┘│
 └────────────────────────┼─────────────┼───┘
                    mssql-data      redis-data (volume)
  • app 連資料庫用 sqlserver:1433(服務名 + 內部埠,ch07)。
  • 你用 SSMS 連 localhost:1433(走 port mapping)。
  • 資料放 volume,down 之後還在(ch06)。

專案結構

myapp/
├── docker-compose.yml           # 共通定義(進版控)
├── docker-compose.override.yml  # 本機開發覆蓋(自動載入)
├── .env                         # 秘密與可變參數(gitignore!)
├── .env.example                 # 範本(進版控,給同事參考)
├── .dockerignore
├── Dockerfile
├── pom.xml
└── src/
🚨 .env 一定要加進 .gitignore,同時提供一份 .env.example(只有變數名沒有真值)。這是團隊專案的標準做法——新人知道要設定哪些變數,但 repo 裡沒有任何真實密碼。
name: myapp

services:
  # ========== 資料庫 ==========
  sqlserver:
    image: mcr.microsoft.com/mssql/server:2022-latest
    environment:
      ACCEPT_EULA: 'Y'
      MSSQL_SA_PASSWORD: ${MSSQL_SA_PASSWORD:?請在 .env 設定 MSSQL_SA_PASSWORD}
      MSSQL_PID: Developer          # 開發版授權(免費)
    ports:
      - '127.0.0.1:1433:1433'       # 只綁本機,同網段的人連不到(ch07)
    volumes:
      - mssql-data:/var/opt/mssql
    healthcheck:
      test:
        - CMD-SHELL
        - /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "$$MSSQL_SA_PASSWORD" -C -Q "SELECT 1" || exit 1
      interval: 10s
      timeout: 5s
      retries: 12
      start_period: 30s
    deploy:
      resources:
        limits:
          memory: 2G              # SQL Server 官方最低需求 2 GB
    restart: unless-stopped

  # ========== 快取 ==========
  redis:
    image: redis:7-alpine
    ports:
      - '127.0.0.1:6379:6379'
    volumes:
      - redis-data:/data
    command: redis-server --appendonly yes
    healthcheck:
      test: ['CMD', 'redis-cli', 'ping']
      interval: 10s
      timeout: 3s
      retries: 5
    restart: unless-stopped

  # ========== 資料庫結構初始化(一次性任務)==========
  db-init:
    image: mcr.microsoft.com/mssql-tools
    depends_on:
      sqlserver:
        condition: service_healthy   # 等 DB 真的可以連了才跑
    volumes:
      - ./db/init:/scripts:ro
    entrypoint:
      - /bin/bash
      - -c
      - /opt/mssql-tools/bin/sqlcmd -S sqlserver -U sa -P "$$MSSQL_SA_PASSWORD" -i /scripts/01-create-db.sql
    environment:
      MSSQL_SA_PASSWORD: ${MSSQL_SA_PASSWORD}
    restart: 'no'                    # 跑完就結束,不要重啟

  # ========== 應用程式 ==========
  app:
    build:
      context: .
      dockerfile: Dockerfile
    image: myapp:local
    ports:
      - '8080:8080'
    environment:
      SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE:-dev}
      SPRING_DATASOURCE_URL: 'jdbc:sqlserver://sqlserver:1433;databaseName=mydb;encrypt=true;trustServerCertificate=true'
      SPRING_DATASOURCE_USERNAME: sa
      SPRING_DATASOURCE_PASSWORD: ${MSSQL_SA_PASSWORD}
      SPRING_DATA_REDIS_HOST: redis
      SPRING_DATA_REDIS_PORT: '6379'
      TZ: Asia/Taipei
    depends_on:
      sqlserver:
        condition: service_healthy
      redis:
        condition: service_healthy
      db-init:
        condition: service_completed_successfully   # 等初始化跑完
    healthcheck:
      test: ['CMD', 'wget', '-qO-', 'http://localhost:8080/actuator/health']
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 60s
    deploy:
      resources:
        limits:
          memory: 1G
    restart: unless-stopped

volumes:
  mssql-data:
  redis-data:
💡 幾個刻意的設計:
• ${MSSQL_SA_PASSWORD:?...} —— 沒設定就直接報錯中止,比啟動後才神秘失敗好得多(ch11)。
• 127.0.0.1:1433:1433 —— 只綁本機,咖啡廳 Wi-Fi 上不會被人連(ch07)。
• trustServerCertificate=true —— SQL Server 2022 的 JDBC driver 預設要求加密連線,開發環境用自簽憑證必須加這個,否則會噴 SSL 握手失敗(極常見)。

如果你查過 PostgreSQL 或 MySQL 的教學,會看到這種寫法:

# PostgreSQL / MySQL 可以這樣(把 .sql 丟進去就自動執行)
volumes:
  - ./init:/docker-entrypoint-initdb.d
但 SQL Server 的官方映像完全沒有這個機制。你把 .sql 檔掛到那個路徑,它會被安靜地忽略——沒有錯誤訊息,只是什麼都不會發生。很多人在這裡卡很久,因為找不到任何線索。

三種可行的做法

做法 A:一次性 init 容器(上面設定用的方式)

  • 另開一個服務,用 mssql-tools 映像跑 sqlcmd 執行你的 SQL 腳本。
  • 靠 depends_on: condition: service_healthy 確保資料庫已就緒,再靠 service_completed_successfully 讓 app 等它跑完。
  • 優點:純 Compose 就能做到,職責清楚。
  • 缺點:每次 up 都會跑一次——你的 SQL 必須寫成可重複執行的(idempotent):
    IF DB_ID('mydb') IS NULL
        CREATE DATABASE mydb;
    GO
    IF OBJECT_ID('dbo.users') IS NULL
        CREATE TABLE dbo.users (...);
    GO

做法 B:自訂映像,包一層 entrypoint

FROM mcr.microsoft.com/mssql/server:2022-latest
USER root
COPY init.sql /init/
COPY entrypoint.sh /
RUN chmod +x /entrypoint.sh
USER mssql
ENTRYPOINT ["/entrypoint.sh"]

entrypoint.sh 先在背景啟動 SQL Server,輪詢等它就緒後執行腳本,最後 wait。缺點是要自己維護一個映像,而且容易寫錯 PID 1 的處理(ch05)。

做法 C:交給應用層的資料庫遷移工具(強烈推薦)

<!-- pom.xml -->
<dependency>
  <groupId>org.flywaydb</groupId>
  <artifactId>flyway-sqlserver</artifactId>
</dependency>

# src/main/resources/db/migration/V1__create_users.sql
# Spring Boot 啟動時自動執行未套用的遷移
⭐ 為什麼 Flyway/Liquibase 才是正解:
① 版本控管——每個 schema 變更都是一個帶版本號的檔案,跟程式碼一起進 Git。
② 環境一致——開發、測試、正式全都跑同一套遷移,不會有「正式環境少一個欄位」這種事。
③ 不依賴容器機制——不管資料庫跑在容器、VM 還是 RDS 都一樣運作。
④ 自動記錄狀態——它有一張表記錄哪些遷移跑過了,天生冪等。

Compose 只負責「把資料庫服務叫起來」,schema 的事交給應用層——這是職責分離的正確切法。

docker-compose.override.yml 會被自動載入並合併(ch11),拿來放只有本機開發才需要的東西最合適。

# docker-compose.override.yml —— 本機開發專用
services:
  app:
    build:
      target: dev              # 用 Dockerfile 裡的 dev 階段(有 shell 好除錯)
    environment:
      SPRING_PROFILES_ACTIVE: dev
      SPRING_DEVTOOLS_RESTART_ENABLED: 'true'
      JAVA_TOOL_OPTIONS: '-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005'
    ports:
      - '5005:5005'            # 遠端除錯埠
    volumes:
      - ./target/classes:/app/classes   # 編譯輸出掛進去,配合 DevTools 熱重載

  # 開發時才需要的資料庫管理介面
  adminer:
    image: adminer
    ports:
      - '8081:8080'
    profiles: ['tools']        # 預設不啟動,要用才 --profile tools

IDE 遠端除錯設定

  • IntelliJ IDEA:Run → Edit Configurations → 新增 Remote JVM Debug → Host: localhost, Port: 5005。
  • 連上之後,中斷點就跟本機執行一樣好用——這是很多人不知道容器也能做到的事。
  • suspend=n 表示不等除錯器連上就啟動;改成 y 則會卡住等你連(要除錯啟動流程時用)。

兩種開發循環,選一個

  • 循環 A:重建映像(單純可靠)
    mvn package -DskipTests && docker compose up -d --build --no-deps app
    約 30~60 秒。行為和正式環境完全一致,不會有「本機好好的、映像裡卻不對」的問題。
  • 循環 B:熱重載(快,但有代價)
    掛載 target/classes + Spring Boot DevTools,IDE 一編譯容器內就自動重啟,約 3~5 秒。
    代價:開發環境和正式映像的行為不完全一致,而且 Windows/macOS 上 bind mount 的檔案監聽有時不可靠(ch06 提過的檔案共享層問題)。
⭐ 建議:日常改程式碼用循環 B 求速度,提交前一定要用循環 A 驗證一次——確認真正要部署的那個映像沒問題。

啟動與停止

docker compose up -d                    # 啟動全部(背景)
docker compose up -d --build            # 重新建置後啟動
docker compose --profile tools up -d    # 連 adminer 一起啟動
docker compose stop                     # 只停止,容器保留
docker compose down                     # 停止並移除容器/網路(資料保留)
docker compose down -v                  # 💣 連資料一起重置

開發循環(最常用)

docker compose up -d --build --no-deps app   # 只重建重啟 app,不動 DB ⭐
docker compose logs -f app                    # 跟隨 app 日誌
docker compose logs -f --tail 100             # 全部服務,只看最近 100 行
docker compose restart app                    # 單純重啟(不重建)
docker compose ps                             # 看誰活著、誰 healthy

進去操作

# 進 app 容器
docker compose exec app bash

# 直接對資料庫下 SQL(不用裝任何客戶端工具)
docker compose exec sqlserver /opt/mssql-tools18/bin/sqlcmd \
  -S localhost -U sa -P "$MSSQL_SA_PASSWORD" -C -Q "SELECT name FROM sys.databases"

# 進 Redis
docker compose exec redis redis-cli

環境重置(常常需要)

# 完全重來:刪容器、網路、資料,重新建置
docker compose down -v && docker compose up -d --build

# 只重置資料庫,保留其他服務
docker compose stop sqlserver
docker compose rm -f sqlserver
docker volume rm myapp_mssql-data
docker compose up -d sqlserver

排查

docker compose config                   # 看變數展開與合併後的最終設定 ⭐
docker compose ps -a                    # 含已結束的(看 db-init 有沒有成功)
docker compose logs db-init             # 初始化任務的輸出
docker compose top                      # 各容器內的 process(驗證 PID 1)
docker stats                            # 即時資源用量
💡 把常用組合寫成腳本或 Makefile,團隊成員不用記這些:
make up        # docker compose up -d --build
make logs      # docker compose logs -f app
make reset     # docker compose down -v && make up
make db        # 進 sqlcmd

你現在有一套很好用的開發環境。但直接把它搬到正式環境會出事——先看清楚差異在哪。

開發環境的設定,哪些必須改

  • 密碼:.env 檔換成雲端的秘密管理服務(AWS Secrets Manager、K8s Secret)。
  • 資料庫:正式環境不要把資料庫跑在容器裡——用受管服務(RDS、Azure SQL)。備份、故障轉移、版本升級、監控都由雲端商負責,這是你自己維護容器化資料庫很難做到的(AWS ch05)。
  • 埠對外:資料庫的 ports 整段拿掉,只讓內部網路連得到。
  • 映像:從 build: 改成 image: 指向 registry 上有明確版本(甚至 digest)的映像(ch04)——正式環境不該在機器上現場編譯。
  • 資源限制:務必明確設定,不能靠預設(ch02)。
  • 日誌:接到集中式日誌系統,而不是留在本機。

Compose 在正式環境的三個結構性限制

  1. 單機:所有容器在同一台機器上。機器掛了,服務就全沒了——沒有跨機器的備援。
  2. 沒有自我修復與調度:restart: unless-stopped 只能重啟同一台機器上的容器。機器本身故障、或需要把工作負載搬到另一台,Compose 做不到。
  3. 沒有滾動更新:docker compose up 更新服務時會有停機空窗。沒有「先起新的、確認健康、再關舊的」這種零停機機制。
📌 這三個限制正好就是 Kubernetes 存在的理由——下一章(ch14)會從這裡接下去:當你需要「機器掛了服務自己搬家」「更新不能停機」「流量大了自動長出更多實例」,你需要的就是編排系統。
⭐ 但別急著上 K8s:如果你的服務就是單機規模、可以容忍幾分鐘的維護停機,Compose 加上一台機器就是完全合理的正式部署方案。K8s 的複雜度是有代價的——很多團隊上了 K8s 之後,維運成本反而超過它解決的問題。選型要看實際需求,不是看流行。
SQL Server 資料庫初始化的三種做法
做法怎麼做優點缺點
❌ initdb.d 掛載掛 .sql 到 /docker-entrypoint-initdb.d—SQL Server 官方映像根本沒這機制,會被安靜忽略
A. 一次性 init 容器mssql-tools 映像跑 sqlcmd + service_healthy/completed_successfully純 Compose 可達成,職責清楚每次 up 都會跑,SQL 必須寫成冪等
B. 自訂映像 entrypoint背景啟動 DB、輪詢就緒、執行腳本、wait只需要一個服務要自己維護映像,容易寫錯 PID 1 處理
C. Flyway/Liquibase應用啟動時自動執行版本化遷移版本控管、環境一致、不依賴容器機制、天生冪等需要引入依賴(但幾乎都值得)
開發環境 vs 正式環境:必須改的設定
項目開發(Compose)正式環境
密碼.env 檔(gitignore)Secrets Manager/K8s Secret
資料庫容器 + volume(方便重置)受管服務(RDS、Azure SQL)
映像來源build: 本機建置image: registry 上的明確版本/digest
資料庫對外埠127.0.0.1:1433(方便用 SSMS)整段拿掉,只走內部網路
日誌docker compose logs集中式日誌系統
資源限制可選必設
更新方式up -d(有停機空窗)滾動更新(零停機)→ 需要編排系統

練習題 點選選項查看解析

0 / 9
01 / 9
你把 init.sql 掛到 SQL Server 容器的 /docker-entrypoint-initdb.d,結果什麼都沒發生也沒有錯誤訊息。為什麼?
A 檔案權限不足
B SQL Server 的官方映像根本沒有這個機制——那是 PostgreSQL 和 MySQL 才有的,掛上去的檔案會被安靜忽略
C SQL 語法錯誤
D 需要設定 ACCEPT_EULA
解析
這是很多人卡很久的坑,因為完全沒有線索可查。SQL Server 要初始化 schema,得改用:一次性 init 容器跑 sqlcmd、自訂映像包 entrypoint、或最推薦的 Flyway/Liquibase 應用層遷移。
02 / 9
為什麼推薦用 Flyway/Liquibase 而不是容器層的初始化腳本?
A 因為執行速度比較快
B 因為每個 schema 變更都是帶版本號的檔案、跟程式碼一起進 Git;開發測試正式跑同一套遷移;不依賴容器機制;且工具自帶狀態表天生冪等
C 因為 Docker 不支援 SQL 腳本
D 因為它可以自動最佳化資料庫
解析
這是職責分離的正確切法:Compose 只負責把資料庫服務叫起來,schema 的演進交給應用層。不管資料庫跑在容器、VM 還是 RDS 都一樣運作——正式環境用受管資料庫時尤其重要。
03 / 9
compose 檔中 ${MSSQL_SA_PASSWORD:?請在 .env 設定} 這個寫法的作用是?
A 設定一個預設密碼
B 如果變數沒設定就直接報錯中止,比啟動之後才神秘失敗好得多
C 把密碼加密
D 提示使用者輸入密碼
解析
${VAR:?訊息} 是強制必填語法,${VAR:-預設值} 才是給預設值。用在密碼這類必要參數上,可以讓新同事第一次 up 就得到清楚的錯誤指引,而不是看到一堆莫名其妙的連線失敗。
04 / 9
Spring Boot 連 SQL Server 2022 時噴 SSL 握手失敗,最常見的原因與解法?
A 密碼錯誤,需要重設
B 新版 JDBC driver 預設要求加密連線,開發環境用自簽憑證時要在連線字串加 encrypt=true;trustServerCertificate=true
C 需要開放 1433 以外的埠
D SQL Server 版本太舊
解析
這是 SQL Server 容器化開發極常見的攔路虎,而且錯誤訊息不會直接告訴你要加什麼參數。注意 trustServerCertificate=true 只適合開發環境——正式環境應該用正式憑證。
05 / 9
docker-compose.override.yml 的作用是?
A 取代主要的 compose 檔
B 被自動載入並與 docker-compose.yml 合併,適合放只有本機開發才需要的設定(原始碼掛載、除錯埠、DevTools)
C 只在正式環境使用
D 用來備份設定
解析
這個機制讓「共通定義」與「本機專屬」自然分離:docker-compose.yml 進版控大家共用,override 檔可以 gitignore 讓每個人有自己的偏好。注意合併規則的陷阱——清單型別(ports、volumes)預設是相加不是取代(ch11)。
06 / 9
要讓 IDE 對容器裡的 Spring Boot 下中斷點,需要什麼?
A 無法對容器內的程式除錯
B 在容器加上 JDWP 參數(-agentlib:jdwp=...address=*:5005)並映射 5005 埠,IDE 用 Remote JVM Debug 連 localhost:5005
C 必須把程式碼複製到容器外執行
D 需要安裝特殊的 Docker 外掛
解析
連上之後中斷點跟本機執行一樣好用,這是很多人不知道容器也能做到的事。suspend=n 表示不等除錯器就啟動;要除錯啟動流程時改成 suspend=y 讓它卡住等你連上。
07 / 9
開發時只想重建 app 而不重啟資料庫,正確指令是?
A docker compose restart
B docker compose up -d --build --no-deps app
C docker compose down && docker compose up -d
D docker compose exec app rebuild
解析
--no-deps 表示不連帶處理相依服務。SQL Server 重啟一次要 30~60 秒,沒必要每次改程式碼都重來。另外注意 restart 不會重新建置映像——改了程式碼用 restart 是不會生效的。
08 / 9
為什麼正式環境不建議把資料庫跑在容器裡?
A 容器不支援資料庫
B 受管服務(RDS、Azure SQL)幫你處理備份、故障轉移、版本升級與監控,這些是自行維護容器化資料庫很難做好的事
C 容器的儲存速度太慢
D 資料庫映像太大
解析
開發環境用容器跑資料庫非常合理(隨時可重置、環境一致);但正式環境的資料庫是「有狀態、不能掉、要備份、要能故障轉移」的關鍵元件,交給受管服務通常是更好的工程決策。
09 / 9
Compose 用於正式環境的三個結構性限制是?
A 設定檔太複雜、不支援環境變數、無法建置映像
B ① 單機:機器掛了服務全沒;② 沒有跨機器的自我修復與調度;③ 更新時有停機空窗,沒有滾動更新機制
C 只能執行三個容器、不支援網路、無法持久化
D 不支援 Windows、不支援 ARM、不支援 GPU
解析
這三個限制正好就是 Kubernetes 存在的理由(ch14)。但也別急著上 K8s——如果服務就是單機規模、可以容忍幾分鐘的維護停機,Compose 加一台機器就是完全合理的正式方案。K8s 的複雜度是有代價的。

點擊卡片翻面查看答案,共 11 張。

QUESTION
團隊專案的 .env 標準做法?
點擊翻面
ANSWER
.env 加進 .gitignore(含真實密碼)
.env.example 進版控(只有變數名,沒有真值)
→ 新人知道要設哪些變數,repo 裡沒有任何密碼
點擊翻回
QUESTION
SQL Server 初始化最大的坑?
點擊翻面
ANSWER
官方映像沒有 /docker-entrypoint-initdb.d 機制
(那是 PostgreSQL / MySQL 才有的)
掛上去會被安靜忽略,沒有任何錯誤訊息
點擊翻回
QUESTION
SQL Server 初始化的三種可行做法?
點擊翻面
ANSWER
A. 一次性 init 容器跑 sqlcmd(SQL 要寫成冪等)
B. 自訂映像包 entrypoint(要自己維護)
C. Flyway/Liquibase 應用層遷移 ✅ 推薦
點擊翻回
QUESTION
為什麼 Flyway 是正解?
點擊翻面
ANSWER
① 版本控管,跟程式碼一起進 Git
② 開發/測試/正式跑同一套遷移
③ 不依賴容器機制(容器、VM、RDS 都一樣)
④ 自帶狀態表,天生冪等
點擊翻回
QUESTION
SQL Server + JDBC 的必加參數?
點擊翻面
ANSWER
encrypt=true;trustServerCertificate=true
新版 driver 預設要求加密連線
沒加會噴 SSL 握手失敗(開發環境極常見)
⚠️ trustServerCertificate 只適合開發環境
點擊翻回
QUESTION
健康檢查裡的 $$ 是什麼?
點擊翻面
ANSWER
逃脫寫法:Compose 會展開 $VAR
寫 $$ 才能把單一個 $ 原樣傳給容器內的 shell
(healthcheck 的 CMD-SHELL 裡最常遇到)
點擊翻回
QUESTION
容器內的 Spring Boot 怎麼遠端除錯?
點擊翻面
ANSWER
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
映射 5005 埠 → IDE 用 Remote JVM Debug 連 localhost:5005
suspend=y 可用來除錯啟動流程
點擊翻回
QUESTION
兩種開發循環怎麼選?
點擊翻面
ANSWER
A 重建映像:30~60 秒,行為與正式完全一致
B 熱重載(掛 target/classes + DevTools):3~5 秒,但與正式映像有差異
→ 日常用 B,提交前用 A 驗證一次
點擊翻回
QUESTION
開發最常用的三個 compose 指令?
點擊翻面
ANSWER
up -d --build --no-deps app(只重建 app)
logs -f app(跟隨日誌)
config(看變數展開與合併後的最終設定)
點擊翻回
QUESTION
開發環境搬到正式環境要改什麼?
點擊翻面
ANSWER
密碼 → Secrets Manager/K8s Secret
資料庫 → 受管服務(RDS)
build: → image: 明確版本/digest
資料庫 ports → 整段拿掉
資源限制 → 必設
點擊翻回
QUESTION
Compose 的三個結構性限制?
點擊翻面
ANSWER
① 單機——機器掛了服務全沒
② 沒有跨機器自我修復與調度
③ 更新有停機空窗,沒有滾動更新
→ 正是 Kubernetes 存在的理由(ch14)
點擊翻回