Docker 模式 是做什麼的?
Docker 與 Docker Compose 的最佳實務,適用於容器化開發。
用於本地開發的 Docker Compose
標準網頁應用程式堆疊
# docker-compose.yml
services:
app:
build:
context: .
target: dev # 使用多階段 Dockerfile 的 dev 階段
ports:
- "3000:3000"
volumes:
- .:/app # 繫結掛載以支援熱重載
- /app/node_modules # 匿名磁碟區 — 保留容器內部的相依套件
environment:
- DATABASE_URL=postgres://postgres:postgres@db:5432/app_dev
- REDIS_URL=redis://redis:6379/0
- NODE_ENV=development
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
command: npm run dev
db:
image: postgres:16-alpine
ports:
- "5432:5432"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: app_dev
volumes:
- pgdata:/var/lib/postgresql/data
- ./scripts/init-db.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redisdata:/data
mailpit: # 本地電子郵件測試
image: axllent/mailpit
ports:
- "8025:8025" # Web UI
- "1025:1025" # SMTP
volumes:
pgdata:
redisdata:
開發 vs 生產 Dockerfile
# 階段:相依套件
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# 階段:開發(熱重載、除錯工具)
FROM node:22-alpine AS dev
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]
# 階段:建置
FROM node:22-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build && npm prune --production
# 階段:生產(最小映像檔)
FROM node:22-alpine AS production
WORKDIR /app
RUN addgroup -g 1001 -S appgroup && adduser -S appuser -u 1001
USER appuser
COPY --from=build --chown=appuser:appgroup /app/dist ./dist
COPY --from=build --chown=appuser:appgroup /app/node_modules ./node_modules
COPY --from=build --chown=appuser:appgroup /app/package.json ./
ENV NODE_ENV=production
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "dist/server.js"]
覆蓋檔案
# docker-compose.override.yml(自動載入,僅限開發設定)
services:
app:
environment:
- DEBUG=app:*
- LOG_LEVEL=debug
ports:
- "9229:9229" # Node.js 除錯器
# docker-compose.prod.yml(明確指定用於生產)
services:
app:
build:
target: production
restart: always
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
# 開發(自動載入覆蓋檔案)
docker compose up
# 生產
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
網路
服務發現
同一個 Compose 網路中的服務可透過服務名稱解析:
# 從 "app" 容器內部:
postgres://postgres:postgres@db:5432/app_dev # "db" 解析為 db 容器
redis://redis:6379/0 # "redis" 解析為 redis 容器
自訂網路
services:
frontend:
networks:
- frontend-net
api:
networks:
- frontend-net
- backend-net
db:
networks:
- backend-net # 只能從 api 存取,無法從 frontend 存取
networks:
frontend-net:
backend-net:
僅暴露必要項目
services:
db:
ports:
- "127.0.0.1:5432:5432" # 僅可從主機存取,無法從網路存取
# 在生產環境中完全省略 ports — 僅可從 Docker 網路內部存取
磁碟區策略
volumes:
# 具名磁碟區:在容器重新啟動後持續存在,由 Docker 管理
pgdata:
# 繫結掛載:將主機目錄對應到容器內(適用於開發)
# - ./src:/app/src
# 匿名磁碟區:防止繫結掛載覆蓋容器產生的內容
# - /app/node_modules
常見模式
services:
app:
volumes:
- .:/app # 原始碼(繫結掛載以支援熱重載)
- /app/node_modules # 保護容器的 node_modules 不受主機影響
- /app/.next # 保護建置快取
db:
volumes:
- pgdata:/var/lib/postgresql/data # 持久化資料
- ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql # 初始化腳本
容器安全性
Dockerfile 強化
# 1. 使用特定標籤(絕對不要用 :latest)
FROM node:22.12-alpine3.20
# 2. 以非 root 身分執行
RUN addgroup -g 1001 -S app && adduser -S app -u 1001
USER app
# 3. 捨棄權限(在 compose 中設定)
# 4. 盡可能使用唯讀根檔案系統
# 5. 映像檔層中不含機密
Compose 安全性
services:
app:
security_opt:
- no-new-privileges:true
read_only: true
tmpfs:
- /tmp
- /app/.cache
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE # 僅在繫結到低於 1024 的連接埠時需要
機密管理
# 良好作法:使用環境變數(執行時期注入)
services:
app:
env_file:
- .env # 絕對不要將 .env 提交到 git
environment:
- API_KEY # 從主機環境繼承
# 良好作法:Docker secrets(Swarm 模式)
secrets:
db_password:
file: ./secrets/db_password.txt
services:
db:
secrets:
- db_password
# 錯誤作法:在映像檔中硬編碼
# ENV API_KEY=sk-proj-xxxxx # 絕對不要這麼做
強化過的 CLI 安裝程式測試框架
使用容器來測試安裝程式對可丟棄的專案副本的行為,而不允許測試變異原始碼倉庫。
尊重平台邊界
- 針對 Linux 發行版(如 Debian 和 Ubuntu)執行真正的容器。
- macOS 無法作為 Docker 容器執行,因為 Docker 共用一個 Linux 核心。請在 macOS 上原生執行相同的無 shell 測試進入點。
- Windows 容器需要 Windows Docker 引擎。請在原生 Windows CI 執行器上執行與平台無關的邏輯,並保留 Windows 容器給 Windows 主機。
- 保留原生的 Ubuntu / macOS / Windows CI 矩陣,以測試主機特定的路徑、命令 shim、引號處理和檔案系統行為。
請不要聲稱 Linux 容器可以驗證 macOS 或 Windows 的行為。
強制執行隔離合約
- 使用不可變的摘要來固定基礎映像檔,並固定安裝的 CLI 版本。
- 當發行版的使用者帳戶名稱不同時,使用非 root 的數值 UID/GID 執行。
- 將倉庫和原始專案掛載為唯讀。
- 在進行任何變異之前,將原始專案複製到可寫入的
tmpfs工作區。 - 掛載
/workspace時使用noexec、UID/GID 1000 和mode=0700,以便只有容器使用者可以檢查專案資料。 - 將 npm 和 npx 的可執行快取保留在
NPM_CONFIG_CACHE=/tmp/npm-cache下的可執行/tmp掛載點。其預設大小為 2 GiB,可透過ECC_TMPFS_SIZE調整;ECC_WORKSPACE_SIZE則分別控制私有工作區掛載點。 - 設定
read_only: true、no-new-privileges:true、cap_drop: [ALL]和有限的pids_limit。 - 將預設的真實 CLI 服務保留在
network_mode: none。僅透過一個明顯命名的選擇加入服務來添加網路存取,以進行已驗證的提供者會話;絕不要讓它成為一個意外的環境驅動預設值。 - 僅建立工具所需的可寫入臨時路徑。
- 預設情況下,不要將主機憑證傳遞到容器中。
- 預設為試運行 (dry run),並僅允許明確指定的
dry-run、install、plugin和shell模式。 - 使用引數陣列或
spawnSync(..., { shell: false })作為跨平台執行器。絕不要將專案路徑插入到 shell 命令中。
執行 ECC 外掛程式設定框架
使用 docker/plugin-setup/compose.yaml 作為參考實作。它提供:
fixture-tests用於專注的安裝清單、目標和執行器套件。real-cli用於固定的、基於 Debian 的通用 Linux 映像檔。real-cli-ubuntu用於固定的 Ubuntu 映像檔。
在建置前驗證 Compose 模型:
docker compose -f docker/plugin-setup/compose.yaml config --quiet
建置兩個真實的 Linux 映像檔:
docker compose -f docker/plugin-setup/compose.yaml \
build real-cli real-cli-ubuntu
在每個映像檔中執行安全的預設流程:
docker compose -p ecc-plugin-debian-test \
-f docker/plugin-setup/compose.yaml \
run --rm -T real-cli dry-run
docker compose -p ecc-plugin-ubuntu-test \
-f docker/plugin-setup/compose.yaml \
run --rm -T real-cli-ubuntu dry-run
試運行會執行當前的公開命令合約:
ecc install --profile core --target claude-project --dry-run --json
在該命令執行之前,容器會從唯讀的原始碼倉庫使用 npm pack --ignore-scripts 建立一個本地打包的 npm 構件。它會將自行建立的 tarball 解壓縮到 /tmp 下,驗證 ecc-universal 套件名稱、所需的安裝清單和受限的 package.json bin.ecc 對應,然後呼叫解壓縮後的 ecc 執行檔。執行時期保持在 network_mode: none,不會執行套件的生命週期腳本,也不會依賴主機的 node_modules;其精確固定的生產相依套件已存在於映像檔中。
該框架拒絕空的計畫、非 claude-project 的目標、任何在 /workspace/project/.claude 之外的操作,或任何會建立目標目錄的試運行。install 會執行兩次隔離的套用,檢查其管理的安裝狀態,列出已安裝的目標,並執行 doctor。
啟動、開啟、重新連線和清理具名會話
啟動一個分離的容器,不要使用 --rm,這樣離開終端機時不會移除該會話:
docker compose -p ecc-plugin-session \
-f docker/plugin-setup/compose.yaml \
run --detach --name ecc-plugin-shell real-cli shell
該容器會將唯讀的 fixture 複製到穩定的私有目錄 /workspace/project。確認它正在執行,然後發出終端機開啟器 v1 資料合約的 Docker 部分:
docker inspect --format '{{.State.Running}}' ecc-plugin-shell
node docker/plugin-setup/interactive-plan.js \
--container ecc-plugin-shell \
--workdir /workspace/project \
--json \
-- bash
JSON 結果恰好包含 executable 和 argv 邊界(加上 contractVersion: 1):可執行檔是 docker,argv 以 exec、-it 和 -w 開頭。當單獨的終端機開啟器技能安裝完成後,將該資料傳遞給它。這個 Docker 框架刻意不匯入終端機轉接器、不插入 shell 命令,也不管理主機 GUI 程序。在此之前,請直接在當前的主機終端機中開啟同一個 PTY:
docker exec -it -w /workspace/project ecc-plugin-shell bash
退出 shell,不要停止分離的容器。使用相同的 docker exec -it 命令重新連線。完成後,移除該精確的具名容器及其 Compose 專案資源:
docker rm --force ecc-plugin-shell
docker compose -p ecc-plugin-session \
-f docker/plugin-setup/compose.yaml \
down --remove-orphans
主機憑證預設不存在,且憑證目錄也絕不會被掛載。預設服務也沒有網路存取。當一個已驗證的提供者會話確實需要網路時,請先建置 real-cli,然後使用 docker compose --profile networked run real-cli-networked shell 來明確選擇加入。盡量在該可丟棄的會話內部進行驗證。如果 CI 執行必須繼承主機環境憑證,請在呼叫時使用明確的 Compose --env NAME 標誌來選擇加入,了解該值在容器生命週期內是可檢查且可被外洩的,並在之後立即移除該精確的具名容器。
在主機上原生執行相同的聚焦套件:
npm run test:plugin-setup-platform
在信任映像檔之前,檢查產生的身份和環境:
docker image inspect ecc-plugin-setup:debian ecc-plugin-setup:ubuntu
清理每個具名測試專案,而不刪除不相關的磁碟區或映像檔:
docker compose -p ecc-plugin-debian-test \
-f docker/plugin-setup/compose.yaml down --remove-orphans
docker compose -p ecc-plugin-ubuntu-test \
-f docker/plugin-setup/compose.yaml down --remove-orphans
.dockerignore
node_modules
.git
.env
.env.*
dist
coverage
*.log
.next
.cache
docker-compose*.yml
Dockerfile*
README.md
tests/
除錯
常用命令
# 檢視日誌
docker compose logs -f app # 持續追蹤 app 日誌
docker compose logs --tail=50 db # 顯示 db 的最後 50 行
# 在執行中的容器內執行命令
docker compose exec app sh # 進入 app 的 shell
docker compose exec db psql -U postgres # 連線到 postgres
# 檢查
docker compose ps # 顯示執行中的服務
docker compose top # 顯示每個容器中的程序
docker stats # 顯示資源使用情況
# 重新建置
docker compose up --build # 重建映像檔
docker compose build --no-cache app # 強制完整重建
# 清理
docker compose down # 停止並移除容器
docker compose down -v # 一併移除磁碟區(具破壞性)
docker system prune # 移除未使用的映像檔/容器
除錯網路問題
# 檢查容器內的 DNS 解析
docker compose exec app nslookup db
# 檢查連線能力
docker compose exec app wget -qO- http://api:3000/health
# 檢查網路
docker network ls
docker network inspect <專案名稱>_default
反模式
# 錯誤作法:使用 latext
# FROM node:latest
# 錯誤作法:以 root 身分執行
# USER root
# 錯誤作法:在映像檔層中寫入機密
# ENV DB_PASSWORD=supersecret
# 錯誤作法:忽略 .dockerignore
# 這會將 node_modules 複製到建置上下文
# 錯誤作法:直接使用繫結掛載而沒有匿名磁碟區
# volumes:
# - .:/app
# 缺少 /app/node_modules 會導致容器套件被主機的 node_modules 覆蓋