MapleCheng

在浩瀚的網路世界中無限潛水欸少年郎!

0%

DevContainer 實戰:打造 AI 友善的開發環境

「在我的電腦上可以跑啊」— 軟體界最經典的一句話,也是最讓人翻白眼的一句話。

DevContainer 出現之後,這個問題基本上被解決了。用 Docker 容器定義開發環境,誰 clone 下來都是同一套環境,不會因為你是 macOS、他是 Windows、另一個人的 Node.js 版本不對而搞半天。但用了一段時間之後,我發現 DevContainer 還有一個意想不到的好處 — 它讓 AI coding agent 更容易上手你的專案。

這個發現徹底改變了我對「開發環境」這件事的思考方式。

DevContainer 基礎

先簡單介紹一下 DevContainer,已經熟悉的可以跳過這段。

DevContainer 的核心概念很簡單:用 Docker 容器來定義你的開發環境。在專案根目錄放一個 .devcontainer/ 資料夾,裡面有 devcontainer.json(設定檔)和可選的 Dockerfile(客製化 image)。VS Code 和 Cursor 都原生支援,打開專案時會偵測到 .devcontainer/,問你要不要在容器裡開發。

主要優點有三個:

環境一致性 — 所有人用一模一樣的 runtime 版本、一模一樣的工具鏈、一模一樣的設定。不會再有「你的 .NET SDK 版本太舊」或「你的 npm 是全域裝的吧」這種問題。

新人 onboarding 快 — 新進工程師第一天就能開始寫 code。不用花兩天「設定環境」,clone repo、打開 VS Code、等容器跑完,就可以開始了。

不汙染本機 — 專案需要的 SDK、database、各種 CLI tool 都在容器裡。不用在本機裝一堆東西,也不會有專案 A 要 Node 18 但專案 B 要 Node 20 的衝突。

聽起來很美好,但其實推動的過程也不是一帆風順的。團隊一開始的反應是「為什麼要多一層 Docker?直接跑不是比較快嗎?」是啊,直接跑確實比較快 — 在你的電腦上。但你有沒有算過,每次有新人加入或換電腦,花在「設定環境」的時間加起來是多少?

為什麼 DevContainer 讓 AI 更強

這才是這篇文章的重點。

AI coding agent — 不管是 Cursor Agent、Claude Code、還是其他工具 — 它們要做的事情跟一個新進工程師其實很像:讀懂你的 code、改 code、build、跑 test、看結果、再改。但 AI 跟新人有個關鍵差異:AI 不會來問你「這個環境變數要設什麼」「這個指令怎麼跑不起來」。

如果你的開發環境設定在「只有你知道的 local 環境」裡 — 某個 SDK 是你手動裝的、某個 config 是你從 Slack 訊息裡複製的、某個 database 是你本機跑的 — 那 AI 就完全無法 reproduce 你的環境。它寫完 code 之後沒辦法 build,不知道是 code 的問題還是環境的問題。

DevContainer 把環境定義成 code。這意味著 AI 也能用同一套環境跑起你的專案。效果是什麼?AI 可以自己 build、跑 test、看 lint error,然後根據結果調整它的 code。這個「寫 code → build → 看 error → 修 → 再 build」的循環,就是 AI 產出品質大幅提升的關鍵。

沒有 DevContainer 的時候,AI 寫 code 就像閉著眼睛投籃 — 可能進,但很看運氣。有了 DevContainer,AI 等於有了即時回饋,每次投籃都能看到結果,自然越投越準。

一個具體的數據分享

我們團隊有追蹤過一段時間的數據(非正式的,就是每次 AI 提 PR 我會記錄一下 review 結果)。在導入 DevContainer 之前,AI 提的 PR 大概有 60% 需要修改,其中有不少是「build 不過」這種低級問題。導入之後,需要修改的比例降到 30% 左右,而且剩下需要改的大多是業務邏輯的調整,不再是環境問題。

這個數字不精確,但趨勢是很明顯的。

我的 DevContainer 設定策略

分享一下我實際在用的設定,以 .NET + React + SQL Server 的技術棧為例。這在做企業應用的團隊裡算是蠻常見的組合。

Base imagemcr.microsoft.com/devcontainers/dotnet:8.0,微軟官方的 DevContainer image,裡面已經包好 .NET SDK 和常用工具。

額外安裝 Node.js 和 pnpm(前端需要)。這部分直接在 devcontainer.json 裡用 features 來裝:

1
2
3
4
5
6
7
8
{
"features": {
"ghcr.io/devcontainers/features/node:1": {
"version": "22",
"pnpmVersion": "10"
}
}
}

Docker Compose 是另一個重要的部分。App 容器和 SQL Server 容器用 docker-compose.yml 組合起來。SQL Server 用獨立的容器跑,App 容器透過 Docker network 連過去。這樣 database 的生命週期和 app 是分開的,不會每次 rebuild app 容器就要重建 database。

一份簡化的 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
services:
app:
build:
context: .
dockerfile: .devcontainer/Dockerfile
volumes:
- ..:/workspace:cached
- node_modules:/workspace/client/node_modules
depends_on:
- db

