Docker 基礎回顧
Docker 是 CI/CD 流程中不可或缺的容器化工具。理解三個核心概念是掌握 Docker 在 CI/CD 中應用的前提:
- 映像(Image):唯讀的應用程式快照,包含程式碼、執行環境與所有依賴。映像由多個「層(Layer)」疊加而成,每條 Dockerfile 指令產生一層。
- 容器(Container):映像的執行實例。容器是隔離的、短暫的,可隨時建立或銷毀。在 CI 中,每個 Job 通常在全新的容器中執行,確保環境乾淨。
- Dockerfile:定義如何建置映像的文字腳本,從基底映像出發,逐層疊加指令,最終產出可部署的映像。
在 CI/CD 流程中,Docker 扮演三個核心角色:
- 建置環境標準化:每次建置都在相同的容器環境中進行,消除「在我的機器上可以跑」的問題。
- 交付單元一致性:映像即交付物,從開發到生產使用同一份映像,確保環境一致。
- 快速水平擴展:容器啟動時間以秒計,非常適合自動擴縮容(auto-scaling)場景。
Dockerfile 的基本結構包含幾個核心指令:
# Dockerfile 基礎指令速覽
FROM node:20-alpine # 指定基底映像
WORKDIR /app # 設定工作目錄(後續指令的相對路徑基準)
COPY package*.json ./ # 複製檔案進映像
RUN npm ci # 在映像中執行指令(每個 RUN 產生一個新層)
ENV NODE_ENV=production # 設定環境變數
EXPOSE 3000 # 宣告容器監聽的連接埠(僅作說明用)
USER appuser # 切換執行使用者(安全最佳實務)
CMD ["node", "server.js"] # 容器啟動時執行的預設指令alpine 變體(如 node:20-alpine)可大幅縮小映像體積。alpine 的基底大小僅約 5MB,而標準 Debian 基底約 100MB 以上。若需要更高相容性,可考慮 slim 變體作為折衷方案。生產映像應鎖定具體版本號(如 node:20.11-alpine3.19),避免使用 latest tag 造成不可預期的版本漂移。多階段建置(Multi-stage Build)最佳化
多階段建置是減少映像大小最有效的手段。透過在一個 Dockerfile 中定義多個 FROM 階段,可以將龐大的建置工具鏈排除在最終映像之外。最終映像只包含應用程式實際執行所需的最小檔案集合。
FROM 階段輸出為最終映像,前面階段的檔案層完全不會包含在內。# Node.js 應用的生產級多階段 Dockerfile
# ---- 第一階段:安裝依賴並編譯 ----
FROM node:20-alpine AS builder
WORKDIR /app
# 優先複製 package.json,讓依賴層被快取
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# ---- 第二階段:精簡的生產執行映像 ----
FROM node:20-alpine AS runner
WORKDIR /app
# 安全最佳實務:建立非 root 使用者
RUN addgroup -g 1001 -S nodejs && \
adduser -S appuser -u 1001
# 只從建置階段複製必要檔案
COPY --from=builder --chown=appuser:nodejs /app/dist ./dist
COPY --from=builder --chown=appuser:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=appuser:nodejs /app/package.json ./
USER appuser
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "dist/server.js"]scratch 基底映像,即完全空白映像,只複製靜態編譯的二進位檔案)。縮小映像不只節省儲存空間,也能降低攻擊面(Attack Surface),提升安全性。Go 應用的極致精簡範例:
# Go 應用:使用 scratch 基底達到最小映像
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# 靜態編譯,不依賴任何動態函式庫
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o server .
# 最終映像:完全空白,只有執行檔
FROM scratch
COPY --from=builder /app/server /server
# 若需要 HTTPS,需複製 CA 憑證
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8080
ENTRYPOINT ["/server"]
# 結果:映像大小通常 < 15MBCI 中的 Docker Layer Cache 策略
Layer cache 是加速 CI 建置的最重要手段。當 Dockerfile 的某一層未發生變化時,Docker 會直接使用快取層,跳過重新建置,大幅縮短 Pipeline 執行時間。以下是在 GitLab CI 中充分利用快取的兩種主要策略:
# 策略一:使用 Registry 作為遠端快取後端(推薦,適合多機器 Runner)
build-image:
stage: build
image: docker:24.0
services:
- docker:24.0-dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker buildx build \
--cache-from type=registry,ref=$CI_REGISTRY_IMAGE:buildcache \
--cache-to type=registry,ref=$CI_REGISTRY_IMAGE:buildcache,mode=max \
--build-arg BUILDKIT_INLINE_CACHE=1 \
-t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA \
-t $CI_REGISTRY_IMAGE:latest \
--push \
.
# 策略二:使用 GitLab CI cache 機制(適合單機 Runner)
build-image-local-cache:
stage: build
cache:
key: docker-layer-cache-$CI_COMMIT_REF_SLUG
paths:
- .docker-cache/
script:
- docker build \
--cache-from $(cat .docker-cache/tag 2>/dev/null || echo scratch) \
-t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA \
.
- docker save $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA > .docker-cache/image.tar
- echo "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" > .docker-cache/tag
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHApackage.json)放在前面,變動頻率高的指令(如 COPY . .)放在最後,可大幅提升快取效率。一旦某一層的快取失效,其後的所有層都必須重新建置——這是撰寫 Dockerfile 時最重要的排序原則。快取命中率優化的黃金排序:
- 第一層:
FROM基底映像(幾乎不變) - 第二層:安裝系統依賴(
apt-get/apk add,偶爾變動) - 第三層:複製套件定義檔(
package.json/go.mod,較少變動) - 第四層:安裝套件依賴(
npm ci/go mod download,依套件版本而定) - 最後層:複製原始碼(
COPY . .,每次 commit 都變動)
--mount=type=cache 在多次建置之間持久化套件管理器的下載快取(如 npm、pip、cargo),避免每次都重新下載相同的套件。Docker Compose in CI
在 CI 環境中,Docker Compose 常用於啟動整合測試所需的依賴服務(資料庫、快取、訊息佇列等),讓測試在隔離且可重現的環境中執行。使用 healthcheck 搭配 depends_on: condition: service_healthy,可確保服務完全就緒後才開始測試,避免因啟動競態條件(race condition)導致的不穩定測試結果。
# docker-compose.test.yml — 專為 CI 整合測試設計
version: "3.9"
services:
app:
build: .
environment:
NODE_ENV: test
DATABASE_URL: postgres://user:pass@db:5432/testdb
REDIS_URL: redis://cache:6379
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: testdb
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user"]
interval: 5s
timeout: 3s
retries: 5
cache:
image: redis:7-alpine
---
# .gitlab-ci.yml 中的整合測試 job
integration-test:
stage: test
image: docker:24.0
services:
- docker:24.0-dind
script:
- docker compose -f docker-compose.test.yml up --abort-on-container-exit
- docker compose -f docker-compose.test.yml down -v
after_script:
- docker compose -f docker-compose.test.yml logs app--abort-on-container-exit 旗標會在任何一個容器退出時停止整個 Compose 服務群,配合應用程式測試完成後主動退出(process.exit()),可讓 CI job 在測試完成後自動結束,無需等待逾時。記得搭配 down -v 清理匿名 Volume,避免測試資料污染下一次執行。映像安全掃描(Trivy)
在將映像推送到生產環境之前,應透過自動化工具掃描已知的 CVE 漏洞與設定問題。安全掃描應整合到 CI Pipeline 中,作為「品質閘門(Quality Gate)」的一部分——有高危漏洞則自動阻擋合併,而非事後補救。Trivy 是目前最受歡迎的開源容器安全掃描工具,由 Aqua Security 維護,支援映像漏洞掃描、IaC 設定掃描、Secret 掃描等多種功能。
# 在 CI Pipeline 中整合 Trivy 安全掃描
security-scan:
stage: test
image: aquasec/trivy:latest
script:
# 掃描映像漏洞,HIGH 與 CRITICAL 等級會讓 Pipeline 失敗
- trivy image \
--exit-code 1 \
--severity HIGH,CRITICAL \
--no-progress \
--ignore-unfixed \
$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
# 掃描 Dockerfile 本身的設定問題(IaC 設定掃描)
- trivy config \
--exit-code 1 \
--severity HIGH,CRITICAL \
.
# 輸出 SARIF 格式報告,可上傳至 GitLab Security Dashboard
- trivy image \
--format sarif \
--output trivy-results.sarif \
$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
artifacts:
reports:
container_scanning: trivy-results.sarif
allow_failure: false # 有高危漏洞則阻擋部署
# 使用 Snyk 掃描(需要 SNYK_TOKEN,提供更詳細的修復建議)
snyk-scan:
stage: test
image: snyk/snyk:docker
variables:
SNYK_TOKEN: $SNYK_TOKEN
script:
- snyk container test $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA \
--severity-threshold=high \
--file=Dockerfile
- snyk container test $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA \
--sarif-file-output=snyk-container.sarif
artifacts:
reports:
container_scanning: snyk-container.sarif略過無修復版本的漏洞:加上
--ignore-unfixed 旗標,只報告已有修補版本的漏洞,避免阻塞無法立即修復的已知問題。
忽略特定 CVE:建立
.trivyignore 檔案(格式:每行一個 CVE 編號),列出經評估後可接受的漏洞,Trivy 掃描時會自動略過。
定期排程掃描:除了建置時掃描,應設定排程 Job(每日或每週)重新掃描已在線上運行的映像,以應對新發現的漏洞(映像內容不變但 CVE 資料庫持續更新)。
掃描範圍控制:使用
--vuln-type os,library 分別掃描 OS 套件與應用程式依賴,可更精確地定位問題來源。allow_failure: false,強制所有高危漏洞在合併前被修復。初期若有大量既有漏洞,可先以 .trivyignore 記錄並追蹤,逐步清零,而非一開始就放棄掃描或設為 allow_failure: true。Registry 推送策略與 Tag 命名規範
一套清晰的 tag 命名策略,能讓團隊在回滾或追查問題時快速定位目標映像。Tag 策略需兼顧「可追溯性」(每個映像對應到唯一的程式碼版本)與「方便性」(特定環境能用固定的 tag 名稱拉取最新映像)。
主流 Container Registry 比較:
- GitLab Container Registry:與 GitLab CI 深度整合,使用內建變數即可驗證,適合自架環境。支援 Cleanup Policy 自動清理舊映像。
- Docker Hub:最廣泛使用的公開 Registry,免費方案有 pull rate limit(匿名 100次/6小時,登入 200次/6小時),私有映像需付費方案。
- Amazon ECR(Elastic Container Registry):與 AWS ECS/EKS 整合最佳,按儲存量計費(0.10 USD/GB/月),支援映像掃描與 Lifecycle Policy 自動清理。
- Google Artifact Registry(GCR 繼任者):與 GKE/Cloud Run 整合,支援多種 artifact 類型(映像、npm、Maven 等),比舊版 GCR 功能更完整。
# 推薦的 Tag 命名策略與推送邏輯
variables:
# 不可變標籤:以 Commit SHA 作為唯一識別(生產部署應使用此格式)
TAG_SHA: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
# 分支標籤:可被同分支的新 commit 覆蓋(追蹤各分支最新狀態)
TAG_BRANCH: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
# 語意化版本:由 git tag(如 v1.2.3)觸發時設定
TAG_VERSION: $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
build-and-push:
stage: build
script:
- docker build -t $TAG_SHA .
# 加上分支標籤(方便 staging 環境追蹤最新版本)
- docker tag $TAG_SHA $TAG_BRANCH
- docker push $TAG_SHA
- docker push $TAG_BRANCH
# 只有 main branch 才推送 latest 標籤
- |
if [ "$CI_COMMIT_BRANCH" = "main" ]; then
docker tag $TAG_SHA $CI_REGISTRY_IMAGE:latest
docker push $CI_REGISTRY_IMAGE:latest
fi
# 有 git tag 時推送語意化版本標籤(同時推 v1.2.3、1.2、1)
- |
if [ -n "$CI_COMMIT_TAG" ]; then
MAJOR=$(echo $CI_COMMIT_TAG | cut -d. -f1 | tr -d v)
MINOR=$(echo $CI_COMMIT_TAG | cut -d. -f1-2 | tr -d v)
docker tag $TAG_SHA $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
docker tag $TAG_SHA $CI_REGISTRY_IMAGE:$MINOR
docker tag $TAG_SHA $CI_REGISTRY_IMAGE:$MAJOR
docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
docker push $CI_REGISTRY_IMAGE:$MINOR
docker push $CI_REGISTRY_IMAGE:$MAJOR
fi
# 推送至 Amazon ECR(需設定 AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY)
push-to-ecr:
stage: build
image: amazon/aws-cli:latest
script:
- aws ecr get-login-password --region $AWS_REGION | \
docker login --username AWS --password-stdin $ECR_REGISTRY
- docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA \
$ECR_REGISTRY/$ECR_REPOSITORY:$CI_COMMIT_SHA
- docker push $ECR_REGISTRY/$ECR_REPOSITORY:$CI_COMMIT_SHA不可變:
:sha-abc1234(生產部署應永遠使用此格式,確保每次部署的版本可追溯)環境追蹤:
:main、:develop(反映各分支最新狀態,方便 staging 自動部署)版本發布:
:v1.2.3、:1.2、:1(語意化版本,方便依賴方鎖版並享受自動接收 patch 更新的便利)避免使用:
:latest 作為生產部署的唯一標籤,因為它是可變的,無法保證部署版本的一致性,也難以追溯問題發生時線上跑的確切版本。