infra
Platform

모듈 맵

[Docker] 빌드 자동화와 이미지 태그 배포 파이프라인 구축

0 / 27 완료

펼치기
0 / 27 완료0%

Docker · 26 / 27

[Docker] 빌드 자동화와 이미지 태그 배포 파이프라인 구축

GitHub Actions와 GitLab CI에서 이미지 빌드, 테스트, 레지스트리 push를 자동화합니다

CI/CD 파이프라인에서 Docker 활용

🚨INCIDENT ALERT
HIGH

팀원이 각자 로컬에서 이미지를 빌드해 배포하면 "어떤 코드가 올라갔는지" 추적이 끊기기 쉽습니다. 문제 발생 시 롤백 기준이 모호해지고, 자격증명 노출 같은 보안 사고도 CI 로그에서 자주 발생합니다. CI/CD에서 Docker를 표준화하면 빌드 재현성, 태그 추적성, 배포 자동화를 한 번에 확보할 수 있습니다. 이 모듈은 GitHub Actions/GitLab CI에서 바로 적용 가능한 안전한 파이프라인 패턴을 다룹니다.

"이미지는 개발자 로컬에서 빌드한다"는 관행은 팀이 커질수록 문제가 됩니다. 빌드 결과가 개발자 환경마다 달라지고, 누가 언제 무엇을 배포했는지 추적이 어려워집니다. CI/CD 파이프라인에 Docker 빌드를 통합하면 모든 이미지가 동일한 환경에서 만들어지고, git 커밋과 이미지가 1:1로 연결되며, 사람의 손을 거치지 않고 자동으로 레지스트리에 push됩니다.


이번 챕터에서 배울 것

GitHub Actions와 GitLab CI를 중심으로 실무에서 바로 사용할 수 있는 Docker 파이프라인 패턴을 다룹니다. 빌드 캐시 전략과 보안 주의사항까지 포함합니다.

  • 1CI/CD에서 Docker의 역할 — 빌드 표준화와 테스트 환경 일관성
  • 2GitHub Actions로 이미지 빌드 및 GHCR push (docker/build-push-action)
  • 3GitLab CI .gitlab-ci.yml 파이프라인 구성 — 빌드, 테스트, push 단계
  • 4이미지 태그 전략 — git SHA, 브랜치명, semver 혼합 운용
  • 5레이어 캐시를 CI에서 활용 (cache-from/cache-to with registry)
  • 6Docker-in-Docker(DinD) vs Docker socket 마운트 방식 비교
실습 환경 준비

GitHub Actions 실습은 github.com 계정만 있으면 됩니다. GitLab CI 실습은 선택 사항이며 gitlab.com 무료 계정으로 진행할 수 있습니다.

GitHub 계정 및 레포지토리 준비

GHCR(GitHub Container Registry) 사용 — github.com에서 무료로 제공

GitLab 계정 및 프로젝트 준비 (선택)

gitlab.com 무료 티어에서 CI/CD 파이프라인 사용 가능

로컬 Docker BuildKit 확인
docker buildx version
실습용 샘플 앱 디렉토리 생성
mkdir -p ~/cicd-lab && cd ~/cicd-lab
git 레포지토리 초기화
git init && git checkout -b main
💡개념

CI/CD에서 Docker의 역할

CI/CD에서 Docker의 역할

왜 CI에서 이미지를 빌드해야 하는가

로컬 빌드의 문제점은 재현 불가능성입니다. 개발자 A의 맥북, 개발자 B의 리눅스 워크스테이션, 그리고 CI 서버에서 같은 Dockerfile로 빌드하더라도 Node.js 버전, 캐시 상태, 운영체제 차이로 인해 미묘하게 다른 이미지가 만들어질 수 있습니다.

CI/CD 파이프라인에 빌드를 통합하면 다음이 보장됩니다:

  • 빌드 표준화: 항상 동일한 환경(CI Runner)에서 빌드
  • git 연동: 모든 이미지가 특정 커밋과 연결되어 추적 가능
  • 자동화: PR 머지 시 자동 빌드 및 배포
  • 보안 스캔 통합: 빌드 후 자동으로 취약점 스캔 실행

이미지 태그 전략

태그는 이미지의 버전을 식별하는 핵심 정보입니다. 잘못된 태그 전략은 롤백을 불가능하게 만듭니다.