db:
image: mcr.microsoft.com/azure-sql-edge
environment:
ACCEPT_EULA: "Y"
MSSQL_SA_PASSWORD: "YourDev_Password123"
ports:
- "1433:1433"
volumes:
- sqldata:/var/opt/mssql

volumes:
node_modules:
sqldata:

注意 node_modules 用了 named volume,這是效能最佳化的關鍵(後面踩坑經驗會提到)。

提醒一下用 Apple Silicon 的同行:SQL Server 的官方 image 不支援 ARM64,要改用 mcr.microsoft.com/azure-sql-edge 這個 image。功能上對開發來說夠用了,但第一次碰到的時候我確實卡了一陣子才找到這個替代品。

postCreateCommand 設定自動化的初始設定:

1
2
3
{
"postCreateCommand": "dotnet restore && dotnet build && cd client && pnpm install"
}

容器建好之後自動還原套件和 build。這樣不管是人還是 AI 打開專案,第一次就能有一個可以跑的狀態。

VS Code extensionsdevcontainer.json 裡預裝好,進容器就有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"customizations": {
"vscode": {
"extensions": [
"ms-dotnettools.csharp",
"ms-dotnettools.csdevkit",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"eamodio.gitlens"
],
"settings": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[csharp]": {
"editor.defaultFormatter": "ms-dotnettools.csharp"
}
}
}
}
}

這些設定加起來,整個開發環境的定義就是幾個檔案。任何人(或 AI)clone 下來,跑 devcontainer up,等幾分鐘,就有一個完整的開發環境。

AI 友善的開發環境 Checklist

除了 DevContainer 本身,還有一些配套措施可以讓 AI 更容易參與你的專案。我整理了一份 checklist,也是我們團隊目前在遵循的:

1. devcontainer.json 完整 — 這是基本中的基本。AI 可以透過 devcontainer up 指令把整個環境跑起來。如果你的 DevContainer 設定不完整(少了某個套件、缺了某個 config),AI 一樣會卡住。

2. README.md 寫清楚 build/test 指令 — AI 讀了 README 就知道怎麼跑你的專案。不要假設「大家都知道」。寫清楚:怎麼 build、怎麼跑 test、怎麼啟動 dev server。如果有特殊的前置步驟,也要寫。

3. .env.example 列出所有環境變數 — AI 需要知道你的專案需要哪些環境變數。.env.example 裡列出所有需要的 key,value 用假資料或說明填入。AI 看了就知道要設什麼。

4. Seed data / migration script — AI 需要能自己初始化資料庫。如果跑完 migration 之後資料庫是空的,很多功能根本測不了。準備好 seed data,讓 AI 可以 dotnet ef database update 之後就有可用的測試資料。

5. CI/CD config 可本地跑 — 讓 AI 可以在提交前先驗證。如果 CI 只能在 GitHub Actions 上跑,AI 就沒辦法在 local 預先確認自己的改動是否會 break build。

6. CLAUDE.md.cursorrules — 這是給 AI 的專案 onboarding 文件。寫清楚 coding conventions、架構說明、常見的 pattern、地雷區域。把它想成「如果一個新的資深工程師加入團隊,你會跟他說什麼?」那些內容就寫在這裡面。

這最後一點可能是最容易被忽略、但效果最好的。AI 不會讀心,但它會讀檔案。你把 conventions 寫下來,它就會遵守。你不寫,它就猜 — 而猜的結果往往跟你的偏好不同。

踩坑經驗

用了一年多的 DevContainer,踩過的坑也不少。分享幾個比較常見的:

Docker Desktop on macOS 的效能問題

這是每個 Mac 用戶都會碰到的。容器裡的檔案系統存取速度比 native 慢很多,尤其是 node_modules 這種有上萬個小檔案的目錄。

解法是把 node_modules 用 named volume 而非 bind mount,效能可以改善不少。在 docker-compose.yml 裡這樣設定:

1
2
3
volumes:
- ..:/workspace:cached # workspace 用 cached mode
- node_modules:/workspace/client/node_modules # node_modules 用 named volume

cached flag 告訴 Docker 可以接受 host 和 container 之間的檔案延遲同步,減少 I/O 開銷。named volume 則是讓 node_modules 完全在容器裡面,不需要跟 host 同步。

不過這也意味著你不能在 host 直接看到 node_modules 的內容,要在容器裡面操作。

SQL Server 的 ARM64 支援

前面提過了,用 azure-sql-edge。但要注意它的 SQL Server 版本可能跟 production 不完全一致,某些進階功能可能沒有。不過做開發和跑 test 是足夠的。

另一個常見的坑是密碼複雜度。SQL Server 的 SA 密碼有強度要求 — 至少 8 個字、要有大小寫和數字。如果你的密碼太簡單,container 會啟動成功但 SQL Server 服務跑不起來,error log 又藏在容器裡面不好找。第一次碰到的時候我 debug 了半小時才發現是密碼的問題。

Node.js 版本衝突

