본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 11. 26. · 8 Views
Docker CI/CD 파이프라인 완벽 가이드
GitHub Actions와 Docker를 활용하여 코드 푸시부터 자동 배포까지 전 과정을 자동화하는 CI/CD 파이프라인 구축 방법을 배웁니다. 초급 개발자도 쉽게 따라할 수 있도록 실무 예제와 함께 설명합니다.
목차
1. GitHub Actions와 Docker
김개발 씨는 매일 아침 출근하면 같은 일을 반복했습니다. 코드를 작성하고, 테스트를 돌리고, Docker 이미지를 빌드하고, 서버에 배포하는 일련의 과정을 손으로 직접 해야 했습니다.
"이걸 자동으로 할 수는 없을까?" 문득 그런 생각이 들었습니다.
GitHub Actions는 GitHub 저장소에서 발생하는 이벤트(푸시, PR 등)를 감지하여 자동으로 작업을 실행하는 CI/CD 도구입니다. 마치 집에 설치된 스마트 센서가 문이 열리면 자동으로 조명을 켜는 것처럼, 코드가 푸시되면 자동으로 빌드와 배포가 시작됩니다.
Docker와 결합하면 일관된 환경에서 테스트하고 배포할 수 있습니다.
다음 코드를 살펴봅시다.
# .github/workflows/docker-ci.yml
name: Docker CI Pipeline
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
# 저장소 코드를 체크아웃합니다
- uses: actions/checkout@v4
# Docker Buildx를 설정합니다
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# Docker 이미지를 빌드합니다
- name: Build Docker image
run: docker build -t myapp:${{ github.sha }} .
김개발 씨는 입사 6개월 차 주니어 개발자입니다. 매일 아침 출근하면 어김없이 반복되는 작업이 있었습니다.
코드를 수정하고, 로컬에서 테스트하고, Docker 이미지를 빌드하고, 레지스트리에 푸시하고, 서버에 접속해서 새 이미지를 배포하는 일련의 과정이었습니다. 어느 날 실수로 배포 과정 중 하나를 빠뜨려서 서비스에 장애가 발생했습니다.
선배 개발자 박시니어 씨가 다가와 물었습니다. "아직도 수동으로 배포해요?
CI/CD 파이프라인을 구축해보는 건 어때요?" CI/CD란 무엇일까요? CI는 Continuous Integration(지속적 통합), CD는 Continuous Deployment(지속적 배포)의 약자입니다.
쉽게 말해 개발자가 코드를 푸시하면 자동으로 테스트하고 배포까지 해주는 시스템입니다. 마치 자동차 공장의 컨베이어 벨트와 같습니다.
부품이 들어오면 자동으로 조립되고, 검사받고, 완성차로 출고되는 것처럼요. 개발자는 코드만 작성하면 되고, 나머지는 파이프라인이 알아서 처리합니다.
GitHub Actions는 GitHub에서 제공하는 CI/CD 도구입니다. 별도의 서버를 구축하지 않아도 되고, GitHub 저장소와 완벽하게 통합되어 있어서 설정이 간편합니다.
무엇보다 오픈소스 프로젝트는 무료로 사용할 수 있습니다. 위의 코드를 살펴보겠습니다.
먼저 on 섹션에서 어떤 이벤트에 반응할지 정의합니다. main 브랜치에 푸시되거나 PR이 생성되면 워크플로우가 실행됩니다.
jobs 섹션에서는 실제 수행할 작업을 정의합니다. runs-on: ubuntu-latest는 Ubuntu 환경에서 실행된다는 의미입니다.
GitHub에서 제공하는 가상 머신에서 작업이 수행됩니다. steps는 순차적으로 실행되는 단계들입니다.
먼저 코드를 체크아웃하고, Docker Buildx를 설정한 뒤, 이미지를 빌드합니다. **${{ github.sha }}**는 커밋 해시값으로, 각 빌드를 고유하게 식별하는 태그로 사용됩니다.
실제 현업에서는 이 기본 구조 위에 테스트, 보안 스캔, 배포 단계를 추가합니다. 하지만 핵심 원리는 동일합니다.
이벤트가 발생하면 정의된 작업이 순차적으로 실행되는 것입니다. 다시 김개발 씨 이야기로 돌아가 봅시다.
박시니어 씨의 도움을 받아 처음으로 GitHub Actions를 설정한 김개발 씨는 감탄했습니다. "코드만 푸시하면 알아서 빌드되다니, 정말 편하네요!" 이제 김개발 씨는 더 이상 수동 배포로 실수할 일이 없습니다.
여러분도 GitHub Actions로 반복 작업에서 해방되어 보세요.
실전 팁
💡 - 워크플로우 파일은 반드시 .github/workflows/ 디렉토리에 위치해야 합니다
- actions/checkout@v4는 거의 모든 워크플로우에서 첫 번째 스텝으로 사용됩니다
- 민감한 정보는 GitHub Secrets에 저장하고 **${{ secrets.NAME }}**으로 참조하세요
2. 이미지 빌드 자동화
"빌드할 때마다 30분씩 걸리는데, 이거 줄일 방법 없을까요?" 김개발 씨가 한숨을 쉬며 물었습니다. Docker 이미지 빌드가 느려서 배포할 때마다 커피 한 잔을 마시고 와야 했습니다.
박시니어 씨가 웃으며 대답했습니다. "캐시를 활용해보세요."
Docker 이미지 빌드 자동화의 핵심은 캐시 활용과 멀티 스테이지 빌드입니다. 마치 요리할 때 미리 재료를 손질해두면 빠르게 요리할 수 있는 것처럼, 변경되지 않은 레이어는 캐시에서 가져와 빌드 시간을 획기적으로 단축할 수 있습니다.
다음 코드를 살펴봅시다.
# .github/workflows/docker-build.yml
name: Docker Build with Cache
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Docker Hub에 로그인합니다
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
# 캐시를 활용하여 빌드합니다
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: myapp:latest
cache-from: type=registry,ref=myapp:cache
cache-to: type=registry,ref=myapp:cache,mode=max
김개발 씨의 프로젝트는 Node.js 애플리케이션이었습니다. package.json에 명시된 의존성만 500개가 넘었고, 매번 빌드할 때마다 모든 패키지를 새로 설치해야 했습니다.
빌드 시간이 30분이 넘어가면서 개발 생산성이 크게 떨어졌습니다. 박시니어 씨가 김개발 씨의 Dockerfile을 살펴보았습니다.
"문제가 보이네요. 레이어 순서가 잘못되어 있어요.
캐시가 전혀 활용되지 않고 있습니다." Docker 캐시의 원리를 이해하려면 먼저 Docker 이미지가 어떻게 구성되는지 알아야 합니다. Docker 이미지는 여러 레이어로 구성됩니다.
각 Dockerfile 명령어가 하나의 레이어를 생성합니다. 마치 케이크를 쌓는 것과 같습니다.
맨 아래 스폰지 케이크, 그 위에 크림, 그 위에 과일. 만약 과일만 바꾸고 싶다면 아래 레이어는 그대로 두고 위의 레이어만 교체하면 됩니다.
Docker 캐시도 마찬가지입니다. 문제는 레이어 순서입니다.
자주 변경되는 코드를 먼저 복사하면, 그 이후의 모든 레이어가 캐시를 사용할 수 없게 됩니다. 따라서 변경이 적은 의존성 설치를 먼저 하고, 코드 복사를 나중에 해야 합니다.
위의 워크플로우에서 cache-from과 cache-to 옵션을 주목하세요. 이 옵션은 Docker 레지스트리에 캐시를 저장하고 불러옵니다.
GitHub Actions의 러너는 매번 새로 생성되기 때문에, 로컬 캐시가 유지되지 않습니다. 레지스트리 캐시를 사용하면 이 문제를 해결할 수 있습니다.
docker/build-push-action은 Docker 이미지 빌드에 특화된 공식 액션입니다. 빌드, 태깅, 푸시를 한 번에 처리하고, 다양한 캐시 전략을 지원합니다.
mode=max 옵션은 모든 빌드 레이어를 캐시에 저장합니다. 멀티 스테이지 빌드를 사용할 때 중간 스테이지의 레이어도 캐시되어 더 큰 성능 향상을 얻을 수 있습니다.
실제로 캐시를 적용한 후 김개발 씨의 빌드 시간은 30분에서 3분으로 줄었습니다. 10배나 빨라진 것입니다.
변경된 코드만 새로 빌드하고, 나머지는 캐시에서 가져왔기 때문입니다. 주의할 점도 있습니다.
캐시가 오래되면 보안 패치가 적용되지 않은 오래된 패키지가 포함될 수 있습니다. 주기적으로 --no-cache 옵션으로 전체 빌드를 수행하는 것이 좋습니다.
김개발 씨는 이제 빌드를 기다리며 커피를 마실 필요가 없어졌습니다. 코드를 푸시하고 잠깐 다른 작업을 하다 보면 어느새 빌드가 완료되어 있습니다.
실전 팁
💡 - Dockerfile에서 COPY package.json ./* 후 RUN npm install, 그 다음 COPY . . 순서로 작성하세요
- GitHub Actions 캐시와 레지스트리 캐시를 함께 사용하면 더욱 효과적입니다
- 보안을 위해 주 1회 정도는 캐시 없이 전체 빌드를 수행하세요
3. 테스트 컨테이너 활용
"로컬에서는 잘 되는데 CI에서 테스트가 실패해요!" 김개발 씨가 당황한 표정으로 말했습니다. 데이터베이스 연결 테스트가 로컬에서는 통과하는데, GitHub Actions에서는 계속 실패했습니다.
"CI 환경에는 데이터베이스가 없잖아요." 박시니어 씨가 설명했습니다.
테스트 컨테이너는 테스트 실행 시 필요한 외부 서비스(데이터베이스, 캐시, 메시지 큐 등)를 Docker 컨테이너로 실행하는 패턴입니다. 마치 모의고사 때 실제 시험장과 동일한 환경을 만들어주는 것처럼, 테스트 환경을 프로덕션과 동일하게 구성할 수 있습니다.
다음 코드를 살펴봅시다.
# .github/workflows/test.yml
name: Test with Services
jobs:
test:
runs-on: ubuntu-latest
# 테스트에 필요한 서비스 컨테이너를 정의합니다
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Run tests
run: npm test
env:
DATABASE_URL: postgres://test:test@localhost:5432/testdb
김개발 씨가 작성한 API는 PostgreSQL 데이터베이스와 통신합니다. 로컬 개발 환경에는 Docker로 PostgreSQL을 실행해두었기 때문에 테스트가 잘 동작했습니다.
하지만 GitHub Actions 환경에는 데이터베이스가 없었습니다. 처음에는 테스트용 외부 데이터베이스를 사용할까 고민했습니다.
하지만 보안 문제도 있고, 네트워크 지연도 발생할 수 있습니다. 무엇보다 동시에 여러 테스트가 실행되면 데이터가 충돌할 수 있습니다.
박시니어 씨가 해결책을 알려주었습니다. "GitHub Actions의 서비스 컨테이너를 사용하면 돼요.
테스트할 때만 일시적으로 데이터베이스 컨테이너를 실행하는 거죠." 서비스 컨테이너는 GitHub Actions에서 제공하는 기능입니다. services 섹션에 필요한 컨테이너를 정의하면, 해당 job이 시작될 때 자동으로 컨테이너가 실행됩니다.
job이 끝나면 컨테이너도 자동으로 정리됩니다. 마치 시험을 볼 때만 감독관이 배치되고, 시험이 끝나면 퇴장하는 것과 같습니다.
필요한 순간에만 리소스를 사용하고, 끝나면 깔끔하게 정리됩니다. 위의 코드에서 options 부분을 주목하세요.
health-cmd는 컨테이너가 준비되었는지 확인하는 명령입니다. PostgreSQL의 경우 pg_isready 명령으로 데이터베이스가 연결을 받을 준비가 되었는지 확인합니다.
헬스 체크가 없으면 문제가 발생할 수 있습니다. 컨테이너는 시작되었지만 데이터베이스가 아직 준비되지 않은 상태에서 테스트가 실행될 수 있기 때문입니다.
헬스 체크를 설정하면 데이터베이스가 완전히 준비된 후에 테스트가 시작됩니다. ports 설정으로 컨테이너 포트를 호스트에 매핑합니다.
테스트 코드는 localhost:5432로 데이터베이스에 접근할 수 있습니다. 환경 변수로 연결 정보를 전달하면 테스트 코드에서 쉽게 사용할 수 있습니다.
실제 프로젝트에서는 PostgreSQL 외에도 Redis, Elasticsearch, RabbitMQ 등 다양한 서비스가 필요할 수 있습니다. 모두 동일한 방식으로 서비스 컨테이너로 실행할 수 있습니다.
주의할 점은 테스트가 끝나면 데이터가 사라진다는 것입니다. 매 테스트마다 깨끗한 상태에서 시작하므로 테스트 격리에는 좋지만, 테스트 데이터 초기화 코드를 작성해야 합니다.
김개발 씨는 서비스 컨테이너를 설정한 후 CI 테스트가 안정적으로 통과하는 것을 확인했습니다. "로컬이든 CI든 동일한 환경에서 테스트하니까 더 이상 '내 컴퓨터에서는 되는데'라는 말을 할 필요가 없겠네요!"
실전 팁
💡 - 헬스 체크 설정은 필수입니다. 컨테이너 시작과 서비스 준비는 다릅니다
- 여러 서비스가 필요하면 services 섹션에 모두 정의하세요
- 테스트 병렬화 시 각 job이 독립적인 서비스 컨테이너를 가집니다
4. 이미지 태깅 전략
"이 버전이 어떤 커밋에서 빌드된 건가요?" 운영팀에서 긴급 문의가 왔습니다. 프로덕션에서 버그가 발생했는데, 현재 배포된 이미지가 어떤 코드 버전인지 알 수 없었습니다.
모든 이미지에 latest 태그만 붙어 있었기 때문입니다.
이미지 태깅 전략은 Docker 이미지를 식별하고 관리하는 규칙입니다. 마치 도서관에서 책에 고유 번호를 부여하여 찾기 쉽게 하는 것처럼, 이미지에 의미 있는 태그를 붙이면 버전 추적과 롤백이 용이해집니다.
가장 많이 사용되는 전략은 시맨틱 버전, 커밋 해시, 브랜치명 조합입니다.
다음 코드를 살펴봅시다.
# .github/workflows/tagging.yml
name: Image Tagging Strategy
on:
push:
branches: [ main, develop ]
tags: [ 'v*' ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# 태그 메타데이터를 생성합니다
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: myregistry/myapp
tags: |
type=ref,event=branch
type=sha,prefix=
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Build and push
uses: docker/build-push-action@v5
with:
push: true
tags: ${{ steps.meta.outputs.tags }}
김개발 씨의 팀에서는 모든 이미지에 latest 태그만 사용하고 있었습니다. 배포할 때마다 기존 latest를 덮어쓰는 방식이었습니다.
처음에는 간단해서 좋았지만, 프로젝트가 커지면서 문제가 발생했습니다. 어느 날 프로덕션에서 심각한 버그가 발견되었습니다.
이전 버전으로 롤백해야 하는데, 이전 버전이 무엇인지 알 수 없었습니다. latest만 있으니까요.
결국 git 로그를 뒤져가며 수동으로 이전 커밋을 찾아 다시 빌드해야 했습니다. 박시니어 씨가 태깅 전략의 중요성을 설명했습니다.
"이미지 태그는 코드의 특정 시점을 가리키는 이정표와 같아요. 잘 설계된 태깅 전략이 있으면 언제든 원하는 버전으로 돌아갈 수 있습니다." 가장 기본적인 태그는 커밋 해시입니다.
Git 커밋의 SHA 값을 태그로 사용하면, 이미지가 정확히 어떤 코드에서 빌드되었는지 알 수 있습니다. abc123 같은 태그를 보면 바로 해당 커밋을 찾을 수 있습니다.
시맨틱 버전(Semantic Versioning)도 많이 사용됩니다. v1.2.3 형식으로, 주 버전(major), 부 버전(minor), 패치 버전(patch)을 구분합니다.
사용자가 이해하기 쉽고, 버전 간 호환성을 표현할 수 있습니다. 위의 코드에서 docker/metadata-action을 사용합니다.
이 액션은 다양한 태그를 자동으로 생성해줍니다. type=sha는 커밋 해시, type=semver는 Git 태그에서 버전 정보를 추출합니다.
type=semver,pattern={{major}}.{{minor}} 설정을 주목하세요. v1.2.3 태그를 푸시하면 1.2.3과 1.2 두 개의 태그가 생성됩니다.
사용자는 myapp:1.2로 이미지를 가져오면 1.2.x의 최신 패치 버전을 자동으로 사용할 수 있습니다. 브랜치명을 태그로 사용하는 것도 유용합니다.
main 브랜치에서 빌드된 이미지는 myapp:main 태그를 가집니다. develop 브랜치는 myapp:develop으로 구분됩니다.
개발 환경에서 최신 개발 버전을 쉽게 테스트할 수 있습니다. 주의할 점은 latest 태그를 신중하게 사용해야 한다는 것입니다.
latest는 "가장 최신"이 아니라 "기본값"을 의미합니다. 자동으로 업데이트되지 않으므로, 명시적인 버전 태그 사용을 권장합니다.
태깅 전략을 도입한 후, 팀에서는 더 이상 버전 추적에 어려움을 겪지 않았습니다. 문제가 발생하면 커밋 해시로 정확한 코드를 찾고, 시맨틱 버전으로 안정적인 롤백을 수행할 수 있게 되었습니다.
실전 팁
💡 - latest 태그만 사용하지 마세요. 프로덕션에서는 반드시 명시적인 버전을 사용하세요
- 커밋 해시는 짧은 버전(7자리)을 사용해도 충분합니다
- CI에서 자동 생성되는 태그와 수동 릴리스 태그를 구분하면 관리가 편합니다
5. 자동 배포 설정
테스트가 통과하고 이미지가 빌드되었습니다. 하지만 김개발 씨는 여전히 서버에 SSH로 접속해서 수동으로 배포하고 있었습니다.
"CI까지 자동화했는데, CD는 언제 자동화하죠?" 박시니어 씨가 다음 단계를 제안했습니다. "이미지가 푸시되면 자동으로 배포되도록 설정해봅시다."
자동 배포(Continuous Deployment)는 테스트를 통과한 코드가 자동으로 프로덕션에 배포되는 것을 말합니다. 마치 공장에서 품질 검사를 통과한 제품이 자동으로 출하되는 것처럼, 사람의 개입 없이 안전하게 배포가 이루어집니다.
이를 위해서는 충분한 테스트 커버리지와 롤백 전략이 필수입니다.
다음 코드를 살펴봅시다.
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
tags: [ 'v*' ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy to server
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
# 새 이미지를 가져옵니다
docker pull myregistry/myapp:${{ github.ref_name }}
# 기존 컨테이너를 중지하고 새 컨테이너를 시작합니다
docker stop myapp || true
docker rm myapp || true
docker run -d --name myapp \
-p 80:3000 \
myregistry/myapp:${{ github.ref_name }}
김개발 씨는 매주 금요일 오후가 두려웠습니다. 배포하는 날이기 때문입니다.
서버에 접속하고, 이미지를 가져오고, 컨테이너를 재시작하는 과정에서 실수라도 하면 주말 내내 마음이 편하지 않았습니다. 어느 금요일, 급하게 배포하다가 컨테이너 이름을 잘못 입력해서 서비스가 30분간 중단되는 사고가 발생했습니다.
박시니어 씨가 말했습니다. "사람이 하면 실수할 수밖에 없어요.
자동화하면 매번 동일한 과정이 실행되니까 훨씬 안전합니다." 자동 배포의 핵심 원칙은 "반복 가능한 배포"입니다. 누가, 언제 배포하든 동일한 결과가 나와야 합니다.
스크립트로 정의된 배포 과정은 사람의 실수가 개입할 여지가 없습니다. 위의 코드에서 on: push: tags 설정을 보세요.
v로 시작하는 Git 태그가 푸시될 때만 배포가 실행됩니다. 모든 커밋이 아니라, 명시적으로 릴리스된 버전만 배포되는 것입니다.
appleboy/ssh-action은 원격 서버에 SSH로 접속하여 명령을 실행하는 액션입니다. 서버 접속 정보는 GitHub Secrets에 안전하게 저장됩니다.
워크플로우 파일에 비밀번호나 키가 노출되지 않습니다. 배포 스크립트를 살펴보겠습니다.
먼저 docker pull로 새 이미지를 가져옵니다. 그 다음 기존 컨테이너를 중지하고 삭제합니다.
|| true는 컨테이너가 없어도 에러가 발생하지 않게 합니다. 마지막으로 새 컨테이너를 시작합니다.
이 방식은 간단하지만 다운타임이 발생합니다. 기존 컨테이너를 중지하고 새 컨테이너가 시작되는 사이에 서비스가 잠시 중단됩니다.
더 발전된 방식으로는 블루-그린 배포나 롤링 업데이트가 있습니다. 블루-그린 배포는 새 버전(그린)을 별도로 준비하고, 준비가 완료되면 트래픽을 한 번에 전환하는 방식입니다.
문제가 생기면 다시 이전 버전(블루)으로 빠르게 돌아갈 수 있습니다. Kubernetes를 사용한다면 배포가 더 쉬워집니다.
kubectl set image 명령 하나로 롤링 업데이트가 수행되고, 자동 롤백도 지원됩니다. 대규모 서비스에서는 Kubernetes 배포를 권장합니다.
중요한 것은 롤백 계획입니다. 자동 배포가 실패하거나, 배포된 버전에 버그가 있을 때 빠르게 이전 버전으로 돌아갈 수 있어야 합니다.
이미지 태깅 전략과 함께 롤백 스크립트도 준비해두세요. 자동 배포를 도입한 후 김개발 씨의 금요일이 평화로워졌습니다.
버전 태그만 푸시하면 몇 분 후에 프로덕션에 반영됩니다. 더 이상 서버에 접속해서 긴장하며 명령어를 입력할 필요가 없습니다.
실전 팁
💡 - 프로덕션 배포 전에 스테이징 환경에서 먼저 테스트하세요
- 배포 후 헬스 체크를 수행하여 서비스 정상 동작을 확인하세요
- 롤백 스크립트를 미리 준비하고 테스트해두세요
6. Docker in Docker
"CI 환경에서 Docker 명령어가 안 돼요!" 김개발 씨가 당황했습니다. GitHub Actions에서 Docker 이미지를 빌드하려고 하는데, docker 명령어를 찾을 수 없다는 에러가 발생했습니다.
"CI 환경 자체가 컨테이너 안에서 실행되거든요." 박시니어 씨가 Docker in Docker 개념을 설명하기 시작했습니다.
Docker in Docker(DinD)는 Docker 컨테이너 안에서 Docker를 실행하는 기술입니다. 마치 인형 안에 또 다른 인형이 들어있는 러시아 인형(마트료시카)처럼, 컨테이너 안에서 또 다른 컨테이너를 빌드하고 실행할 수 있습니다.
CI/CD 파이프라인에서 Docker 이미지를 빌드할 때 자주 사용됩니다.
다음 코드를 살펴봅시다.
# .github/workflows/dind.yml
name: Docker in Docker
jobs:
build:
runs-on: ubuntu-latest
# DinD 서비스를 실행합니다
services:
docker:
image: docker:dind
options: --privileged
ports:
- 2375:2375
container:
image: docker:cli
env:
DOCKER_HOST: tcp://docker:2375
steps:
- uses: actions/checkout@v4
# DinD 환경에서 Docker 빌드를 수행합니다
- name: Build image
run: |
docker version
docker build -t myapp:test .
docker images
김개발 씨는 복잡한 빌드 파이프라인을 구축하고 있었습니다. 애플리케이션 이미지를 빌드한 후, 그 이미지로 통합 테스트를 실행하고, 테스트가 통과하면 다시 프로덕션 이미지를 빌드해야 했습니다.
이 모든 과정이 CI 환경에서 이루어져야 했습니다. 문제는 GitHub Actions 러너가 이미 컨테이너 안에서 실행된다는 것입니다.
컨테이너 안에서 docker 명령어를 실행하려면 특별한 설정이 필요합니다. 이것이 바로 Docker in Docker 상황입니다.
DinD를 이해하려면 Docker의 아키텍처를 알아야 합니다. Docker는 클라이언트-서버 구조입니다.
우리가 실행하는 docker 명령어는 클라이언트이고, 실제 컨테이너를 관리하는 것은 Docker 데몬(서버)입니다. 클라이언트와 서버는 소켓으로 통신합니다.
마치 레스토랑에서 손님(클라이언트)이 주문을 하면, 주방(서버)에서 요리를 만드는 것과 같습니다. 컨테이너 안에서 Docker를 사용하려면 주방을 어떻게 연결할지 결정해야 합니다.
위의 코드에서 두 가지 핵심을 보세요. 첫째, docker:dind 이미지를 서비스로 실행합니다.
이 컨테이너가 Docker 데몬 역할을 합니다. --privileged 옵션은 컨테이너에 높은 권한을 부여합니다.
Docker 데몬을 실행하려면 시스템 레벨의 권한이 필요하기 때문입니다. 둘째, 실제 작업을 수행하는 컨테이너에서 DOCKER_HOST 환경 변수를 설정합니다.
docker 클라이언트가 dind 서비스의 Docker 데몬에 연결되도록 합니다. tcp://docker:2375는 dind 서비스의 주소입니다.
하지만 DinD에는 보안 문제가 있습니다. --privileged 옵션은 컨테이너에 호스트와 거의 동일한 권한을 부여합니다.
악의적인 코드가 실행되면 호스트 시스템에 접근할 수 있습니다. 또 다른 방식은 Docker 소켓 마운트입니다.
호스트의 /var/run/docker.sock을 컨테이너에 마운트하면, 컨테이너에서 호스트의 Docker 데몬을 사용할 수 있습니다. 이 방식은 DinD보다 간단하지만, 역시 보안 위험이 있습니다.
다행히 GitHub Actions에서는 대부분의 경우 DinD가 필요 없습니다. 기본 러너(ubuntu-latest)에는 Docker가 이미 설치되어 있고, docker/build-push-action 같은 액션을 사용하면 내부적으로 적절한 방식으로 빌드를 처리합니다.
DinD가 정말 필요한 경우는 특수한 테스트 시나리오입니다. 예를 들어 Docker 관련 도구를 개발하거나, 컨테이너 오케스트레이션을 테스트할 때 필요할 수 있습니다.
김개발 씨는 결국 docker/build-push-action을 사용하기로 했습니다. 복잡한 DinD 설정 없이도 원하는 빌드 파이프라인을 구축할 수 있었습니다.
"필요할 때만 복잡한 도구를 사용하고, 가능하면 단순한 방법을 선택하는 게 좋겠어요."
실전 팁
💡 - GitHub Actions에서는 대부분 DinD가 필요 없습니다. docker/build-push-action 사용을 권장합니다
- DinD 사용 시 --privileged 옵션의 보안 위험을 인지하세요
- 가능하면 Buildx나 Kaniko 같은 rootless 빌드 도구를 검토하세요
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
Roundcube 웹메일 인터페이스 완벽 가이드
Docker 컨테이너 기반으로 Roundcube 웹메일을 구축하고, Nginx 리버스 프록시부터 플러그인 관리, 테마 커스터마이징까지 전체 과정을 다룹니다. 초급 개발자도 쉽게 따라할 수 있는 실무 중심 가이드입니다.
메일 클라이언트 연결 설정 완벽 가이드
다양한 메일 클라이언트에서 IMAP, POP3, SMTP 설정을 올바르게 구성하는 방법을 알아봅니다. Outlook부터 모바일 앱까지, 실무에서 바로 적용할 수 있는 설정 가이드입니다.
SSL/TLS 인증서 설정 완벽 가이드 (Let's Encrypt)
메일 서버 운영에 필수적인 SSL/TLS 인증서 설정 방법을 다룹니다. Let's Encrypt를 활용한 무료 인증서 발급부터 자동 갱신까지, 실무에서 바로 적용할 수 있는 내용을 담았습니다.
DNS 레코드 설정 완벽 가이드 MX SPF DKIM DMARC
이메일 서버 운영에 필수적인 DNS 레코드 설정을 다룹니다. MX, SPF, DKIM, DMARC 등 이메일 인증 레코드의 개념과 설정 방법을 초급 개발자도 이해할 수 있도록 쉽게 설명합니다.
Docker 환경 준비 및 docker-mailserver 설치
나만의 메일 서버를 Docker로 구축하는 방법을 처음부터 끝까지 안내합니다. docker-mailserver를 활용하여 실무에서 바로 사용할 수 있는 메일 시스템을 단계별로 설정해봅니다.