🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

Docker 기초부터 실전까지 완벽 가이드 - 슬라이드 1/13
A

AI Generated

2025. 10. 29. · 140 Views

Docker 기초부터 실전까지 완벽 가이드

컨테이너 기술의 핵심인 Docker를 처음 접하는 개발자도 쉽게 이해할 수 있도록 기초 개념부터 실전 활용까지 단계별로 안내합니다. 실무에서 바로 사용할 수 있는 명령어와 베스트 프랙티스를 풍부한 예제와 함께 제공합니다.


목차

  1. Docker 컨테이너 기본 개념
  2. Docker 이미지 생성과 관리
  3. Dockerfile 작성 기초
  4. Docker Compose로 멀티 컨테이너 관리
  5. 볼륨과 네트워크 설정
  6. Docker 레이어 캐싱 최적화
  7. 환경변수와 시크릿 관리
  8. Docker 멀티 스테이지 빌드

1. Docker 컨테이너 기본 개념

시작하며

여러분이 개발한 애플리케이션을 동료에게 전달했을 때 "제 컴퓨터에서는 안 되는데요?"라는 말을 들어본 적 있나요? 로컬에서는 완벽하게 작동하던 앱이 서버에 배포하면 갑자기 오류가 발생하는 경험은 모든 개발자가 한 번쯤 겪는 악몽입니다.

이런 문제는 실제 개발 현장에서 자주 발생합니다. 개발 환경과 운영 환경의 차이, 의존성 버전 불일치, 운영체제 차이 등이 원인입니다.

이로 인해 배포 시간이 길어지고, 디버깅에 많은 시간을 낭비하게 됩니다. 바로 이럴 때 필요한 것이 Docker 컨테이너입니다.

애플리케이션과 그 실행 환경을 하나의 패키지로 묶어서 어디서든 동일하게 실행할 수 있게 해줍니다.

개요

간단히 말해서, Docker 컨테이너는 애플리케이션을 실행하는 데 필요한 모든 것(코드, 런타임, 시스템 도구, 라이브러리)을 포함하는 독립적인 실행 환경입니다. 가상 머신(VM)과 달리 컨테이너는 호스트 OS의 커널을 공유하기 때문에 훨씬 가볍고 빠릅니다.

VM은 수 GB의 용량과 몇 분의 부팅 시간이 필요하지만, 컨테이너는 수십 MB에 불과하며 몇 초 만에 시작됩니다. 마이크로서비스 아키텍처에서 수십 개의 서비스를 동시에 실행해야 하는 경우, 이러한 차이는 엄청난 리소스 절약으로 이어집니다.

기존에는 애플리케이션을 배포할 때마다 서버 환경을 일일이 설정해야 했다면, 이제는 Docker 이미지 하나로 모든 환경에서 동일하게 실행할 수 있습니다. 컨테이너는 격리성, 이식성, 확장성이라는 세 가지 핵심 특징을 가집니다.

각 컨테이너는 독립적으로 실행되므로 서로 영향을 주지 않고, 어떤 환경에서도 동일하게 작동하며, 필요에 따라 쉽게 복제하고 확장할 수 있습니다. 이러한 특징들이 현대적인 클라우드 네이티브 애플리케이션 개발의 기반이 되었습니다.

코드 예제

# Node.js 애플리케이션을 Docker 컨테이너로 실행하기
# 먼저 공식 Node.js 이미지를 기반으로 컨테이너 실행
docker run -d \
  --name my-node-app \
  -p 3000:3000 \
  -v $(pwd):/app \
  -w /app \
  node:18-alpine \
  npm start

# 실행 중인 컨테이너 확인
docker ps

# 컨테이너 로그 확인
docker logs my-node-app

# 컨테이너 내부로 접속하여 디버깅
docker exec -it my-node-app sh

설명

이것이 하는 일: 위 명령어는 Node.js 18 Alpine 이미지를 기반으로 컨테이너를 생성하고, 로컬 디렉토리를 컨테이너 내부와 연결하여 애플리케이션을 실행합니다. 첫 번째로, docker run -d 명령은 컨테이너를 백그라운드에서 실행합니다(-d는 detached 모드).

--name my-node-app으로 컨테이너에 이름을 부여하면 나중에 쉽게 참조할 수 있습니다. 이렇게 하지 않으면 Docker가 무작위로 생성한 이름을 사용해야 하므로 관리가 어렵습니다.

그 다음으로, -p 3000:3000 옵션이 실행되면서 호스트의 3000번 포트와 컨테이너의 3000번 포트를 연결합니다. 이를 포트 바인딩이라고 하며, 외부에서 컨테이너 내부의 애플리케이션에 접근할 수 있게 해줍니다.

-v $(pwd):/app 옵션은 현재 디렉토리를 컨테이너의 /app 디렉토리에 마운트하여, 코드 변경사항이 즉시 반영되도록 합니다. 마지막으로, -w /app 옵션이 작업 디렉토리를 설정하고, node:18-alpine 이미지를 사용하여 npm start 명령을 실행합니다.

Alpine 리눅스는 매우 가벼워 이미지 크기를 크게 줄일 수 있습니다(약 5MB). 여러분이 이 코드를 사용하면 팀원 모두가 동일한 Node.js 버전과 환경에서 작업할 수 있으며, 개발 환경 설정에 소요되는 시간을 몇 시간에서 몇 분으로 단축할 수 있습니다.

또한 CI/CD 파이프라인에서도 동일한 이미지를 사용하여 테스트와 배포를 수행할 수 있어, 환경 차이로 인한 버그를 원천적으로 방지합니다.

실전 팁

💡 Alpine 이미지를 사용하면 이미지 크기를 5-10배 줄일 수 있지만, 일부 네이티브 모듈이 작동하지 않을 수 있으니 프로덕션 배포 전 충분히 테스트하세요.

💡 개발 중에는 볼륨 마운트(-v)로 코드 변경사항을 즉시 반영하고, 프로덕션에서는 코드를 이미지에 복사하여 불변성을 보장하세요.

💡 docker ps -a로 중지된 컨테이너까지 확인하고, docker system prune으로 사용하지 않는 컨테이너와 이미지를 정기적으로 정리하여 디스크 공간을 확보하세요.

💡 컨테이너 내부에서 디버깅할 때는 docker exec -it 명령으로 셸에 접속할 수 있지만, 로그 분석이 우선이므로 docker logs -f로 실시간 로그를 먼저 확인하세요.

💡 여러 컨테이너를 실행할 때는 --network 옵션으로 같은 네트워크에 배치하면 컨테이너 이름으로 서로 통신할 수 있어 IP 주소를 하드코딩할 필요가 없습니다.


2. Docker 이미지 생성과 관리

시작하며

여러분이 팀 프로젝트에서 새로운 개발자가 합류할 때마다 환경 설정 문서를 공유하고, 1-2시간씩 환경 구축을 도와준 경험이 있나요? Node.js 버전, 데이터베이스, Redis, 환경 변수 설정 등 단계마다 실수가 발생하고, "왜 안 되죠?"라는 질문이 반복됩니다.

이런 문제는 실제 개발 현장에서 자주 발생합니다. 특히 마이크로서비스 환경에서는 각 서비스마다 다른 언어와 프레임워크를 사용하므로, 환경 설정이 더욱 복잡해집니다.