如果你同時維護多個專案,不同專案可能需要不同的 Node.js 版本。這正是 DevContainer 的優勢 — 每個專案在自己的容器裡用自己的版本。但記得在 Dockerfile 或 features 裡明確指定版本號,不要用 latest。今天的 latest 跟三個月後的 latest 可能不同,這會造成不可預期的問題。

容器啟動慢

第一次 build image 確實要等一陣子。善用 cacheFrom 指向一個 pre-built image,可以大幅減少重建時間。我們團隊會在 CI 裡自動 build DevContainer image 並 push 到 registry,這樣開發者和 AI agent 都可以直接 pull 現成的 image,不用從頭 build。

devcontainer.json 裡這樣設定:

1
2
3
4
5
{
"build": {
"cacheFrom": "ghcr.io/your-org/your-project-devcontainer:latest"
}
}

postCreateCommand 的時間管理

如果你的 postCreateCommand 太複雜或太久,容器啟動的等待時間會讓人(和 AI)很痛苦。把不必要的步驟拿掉,或是移到 postStartCommand(每次啟動容器時跑)和 postAttachCommand(每次連進容器時跑),根據需要分散。

我的經驗是:postCreateCommand 只放「首次建立環境必要的步驟」(restore、build),其他像是 seed data 或 watch mode 放在 postStartCommand 或寫成手動觸發的 script。

容器內的 Git 身份設定

這個坑比較隱蔽。容器內的 Git 預設可能沒有你的 user.name 和 user.email 設定,導致 commit 的時候出問題。DevContainer 會自動幫你帶入 host 的 Git config,但如果你用 Docker Compose 模式,有時候這個自動帶入不一定生效。

保險的做法是在 devcontainer.json 裡加上:

1
2
3
{
"postCreateCommand": "git config --global user.email 'you@example.com' && git config --global user.name 'Your Name' && dotnet restore && dotnet build"
}

和 AI 協作的實際流程

講了這麼多設定,來說說實際上我們怎麼用 DevContainer 搭配 AI 工作的。

流程大概是這樣:

  1. 開一個 feature branch
  2. 把需求描述清楚(在 issue 裡或是直接告訴 AI agent)
  3. 讓 AI agent 在 DevContainer 裡開始工作
  4. AI 寫 code → build → 看 error → fix → 再 build → 直到 green
  5. AI 提交 PR
  6. 人 review PR,提出修改意見
  7. AI 根據 review 修改 → 再跑一次 build/test → 更新 PR
  8. 人確認沒問題 → merge

整個過程中,人不需要手動設定任何環境。不用跟 AI 說「先裝這個、再裝那個、然後跑這個指令」。AI 開了 DevContainer,環境就是對的,它可以專注在寫 code 上。

這個流程的前提是什麼?就是前面說的那些 — DevContainer 設定完整、README 寫清楚、有 seed data、有 onboarding 文件。前期的投入看起來很花時間,但一旦到位,後續每次 AI 協作都會受益。

而且說實話,這些東西就算不用 AI,對團隊本身也是有好處的。新人 onboarding 更快、環境問題更少、文件更完整。AI 只是讓你有更強的動機去做這些「本來就該做但一直拖延」的事情。

一個實際的協作案例

最近有一個客製需求是加一個批次列印功能。我在 issue 裡寫清楚需求(選擇多筆訂單 → 批次產生 PDF → 壓成 ZIP 下載),然後交給 AI agent。

AI 在 DevContainer 裡:

  1. 讀了 CLAUDE.md 了解架構
  2. 找到現有的單筆列印功能作為參考
  3. 在 server 端加了批次處理的 API
  4. 在 client 端加了多選和批次列印的按鈕
  5. 跑了 dotnet build 確認編譯通過
  6. 跑了 dotnet test 確認沒有 break 既有的 test
  7. 提了 PR

我 review 的時候發現它的 ZIP 壓縮是在記憶體裡做的,大量訂單可能 OOM。提了 comment,AI 改成用 streaming 的方式寫入 temp file,再回傳 file stream。第二次 review 就過了。

整個過程我大概花了 30 分鐘 review,AI 花了大概 15 分鐘從頭到尾。如果是我自己寫,大概要半天。

結語

DevContainer 不只是給人用的,也是給 AI 用的。

當你的開發環境可以用 code 定義,AI 就能成為你開發團隊的一員。它可以 clone repo、啟動環境、build 專案、跑 test、看結果、改 code — 全部自動化,不需要人在旁邊手把手指導。

在 AI coding agent 越來越強大的今天,「開發環境的可重現性」不再只是一個 nice-to-have 的工程實踐,而是決定你能不能有效利用 AI 的關鍵因素。

如果你的專案還沒有 DevContainer,現在開始設定並不晚。從一個簡單的 devcontainer.json 開始,把 base image 和必要的工具定義好,再逐步加入 Docker Compose、seed data、onboarding 文件。不用一次到位,但要開始。

畢竟,你不只是在幫現在的團隊成員打造更好的開發體驗,也是在幫未來的 AI 隊友鋪路。而且說真的,就算完全不考慮 AI,DevContainer 本身就值得導入。少花時間在「設定環境」上,多花時間在「寫好 code」上 — 這不就是我們一直想要的嗎?