[上課筆記][SQLServer資料庫容器化] .NET與Docker Container微服務實務應用
本文承接上一篇文章:[隨手筆記][ASP.NET MVC5 網站容器化] .NET與Docker Container微服務實務應用
將 本機電腦的 SQL Server Express 的資料庫容器化
**匯入方式**:使用本機電腦的資料庫 `.bak` 完整備份檔,不複製進映像檔,改用 Volume 方式, bind mount 掛載本機 Backup 資料夾(`:ro` 唯讀)。
**資料持久化**:使用 Docker 命名 Volume `sqldata`,容器刪除重建後,資料庫的資料才不會消失。
**容器內的SQL Server Express實例名稱**:`MSSQLSERVER`(預設實例,自動監聽 port 1433,不需額外設定容器的 TCP/IP 登錄)。
**容器化後的資料庫連線方式**:同一份 .yml (多個容器相同網路)的容器彼此之間使用 SQL Authentication 連線登入和DNS(`Server=db`),不使用 Windows Authentication(跨容器需 AD + gMSA,太複雜)。
本機電腦的SSMS連線到容器內的SQL Server Express,則是(`Server=localhost,1433`)+ SQL Authentication 連線登入。
### 步驟 1:備份本機 Northwind 資料庫
**如何做?**:用 SSMS 將本機 Northwind 資料庫完整備份為 `.bak` 檔,並用 `RESTORE FILELISTONLY` 查出邏輯檔案名稱。
- 將 .bak 檔存到本機電腦路徑:`C:\Program Files\Microsoft SQL Server\MSSQL16.SQLEXPRESS\MSSQL\Backup\Northwind.bak`
- 手動進入 SSMS 用 RESTORE FILELISTONLY 的 SQL 語句查詢上述路徑的備份檔的邏輯檔案名稱(還原至容器內的 SQL Server 時需要),通常是 SQL Server 內部用的別名
RESTORE FILELISTONLY FROM DISK = N'C:\Program Files\Microsoft SQL Server\MSSQL16.SQLEXPRESS\MSSQL\Backup\Northwind.bak' - LogicalName:`Northwind`(資料檔 Type=D)、`Northwind_log`(日誌檔 Type=L)
**原因**:最一開始的容器內的 SQL Server 需要從 `.bak` 還原資料庫。`RESTORE FILELISTONLY` 取得的邏輯名稱用於 `WITH MOVE`,將資料檔指向容器內的 Volume 掛載點。
### 步驟 2:在.Net 方案目錄下建立 `docker/Dockerfile.db` — 映像檔腳本
**映像檔內容**:建立 SQL Server Express Windows 容器的 Dockerfile。
**原因**:Microsoft 官方只有 Linux 版 SQL Server 映像,而本範例的 Web 容器是 Windows 容器(Docker Desktop 同時只能跑一種),所以必須自建(基底映像檔為微軟的Windows Server 2022 + 為容器下載安裝 SQL Server Express,微軟的連結會自動導向下載最新版的SQL Server Express)。
關鍵設計:
- 基底映像:`mcr.microsoft.com/windows/servercore:ltsc2022`(與 Web 容器相同 Windows 版本)
- SQL Server實例名稱:`MSSQLSERVER`(預設實例,自動使用 port 1433)
- 啟用 SQL Authentication,SA 密碼在安裝時設定
本機開發環境的SQL登入帳號用sa,但實際的正式上線環境最好別使用sa帳號。
/SAPWD=YourStrong!Passw0rd ← 在安裝時候就設定 sa 的密碼。
- .bak 不 COPY 進映像(避免映像檔肥大),由 docker-compose.yml 透過 Volumn 掛載
Dockerfile.db 內容如下
# 使用 Windows Server Core 2022 作為基底映像(與 Web 容器相同 Windows 版本)
FROM mcr.microsoft.com/windows/servercore:ltsc2022
# 使用 PowerShell 作為預設 Shell
SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"]
# 1) 下載 SQL Server Express 安裝引導程式(由微軟提供,實際版本依下載時間而定)
RUN Invoke-WebRequest -Uri 'https://go.microsoft.com/fwlink/p/?linkid=2216019&clcid=0x404&culture=zh-tw&country=tw' -OutFile 'C:\\SQLEXPR-SSEI.exe'
# 2) 用引導程式下載 SQL Server Express 安裝媒體
RUN Start-Process -FilePath 'C:\\SQLEXPR-SSEI.exe' \
-ArgumentList '/Action=Download', '/MediaPath=C:\\SQLMedia', '/MediaType=Core', '/Quiet' \
-Wait -NoNewWindow ; \
Remove-Item 'C:\\SQLEXPR-SSEI.exe' -Force
# 3) 解壓並靜默安裝 SQL Server Express
RUN $setupExe = (Get-ChildItem 'C:\\SQLMedia\\*.exe' | Select-Object -First 1).FullName ; \
Start-Process -FilePath $setupExe -ArgumentList '/x:C:\\SQLSetup', '/q' -Wait -NoNewWindow ; \
Start-Process -FilePath 'C:\\SQLSetup\\setup.exe' -ArgumentList \
'/Q', \
'/IACCEPTSQLSERVERLICENSETERMS', \
'/ACTION=Install', \
'/FEATURES=SQLEngine', \
'/INSTANCENAME=MSSQLSERVER', \
'/SECURITYMODE=SQL', \
'/SAPWD=YourStrong!Passw0rd', \
'/SQLSVCSTARTUPTYPE=Automatic', \
'/TCPENABLED=1', \
'/UPDATEENABLED=False' \
-Wait -NoNewWindow ; \
Remove-Item -Recurse -Force 'C:\\SQLMedia', 'C:\\SQLSetup'
# 4) 建立備份檔與資料檔的目錄
RUN New-Item -ItemType Directory -Path 'C:\\backup' -Force ; \
New-Item -ItemType Directory -Path 'C:\\SQLData' -Force
# 5) 複製啟動腳本到容器(檔案已移至 docker/ 資料夾,build context 為專案根目錄)
COPY docker/DbContainer-StartScript.ps1 C:/DbContainer-StartScript.ps1
# 宣告容器使用 port 1433
EXPOSE 1433
# 容器啟動時執行啟動腳本
ENTRYPOINT ["powershell", "-File", "C:\\DbContainer-StartScript.ps1"]
容器內沒有安裝 SQL Server Browser 服務(負責解析實例名稱),所以Web容器內的應用程式的資料庫連線字串要連線至 另一資料庫容器內的SQL Server不能用Server=db\MSSQLSERVER,須直接使用 DNS 的方式(預設 1433 Port):Server=db,這樣才能跳過實例名稱的解析,連線至容器內的SQL Server。
### 步驟 3:在.Net 方案目錄下建立 `docker/DbContainer-StartScript.ps1` — 資料庫容器一啟動就要執行的 powershell 腳本,主要用途還原掛載資料庫的資料
**原因**:容器啟動時需要啟動 SQL Server 服務、等待完全就緒、還原or從Volumn掛載資料庫。
Dockerfile 的 `RUN` 只在建置映像檔時執行,啟動容器時的邏輯要放在 ENTRYPOINT 腳本裡。
腳本流程:
1. **設定區**(檔案最上方,集中宣告變數,方便未來修改):資料庫名稱、邏輯檔名、.bak 路徑、.mdf/.ldf 路徑
2. **讀取 SA 密碼**:從環境變數 `SA_PASSWORD` 讀取(docker-compose.yml 傳入),若未設定用預設值
3. **啟動 SQL Server 服務**:`Start-Service 'MSSQLSERVER'`
4. **等待SQL Server完全就緒**:用 sqlcmd 重試迴圈(最多 30 次,每次 2 秒)— 業界標準做法,SQL Server 沒有就緒通知機制
5. **三段式判斷資料庫狀態**:
- `sys.databases` 已有(容器僅重啟)→ 跳過還原or掛載資料庫。
- `sys.databases` 沒有(master資料庫無資料),但 Volume 上有 .mdf檔。
(容器刪除重建導致 master資料庫的資料消失,但 Volume 的 .mdf 檔案還健在的情況)→ 使用 T-SQL 的 **FOR ATTACH**指令,掛載 Volumn 的 .mdf 資料庫。
- `sys.databases` 和 Volumn 都沒有資料(最一開始的部署) → 從本機電腦的 `.bak` 完整備份檔還原資料庫至容器內的SQL Server。
6. **保持容器運行**:`Wait-Event`(無限等待)
還原 .bak 的關鍵:WITH MOVE
- `WITH MOVE` 將資料檔案指向 Volume 掛載的 `C:\SQLData`
RESTORE DATABASE [Northwind]
FROM DISK = N'C:\backup\Northwind.bak'
WITH MOVE '邏輯資料檔名' TO 'C:\SQLData\Northwind.mdf',
MOVE '邏輯日誌檔名' TO 'C:\SQLData\Northwind_log.ldf',
REPLACE DbContainer-StartScript.ps1 內容
# 容器啟動後,要執行的 powershell 腳本
# 啟動 SQL Server → 等待 SQL Server完全就緒 → 還原 Northwind(如果尚未存在)→ 保持容器運行
# ============================================================
# 設定區(未來要修改請改這裡)
# ============================================================
# 要還原的資料庫名稱
$dbName = 'Northwind'
# .bak 備份檔中的資料檔邏輯名稱(用 RESTORE FILELISTONLY 查詢)
$dbLogicalData = 'Northwind'
# .bak 備份檔中的日誌檔邏輯名稱(用 RESTORE FILELISTONLY 查詢)
$dbLogicalLog = 'Northwind_log'
# .bak 備份檔在容器內的路徑
$bakPath = 'C:\backup\Northwind.bak'
# 容器內,資料庫還原後的資料檔存放路徑(對應 Volume 掛載點)
$mdfPath = 'C:\SQLData\Northwind.mdf'
# 容器內,資料庫還原後的日誌檔存放路徑(對應 Volume 掛載點)
$ldfPath = 'C:\SQLData\Northwind_log.ldf'
# ============================================================
# 從環境變數讀取 SA 密碼(若未設定則使用預設值)
$saPassword = $env:SA_PASSWORD
if (-not $saPassword) { $saPassword = 'YourStrong!Passw0rd' }
# 啟動 SQL Server 服務
Write-Host 'Starting SQL Server service...'
Start-Service 'MSSQLSERVER'
Write-Host 'SQL Server service started.'
# 等待 SQL Server 就緒(最多重試 30 次,每次間隔 2 秒)
Write-Host 'Waiting for SQL Server to become ready...'
$retries = 30
$ready = $false
for ($i = 0; $i -lt $retries; $i++) {
try {
$result = & sqlcmd -S 'localhost' -C -U sa -P $saPassword -Q "SELECT 1" -b 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Host 'SQL Server is ready.'
$ready = $true
break
}
} catch { }
Write-Host " Attempt $($i+1)/$retries - waiting 2 seconds..."
Start-Sleep -Seconds 2
}
if (-not $ready) {
Write-Host 'ERROR: SQL Server did not start in time.' -ForegroundColor Red
exit 1
}
# 可以透過 sqlcmd 連線 SQL Server 了
# 判斷資料庫狀態並決定動作:
# 1. sys.databases 已有 → 跳過(同一容器重啟的情況)
# 2. sys.databases 沒有,但 Volume 上有 .mdf → 掛載(容器刪除重建,但 Volume 資料還在)
# 3. 都沒有 → 從 .bak 還原(全新部署)
$dbCheck = & sqlcmd -S 'localhost' -C -U sa -P $saPassword -Q "SELECT name FROM sys.databases WHERE name = '$dbName'" -h -1 -W -b 2>&1
if ($dbCheck -match $dbName) {
# 情況 1:資料庫已註冊在 master 中(容器只是重啟,沒有被刪除重建)
Write-Host "$dbName database already exists. Skipping restore."
} elseif (Test-Path $mdfPath) {
# 情況 2:master 不認識此資料庫(容器被刪除重建),但 Volume 上仍有資料檔
Write-Host "Attaching $dbName database from existing Volume data..."
& sqlcmd -S 'localhost' -C -U sa -P $saPassword -Q @"
CREATE DATABASE [$dbName] ON
(FILENAME = N'$mdfPath'),
(FILENAME = N'$ldfPath')
FOR ATTACH
"@ -b
if ($LASTEXITCODE -eq 0) {
Write-Host "$dbName database attached successfully."
} else {
Write-Host "ERROR: Failed to attach $dbName database." -ForegroundColor Red
}
} else {
# 情況 3:全新部署,Volume 上沒有資料檔
Write-Host "Restoring $dbName database from backup..."
# 確保資料目錄存在
$dataDir = Split-Path $mdfPath -Parent
if (-not (Test-Path $dataDir)) { New-Item -ItemType Directory -Path $dataDir | Out-Null }
& sqlcmd -S 'localhost' -C -U sa -P $saPassword -Q @"
RESTORE DATABASE [$dbName]
FROM DISK = N'$bakPath'
WITH MOVE '$dbLogicalData' TO '$mdfPath',
MOVE '$dbLogicalLog' TO '$ldfPath',
REPLACE
"@ -b
if ($LASTEXITCODE -eq 0) {
Write-Host "$dbName database restored successfully."
} else {
Write-Host "ERROR: Failed to restore $dbName database." -ForegroundColor Red
}
}
# 保持容器運行(無限等待)
Write-Host 'SQL Server is running.'
Wait-Event
### 步驟 4:在.Net 方案目錄下建立 `docker-compose.yml`
- 新增 `db` 服務(資料庫容器)
- 對外開放 port `1433:1433`(開發時可用 SSMS 從本機電腦連線至容器內的SQL Server)
- 設定資料庫容器的 `SA_PASSWORD` 環境變數
**目標**:讓資料庫容器 + Web 容器可以一鍵啟動,共用同一個 Docker 網路(服務名稱 = DNS 名稱)。
資料庫和網站的容器啟動設定都放在同一個 docker-compose.yml 的好處是:
兩個容器自動在同一個網路,Web 容器可以直接用 db 這個服務名稱(DNS)連到資料庫。
docker-compose.yml 的 結構(由上到下):
1. **volumes 宣告**:`sqldata` 命名 Volume,掛載到容器內的路徑 `C:/SQLData`(資料持久化)
2. **db 服務**:SQL Server 容器,bind mount Backup 資料夾(`:ro`)+ sqldata Volume
3. **web 服務**:ASP.NET 容器,`depends_on: db` 確保 db 先啟動
- **SA 密碼**:開發用先寫死在 docker-compose.yml,正式環境改用 `.env` 檔或 Docker secrets
容器間網路連線
- Docker Compose 自動建立網路,Web、DB 兩個服務在同一網路
- Web 容器用主機名稱 `db` 連線 SQL Server(服務名稱 = DNS 名稱),但本機電腦的 SSMS 使用 localhost,1433 連線 容器內的 SQL Server。
docker-compose.yml 內容
# 宣告命名 Volume(由 Docker 管理儲存位置,不需指定本機路徑)
# 用途:讓容器內的資料可以持久保存,即使容器被刪除重建,Volume 中的資料依然存在
# 查看:docker volume ls(列出所有 Volume)
# 刪除:docker-compose down -v(加 -v 才會刪除 Volume,不加則保留)
volumes:
sqldata: # 存放 SQL Server 的資料檔(.mdf/.ldf),供下方 db 服務使用
# 定義要執行的容器服務
services:
# ===== 資料庫容器 =====
db: # 服務名稱,也是容器在 Docker 網路中的 DNS 名稱(其他容器可用 "db" 來連線此容器)
image: webnet-db:v20260325 # 建置出來的映像檔名稱
container_name: webnet-db-v20260325 # 在 Docker Desktop 裡看到的名字
build:
context: . # 建置的根目錄(專案根目錄)
dockerfile: docker/Dockerfile.db # 使用 docker 資料夾裡的 Dockerfile.db
# port 對應(本機電腦主機:容器)
ports:
- "1433:1433" # 本機電腦的 1433 port 對應 容器內的 1433 port(可用 SSMS 從本機連線至容器內的 SQL Server)
environment:
- SA_PASSWORD=YourStrong!Passw0rd # SA 帳號密碼(啟動腳本會讀取此環境變數)
volumes:
# 掛載本機的 Backup 資料夾到容器內(Windows 容器不支援掛載單一檔案,只能掛載整個資料夾)
# :ro 表示容器只能讀取,不能修改本機的檔案
- "C:/Program Files/Microsoft SQL Server/MSSQL16.SQLEXPRESS/MSSQL/Backup:C:/backup:ro"
# 使用上方宣告的 sqldata Volume,掛載到容器內的 C:/SQLData
- sqldata:C:/SQLData
restart: unless-stopped # 自動重啟,但手動停止後不會再自動重啟
# ===== 網站容器 =====
web: # 服務名稱,也是容器在 Docker 網路中的 DNS 名稱(其他容器可用 "web" 來連線此容器)
image: webnet:v20260325 # 建置出來的映像檔名稱
# 容器的名稱(在 Docker Desktop 裡看到的名字)
container_name: webnet-v20260325 # 容器名稱不能用冒號,改用減號
# 建置映像檔的設定
build:
context: . # 建置的根目錄(專案根目錄)
dockerfile: docker/Dockerfile.web # 使用 docker 資料夾裡的 Dockerfile.web
# port 對應(本機電腦主機:容器)
ports:
- "8443:443" # 本機電腦的 8443 port 對應 容器內的 443 port
depends_on:
- db # 一鍵啟動時,自動先啟動 db 容器,再啟動 web 容器
restart: unless-stopped # 自動重啟,但手動停止後不會再自動重啟
### 步驟 5:改寫網站應用程式的資料庫連線字串
**目標**:讓 Web 容器內的網站能透過 Docker 網路連線到 DB 容器的 SQL Server 存取資料。
修改的檔案:
- `網站根目錄下的Web.config`:確認 `connectionStrings`區段的資料庫連線字串為: `Server=db;Database=Northwind;User Id=sa;Password=YourStrong!Passw0rd;TrustServerCertificate=True;`
### 步驟 6:建置與測試
**如何做?**:`docker-compose build` → `docker-compose up -d` 建立並啟動容器→ 驗證所有功能(查詢資料庫、瀏覽網站)。
在 docker-compose.yml 所在目錄(通常是.Net 方案目錄)開啟 cmd 終端機 ,執行 docker-compose build,建置所有映像檔。
- **建置映像檔時間**:db 映像第一次建置大約需 20-40 分鐘(因為要下載安裝 SQL Server Express),之後在Docker有快取,再次docker-compose build速度會快很多
建置過程中會看到一堆輸出,耐心等待,看到 Successfully built 就代表成功。映像檔全部建置成功後,再執行 cmd指令 docker-compose up -d (建立並啟動所有容器)
┌──────┬────────────────────────────────────┐
│ 參數 │ 意思 │
├──────┼────────────────────────────────────┤
│ up │ 建立並啟動所有容器 │
├──────┼────────────────────────────────────┤
│ -d │ 背景執行(detach),不會佔住終端機 │
└──────┴────────────────────────────────────┘
因為有 depends_on,Docker 會自動先啟動 db,再啟動 web。
查看容器狀態的指令:docker-compose ps,應該會看到兩個容器都是 Up 狀態。
查看 db 容器的啟動日誌: docker-compose logs db
確認看到以下關鍵訊息:
- SQL Server is ready.
- 顯示 SQL Server 啟動成功
- Northwind database restored successfully.
- 用 SSMS 連線 `localhost,1433`(帳號 `sa`、密碼YourStrong!Passw0rd)連線容器 SQL Server 確認資料庫與資料表都能正常存取。
- 測試 資料庫容器的 Volumn 資料持久化:docker-compose down 全部容器下架刪除→ docker-compose up -d 重建啟動容器,再用 SSMS 連線查詢 DB容器的資料庫並確認資料仍然保留。
開啟瀏覽器並瀏覽網站: https://localhost:8443
(因為是自簽憑證,瀏覽器會警告不安全,點「繼續前往」即可)
踩過的地雷坑
### 地雷 1:微軟基底映像檔的下載連結自動升級 SQL Server 版本
**現象**:Dockerfile 中用微軟官方下載連結下載 SQL Server Express,預期下載 2022 版(版本 16),實際下載到 2025 版(版本 17)。導致登錄路徑 `MSSQL16.SQLEXPRESS` 不存在,`docker-compose build` 失敗。
**原因**:微軟的 `go.microsoft.com/fwlink` 連結是動態的,會指向最新版本。
**解法**:最終改用**預設實例 `MSSQLSERVER`**(`/INSTANCENAME=MSSQLSERVER`),搭配 `/TCPENABLED=1` 安裝參數,預設實例自動監聽 port 1433,完全不需要手動修改登錄檔設定 TCP/IP,從根本避開了版本號問題。
### 地雷 2:Windows 容器不支援掛載單一檔案
**現象**:docker-compose.yml 中嘗試 bind mount 單一 `.bak` 檔案,`docker-compose up -d` 報錯 `source path must be a directory`。
**原因**:Windows 容器只支援掛載**整個資料夾**,不支援掛載單一檔案(Linux 容器可以)。
**解法**:改為掛載 `.bak` 所在的整個 `Backup` 資料夾:
```yaml
- "C:/Program Files/Microsoft SQL Server/MSSQL16.SQLEXPRESS/MSSQL/Backup:C:/backup:ro"
```
### 地雷 3:ODBC Driver 18 預設要求加密連線(SSL 憑證信任錯誤)
**現象**:資料庫容器啟動後,`sqlcmd` 連線 SQL Server 失敗,錯誤訊息 `The certificate chain was issued by an authority that is not trusted`。所有 30 次重試都失敗,資料庫無法還原。
**原因**:下載到的 SQL Server 2025 內含 ODBC Driver 18,預設要求加密連線並驗證 SSL 憑證。容器內的 SQL Server 使用自簽憑證,不被信任。
**解法**:
- `DbContainer-StartScript.ps1`:所有 `sqlcmd` 呼叫加上 **`-C`** 旗標(trust server certificate)
- `網站應用程式根目錄下的Web.config`:連線字串加上 **`TrustServerCertificate=True;`**
### 地雷 4:容器刪除重建後資料庫被重新還原(Volume 持久化失效)
**現象**:`docker-compose down` → `up -d` 後,日誌顯示資料庫又從 `.bak` 重新還原,而不是使用 Volume 上已有的資料。
**原因**:啟動腳本透過查詢 `sys.databases` 判斷資料庫是否存在。但 `sys.databases` 存在 **master 資料庫**中,而 master 不在 Volume 上(它在容器檔案系統中)。容器被刪除重建時 master 是全新的,不認識 Northwind,所以導致每次都從本機電腦重新還原資料庫。
**解法**:改為**三段式判斷**:
1. `sys.databases` 有 → 跳過(容器僅重啟)
2. `sys.databases` 沒有,但 `.mdf` 檔案存在(Volume 上)→ `CREATE DATABASE ... FOR ATTACH`(快速掛載)
3. 都沒有 → 從 `.bak` 還原(全新部署)
### 地雷 5:PowerShell 字串內嵌在 Dockerfile RUN 中的展開問題
**現象**:嘗試在資庫料容器的映像檔腳本 Dockerfile 的 `RUN` 指令中使用 PowerShell 動態查詢 SQLServer Express 具名實例的登錄路徑,變數展開和字串跳脫出錯,`docker-compose build` 失敗。
**原因**:Dockerfile 的 `RUN` 指令中的 PowerShell 語法受到多層跳脫影響(Docker 解析 → PowerShell 解析),複雜的字串操作容易出問題。
**解法**:改用預設實例 `MSSQLSERVER`,從根本消除了需要動態查詢SQL Server具名實例的登錄路徑並指派 Port 1433 的必要性。不需要額外PowerShell 修改登錄檔設定 TCP/IP,從根本避開了資料庫版本號問題。
## 常用指令速查
| 指令 | 用途 |
|------|------|
| `docker-compose build` | 建置映像檔(首次約 20-40 分鐘) |
| `docker-compose up -d` | 建立&啟動所有容器(背景執行,不佔用終端機) |
| `docker-compose down` | 停止並刪除所有容器(Volume 資料保留不刪除) |
| `docker-compose down -v` | 停止並刪除所有容器 + `-v` 旗標表示Volume資料也刪除) |
| `docker-compose logs db` | 查看 DB 容器日誌 |
| `docker-compose restart db` | 重啟 DB 容器 |
| `docker-compose rm -sf db` | 只刪除 DB 容器(不影響 web 容器) |
| `docker-compose ps` | 查看容器狀態 |