이로 인해 온보딩 시간이 길어지고, 개발 생산성이 떨어집니다. 바로 이럴 때 필요한 것이 Docker 이미지입니다.

한 번 이미지를 만들어두면 누구나 단 한 줄의 명령어로 동일한 환경을 구축할 수 있으며, 이를 Docker Hub나 사설 레지스트리에 저장하여 팀 전체가 공유할 수 있습니다.

개요

간단히 말해서, Docker 이미지는 컨테이너를 실행하기 위한 템플릿입니다. 애플리케이션 코드, 런타임, 시스템 라이브러리, 환경 설정 등이 모두 포함된 읽기 전용 스냅샷이라고 생각하면 됩니다.

이미지는 레이어 구조로 이루어져 있어 효율적입니다. 예를 들어, Node.js 기본 이미지 위에 여러분의 애플리케이션 코드를 추가하면, Node.js 레이어는 재사용되고 변경된 부분만 새로운 레이어로 추가됩니다.

여러 프로젝트에서 같은 기본 이미지를 사용하면 디스크 공간과 다운로드 시간을 크게 절약할 수 있습니다. 기존에는 수동으로 서버에 접속하여 패키지를 설치하고 설정 파일을 수정했다면, 이제는 Dockerfile에 모든 과정을 코드로 작성하여 자동화할 수 있습니다.

이를 Infrastructure as Code(IaC)라고 합니다. Docker 이미지는 버전 관리가 가능하고, 불변성을 보장하며, 이식성이 뛰어나다는 특징이 있습니다.

한 번 빌드한 이미지는 절대 변경되지 않으므로, 프로덕션에서 예상치 못한 변경으로 인한 버그를 방지할 수 있습니다. 또한 태그를 사용하여 여러 버전을 관리하고, 문제 발생 시 이전 버전으로 즉시 롤백할 수 있습니다.

코드 예제

# 현재 디렉토리의 Dockerfile로 이미지 빌드
# -t 옵션으로 이미지에 이름과 태그 지정
docker build -t my-app:1.0.0 .

# 이미지 목록 확인
docker images

# 이미지로부터 컨테이너 실행
docker run -d -p 8080:8080 my-app:1.0.0

# Docker Hub에 이미지 푸시하기 전 태그 지정
docker tag my-app:1.0.0 username/my-app:1.0.0

# Docker Hub에 로그인
docker login

# 이미지를 레지스트리에 푸시
docker push username/my-app:1.0.0

# 다른 환경에서 이미지 가져오기
docker pull username/my-app:1.0.0

설명

이것이 하는 일: 위 명령어들은 Dockerfile로부터 이미지를 빌드하고, 태그를 지정하여 버전 관리하며, Docker Hub에 업로드하여 팀원들과 공유하는 전체 워크플로우를 보여줍니다. 첫 번째로, docker build -t my-app:1.0.0 . 명령은 현재 디렉토리의 Dockerfile을 읽어 이미지를 생성합니다.

-t 옵션은 이미지에 "이름:태그" 형식으로 레이블을 붙이는데, 이는 나중에 이미지를 쉽게 식별하고 버전을 관리하는 데 필수적입니다. 태그를 지정하지 않으면 기본값인 latest가 사용되지만, 프로덕션 환경에서는 명확한 버전 번호를 사용하는 것이 베스트 프랙티스입니다.

그 다음으로, docker images 명령으로 로컬에 저장된 이미지 목록을 확인할 수 있습니다. 여기서 각 이미지의 크기, 생성 시간, 태그 정보를 볼 수 있습니다.

이미지가 너무 크다면(수백 MB 이상) 최적화가 필요하다는 신호입니다. docker tag 명령은 기존 이미지에 새로운 이름을 부여하는데, Docker Hub에 푸시하려면 "username/repository:tag" 형식이 필요합니다.

마지막으로, docker push 명령이 이미지를 원격 레지스트리에 업로드하여 다른 개발자나 서버에서 접근할 수 있게 합니다. 푸시할 때는 레이어 단위로 전송되므로, 이전에 푸시한 레이어는 건너뛰어 시간을 절약합니다.

docker pull 명령으로 어떤 환경에서든 이 이미지를 다운로드받아 즉시 사용할 수 있습니다. 여러분이 이 워크플로우를 사용하면 새로운 팀원이 합류했을 때 "docker pull" 한 번으로 전체 개발 환경을 구축할 수 있습니다.

또한 CI/CD 파이프라인에서 빌드한 이미지를 스테이징과 프로덕션 환경에 배포할 때, 정확히 같은 이미지를 사용하여 "개발에서는 되는데 프로덕션에서는 안 돼요" 문제를 완전히 제거할 수 있습니다. 버전 태그를 사용하면 언제든 이전 버전으로 롤백할 수 있어 배포의 안정성이 크게 향상됩니다.

실전 팁

💡 이미지 태그는 의미 있는 버전 번호(1.0.0, 1.0.1)나 Git 커밋 해시를 사용하세요. latest 태그만 사용하면 어떤 버전이 배포되었는지 추적하기 어렵습니다.

💡 민감한 정보(API 키, 비밀번호)는 절대 이미지에 포함하지 마세요. 환경 변수나 Docker secrets를 사용하여 런타임에 주입하세요.

💡 docker image prune -a로 사용하지 않는 이미지를 정리하면 디스크 공간을 크게 확보할 수 있습니다. 특히 빌드를 자주 하는 개발 환경에서는 필수입니다.

💡 프라이빗 레지스트리(AWS ECR, Google GCR, Azure ACR)를 사용하면 회사 내부 이미지를 안전하게 관리할 수 있으며, 네트워크 전송 비용도 절감됩니다.

💡 멀티 플랫폼 이미지를 빌드하려면 docker buildx build --platform linux/amd64,linux/arm64 명령을 사용하여 Intel과 ARM CPU 모두에서 작동하는 이미지를 만들 수 있습니다.


3. Dockerfile 작성 기초

시작하며

여러분이 애플리케이션을 컨테이너화하려고 할 때, 어떻게 시작해야 할지 막막했던 경험이 있나요? "Node.js 앱을 Docker에 올려야 하는데, 무엇부터 해야 하지?" 하는 고민에 빠지거나, 인터넷에서 찾은 Dockerfile을 복사했는데 제대로 작동하지 않는 경우가 많습니다.

이런 문제는 실제 개발 현장에서 자주 발생합니다. Dockerfile 작성 방법을 모르면 이미지가 불필요하게 크거나, 빌드가 오래 걸리거나, 보안 취약점이 생길 수 있습니다.

또한 레이어 캐싱을 제대로 활용하지 못해 코드 한 줄 변경할 때마다 전체를 다시 빌드하는 비효율이 발생합니다. 바로 이럴 때 필요한 것이 올바른 Dockerfile 작성 방법입니다.

각 명령어의 의미를 이해하고 최적화 기법을 적용하면, 빠르고 안전하며 효율적인 이미지를 만들 수 있습니다.

개요

간단히 말해서, Dockerfile은 Docker 이미지를 빌드하기 위한 설명서입니다. 각 줄이 하나의 명령어로, 기본 이미지 선택부터 파일 복사, 패키지 설치, 실행 명령까지 모든 과정을 정의합니다.