로컬 터미널
# 실습 디렉토리 준비
mkdir -p /tmp/docker/part5/exam_26 && cd /tmp/docker/part5/exam_26

# 안티패턴: latest만 사용
docker push myapp:latest  # 이전 버전으로 돌아갈 방법 없음

# 권장 패턴 1: git SHA 기반 (추적 가능성 최대)
docker push myapp:a1b2c3d4

# 권장 패턴 2: 브랜치 + SHA 조합
docker push myapp:main-a1b2c3d4
docker push myapp:main-latest  # 브랜치의 최신 이미지

# 권장 패턴 3: semver (릴리스 이미지)
docker push myapp:1.2.3
docker push myapp:1.2
docker push myapp:1

실무에서는 **SHA 태그(추적용) + 브랜치 태그(편의용)**를 동시에 push하는 방식이 가장 일반적입니다.

💡개념

GitHub Actions 워크플로우 구성

main 브랜치에 push할 때마다 이미지를 자동으로 빌드해서 레지스트리에 올리고 싶습니다. 매번 로컬에서 docker build, docker push를 수동으로 실행하는 것은 실수가 생기고, 누가 빌드했는지 추적도 안 됩니다. GitHub Actions에 워크플로우 파일 하나를 추가하면 코드 push 시점에 자동으로 빌드, 태그, push까지 실행됩니다.

GitHub Actions 워크플로우

docker/build-push-action으로 이미지 빌드 및 push

GitHub Actions의 공식 Docker 액션인 docker/build-push-action은 BuildKit 기반으로 멀티플랫폼 빌드, 레지스트리 캐시, GHCR push를 지원합니다.

YAML
# .github/workflows/docker-build.yml
name: Docker 이미지 빌드 및 Push

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}  # owner/repo 형태

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write  # GHCR에 push하려면 packages 권한 필요

    steps:
      - name: 코드 체크아웃
        uses: actions/checkout@v4

      - name: Docker Buildx 설정
        uses: docker/setup-buildx-action@v3

      - name: GHCR 로그인
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}  # 자동 제공되는 토큰

      - name: 이미지 메타데이터 추출 (태그 및 레이블 자동 생성)
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=ref,event=branch
            type=ref,event=pr
            type=sha,prefix={{branch}}-
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}

      - name: 빌드 및 Push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: ${{ github.event_name != 'pull_request' }}  # PR은 빌드만, push는 배포
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache
          cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max

metadata-action이 생성하는 태그 예시

위 워크플로우에서 main 브랜치에 커밋 a1b2c3d를 push하면:

ghcr.io/myorg/myapp:main
ghcr.io/myorg/myapp:main-a1b2c3d

v1.2.3 태그를 달면:

ghcr.io/myorg/myapp:1.2.3
ghcr.io/myorg/myapp:1.2
ghcr.io/myorg/myapp:1

멀티 아키텍처 빌드 (AMD64 + ARM64)

Apple Silicon 맥 사용자가 늘면서 ARM64 이미지를 함께 배포하는 것이 중요해졌습니다:

YAML
      - name: QEMU 설정 (크로스 플랫폼 빌드용)
        uses: docker/setup-qemu-action@v3

      - name: Docker Buildx 설정
        uses: docker/setup-buildx-action@v3

      - name: 멀티 아키텍처 빌드 및 Push
        uses: docker/build-push-action@v5
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache
          cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max
💡개념

GitLab CI 파이프라인 구성

회사가 GitLab을 씁니다. GitHub Actions 예시는 많은데 .gitlab-ci.yml로 Docker 이미지를 빌드하고 GitLab Container Registry에 push하는 방법은 구조가 다릅니다. GitLab CI는 자체 레지스트리와 CI 변수가 미리 연결되어 있어서, 인증 토큰 설정 없이도 GitLab이 제공하는 사전 정의 변수만 쓰면 됩니다.

GitLab CI 파이프라인

.gitlab-ci.yml 전체 구성

GitLab CI는 사전 정의 변수(CI_REGISTRY_IMAGE, CI_COMMIT_SHA 등)를 자동으로 제공하여 편리하게 파이프라인을 구성할 수 있습니다.

YAML
# .gitlab-ci.yml
stages:
  - build
  - test
  - push
  - deploy

variables:
  DOCKER_DRIVER: overlay2
  DOCKER_TLS_CERTDIR: "/certs"
  IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  IMAGE_BRANCH: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG

