cover

上一篇聊了怎麼選 Registry,這篇直接上手——各平台的 login/push 指令、CI Pipeline 範例、還有自動清理舊 image 的設定。

先講結論

  • 每個 Registry 的 docker login 方式都不同,CI 裡用變數管理 Registry 前綴,換平台時只改變數
  • ECR 的 Lifecycle Policy 是省儲存費的神器,第一天就該設
  • GitHub Actions + ghcr.io 的整合體驗最無痛,GITHUB_TOKEN 搞定一切

各 Registry 的 docker login / push 速查

# Docker Hub
docker login -u myuser -p mytoken
 
# AWS ECR(token 有效 12 小時)
aws ecr get-login-password --region ap-northeast-1 \
  | docker login --username AWS --password-stdin 123456789.dkr.ecr.ap-northeast-1.amazonaws.com
 
# Google Artifact Registry
gcloud auth configure-docker asia-east1-docker.pkg.dev
 
# Azure ACR
az acr login --name myregistry
 
# GitHub Container Registry
echo $GITHUB_TOKEN | docker login ghcr.io -u USERNAME --password-stdin
 
# 自建 Harbor
docker login harbor.example.com -u admin -p Harbor12345

image tag 格式的差異是最容易搞混的地方:Docker Hub 用 username/image,ECR 用一長串 AWS endpoint,ghcr.io 用 ghcr.io/org/image。建議在 CI 設定裡把 Registry 前綴抽成變數,換平台時改一個地方就好。


GitLab CI 推 ECR 的完整 Pipeline

這個範例涵蓋 test → build → deploy 三個 stage,重點在 before_script 用 AWS CLI 取得短期 token 登入 ECR:

# .gitlab-ci.yml
variables:
  AWS_REGION: "ap-northeast-1"
  AWS_ACCOUNT_ID: "123456789012"
  ECR_REGISTRY: "${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com"
  ECR_REPOSITORY: "myapp"
  IMAGE: "${ECR_REGISTRY}/${ECR_REPOSITORY}"
 
stages:
  - test
  - build
  - deploy
 
test:
  stage: test
  image: node:20-alpine
  script:
    - npm ci
    - npm test
 
build:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  before_script:
    - apk add --no-cache aws-cli
    - aws ecr get-login-password --region $AWS_REGION
      | docker login --username AWS --password-stdin $ECR_REGISTRY
  script:
    # 不存在就自動建 repository
    - aws ecr describe-repositories --repository-names $ECR_REPOSITORY --region $AWS_REGION
      || aws ecr create-repository --repository-name $ECR_REPOSITORY --region $AWS_REGION
    - docker build
      --label "git.commit=${CI_COMMIT_SHA}"
      -t ${IMAGE}:${CI_COMMIT_SHA}
      -t ${IMAGE}:${CI_COMMIT_REF_SLUG}
      .
    - docker push ${IMAGE}:${CI_COMMIT_SHA}
    - docker push ${IMAGE}:${CI_COMMIT_REF_SLUG}
    - |
      if [ -n "$CI_COMMIT_TAG" ]; then
        docker tag ${IMAGE}:${CI_COMMIT_SHA} ${IMAGE}:${CI_COMMIT_TAG}
        docker push ${IMAGE}:${CI_COMMIT_TAG}
      fi
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
    - if: $CI_COMMIT_TAG =~ /^v\d+/
 
deploy:dev:
  stage: deploy
  image: amazon/aws-cli:latest
  script:
    - aws ecs update-service
      --cluster my-cluster
      --service myapp-service
      --force-new-deployment
      --region $AWS_REGION
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

這裡有個小 trick:aws ecr describe-repositories ... || aws ecr create-repository ... 讓你不用手動先去 console 建 repository,CI 第一次跑就會自動建好。


ECR Lifecycle Policy — 不設等著被帳單嚇到

CI 每次 build 都會產生新的 image layer。沒有清理機制,儲存費用就是只增不減。Lifecycle Policy 是 ECR 最實用的功能之一:

{
  "rules": [
    {
      "rulePriority": 1,
      "description": "保留最近 5 個 v* tag",
      "selection": {
        "tagStatus": "tagged",
        "tagPrefixList": ["v"],
        "countType": "imageCountMoreThan",
        "countNumber": 5
      },
      "action": { "type": "expire" }
    },
    {
      "rulePriority": 10,
      "description": "14 天後刪除 untagged image",
      "selection": {
        "tagStatus": "untagged",
        "countType": "sinceImagePushed",
        "countUnit": "days",
        "countNumber": 14
      },
      "action": { "type": "expire" }
    },
    {
      "rulePriority": 20,
      "description": "30 天後刪除所有非版本號 image",
      "selection": {
        "tagStatus": "any",
        "countType": "sinceImagePushed",
        "countUnit": "days",
        "countNumber": 30
      },
      "action": { "type": "expire" }
    }
  ]
}
aws ecr put-lifecycle-policy \
  --repository-name myapp \
  --lifecycle-policy-text file://lifecycle-policy.json \
  --region ap-northeast-1

規則按 rulePriority 從小到大評估,符合條件的 image 會在 24 小時內自動刪除。我的建議是至少設 untagged 的清理規則,這是最低限度的防線。


GitHub Actions 推 ghcr.io — 最無痛的體驗

如果你的 CI 已經在 GitHub Actions 上,ghcr.io 的整合體驗真的是零摩擦:

# .github/workflows/build-push.yml
name: Build and Push to GHCR
 
on:
  push:
    branches: [main]
    tags: ['v*']
  pull_request:
    branches: [main]
 
env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}
 
jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
 
      - uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
 
      - id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=ref,event=branch
            type=semver,pattern={{version}}
            type=sha
 
      - uses: docker/build-push-action@v5
        with:
          context: .
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

幾個亮點:docker/metadata-action 自動根據 git ref 生成合適的 tag;permissions.packages: write + GITHUB_TOKEN 就能 push,不用建 PAT;PR 時只 build 不 push,避免未 review 的 image 進 Registry;cache-from: type=gha 利用 Actions cache 加速 build。


回顧

Registry 的選擇決定了你 CI/CD 的日常體驗。選對了,push/pull 像呼吸一樣自然;選錯了,每天都在跟認證過期和 rate limit 搏鬥。

系列文:上一篇:怎麼選 Registry?


設定好 Lifecycle Policy 的那一刻,就是你不再害怕打開 AWS 帳單的開始。