目錄

Docker 基礎概念

Docker 是一個開放原始碼的容器化平台,把應用程式和它的依賴項打包成獨立容器,讓同一份設定在本機開發、測試機、正式環境都能跑出相同結果。

這套技術組合讓你少踩的坑:「我本機跑得起來但 CI 就壞了」基本上絕跡、新人 onboarding 不再花一天裝環境、版本升級可以先切新容器測試再切流量。代價是要花時間學 Dockerfile 和 Compose 的寫法——這篇就是從這裡開始。

環境準備

  1. 安裝 Docker Desktop

    1
    2
    3
    4
    5
    6
    7
    8
    # Windows/Mac 下載並安裝 Docker Desktop
    https://www.docker.com/products/docker-desktop/

    # Linux (Ubuntu) 安裝 Docker Engine
    sudo apt update
    sudo apt install docker.io
    sudo systemctl start docker
    sudo systemctl enable docker
  2. 確認安裝

    1
    2
    3
    4
    5
    6
    7
    docker --version

    # Docker Desktop 現在預設附的是 Compose v2 plugin,用這個
    docker compose version

    # 若你環境裡還裝著舊的 v1 獨立執行檔,以下指令才有效
    # docker-compose --version

SQL Server 容器設定

  1. 建立 SQL Server 容器
    1
    2
    3
    4
    5
    6
    docker pull mcr.microsoft.com/mssql/server:2022-latest

    docker run -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=YourStrong@Passw0rd" \
    -p 1433:1433 --name sqlserver \
    -v sqlvolume:/var/opt/mssql \
    -d mcr.microsoft.com/mssql/server:2022-latest

兩個地方值得留意。一是密碼環境變數:舊文常見的 SA_PASSWORD 已被標記為 deprecated,新版映像建議改用 MSSQL_SA_PASSWORD,舊名目前仍可動但會跳警告。二是映像版本:這裡用 2022-latest。SQL Server 2019 主流支援已於 2025 年 2 月 28 日結束,截至本文更新時 SQL Server 2022 是當前正式版(2025 仍在 preview),新專案直接從 2022 起步。

  1. 確認容器運行狀態
    1
    docker ps

.NET Core API 容器化

版本提醒:本文示範用 .NET 6。.NET 6 已於 2024 年 11 月終止支援,新專案請改用仍在支援週期內的 LTS(例如 .NET 8 或更新版),把下面 sdk:6.0 / aspnet:6.0 的 tag 換掉即可,其餘結構不變。

  1. 在專案根目錄建立 Dockerfile
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
    WORKDIR /src
    COPY ["YourProject.csproj", "./"]
    RUN dotnet restore
    COPY . .
    RUN dotnet publish -c Release -o /app/publish

    FROM mcr.microsoft.com/dotnet/aspnet:6.0
    WORKDIR /app
    COPY --from=build /app/publish .
    ENTRYPOINT ["dotnet", "YourProject.dll"]

這裡用 dotnet publish 而不是 dotnet buildpublish 會輸出真正要部署的執行期資產(已做發佈最佳化、不含開發期多餘檔案),是容器化的標準做法;build 的輸出帶著開發期檔案、也缺少 publish 的處理,丟進 runtime 映像並不適合。

  1. 設定 .dockerignore

    1
    2
    3
    bin/
    obj/
    node_modules/
  2. 建立映像檔和容器

    1
    2
    docker build -t myapi .
    docker run -d -p 5000:80 --name api myapi

Vue3 前端容器化

  1. 建立 Dockerfile(node:16 早已 EOL,這裡用仍在支援的 LTS)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    FROM node:20 as build-stage
    WORKDIR /app
    COPY package*.json ./
    RUN npm install
    COPY . .
    RUN npm run build

    FROM nginx:stable-alpine as production-stage
    COPY --from=build-stage /app/dist /usr/share/nginx/html
    EXPOSE 80
    CMD ["nginx", "-g", "daemon off;"]

若 Vue Router 使用 history mode(createWebHistory),直接訪問非根路徑或在瀏覽器重新整理會得到 nginx 404,因為伺服器找不到對應靜態檔案。解法是在容器裡放自訂 nginx 設定,讓所有路徑都回到 index.html