Dockerfile을 잘 작성하면 이미지 크기를 수 GB에서 수십 MB로 줄일 수 있습니다. 예를 들어, 불필요한 개발 도구를 포함하지 않고, 멀티 스테이지 빌드를 사용하면 최종 이미지에는 실행에 필요한 파일만 포함됩니다.

이는 배포 시간을 단축하고, 네트워크 비용을 절감하며, 공격 표면을 줄여 보안을 강화합니다. 기존에는 수동으로 컨테이너에 접속하여 명령어를 실행하고 이미지를 커밋했다면, 이제는 Dockerfile에 모든 과정을 선언적으로 작성하여 자동화하고 버전 관리할 수 있습니다.

Dockerfile의 핵심은 레이어 구조를 이해하는 것입니다. 각 명령어(FROM, RUN, COPY 등)가 새로운 레이어를 생성하며, 변경되지 않은 레이어는 캐시에서 재사용됩니다.

따라서 자주 변경되는 파일(애플리케이션 코드)은 나중에 복사하고, 거의 변경되지 않는 작업(의존성 설치)은 먼저 수행하는 것이 빌드 속도를 크게 향상시킵니다.

코드 예제

# 1단계: 가벼운 Node.js 베이스 이미지 선택
FROM node:18-alpine

# 2단계: 작업 디렉토리 설정
WORKDIR /app

# 3단계: 의존성 파일만 먼저 복사 (캐싱 최적화)
COPY package*.json ./

# 4단계: 프로덕션 의존성만 설치
RUN npm ci --only=production

# 5단계: 애플리케이션 코드 복사
COPY . .

# 6단계: 포트 노출 (문서화 목적)
EXPOSE 3000

# 7단계: 컨테이너 시작 시 실행할 명령
CMD ["node", "server.js"]

설명

이것이 하는 일: 위 Dockerfile은 Node.js 애플리케이션을 최적화된 방식으로 컨테이너화합니다. 레이어 캐싱을 활용하여 빌드 속도를 최대화하고, 이미지 크기를 최소화합니다.

첫 번째로, FROM node:18-alpine 명령은 Alpine Linux 기반의 Node.js 18 이미지를 기본 레이어로 선택합니다. Alpine은 일반 Linux 배포판보다 10배 이상 작아(약 5MB) 최종 이미지 크기를 크게 줄입니다.

WORKDIR /app은 이후 모든 명령이 실행될 작업 디렉토리를 설정하며, 디렉토리가 없으면 자동으로 생성합니다. 그 다음으로, COPY package*.json ./ 명령이 실행되는데, 이것이 핵심 최적화 포인트입니다.

전체 코드를 복사하기 전에 의존성 파일만 먼저 복사하는 이유는, package.json이 변경되지 않으면 npm ci 레이어가 캐시에서 재사용되기 때문입니다. 만약 전체 코드를 먼저 복사하면 코드 한 줄만 변경해도 의존성을 다시 설치해야 하므로 빌드 시간이 몇 초에서 몇 분으로 늘어납니다.

npm ci --only=production은 package-lock.json을 기반으로 정확한 버전을 설치하며, 개발 의존성은 제외하여 이미지 크기를 줄입니다. 마지막으로, `COPY .

.명령이 애플리케이션 코드를 복사하고,EXPOSE 3000은 컨테이너가 3000번 포트를 사용한다는 것을 문서화합니다(실제로 포트를 열지는 않으며, docker run -p로 바인딩해야 합니다). CMD ["node", "server.js"]`는 컨테이너 시작 시 실행할 명령을 배열 형식(exec form)으로 지정하는데, 이 방식이 셸 형식보다 안전하고 시그널 처리가 올바르게 작동합니다.

여러분이 이 Dockerfile을 사용하면 코드를 수정할 때마다 1-2초 만에 빌드가 완료되며, 최종 이미지 크기는 100MB 이하로 유지됩니다. 또한 프로덕션 환경에서 필요한 것만 포함되므로 보안 취약점이 줄어들고, 컨테이너 시작 시간도 빨라집니다.

이 패턴은 Python, Go, Java 등 다른 언어에도 동일하게 적용할 수 있습니다.

실전 팁

💡 .dockerignore 파일을 작성하여 node_modules, .git, 로그 파일 등을 빌드 컨텍스트에서 제외하면 빌드 속도가 크게 향상됩니다.