# 빌드 스테이지: 이미지 빌드 및 임시 저장
build-image:
  stage: build
  image: docker:24-dind
  services:
    - docker:24-dind
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
    # 레지스트리 캐시에서 이전 레이어 가져오기
    - docker pull $IMAGE_BRANCH || true
    - docker build
        --cache-from $IMAGE_BRANCH
        --tag $IMAGE_TAG
        --tag $IMAGE_BRANCH
        --label "git-commit=$CI_COMMIT_SHA"
        --label "git-branch=$CI_COMMIT_REF_NAME"
        .
    # 테스트용 임시 태그로 push
    - docker push $IMAGE_TAG
    - docker push $IMAGE_BRANCH
  only:
    - branches
    - merge_requests

# 테스트 스테이지: 빌드된 이미지로 테스트 실행
test-unit:
  stage: test
  image: $IMAGE_TAG
  script:
    - npm test
  dependencies:
    - build-image

test-integration:
  stage: test
  image: $IMAGE_TAG
  services:
    - postgres:15-alpine
    - redis:7-alpine
  variables:
    DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/testdb"
    REDIS_URL: "redis://redis:6379"
    POSTGRES_PASSWORD: "postgres"
    POSTGRES_DB: "testdb"
  script:
    - npm run test:integration
  dependencies:
    - build-image

# push 스테이지: main 브랜치 머지 시 프로덕션 태그 push
push-production:
  stage: push
  image: docker:24-dind
  services:
    - docker:24-dind
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
    - docker pull $IMAGE_TAG
    - docker tag $IMAGE_TAG $CI_REGISTRY_IMAGE:latest
    - docker push $CI_REGISTRY_IMAGE:latest
  only:
    - main

# semver 태그 릴리스 (v* 태그 push 시 자동 실행)
push-release:
  stage: push
  image: docker:24-dind
  services:
    - docker:24-dind
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
    - docker pull $IMAGE_TAG
    - docker tag $IMAGE_TAG $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
  only:
    - /^v\d+\.\d+\.\d+$/  # v1.2.3 형식 태그에만 실행
  except:
    - branches
💡개념

CI에서 레이어 캐시 활용

레이어 캐시 전략

캐시 전략별 빌드 시간 비교

레이어 캐시 없이 빌드하면 Node.js 의존성 설치에만 23분이 소요될 수 있습니다. 캐시를 활용하면 코드만 변경된 경우 1020초로 단축됩니다.

캐시 없음:
  FROM node:20-alpine      → 다운로드 30초
  COPY package*.json ./    → 1초
  RUN npm ci               → 2분 30초  ← 매번 재실행
  COPY . .                 → 1초
  RUN npm run build        → 30초
  총: ~3분 30초

레지스트리 캐시 사용:
  FROM node:20-alpine      → 캐시 HIT
  COPY package*.json ./    → 캐시 HIT (package.json 변경 없음)
  RUN npm ci               → 캐시 HIT ← 이 레이어 재사용!
  COPY . .                 → 캐시 MISS (코드 변경)
  RUN npm run build        → 30초
  총: ~35초

BuildKit inline 캐시 vs registry 캐시

YAML
# 방법 1: inline 캐시 (이미지에 캐시 메타데이터 포함)
# 장점: 별도 캐시 이미지 불필요
# 단점: 프로덕션 이미지 크기 증가, mode=max 미지원
- name: 빌드 (inline 캐시)
  uses: docker/build-push-action@v5
  with:
    cache-from: type=registry,ref=ghcr.io/org/app:latest
    cache-to: type=inline

# 방법 2: registry 캐시 (별도 캐시 태그 사용) — 권장
# 장점: 프로덕션 이미지와 분리, mode=max로 모든 중간 레이어 캐싱
# 단점: 별도 캐시 이미지 태그 관리 필요
- name: 빌드 (registry 캐시)
  uses: docker/build-push-action@v5
  with:
    cache-from: type=registry,ref=ghcr.io/org/app:buildcache
    cache-to: type=registry,ref=ghcr.io/org/app:buildcache,mode=max

# 방법 3: GitHub Actions 캐시 (actions/cache 연동)
- name: 빌드 (gha 캐시)
  uses: docker/build-push-action@v5
  with:
    cache-from: type=gha
    cache-to: type=gha,mode=max