1
2
3
4
5
6
7
8
9
10
# nginx.conf(放在專案根,COPY 進映像)
server {
listen 80;
root /usr/share/nginx/html;
index index.html;

location / {
try_files $uri $uri/ /index.html;
}
}

Dockerfile 對應改成:

1
2
3
4
5
FROM nginx:stable-alpine as production-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

若用的是 createWebHashHistory(URL 帶 #),nginx 不需要額外設定,但一般正式環境會用 history mode。

  1. 建立映像檔和容器
    1
    2
    docker build -t myvue .
    docker run -d -p 8080:80 --name frontend myvue

使用 Docker Compose 整合服務

  1. 建立 docker-compose.yml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    version: '3.8'

    services:
    db:
    image: mcr.microsoft.com/mssql/server:2022-latest
    environment:
    - ACCEPT_EULA=Y
    - MSSQL_SA_PASSWORD=YourStrong@Passw0rd
    ports:
    - "1433:1433"
    volumes:
    - sqlvolume:/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: 10
    start_period: 30s

    api:
    build:
    context: ./api
    dockerfile: Dockerfile
    ports:
    - "5000:80"
    depends_on:
    db:
    condition: service_healthy
    environment:
    - ConnectionStrings__DefaultConnection=Server=db;Database=YourDB;User=sa;Password=YourStrong@Passw0rd;TrustServerCertificate=True;

    frontend:
    build:
    context: ./frontend
    dockerfile: Dockerfile
    ports:
    - "8080:80"
    depends_on:
    - api

    volumes:
    sqlvolume:
  2. 啟動所有服務

    1
    docker compose up -d
  3. 檢查服務狀態

    1
    docker compose ps

這裡有個首次啟動最常卡住的地方。depends_on 預設只保證容器的「啟動順序」,不會等到 SQL Server「真的可以連線」才放行——而 SQL Server 容器初始化要數十秒。如果 API 一啟動就連 DB,這段空窗期會連不上而崩掉。所以上面 db 服務加了 healthcheck,API 的 depends_on 也改成 condition: service_healthy,讓 API 等到資料庫健康檢查通過才啟動。healthcheck 用的是 SQL Server 2022 映像內建的 /opt/mssql-tools18/bin/sqlcmd(2022 CU14 起改放在 mssql-tools18),-C 旗標用來信任容器的自簽憑證。test 指令裡密碼寫成 $$MSSQL_SA_PASSWORD 而非 $MSSQL_SA_PASSWORD:Compose 在讀取 YAML 時會對 $ 開頭的字串做變數替換,雙 $$ 是跳脫寫法,讓 Compose 把它當作字面的 $MSSQL_SA_PASSWORD 傳給 shell,由 shell 在容器內展開;若只寫單 $,Compose 會在啟動前嘗試從主機環境讀取這個變數,讀不到就傳入空字串,導致 sqlcmd 認證失敗。光靠 healthcheck 仍不能完全取代 API 端的連線重試,正式環境兩者都要做。

資源管理與監控

容器資源限制

下面的片段展示如何替主 docker-compose.yml 裡的 apidb 服務加上資源限制。注意:db 區塊僅列出 deployvolumes 這兩個新增欄位,要疊加到主檔的 db 服務上,而非整段替換——原本的 imageenvironmentportshealthcheck 一行都不能刪,否則資料庫起不來。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 以下為疊加片段,請合併至主 docker-compose.yml 對應服務,勿直接整段貼上取代
services:
api:
deploy:
resources:
limits:
cpus: '0.5' # 限制最多使用 50% CPU
memory: 512M # 限制最大記憶體使用量
reservations:
cpus: '0.25' # 保證至少 25% CPU
memory: 256M # 保證最少記憶體

db:
deploy:
resources:
limits:
memory: 2G
volumes:
- sqlvolume:/var/opt/mssql

關於 deploy.resources 的支援範圍:Docker Compose v2 CLIdocker compose,現在 Docker Desktop 預設附帶的外掛版)在非 Swarm 模式下直接支援這些資源限制,設完就有效。但若你的環境還是用舊的 docker-compose v1 獨立執行檔(Python 版),deploy 區塊的 resources 在非 Swarm 模式預設被忽略,需要加 --compatibility flag 才能讓限制生效:docker-compose --compatibility up -d。可以用 docker compose versiondocker-compose --version 分別確認你裝的是哪個版本。