💡 RUN 명령은 가능한 한 합쳐서 레이어 수를 줄이세요. RUN apt-get update && apt-get install -y package1 package2 && rm -rf /var/lib/apt/lists/* 처럼 한 줄로 작성하면 중간 파일이 남지 않습니다.

💡 USER 명령으로 non-root 사용자로 전환하면 보안이 강화됩니다. RUN addgroup -S appgroup && adduser -S appuser -G appgroupUSER appuser를 추가하세요.

💡 ENTRYPOINT와 CMD의 차이를 이해하세요. ENTRYPOINT는 컨테이너를 실행 파일처럼 만들고, CMD는 기본 인자를 제공합니다. 둘을 함께 사용하면 유연성이 높아집니다.

💡 빌드 인자(ARG)를 사용하면 빌드 시점에 값을 전달할 수 있습니다. ARG NODE ENV=production으로 환경을 구분하여 개발/프로덕션 이미지를 다르게 빌드할 수 있습니다.


4. Docker Compose로 멀티 컨테이너 관리

시작하며

여러분이 웹 애플리케이션, 데이터베이스, Redis 캐시, 메시지 큐를 함께 실행해야 할 때, 각각의 docker run 명령어를 일일이 입력하고 네트워크와 볼륨을 수동으로 설정한 경험이 있나요? 컨테이너 하나하나를 시작하고, 포트 번호를 기억하고, 환경 변수를 설정하는 과정이 번거롭고 오류가 발생하기 쉽습니다.

이런 문제는 실제 개발 현장에서 자주 발생합니다. 마이크로서비스 아키텍처에서는 수십 개의 서비스가 서로 연결되어 작동하므로, 수동으로 관리하는 것은 사실상 불가능합니다.

또한 팀원마다 설정이 달라져서 "내 로컬에서는 되는데" 문제가 다시 발생합니다. 바로 이럴 때 필요한 것이 Docker Compose입니다.

YAML 파일 하나로 여러 컨테이너의 설정을 정의하고, docker-compose up 명령 하나로 전체 스택을 실행할 수 있습니다. 개발 환경을 코드로 관리하여 팀 전체가 동일한 환경에서 작업할 수 있습니다.

개요

간단히 말해서, Docker Compose는 여러 컨테이너로 구성된 애플리케이션을 정의하고 실행하는 도구입니다. docker-compose.yml 파일에 서비스, 네트워크, 볼륨을 선언적으로 작성하면 복잡한 명령어 없이 전체 환경을 한 번에 구축할 수 있습니다.

Compose를 사용하면 각 서비스 간의 의존성을 관리할 수 있습니다. 예를 들어, 데이터베이스가 준비된 후에 웹 서버를 시작하도록 순서를 지정할 수 있습니다.

또한 서비스들이 자동으로 같은 네트워크에 배치되므로, 서비스 이름으로 서로 통신할 수 있어 IP 주소를 하드코딩할 필요가 없습니다. 개발 중에 데이터베이스 컨테이너를 재시작해도 웹 서버는 자동으로 재연결됩니다.

기존에는 각 컨테이너를 별도로 실행하고 수동으로 연결했다면, 이제는 docker-compose.yml 하나로 전체 인프라를 코드로 관리하고 Git으로 버전 관리할 수 있습니다. Compose의 강력한 기능은 오버라이드 파일입니다.

docker-compose.yml에 기본 설정을 작성하고, docker-compose.override.yml에 개발 환경 전용 설정(볼륨 마운트, 디버그 포트 등)을 추가할 수 있습니다. 프로덕션에서는 docker-compose.prod.yml로 다른 설정을 적용하여 환경별로 유연하게 대응할 수 있습니다.

코드 예제

# docker-compose.yml - Node.js 앱 + PostgreSQL + Redis
version: '3.8'

services:
  # 웹 애플리케이션 서비스
  web:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgresql://postgres:password@db:5432/myapp
      - REDIS_URL=redis://redis:6379
    depends_on:
      - db
      - redis
    volumes:
      - .:/app
      - /app/node_modules

  # PostgreSQL 데이터베이스
  db:
    image: postgres:15-alpine
    environment:
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=myapp
    volumes:
      - postgres_data:/var/lib/postgresql/data

  # Redis 캐시
  redis:
    image: redis:7-alpine

volumes:
  postgres_data:

설명

이것이 하는 일: 위 docker-compose.yml 파일은 Node.js 웹 애플리케이션, PostgreSQL 데이터베이스, Redis 캐시를 하나의 스택으로 정의하고, 서비스 간 연결과 데이터 영속성을 자동으로 구성합니다. 첫 번째로, services 섹션에서 세 개의 서비스(web, db, redis)를 정의합니다.

각 서비스는 독립적인 컨테이너로 실행되지만 같은 네트워크에 배치되어 서비스 이름으로 통신할 수 있습니다. web 서비스는 build: .로 현재 디렉토리의 Dockerfile을 빌드하고, ports로 호스트의 3000번 포트와 연결합니다.

depends_on은 db와 redis가 먼저 시작되도록 순서를 지정하지만, 실제로 준비될 때까지 기다리지는 않으므로 애플리케이션에서 재시도 로직이 필요합니다. 그 다음으로, environment 섹션에서 환경 변수를 설정하는데, 여기서 주목할 점은 DATABASE_URL에 @db:5432를 사용한다는 것입니다.

Docker Compose는 자동으로 각 서비스 이름을 DNS로 등록하므로, db라는 호스트명으로 PostgreSQL에 접근할 수 있습니다. 이는 IP 주소를 하드코딩하지 않아도 되므로 환경 이식성이 크게 향상됩니다.

volumes 섹션에서 .:/app은 현재 디렉토리를 컨테이너의 /app에 마운트하여 코드 변경사항이 즉시 반영되도록 하고, /app/node_modules는 익명 볼륨으로 호스트의 node_modules를 덮어쓰지 않도록 보호합니다. 마지막으로, volumes 최상위 섹션에서 postgres_data라는 named volume을 정의합니다.

이는 컨테이너가 삭제되어도 데이터베이스 데이터가 유지되도록 합니다. 만약 이 설정이 없다면 docker-compose down을 실행할 때마다 모든 데이터가 사라집니다.

이미지는 postgres:15-alpineredis:7-alpine처럼 공식 이미지를 사용하여 별도의 Dockerfile 없이 바로 실행할 수 있습니다. 여러분이 이 Compose 파일을 사용하면 docker-compose up -d 명령 하나로 전체 개발 환경이 구축됩니다.

새로운 팀원이 합류했을 때도 리포지토리를 클론하고 이 명령어 하나면 즉시 개발을 시작할 수 있습니다. 또한 docker-compose logs -f web로 특정 서비스의 로그를 실시간으로 확인하거나, docker-compose exec db psql -U postgres로 데이터베이스에 직접 접속하여 디버깅할 수 있습니다.

작업이 끝나면 docker-compose down으로 모든 컨테이너를 정리하지만, 데이터는 볼륨에 안전하게 보관됩니다.

실전 팁

💡 docker-compose up --build로 코드 변경 후 이미지를 강제로 다시 빌드하고, -d 옵션으로 백그라운드에서 실행하세요.

💡 .env 파일을 사용하여 민감한 정보(비밀번호, API 키)를 관리하고, .gitignore에 추가하여 Git에 커밋되지 않도록 하세요. environment: - DB_PASSWORD=${DB_PASSWORD} 형식으로 참조합니다.

💡 개발 환경에서는 docker-compose.override.yml에 볼륨 마운트와 포트를 추가하고, 프로덕션에서는 docker-compose -f docker-compose.yml -f docker-compose.prod.yml up으로 다른 설정을 적용하세요.

💡 healthcheck를 추가하면 컨테이너가 실제로 준비될 때까지 기다릴 수 있습니다. healthcheck: test: ["CMD", "pg_isready", "-U", "postgres"]처럼 설정하세요.

💡 docker-compose down -v는 볼륨까지 삭제하므로 주의하세요. 데이터베이스를 초기화할 때만 사용하고, 일반적으로는 docker-compose down만 실행하세요.


5. 볼륨과 네트워크 설정

시작하며

여러분이 Docker 컨테이너를 재시작했을 때 데이터베이스의 모든 데이터가 사라진 경험이 있나요? 또는 한 컨테이너에서 다른 컨테이너로 접속하려고 할 때 "Connection refused" 오류가 발생하여 몇 시간을 낭비한 적이 있나요?

컨테이너는 기본적으로 격리되어 있고 상태를 유지하지 않으므로, 데이터 영속성과 컨테이너 간 통신을 제대로 설정하지 않으면 이런 문제가 발생합니다. 이런 문제는 실제 개발 현장에서 자주 발생합니다.

특히 스테이트풀(stateful) 애플리케이션을 컨테이너화할 때 데이터 손실 위험이 있고, 마이크로서비스 간 통신 설정이 복잡하면 개발 효율이 떨어집니다. 또한 보안 측면에서 불필요하게 모든 컨테이너가 서로 통신할 수 있으면 공격 표면이 넓어집니다.

바로 이럴 때 필요한 것이 Docker 볼륨과 네트워크 설정입니다. 볼륨으로 데이터를 영속적으로 보관하고, 네트워크로 컨테이너 간 통신을 안전하게 제어할 수 있습니다.

개요

간단히 말해서, Docker 볼륨은 컨테이너의 생명주기와 독립적으로 데이터를 저장하는 메커니즘입니다. 컨테이너가 삭제되거나 재시작되어도 볼륨의 데이터는 유지되므로, 데이터베이스, 로그, 업로드 파일 등을 안전하게 보관할 수 있습니다.

볼륨에는 세 가지 유형이 있습니다. Named volume은 Docker가 관리하는 영역에 데이터를 저장하며 가장 권장되는 방식입니다.

Bind mount는 호스트의 특정 디렉토리를 컨테이너에 마운트하여 개발 중 코드 변경사항을 즉시 반영할 때 유용합니다. 익명 볼륨은 임시 데이터를 저장하며 컨테이너 삭제 시 함께 제거됩니다.

프로덕션 데이터는 named volume을, 개발 소스 코드는 bind mount를 사용하는 것이 베스트 프랙티스입니다. Docker 네트워크는 컨테이너 간 통신을 제어합니다.

기본적으로 같은 네트워크에 있는 컨테이너끼리만 서비스 이름으로 통신할 수 있으며, 다른 네트워크의 컨테이너는 격리됩니다. 이를 활용하면 프론트엔드, 백엔드, 데이터베이스를 각각 다른 네트워크에 배치하여 보안을 강화할 수 있습니다.

네트워크 드라이버에는 여러 종류가 있습니다. Bridge(기본값)는 같은 호스트의 컨테이너를 연결하고, Host는 컨테이너가 호스트의 네트워크를 직접 사용하며, Overlay는 여러 Docker 호스트에 걸친 컨테이너를 연결합니다(Swarm이나 Kubernetes에서 사용).

대부분의 경우 Bridge 네트워크로 충분합니다.

코드 예제

# Named volume 생성
docker volume create app_data

# 볼륨을 사용하는 컨테이너 실행
docker run -d \
  --name postgres \
  -v app_data:/var/lib/postgresql/data \
  -e POSTGRES_PASSWORD=secret \
  postgres:15-alpine

# 사용자 정의 네트워크 생성
docker network create my_network

# 네트워크에 컨테이너 연결하여 실행
docker run -d \
  --name web \
  --network my_network \
  -p 3000:3000 \
  my-app:latest

# 같은 네트워크에 다른 컨테이너 추가
docker run -d \
  --name api \
  --network my_network \
  my-api:latest

# 볼륨 및 네트워크 정보 확인
docker volume ls
docker network ls
docker network inspect my_network

설명

이것이 하는 일: 위 명령어들은 Docker 볼륨으로 데이터를 영속적으로 저장하고, 사용자 정의 네트워크로 컨테이너 간 통신을 구성하는 전체 워크플로우를 보여줍니다. 첫 번째로, docker volume create app_data 명령은 명시적으로 named volume을 생성합니다.

실제로는 이 명령 없이 docker run -v 옵션에서 바로 볼륨을 지정해도 자동으로 생성되지만, 명시적으로 생성하면 볼륨의 존재와 목적을 명확히 할 수 있습니다. PostgreSQL 컨테이너를 실행할 때 -v app_data:/var/lib/postgresql/data로 볼륨을 마운트하면, 데이터베이스 파일이 이 볼륨에 저장됩니다.

컨테이너를 삭제하고 다시 생성해도 같은 볼륨을 마운트하면 데이터가 그대로 유지됩니다. 그 다음으로, docker network create my_network 명령이 사용자 정의 브리지 네트워크를 생성합니다.

기본 브리지 네트워크와 달리 사용자 정의 네트워크에서는 자동 DNS 해석이 작동하여 컨테이너 이름으로 서로 찾을 수 있습니다. --network my_network 옵션으로 web과 api 컨테이너를 같은 네트워크에 배치하면, web 컨테이너에서 http://api:포트로 API 서버에 접근할 수 있습니다.

이는 IP 주소를 하드코딩하지 않아도 되므로 컨테이너가 재시작되어 IP가 변경되어도 문제없이 작동합니다. 마지막으로, docker volume lsdocker network ls로 생성된 볼륨과 네트워크를 확인할 수 있으며, docker network inspect my_network로 어떤 컨테이너가 연결되어 있고 각 컨테이너의 IP 주소가 무엇인지 자세히 볼 수 있습니다.

이는 네트워크 문제를 디버깅할 때 매우 유용합니다. 여러분이 이 설정을 사용하면 데이터베이스를 안전하게 관리할 수 있고, 컨테이너 재시작이나 업데이트 시에도 데이터 손실 걱정이 없습니다.

또한 마이크로서비스 아키텍처에서 프론트엔드 네트워크, 백엔드 네트워크, 데이터베이스 네트워크를 분리하여 보안을 강화할 수 있습니다. 예를 들어, 데이터베이스는 백엔드 네트워크에만 연결하여 프론트엔드에서 직접 접근하지 못하도록 차단할 수 있습니다.

볼륨 백업은 docker run --rm -v app_data:/data -v $(pwd):/backup alpine tar czf /backup/backup.tar.gz -C /data . 명령으로 간단히 수행할 수 있습니다.

실전 팁

💡 Bind mount는 절대 경로를 사용하세요. 상대 경로는 예상치 못한 위치에 마운트될 수 있습니다. $(pwd):/app 형식이 안전합니다.

💡 데이터베이스 볼륨은 정기적으로 백업하세요. docker run --rm -v volume_name:/data -v $(pwd):/backup alpine 컨테이너로 데이터를 추출할 수 있습니다.

💡 네트워크 격리로 보안을 강화하세요. 프론트엔드는 public_network에, 백엔드는 backend_network에, DB는 db_network에 배치하고 필요한 것만 연결하세요.

💡 docker volume prune으로 사용하지 않는 볼륨을 정리하지만, 중요한 데이터가 없는지 반드시 확인 후 실행하세요.

💡 성능이 중요한 경우 tmpfs 마운트를 사용하세요. --tmpfs /tmp는 메모리에 임시 데이터를 저장하여 I/O 속도가 매우 빠르지만, 컨테이너 재시작 시 데이터가 사라집니다.


6. Docker 레이어 캐싱 최적화

시작하며

여러분이 코드 한 줄을 수정하고 이미지를 다시 빌드했을 때, 수백 개의 npm 패키지를 다시 다운로드하고 설치하는 것을 지켜본 적이 있나요? 5분이 넘게 걸리는 빌드 시간 때문에 개발 속도가 느려지고, CI/CD 파이프라인에서 매번 전체를 빌드하느라 비용이 증가합니다.

이런 문제는 실제 개발 현장에서 자주 발생합니다. Dockerfile을 잘못 작성하면 작은 변경에도 모든 레이어가 무효화되어 처음부터 다시 빌드해야 합니다.

이는 개발자의 시간을 낭비하고, 클라우드 빌드 서비스 비용을 증가시키며, 배포 속도를 느리게 만듭니다. 바로 이럴 때 필요한 것이 Docker 레이어 캐싱 최적화입니다.

Dockerfile의 명령어 순서를 조정하고 BuildKit을 활용하면 빌드 시간을 몇 분에서 몇 초로 단축할 수 있습니다.

개요

간단히 말해서, Docker는 각 명령어(RUN, COPY 등)의 결과를 레이어로 저장하고 캐시합니다. 명령어나 복사되는 파일이 변경되지 않으면 캐시된 레이어를 재사용하여 빌드 시간을 크게 단축합니다.

레이어 캐싱의 핵심은 순서입니다. Docker는 위에서 아래로 명령어를 실행하다가 변경된 부분을 만나면 그 이후의 모든 캐시를 무효화합니다.

따라서 자주 변경되지 않는 작업(기본 패키지 설치)은 위에, 자주 변경되는 작업(애플리케이션 코드 복사)은 아래에 배치해야 합니다. 예를 들어, package.json을 먼저 복사하여 의존성을 설치하고, 그 후에 소스 코드를 복사하면 코드 변경 시 의존성 설치 레이어는 캐시에서 재사용됩니다.

기존에는 빌드할 때마다 모든 단계를 반복했다면, 이제는 변경된 부분만 다시 빌드하여 시간과 비용을 절약할 수 있습니다. BuildKit은 Docker의 차세대 빌드 엔진으로, 병렬 빌드, 더 스마트한 캐싱, 빌드 시크릿 관리 등 강력한 기능을 제공합니다.

DOCKER_BUILDKIT=1 docker build로 활성화하면 의존성이 없는 레이어들을 동시에 빌드하여 속도가 더욱 향상됩니다. 또한 --mount=type=cache를 사용하면 패키지 매니저의 캐시 디렉토리를 여러 빌드 간에 공유하여 다운로드 시간을 줄일 수 있습니다.

코드 예제

# BuildKit을 활성화하고 캐시 최적화된 Node.js 이미지 빌드
# syntax=docker/dockerfile:1
FROM node:18-alpine

WORKDIR /app

# 1단계: 의존성 파일만 먼저 복사 (변경 빈도 낮음)
COPY package*.json ./

# 2단계: BuildKit 캐시 마운트로 npm 캐시 재사용
RUN --mount=type=cache,target=/root/.npm \
    npm ci --only=production

# 3단계: 소스 코드 복사 (변경 빈도 높음)
COPY . .

# 4단계: 빌드가 필요한 경우 (TypeScript 컴파일 등)
RUN --mount=type=cache,target=/root/.npm \
    npm run build

CMD ["node", "dist/server.js"]

# 빌드 명령어
# DOCKER_BUILDKIT=1 docker build -t my-app:latest .

설명

이것이 하는 일: 위 Dockerfile은 레이어 캐싱을 최대한 활용하여 빌드 시간을 최소화합니다. 의존성 설치와 소스 코드 복사를 분리하고, BuildKit의 캐시 마운트 기능으로 npm 다운로드를 가속화합니다.

첫 번째로, # syntax=docker/dockerfile:1 주석은 BuildKit의 최신 기능을 사용하겠다는 선언입니다. 이는 반드시 Dockerfile의 첫 줄에 작성해야 합니다.

COPY package*.json ./ 명령이 소스 코드 전체가 아닌 의존성 파일만 먼저 복사하는 것이 핵심 최적화입니다. package.json이 변경되지 않으면 다음 RUN npm ci 레이어가 캐시에서 재사용되어 수백 개의 패키지를 다시 다운로드하지 않습니다.

실제로 코드 변경은 자주 일어나지만 의존성 변경은 드물기 때문에, 이 분리만으로도 90% 이상의 빌드에서 캐시를 활용할 수 있습니다. 그 다음으로, RUN --mount=type=cache,target=/root/.npm 명령이 BuildKit의 캐시 마운트 기능을 사용합니다.

이는 npm의 캐시 디렉토리를 여러 빌드 간에 공유하여, 이전 빌드에서 다운로드한 패키지를 재사용합니다. 일반 레이어 캐싱은 Dockerfile이나 파일이 변경되면 무효화되지만, 캐시 마운트는 빌드 컨텍스트와 독립적으로 유지됩니다.

따라서 package.json이 변경되어 의존성을 다시 설치할 때도, 이미 다운로드한 패키지는 캐시에서 가져와 네트워크 시간을 절약합니다. 마지막으로, `COPY .

.` 명령이 소스 코드를 복사하는데, 이것이 Dockerfile의 뒤쪽에 배치된 이유는 소스 코드가 가장 자주 변경되기 때문입니다. 이 명령 이후의 레이어는 코드 변경 시마다 다시 빌드되지만, 그 이전의 의존성 설치 레이어는 캐시에서 재사용됩니다.

TypeScript나 Webpack 빌드가 필요한 경우에도 캐시 마운트를 사용하여 node_modules 캐시를 공유할 수 있습니다. 여러분이 이 최적화를 적용하면 첫 번째 빌드는 여전히 몇 분이 걸리지만, 이후 빌드는 5-10초 내에 완료됩니다.

특히 CI/CD 환경에서는 캐시를 레지스트리에 저장하여 여러 빌드 서버에서 공유할 수 있습니다(docker build --cache-from 옵션). 이는 클라우드 빌드 서비스(GitHub Actions, GitLab CI)의 빌드 시간과 비용을 크게 줄여줍니다.

개발 중에는 docker build --progress=plain으로 어떤 레이어가 캐시에서 재사용되는지 확인하여 Dockerfile을 더욱 최적화할 수 있습니다.

실전 팁

💡 .dockerignore에 node_modules, .git, 테스트 파일을 추가하여 빌드 컨텍스트 크기를 줄이면 COPY 명령이 빠라집니다.

💡 RUN 명령을 여러 줄로 나누지 말고 &&로 연결하세요. 각 RUN은 새로운 레이어를 생성하므로 이미지 크기가 커지고 캐싱이 비효율적입니다.

💡 GitHub Actions에서는 actions/cache로 Docker 레이어를 캐싱하거나, docker/build-push-actioncache-from, cache-to 옵션으로 레지스트리 캐시를 사용하세요.

💡 멀티 스테이지 빌드와 함께 사용하면 더욱 강력합니다. 빌드 스테이지와 런타임 스테이지를 분리하여 최종 이미지 크기를 최소화하면서도 캐싱 효과를 누릴 수 있습니다.

💡 docker builder prune으로 빌드 캐시를 정리할 수 있지만, 개발 중에는 캐시를 유지하는 것이 유리합니다. 디스크 공간이 부족할 때만 실행하세요.


7. 환경변수와 시크릿 관리

시작하며

여러분이 API 키나 데이터베이스 비밀번호를 Dockerfile에 하드코딩하고 Git에 커밋한 후, 나중에 보안 문제를 발견하고 당황한 경험이 있나요? 또는 개발, 스테이징, 프로덕션 환경마다 다른 설정 값을 사용해야 하는데, 매번 이미지를 다시 빌드해야 해서 불편했던 적이 있나요?

이런 문제는 실제 개발 현장에서 자주 발생합니다. 민감한 정보를 이미지에 포함하면 보안 취약점이 생기고, 이미지가 유출될 경우 시스템 전체가 위험에 노출됩니다.

또한 설정을 이미지에 고정하면 환경별로 다른 이미지를 관리해야 하므로 "어떤 이미지가 어느 환경용인가?" 혼란이 발생합니다. 바로 이럴 때 필요한 것이 올바른 환경변수와 시크릿 관리입니다.

설정을 런타임에 주입하고, 민감한 정보는 안전하게 암호화하여 이미지와 분리하면 보안과 유연성을 모두 확보할 수 있습니다.

개요

간단히 말해서, 환경변수는 컨테이너 실행 시 애플리케이션에 전달되는 키-값 쌍입니다. API URL, 포트 번호, 기능 플래그 등 환경별로 다른 설정을 코드 변경 없이 조정할 수 있게 해줍니다.

환경변수는 여러 방법으로 전달할 수 있습니다. docker run -e 옵션으로 직접 지정하거나, .env 파일에 작성하여 --env-file 옵션으로 일괄 로드하거나, Docker Compose의 environment 섹션에 정의할 수 있습니다.

개발 환경에서는 .env.development, 프로덕션에서는 .env.production처럼 파일을 분리하여 관리하는 것이 일반적입니다. 중요한 점은 이러한 파일들을 .gitignore에 추가하여 Git에 커밋되지 않도록 해야 한다는 것입니다.

시크릿(비밀번호, 토큰, 인증서 등)은 일반 환경변수보다 더 신중하게 다뤄야 합니다. Docker Swarm이나 Kubernetes는 시크릿 전용 관리 시스템을 제공하여 암호화된 상태로 저장하고, 필요한 컨테이너에만 런타임에 마운트합니다.

단순 Docker 환경에서는 .env 파일과 적절한 파일 권한(chmod 600), 그리고 AWS Secrets Manager나 HashiCorp Vault 같은 외부 시크릿 관리 서비스를 사용할 수 있습니다. 빌드 시점에 필요한 시크릿(예: 프라이빗 npm 레지스트리 토큰)은 BuildKit의 --secret 기능을 사용하세요.

이는 시크릿을 이미지 레이어에 포함하지 않고 빌드 중에만 임시로 마운트하여 보안을 유지합니다.

코드 예제

# .env 파일 (Git에 커밋하지 말 것!)
NODE_ENV=production
DATABASE_URL=postgresql://user:password@db:5432/myapp
REDIS_URL=redis://redis:6379
API_KEY=your-secret-api-key
JWT_SECRET=your-jwt-secret

# docker-compose.yml에서 환경변수 사용
version: '3.8'

services:
  web:
    build: .
    env_file:
      - .env
    environment:
      - PORT=3000
      - LOG_LEVEL=info
    secrets:
      - db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt

# Dockerfile에서 BuildKit secret 사용 (빌드 시점)
# syntax=docker/dockerfile:1
FROM node:18-alpine
RUN --mount=type=secret,id=npm_token \
    echo "//registry.npmjs.org/:_authToken=$(cat /run/secrets/npm_token)" > ~/.npmrc && \
    npm install && \
    rm ~/.npmrc

설명

이것이 하는 일: 위 예제는 환경변수를 .env 파일로 관리하고, Docker Compose로 일괄 주입하며, BuildKit secret으로 빌드 시점의 민감한 정보를 안전하게 처리하는 전체 워크플로우를 보여줍니다. 첫 번째로, .env 파일에 모든 설정 값을 키-값 쌍으로 작성합니다.

이 파일은 반드시 .gitignore에 추가해야 하며, 대신 .env.example 파일을 커밋하여 필요한 변수 목록을 팀원들과 공유합니다. DATABASE_URL처럼 민감한 정보는 프로덕션에서는 환경변수로 전달하고, 로컬 개발에서만 .env 파일을 사용하는 것이 안전합니다.

환경변수 이름은 대문자와 언더스코어를 사용하는 것이 관례입니다(UPPER_SNAKE_CASE). 그 다음으로, Docker Compose 파일에서 env_file 옵션으로 .env 파일 전체를 로드하고, environment 섹션에서 추가 변수를 직접 지정할 수 있습니다.

두 방법을 혼합하여 사용하면 공통 설정은 .env 파일에, 서비스별 설정은 environment에 작성하여 관리하기 쉽습니다. secrets 섹션은 Docker Swarm의 기능으로, 파일 내용을 /run/secrets/ 디렉토리에 마운트하여 애플리케이션에서 읽을 수 있게 합니다.

이는 환경변수보다 안전한데, docker inspect로 환경변수는 볼 수 있지만 secret은 볼 수 없기 때문입니다. 마지막으로, Dockerfile의 RUN --mount=type=secret 명령은 BuildKit의 secret 마운트 기능을 사용합니다.

빌드 시 docker build --secret id=npm_token,src=./npm_token.txt로 시크릿을 전달하면, 빌드 중에만 /run/secrets/npm_token으로 접근할 수 있고 최종 이미지에는 포함되지 않습니다. 이는 프라이빗 패키지를 설치할 때 토큰을 안전하게 사용하는 표준 방법입니다.

예제에서는 npm 설치 후 .npmrc를 삭제하여 토큰이 레이어에 남지 않도록 합니다. 여러분이 이 패턴을 사용하면 개발 환경과 프로덕션 환경에서 같은 이미지를 사용하면서도 다른 설정을 적용할 수 있습니다.

프로덕션에서는 Kubernetes Secrets나 AWS Secrets Manager에서 환경변수를 주입하고, 로컬에서는 .env 파일을 사용하여 간편하게 개발할 수 있습니다. 또한 이미지를 공개 레지스트리에 푸시하거나 팀원과 공유할 때 민감한 정보가 유출될 위험이 없습니다.

CI/CD 파이프라인에서는 GitHub Secrets나 GitLab Variables에 시크릿을 저장하고, 빌드/배포 시점에 주입하여 코드 저장소에 비밀번호가 남지 않도록 합니다.

실전 팁

💡 .env.example 파일을 커밋하여 필요한 환경변수 목록을 문서화하세요. 실제 값은 비워두고 "# Database connection string" 같은 주석을 추가합니다.

💡 프로덕션에서는 절대 .env 파일을 사용하지 마세요. 클라우드 제공자의 시크릿 관리 서비스(AWS Secrets Manager, GCP Secret Manager)나 Kubernetes Secrets를 사용하세요.

💡 환경변수가 제대로 전달되었는지 확인하려면 docker exec container_name env 명령으로 컨테이너 내부의 환경변수를 볼 수 있지만, 민감한 정보는 로그에 출력하지 마세요.

💡 docker run --env API_KEY=$(aws secretsmanager get-secret-value --secret-id my-api-key --query SecretString --output text)처럼 실행 시점에 외부 시스템에서 시크릿을 가져와 주입할 수 있습니다.

💡 민감한 환경변수는 애플리케이션 시작 시 존재 여부를 검증하고, 없으면 즉시 종료하여 잘못된 설정으로 실행되는 것을 방지하세요. Node.js에서는 if (!process.env.API KEY) throw new Error('API KEY is required')처럼 검증합니다.


8. Docker 멀티 스테이지 빌드

시작하며

여러분이 TypeScript나 Go로 작성한 애플리케이션을 Docker 이미지로 빌드했을 때, 이미지 크기가 1GB를 넘어서 놀란 경험이 있나요? 컴파일러, 빌드 도구, 소스 코드 등 실행에 필요하지 않은 것들이 모두 포함되어 이미지가 비대해지고, 배포 시간이 길어지며, 보안 취약점이 증가합니다.

이런 문제는 실제 개발 현장에서 자주 발생합니다. 특히 컴파일이 필요한 언어(TypeScript, Go, Rust, Java)에서는 빌드 도구가 수백 MB를 차지하지만 실행 시에는 전혀 필요하지 않습니다.

또한 node_modules의 devDependencies나 테스트 파일 등도 프로덕션에는 불필요합니다. 바로 이럴 때 필요한 것이 Docker 멀티 스테이지 빌드입니다.

빌드 단계와 실행 단계를 분리하여 최종 이미지에는 실행에 필요한 파일만 포함시켜 크기를 10배 이상 줄일 수 있습니다.

개요

간단히 말해서, 멀티 스테이지 빌드는 하나의 Dockerfile에서 여러 개의 FROM 명령을 사용하여 각 단계를 독립적으로 실행하고, 이전 단계에서 필요한 파일만 복사하는 기법입니다. 첫 번째 스테이지(빌드 스테이지)에서는 컴파일러와 빌드 도구가 포함된 큰 이미지를 사용하여 애플리케이션을 빌드합니다.

예를 들어, TypeScript를 JavaScript로 컴파일하거나 Go 소스를 바이너리로 빌드합니다. 두 번째 스테이지(런타임 스테이지)에서는 가벼운 기본 이미지를 사용하고, 첫 번째 스테이지에서 생성된 빌드 결과물만 복사합니다.

이렇게 하면 최종 이미지에는 빌드 도구가 전혀 포함되지 않습니다. 기존에는 빌드와 실행을 위한 별도의 Dockerfile을 만들거나, 빌드 후 수동으로 파일을 추출해야 했다면, 이제는 하나의 Dockerfile에서 모든 과정을 자동화할 수 있습니다.

멀티 스테이지 빌드의 장점은 이미지 크기 감소뿐만 아니라 보안 강화에도 있습니다. 공격자가 컨테이너에 침입하더라도 컴파일러나 디버깅 도구가 없으므로 악용하기 어렵습니다.

또한 이미지 스캔 시 빌드 도구의 취약점이 보고되지 않아 보안 관리가 쉬워집니다. Node.js 애플리케이션의 경우 최종 이미지에 npm도 포함하지 않을 수 있어 npm 관련 취약점을 완전히 제거할 수 있습니다.

코드 예제

# 멀티 스테이지 빌드 - TypeScript Node.js 앱 예제
# syntax=docker/dockerfile:1

# 1단계: 빌드 스테이지 (build라는 이름 지정)
FROM node:18 AS build

WORKDIR /app

# 의존성 설치 (devDependencies 포함)
COPY package*.json ./
RUN npm ci

# TypeScript 컴파일
COPY tsconfig.json ./
COPY src ./src
RUN npm run build

# 프로덕션 의존성만 재설치
RUN npm ci --only=production

# 2단계: 런타임 스테이지 (최종 이미지)
FROM node:18-alpine

# 보안을 위해 non-root 유저 생성
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

WORKDIR /app

# 빌드 스테이지에서 필요한 파일만 복사
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 ./

EXPOSE 3000

CMD ["node", "dist/server.js"]

설명

이것이 하는 일: 위 Dockerfile은 두 개의 스테이지로 나뉘어 있습니다. 첫 번째 스테이지에서 TypeScript를 컴파일하고, 두 번째 스테이지에서는 컴파일된 JavaScript와 프로덕션 의존성만 복사하여 최종 이미지를 최소화합니다.

첫 번째로, FROM node:18 AS build 명령은 첫 번째 스테이지를 "build"라는 이름으로 시작합니다. 이 스테이지에서는 full Node.js 이미지를 사용하여 TypeScript 컴파일러와 모든 개발 도구를 사용할 수 있습니다.

npm ci로 devDependencies를 포함한 모든 패키지를 설치하고, npm run build로 TypeScript를 JavaScript로 컴파일합니다. 그 후 npm ci --only=production을 다시 실행하여 node_modules를 프로덕션 의존성만 남깁니다.

이 단계는 크고 무겁지만 최종 이미지에는 포함되지 않으므로 문제없습니다. 그 다음으로, FROM node:18-alpine 명령으로 두 번째 스테이지가 시작되는데, 이것이 최종 이미지가 됩니다.

Alpine 이미지는 5MB 정도로 매우 가볍습니다. RUN addgroup -S appgroup && adduser -S appuser -G appgroup 명령으로 non-root 사용자를 생성하고 USER appuser로 전환하여 보안을 강화합니다.

Root 권한으로 실행하면 컨테이너 탈출 시 호스트 시스템이 위험에 노출될 수 있기 때문입니다. 마지막으로, COPY --from=build 명령이 핵심입니다.

이는 "build"라는 이름의 이전 스테이지에서 파일을 복사해옵니다. /app/dist는 컴파일된 JavaScript 파일이고, /app/node_modules는 프로덕션 의존성만 남은 디렉토리입니다.

--chown=appuser:appgroup 옵션으로 파일 소유권을 non-root 유저로 설정합니다. 이렇게 하면 최종 이미지에는 TypeScript 소스 코드, tsconfig.json, TypeScript 컴파일러, 개발 의존성 등이 전혀 포함되지 않습니다.

여러분이 이 기법을 사용하면 빌드 스테이지는 500MB-1GB가 되지만, 최종 이미지는 50-100MB 정도로 줄어듭니다. 이는 Docker Hub나 클라우드 레지스트리로 푸시할 때 네트워크 비용을 크게 절감하고, 컨테이너 시작 시간을 단축하며, 보안 취약점을 줄입니다.

Go 언어의 경우 더욱 극적인데, FROM scratch를 사용하면 실행 바이너리만 포함된 10MB 미만의 이미지를 만들 수 있습니다. 이는 서버리스 환경이나 엣지 컴퓨팅에서 특히 유용합니다.

실전 팁

💡 스테이지에 의미 있는 이름을 붙이세요. AS builder, AS dependencies, AS runtime처럼 명확히 하면 복잡한 Dockerfile을 이해하기 쉽습니다.

💡 중간 스테이지를 여러 개 만들 수 있습니다. 예: dependencies 스테이지 → build 스테이지 → test 스테이지 → runtime 스테이지로 분리하여 각각의 캐싱을 최적화할 수 있습니다.

💡 Go나 Rust처럼 정적 바이너리를 생성하는 언어는 FROM scratch 또는 FROM gcr.io/distroless/static을 사용하면 1-10MB의 극도로 작은 이미지를 만들 수 있습니다.

💡 특정 스테이지까지만 빌드하려면 docker build --target build -t my-app:build . 명령을 사용하세요. 테스트 스테이지를 만들어 CI에서만 실행할 수 있습니다.

💡 --chown 옵션을 사용하여 복사 시점에 파일 소유권을 설정하면 별도의 RUN chown 명령이 필요 없어 레이어가 줄어듭니다.


#Docker#Container#Dockerfile#DockerCompose#ImageManagement#TypeScript

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.

함께 보면 좋은 카드 뉴스

마이크로서비스 배포 완벽 가이드

Kubernetes를 활용한 마이크로서비스 배포의 핵심 개념부터 실전 운영까지, 초급 개발자도 쉽게 따라할 수 있는 완벽 가이드입니다. 실무에서 바로 적용 가능한 배포 전략과 노하우를 담았습니다.

쿠버네티스 아키텍처 완벽 가이드

초급 개발자를 위한 쿠버네티스 아키텍처 설명서입니다. 클러스터 구조부터 Control Plane, Worker Node, 파드, 네트워킹까지 실무 관점에서 쉽게 풀어냅니다. 점프 투 자바 스타일로 술술 읽히는 이북 형식으로 작성되었습니다.

EFK 스택 로깅 완벽 가이드

마이크로서비스 환경에서 로그를 효과적으로 수집하고 분석하는 EFK 스택(Elasticsearch, Fluentd, Kibana)의 핵심 개념과 실전 활용법을 초급 개발자도 쉽게 이해할 수 있도록 정리한 가이드입니다.

Spring Boot 상품 서비스 구축 완벽 가이드

실무 RESTful API 설계부터 테스트, 배포까지 Spring Boot로 상품 서비스를 만드는 전 과정을 다룹니다. JPA 엔티티 설계, OpenAPI 문서화, Docker Compose 배포 전략을 초급 개발자도 쉽게 따라할 수 있도록 스토리텔링으로 풀어냅니다.

Docker로 컨테이너화 완벽 가이드

Spring Boot 애플리케이션을 Docker로 컨테이너화하는 방법을 초급 개발자도 쉽게 이해할 수 있도록 실무 중심으로 설명합니다. Dockerfile 작성부터 멀티스테이지 빌드, 이미지 최적화, Spring Boot의 Buildpacks까지 다룹니다.

이전7/7
다음