Dockerfile 캐시 최적화 — CI를 고려한 레이어 순서

Dockerfile
# CI 최적화 Dockerfile
FROM node:20-alpine AS deps

WORKDIR /app

# 1단계: 의존성 파일만 먼저 복사 (자주 변경 안 됨)
COPY package.json package-lock.json ./

# 2단계: 의존성 설치 (package.json 변경 시에만 레이어 무효화)
RUN npm ci --only=production

# ─────────────────────────────
FROM node:20-alpine AS builder

WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci

# 3단계: 소스코드 복사 (자주 변경됨 — 최대한 뒤에 배치)
COPY . .
RUN npm run build

# ─────────────────────────────
FROM node:20-alpine AS runtime

WORKDIR /app

# 프로덕션 의존성만 복사
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist

EXPOSE 3000
CMD ["node", "dist/server.js"]
💡개념

DinD vs Docker Socket 마운트

CI 파이프라인을 컨테이너에서 실행하고 있는데, 파이프라인 안에서 docker build를 쓰려니 "Cannot connect to the Docker daemon" 오류가 납니다. 컨테이너 안에서 Docker를 쓰는 방법이 두 가지 있는데, 어떤 걸 써야 하는지 그리고 각각의 보안 리스크가 무엇인지를 모르면 판단이 어렵습니다. 잘못 선택하면 호스트 서버 전체가 노출될 수 있습니다.

DinD vs Docker Socket 마운트

두 가지 접근 방식 비교

CI Runner 컨테이너 안에서 Docker 명령을 실행하는 방법은 크게 두 가지입니다.

┌─────────────────────────────────────────────────────┐
│  방법 1: Docker-in-Docker (DinD)                    │
│                                                     │
│  CI Runner 컨테이너                                  │
│  ┌─────────────────────────────┐                    │
│  │  docker:dind 서비스          │                    │
│  │  (독립 Docker 데몬 실행)      │                    │
│  │  ┌──────────────────────┐   │                    │
│  │  │ 빌드된 이미지/컨테이너 │   │                    │
│  │  └──────────────────────┘   │                    │
│  └─────────────────────────────┘                    │
│  특징: 완전 격리, --privileged 필요                   │
└─────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│  방법 2: Docker Socket 마운트                        │
│                                                     │
│  호스트                                              │
│  /var/run/docker.sock ←─────────────────────┐      │
│                          CI Runner 컨테이너   │      │
│  호스트 Docker 데몬       ┌───────────────────┘      │
│  ┌──────────────────┐    │ -v /var/run/docker.sock  │
│  │ 모든 컨테이너     │◄───┘   :/var/run/docker.sock │
│  └──────────────────┘                              │
│  특징: 빠름, 호스트 Docker에 직접 접근 (보안 위험)    │
└─────────────────────────────────────────────────────┘
YAML
# GitLab CI에서 DinD 설정 (권장)
build-dind:
  image: docker:24-cli
  services:
    - name: docker:24-dind
      alias: docker
  variables:
    DOCKER_HOST: tcp://docker:2376
    DOCKER_TLS_CERTDIR: "/certs"
    DOCKER_CERT_PATH: "/certs/client"
    DOCKER_TLS_VERIFY: "1"
  script:
    - docker build -t myapp:latest .

# Docker socket 마운트 (빠르지만 보안 위험 있음)
# gitlab-runner config.toml에서 설정:
# [[runners.docker.volumes]]
# "/var/run/docker.sock:/var/run/docker.sock"
항목DinDSocket 마운트
격리 수준높음 (독립 데몬)낮음 (호스트 공유)
빌드 속도느림 (데몬 시작 오버헤드)빠름
레이어 캐시 공유없음 (매번 새 데몬)있음 (호스트 캐시 공유)
보안 위험--privileged 필요호스트 Docker 전체 접근 가능
권장 환경보안 중요 환경신뢰할 수 있는 내부 CI

기본 실습

1샘플 Node.js 앱과 Dockerfile 작성

실습용 최소 Node.js 앱을 만듭니다.

실습 전 디렉토리와 예제 파일을 먼저 준비합니다.

로컬 터미널
# 실습 디렉토리 준비
mkdir -p /tmp/docker/part4/exam_5 && cd /tmp/docker/part4/exam_5