要限制資料庫佔用的磁碟空間,實務上做法是控制掛載 volume 所在的底層磁碟或分割區配額,而非依賴 Compose 的 storage_opt——storage_opt 只在特定 storage driver(如 devicemapper)下有效,overlay2 等常見驅動並不支援。

監控工具設定

1. Portainer 安裝

Portainer 提供了直覺的 Web 介面來管理 Docker 容器:

1
2
3
4
5
docker volume create portainer_data
docker run -d -p 9000:9000 --name=portainer --restart=always \
-v /var/run/docker.sock:/var/run/docker.sock \
-v portainer_data:/data \
portainer/portainer-ce

安裝完成後可以訪問 http://localhost:9000 進行設定。

2. 日誌管理

在 docker-compose.yml 中加入日誌設定:

1
2
3
4
5
6
7
services:
api:
logging:
driver: "json-file"
options:
max-size: "200m"
max-file: "10"

開發環境設定

Hot Reload 配置

Hot Reload 讓你改完程式碼不用重建映像就能看到效果,但這裡有個容易踩到的坑:dotnet watch 是 SDK 才有的工具,前面 Dockerfile 最終階段用的 mcr.microsoft.com/dotnet/aspnet:6.0 是純 runtime 映像,裡面沒有 dotnet watch,直接套這段 override 會噴 command not found

所以開發環境要讓 API 跑在 SDK 映像上。做法是在 Dockerfile 多留一個 build 階段當作開發目標,override 改成 build 這個階段:

1
2
3
4
# Dockerfile 在原本的 build 階段後,可直接把 build 階段當開發用映像
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
# ...(restore / copy 如前)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# docker-compose.override.yml
services:
api:
build:
context: ./api
dockerfile: Dockerfile
target: build # 指定建到含 SDK 的 build 階段,dotnet watch 才存在
volumes:
- ./api:/src
- /src/bin
- /src/obj
environment:
- ASPNETCORE_ENVIRONMENT=Development
- DOTNET_USE_POLLING_FILE_WATCHER=true
working_dir: /src
command: dotnet watch run

frontend:
volumes:
- ./src:/app/src
command: npm run dev

dotnet watch run 在較新版本可簡寫成 dotnet watch(會預設執行 run),兩種寫法都行。掛載路徑要對齊 build 階段的 WORKDIR /src,不要沿用 runtime 階段的 /app,否則 watch 監看的目錄會對不上原始碼。

Debug 設定

在 VS Code 中設定容器除錯,前提是容器內 /vsdbg/vsdbg 必須存在——aspnet runtime 映像本身不含 vsdbg,需要額外準備。有兩種方式:

方式一:在 Dockerfile 安裝 vsdbg

SDK/build 階段(也就是 FROM mcr.microsoft.com/dotnet/sdk:... AS build 那個階段)加入這行,不要加在 runtime 階段——runtime 映像沒有 apt-get 也不需要偵錯器:

1
2
RUN apt-get update && apt-get install -y --no-install-recommends curl unzip \
&& curl -sSL https://aka.ms/getvsdbgsh | bash /dev/stdin -v latest -l /vsdbg

方式二:以 volume 掛入本機下載好的 vsdbg

映像本身不需要包含偵錯器。先在本機下載 vsdbg:

1
2
# Linux/macOS
curl -sSL https://aka.ms/getvsdbgsh | bash /dev/stdin -v latest -l ~/.vsdbg
1
2
3
# Windows:直接在 VS Code 對容器做一次 C# Dev Kit attach,
# 擴充功能會自動把 vsdbg 下載到 %USERPROFILE%\.vsdbg\
# 下載完成後再按下面的步驟掛入容器即可

然後在 docker-compose.override.yml 把本機 vsdbg 路徑掛進容器:

1
2
3
4
services:
api:
volumes:
- ~/.vsdbg:/vsdbg:ro # 本機 vsdbg 路徑:容器路徑

Windows 路徑寫法:

1
2
volumes:
- "${USERPROFILE}/.vsdbg:/vsdbg:ro"

確認 vsdbg 已就位後,在 .vscode/launch.json 加入以下設定即可 attach:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// .vscode/launch.json
{
"configurations": [
{
"name": "Docker .NET Core Attach",
"type": "coreclr",
"request": "attach",
"processName": "YourProject",
"pipeTransport": {
"pipeProgram": "docker",
"pipeArgs": ["exec", "-i", "api"],
"debuggerPath": "/vsdbg/vsdbg"
}
}
]
}

debuggerPath/vsdbg/vsdbg(Linux 容器內路徑,不受本機 OS 影響)。不論哪種方式,都要確認此路徑確實存在於容器內,否則 attach 會報路徑找不到而失敗。

網路配置

網路隔離

讓前端和資料庫永遠不在同一個網路裡,是隔離洩漏風險最直接的做法。把服務分進不同 network:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# docker-compose.yml
networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: true # 只允許內部存取

services:
api:
networks:
- backend
- frontend

db:
networks:
- backend

frontend:
networks:
- frontend

部署與維護

CI/CD 整合

以下是一個使用 GitHub Actions 的範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# .github/workflows/docker-publish.yml
name: Docker Build and Push

on:
push:
branches: [ main ]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}

- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: user/myapp:latest

環境變數管理

建立不同環境的設定檔:

1
2
3
4
5
6
7
# .env.development
DB_PASSWORD=dev_password
API_URL=http://localhost:5000

# .env.production
DB_PASSWORD=${PROD_DB_PASSWORD}
API_URL=https://api.example.com

自動化部署腳本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/bash
# deploy.sh

# 拉取最新映像檔
docker compose pull

# 使用生產環境設定檔
export COMPOSE_FILE=docker-compose.yml:docker-compose.prod.yml

# 更新服務
docker compose up -d --remove-orphans

# 清理舊映像檔
docker image prune -f

常見問題與解決方案

1. SQL Server 連線問題

  • 確認連線字串中的主機名稱使用服務名稱 (db)
  • 確認 SA 密碼符合複雜度要求
  • 檢查防火牆設定
  • 連線字串記得帶 TrustServerCertificate=True。從 Microsoft.Data.SqlClient 4.0(.NET 6 時期)起,連 SQL Server 預設 Encrypt=true,會去驗證伺服器憑證。容器裡的 SQL Server 用的是自簽憑證,用戶端不信任就會丟「The certificate chain was issued by an authority that is not trusted」而連不上。這也是這個技術棧最常見的「照抄就壞」狀況。正式環境若要真正加密,應該換成受信任的憑證,而不是長期靠 TrustServerCertificate=True 跳過驗證。

2. .NET Core API 容器化問題

  • 專案名稱不符:Dockerfile 裡 COPY ["YourProject.csproj", "./"]ENTRYPOINT ["dotnet", "YourProject.dll"] 的名稱必須和實際 .csproj 檔名一致,否則 dotnet restoredotnet publish 會靜默失敗或 entrypoint 找不到 DLL。先用 docker build 看 build log,或進容器 ls /app/publish 確認輸出檔名。
  • ASPNETCORE_URLS 問題:容器內 API 預設監聽 http://+:80(aspnet 映像設定),若你本機跑慣了 https://localhost:5001 直接複製 URL 訪問會連不上。確認 docker-compose.yml 裡 ports 的容器端(右邊)和 ASPNETCORE_URLSASPNETCORE_HTTP_PORTS 對齊,例如 ports 5000:80 對應 ASPNETCORE_URLS=http://+:80
  • 埠號衝突:本機已有其他服務佔用 5000 port 時,docker run -p 5000:80 會直接報 address already in use。改換主機端口(左邊數字)即可,容器內部不受影響。

3. Vue3 建置問題

  • node_modules 沒被忽略:若 .dockerignore 漏了 node_modules/COPY . . 會把本機的 node_modules 全複製進去,覆蓋 npm install 的結果,且映像會暴增幾百 MB。確認 .dockerignore 有這一行,並用 docker build --no-cache 重新建置。
  • 環境變數 .env 問題:Vite 建置時只把 VITE_ 前綴的變數打包進靜態檔,普通的環境變數不會帶入。若前端無法讀到 API URL,確認 .env 裡是 VITE_API_URL=... 而非 API_URL=...,並確認 .envCOPY . . 之前已存在(或改在 npm run build 時以 --mode 指定)。