# CI/CD 파이프라인 실습용 기본 구조 생성
mkdir -p app .github/workflows

# 기본 Dockerfile 생성
cat > app/Dockerfile << 'EOF'
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
USER node
CMD ["node", "server.js"]
EOF

# .dockerignore 생성
cat > app/.dockerignore << 'EOF'
node_modules
npm-debug.log
.git
.github
*.test.js
EOF

이제 실습을 진행합니다.

로컬 터미널
# package.json 생성
cat > ~/cicd-lab/app/package.json << 'EOF'
{
  "name": "cicd-demo",
  "version": "1.0.0",
  "scripts": {
    "start": "node server.js",
    "test": "echo 'Tests passed' && exit 0"
  }
}
EOF

# 서버 코드 생성
cat > ~/cicd-lab/app/server.js << 'EOF'
const http = require('http');
const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({
    status: 'ok',
    version: process.env.APP_VERSION || '1.0.0',
    commit: process.env.GIT_COMMIT || 'unknown'
  }));
});
server.listen(3000, () => console.log('서버 시작: http://localhost:3000'));
EOF

# Dockerfile 생성
cat > ~/cicd-lab/app/Dockerfile << 'EOF'
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
ARG GIT_COMMIT=unknown
ARG APP_VERSION=dev
ENV GIT_COMMIT=${GIT_COMMIT}
ENV APP_VERSION=${APP_VERSION}
EXPOSE 3000
CMD ["node", "server.js"]
EOF

git SHA를 빌드 인수로 전달하여 이미지에 커밋 정보를 포함시킵니다:

로컬 터미널
cd ~/cicd-lab/app
git init && git add -A && git commit -m "초기 커밋"
GIT_SHA=$(git rev-parse --short HEAD)
docker build \
  --build-arg GIT_COMMIT=${GIT_SHA} \
  --build-arg APP_VERSION=1.0.0 \
  -t cicd-demo:${GIT_SHA} \
  -t cicd-demo:latest \
  .
mkdir -p ~/cicd-lab/app && cd ~/cicd-lab/app
2GitHub Actions 워크플로우 파일 생성
로컬 터미널
cat > ~/cicd-lab/app/.github/workflows/docker-build.yml << 'EOF'
name: Docker 빌드 및 GHCR Push

on:
  push:
    branches: [main]
    tags: ['v*.*.*']
  pull_request:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - uses: actions/checkout@v4

      - name: Docker Buildx 설정
        uses: docker/setup-buildx-action@v3

      - name: GHCR 로그인
        if: github.event_name != 'pull_request'
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: 이미지 메타데이터 추출
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=ref,event=branch
            type=sha,prefix=sha-
            type=semver,pattern={{version}}

      - name: 빌드 및 Push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          build-args: |
            GIT_COMMIT=${{ github.sha }}
            APP_VERSION=${{ github.ref_name }}
          cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache
          cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max
EOF

echo "워크플로우 파일 생성 완료"
cat ~/cicd-lab/app/.github/workflows/docker-build.yml
mkdir -p ~/cicd-lab/app/.github/workflows
3GitLab CI 파이프라인 파일 생성
로컬 터미널
cat > ~/cicd-lab/app/.gitlab-ci.yml << 'EOF'
stages:
  - build
  - test
  - release

variables:
  DOCKER_DRIVER: overlay2
  DOCKER_TLS_CERTDIR: "/certs"
  IMAGE_SHA: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
  IMAGE_BRANCH: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG

build:
  stage: build
  image: docker:24-cli
  services:
    - docker:24-dind
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
    - docker pull $IMAGE_BRANCH || true
    - docker build
        --cache-from $IMAGE_BRANCH
        --tag $IMAGE_SHA
        --tag $IMAGE_BRANCH
        --build-arg GIT_COMMIT=$CI_COMMIT_SHA
        --build-arg APP_VERSION=$CI_COMMIT_TAG
        .
    - docker push $IMAGE_SHA
    - docker push $IMAGE_BRANCH

test:
  stage: test
  image: $IMAGE_SHA
  script:
    - npm test
  needs: [build]

release:
  stage: release
  image: docker:24-cli
  services:
    - docker:24-dind
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
    - docker pull $IMAGE_SHA
    - docker tag $IMAGE_SHA $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
  only:
    - /^v\d+\.\d+\.\d+$/
  except:
    - branches
EOF