4. 效能優化相關

映像檔優化

  • 多階段建置:.NET Core 多階段建置後,映像從約 800 MB(SDK)壓到 200 MB 以內(aspnet runtime)。確認 Dockerfile 最終 FROM 用的是 aspnet 而非 sdk
  • alpine 基底:nginx 用 nginx:stable-alpine(約 23 MB)而非 nginx:stable(約 142 MB)。.NET Core 的 aspnet:8.0-alpine 比標準版再小 50-70 MB,但要注意 alpine 用的是 musl libc,少數 NuGet 套件有相容問題,遇到再換回標準版。
  • .dockerignore:至少要排 bin/obj/.git/node_modules/。可以用 docker history <image> 觀察每個 layer 大小,找出意外肥大的 COPY 步驟。

容器效能優化

  • 資源限制設定後確認有效docker stats 會顯示每個容器的 CPU/記憶體即時使用量與上限,設完就用這個確認限制是否生效。
  • Volume vs bind mount:Volume 走 Docker 管理路徑,在 macOS/Windows Docker Desktop 上比 bind mount 快 2-5 倍(bind mount 需要跨 VM 同步)。資料庫資料一定用 Volume,原始碼開發時才用 bind mount。
  • 清理指令docker system prune -f 一次清掉停止的容器、無用 network 和 dangling image;加 -a 會連沒在用的 image 一起刪,空間不夠時再用。

網路效能

  • 同一個 Compose 專案的服務自動在同一個 bridge network 裡,用服務名稱(如 dbapi)互連,不需要走 host port,延遲遠低於繞出去再繞回來。
  • 需要前後端隔離時,按本文「網路配置」一節建立 backend internal: true,只讓 API 跨兩個 network,不要讓 frontend 直接碰到 db。

安全性建議

除了之前提到的最佳實踐外,還要注意:

容器安全

1
2
3
4
5
6
7
services:
api:
security_opt:
- no-new-privileges:true
read_only: true
tmpfs:
- /tmp

read_only: true 把容器的根檔案系統設成唯讀,能防止惡意程序寫入。但 ASP.NET 的 runtime 需要在 /tmp 寫入暫存檔,不另外掛 tmpfs 直接加 read_only: true 會讓 API 起不來。上面這段已同時掛了 tmpfs: - /tmp,兩者要配合使用。

非 root 使用者

「使用非 root 使用者運行容器」是常見建議,但要在 Dockerfile 裡真的寫出來。以 .NET Core 映像為例:

1
2
3
4
5
6
7
8
9
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY --from=build /app/publish .

# 建立非 root 使用者,並切換
RUN adduser --disabled-password --gecos "" appuser
USER appuser

ENTRYPOINT ["dotnet", "YourProject.dll"]

SQL Server 官方映像(mssql/server)已內建用 mssql 使用者跑,不需要額外設定。

機密管理

使用 Docker Secrets:

1
2
3
4
5
6
7
8
services:
api:
secrets:
- db_password

secrets:
db_password:
file: ./secrets/db_password.txt

最佳實踐建議

  1. 安全性考量

    • 不要在映像檔中存儲敏感資訊
    • 定期更新基礎映像檔
    • 使用非 root 使用者運行容器
  2. 開發流程

    • 使用 docker-compose.override.yml 管理開發環境設定
    • 實作健康檢查機制
    • 建立完整的 CI/CD 流程

結論

本文涵蓋了以下幾個核心部分:可實際運行的 docker-compose.yml(含 healthcheck 確保 API 在資料庫就緒後才啟動)、Hot Reload 開發環境設定、nginx history mode 路由修正,以及常見問題的排查方向。

後續建議優先處理 .env 分層管理(開發/staging/正式環境各自獨立)與 CI/CD 自動建置推送。這兩項到位後,本機與正式環境的差異將收斂至環境變數層面,可有效避免因環境不一致引發的部署問題。

參考資源