echo "GitLab CI 파일 생성 완료"
cd ~/cicd-lab/app
4빌드 캐시 효과 측정

캐시 없는 빌드와 캐시 있는 빌드의 시간 차이를 측정합니다:

로컬 터미널
# 캐시 없이 빌드 (첫 빌드 또는 --no-cache)
time docker build --no-cache -t cicd-demo:nocache .

# 코드만 변경 후 캐시 있는 빌드
echo "// 코드 변경" >> server.js
time docker build -t cicd-demo:withcache .

# 레이어 캐시 상태 확인
docker history cicd-demo:withcache

# 빌드 캐시 목록 확인
docker buildx du

의존성 파일(package.json)이 변경되지 않으면 npm ci 레이어가 캐시에서 재사용되어 빌드 시간이 크게 단축됩니다.

cd ~/cicd-lab/app
🔍실행 후 확인할 것
  • 커밋 SHA 기반 태그로 이미지와 소스 버전을 1:1 추적할 수 있는가?
  • CI에서 로그인 권한/시크릿 주입이 실패 없이 동작하고 로그 노출이 없는가?
  • cache-from/cache-to 설정으로 재빌드 시간이 실제로 단축됐는가?

트러블슈팅

GHCR 또는 Docker Hub 로그인이 실패하는 오류입니다. CI 환경에서 자주 발생합니다.

GitHub Actions 원인: GITHUB_TOKENpackages: write 권한이 누락되었거나, 레포지토리 Settings > Actions > General에서 워크플로우 권한이 읽기 전용으로 설정된 경우입니다.

YAML
# 해결: jobs 레벨에서 permissions 명시
jobs:
  build:
    permissions:
      contents: read
      packages: write  # 이 줄이 반드시 필요

GitLab CI 원인: CI_REGISTRY_USER, CI_REGISTRY_PASSWORD 변수가 제공되지 않는 경우는 GitLab CI의 container registry 기능이 비활성화되었을 때입니다. Project Settings > General > Visibility에서 Container Registry가 활성화되어 있는지 확인합니다.

멀티 아키텍처 빌드 시 buildx 드라이버가 기본 docker 드라이버로 설정되어 있을 때 발생합니다.

Docker
# 현재 buildx 드라이버 확인
docker buildx ls

# docker-container 드라이버로 새 빌더 생성
docker buildx create --name mybuilder --driver docker-container --use

# 빌더 부트스트랩 (BuildKit 컨테이너 시작)
docker buildx inspect --bootstrap

# 이제 멀티 플랫폼 빌드 가능
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest .

GitHub Actions에서는 docker/setup-buildx-action@v3을 사용하면 자동으로 올바른 드라이버가 설정됩니다.

Dockerfile ARG로 받은 시크릿이 docker history나 빌드 로그에 노출되는 문제입니다.

Dockerfile
# 위험: ARG 값이 이미지 레이어에 노출될 수 있음
ARG DATABASE_PASSWORD
RUN echo $DATABASE_PASSWORD  # 절대 금지!

# 안전: BuildKit --secret 마운트 사용 (파일 시스템에만 일시적으로 존재)
# syntax=docker/dockerfile:1
RUN --mount=type=secret,id=db_password \
    DB_PASS=$(cat /run/secrets/db_password) && \
    ./setup-db.sh
Docker
# CI에서 BuildKit secret 전달
docker build \
  --secret id=db_password,env=DATABASE_PASSWORD \
  -t myapp:latest .

또한 .dockerignore.env, *.pem, *credentials* 등을 반드시 추가하여 빌드 컨텍스트에서 제외합니다.

GitLab CI에서 DinD 서비스 연결에 실패하는 경우입니다.

YAML
# 잘못된 설정: TLS 없는 DinD에 TLS 연결 시도
variables:
  DOCKER_HOST: tcp://docker:2376  # TLS 포트
  DOCKER_TLS_CERTDIR: "/certs"

# 올바른 설정 1: TLS 사용 (권장)
variables:
  DOCKER_HOST: tcp://docker:2376
  DOCKER_TLS_CERTDIR: "/certs"
  DOCKER_CERT_PATH: "/certs/client"
  DOCKER_TLS_VERIFY: "1"
services:
  - name: docker:24-dind
    variables:
      DOCKER_TLS_CERTDIR: "/certs"

# 올바른 설정 2: TLS 없이 (개발 환경)
variables:
  DOCKER_HOST: tcp://docker:2375
  DOCKER_TLS_CERTDIR: ""
services:
  - docker:24-dind --insecure-registry registry.example.com

실무 맥락

💼
실무 맥락팀 내 '이미지는 로컬에서 빌드해 Slack으로 공유' 관행을 CI/CD로 전환하는 과정
현업 패턴

초기 스타트업에서는 개발자가 로컬에서 이미지를 빌드하고 Docker Hub에 push한 뒤 동료에게 태그를 알려주는 방식이 흔합니다. 팀이 3~4명일 때는 작동하지만 규모가 커지면 다음 문제가 생깁니다.

문제 1: "내 맥에서는 됐는데" — 개발자 A가 Node.js 18로 빌드했지만 개발자 B는 Node.js 20이 설치되어 있어 다른 이미지가 만들어집니다. CI Runner는 항상 동일한 환경을 보장합니다.

문제 2: 배포 추적 불가 — "어제 배포한 버전이 뭐야?"라는 질문에 답할 수 없습니다. git SHA 태그를 사용하면 docker inspect myapp:a1b2c3d로 정확히 어떤 코드가 배포되었는지 확인할 수 있습니다.

전환 순서: 먼저 기존 로컬 빌드와 동일한 CI 파이프라인을 만들어 병행 운영합니다. CI 빌드 결과가 안정적임을 확인한 후 로컬 빌드를 금지하는 팀 규칙을 도입합니다. 마지막으로 레지스트리 접근 권한을 CI 서비스 계정에만 부여하여 개발자가 직접 push할 수 없도록 합니다.

캐시 전략 도입: 처음에는 캐시 없이 시작해도 됩니다. 빌드 시간이 3분을 넘기 시작하면 레지스트리 캐시를 도입합니다. Dockerfile을 캐시 친화적으로 리팩터링(의존성 파일 먼저 COPY)하는 것만으로도 50% 이상 시간을 단축할 수 있습니다.


핵심 요약

개념명령/설정설명
GHCR 로그인docker/login-action + GITHUB_TOKENGitHub Actions에서 자동 인증
이미지 태그 생성docker/metadata-action브랜치, SHA, semver 태그 자동 생성
빌드 및 Pushdocker/build-push-actionBuildKit 기반 이미지 빌드 및 레지스트리 push
레지스트리 캐시cache-from/cache-to: type=registry이전 빌드 레이어를 레지스트리에서 재사용
GitLab 빌트인 변수CI_REGISTRY_IMAGE, CI_COMMIT_SHAGitLab이 자동 제공하는 레지스트리 및 커밋 정보
DinDdocker:24-dind 서비스CI 컨테이너 내 독립 Docker 데몬 실행
멀티 아키텍처platforms: linux/amd64,linux/arm64AMD64와 ARM64 동시 빌드
시크릿 보호--secret id=key,env=VARBuildKit secret 마운트로 레이어에 노출 방지

지식 확인

퀴즈 — 5문제

Q1

GitHub Actions에서 docker/build-push-action을 사용할 때 레지스트리 캐시를 재사용하려면 어떤 옵션을 설정해야 하는가?

Q2

Docker-in-Docker(DinD) 방식과 Docker socket 마운트 방식의 가장 큰 차이점은?

Q3

CI/CD에서 이미지 태그 전략으로 가장 권장되는 방식은?

Q4

GitLab CI에서 이미지 빌드 시 CI_REGISTRY_IMAGE 변수의 역할은?

Q5

CI 환경에서 Docker 빌드 시 secrets(API 키, 비밀번호)를 안전하게 다루는 방법으로 올바른 것은?

0 / 5 답변

🧪 실습으로 확인하기

Docker Compose 멀티 서비스 구성

초급

docker-compose.yml로 nginx + 앱 컨테이너를 함께 정의하고, 서비스 간 통신과 볼륨 마운트를 구성한다.

35📋 4단계💻 직접 환경
실습 시작하기 →

이것도 배워보세요

networking입문 · 45
[Network] OSI 7계층과 TCP/IP 4계층 모델 실무적 관점 분석
Networking 트랙 시작점
linux입문 · 30
[Linux] 개발자가 왜 리눅스 서버와 커맨드라인을 반드시 배워야 하는가
Linux 트랙 시작점