개발자를 위한 Docker: 컨테이너, 이미지, 그리고 Compose
· 12분 읽기
목차
Docker 이해하기: 왜 중요한가
Docker는 개발자들이 애플리케이션을 빌드하고, 배포하고, 실행하는 방식을 근본적으로 변화시켰습니다. Docker 이전에는 개발 환경을 설정하는 것이 의존성 충돌, 버전 불일치, 그리고 악명 높은 "내 컴퓨터에서는 작동하는데" 문제의 악몽이었습니다.
Docker는 애플리케이션이 실행하는 데 필요한 모든 것—코드, 런타임, 시스템 도구, 라이브러리, 설정—을 컨테이너라는 표준화된 단위로 패키징하여 이 문제를 해결합니다. 이 컨테이너는 여러분의 노트북, 동료의 컴퓨터, 그리고 프로덕션 서버에서 동일하게 실행됩니다.
이점은 즉각적이고 실질적입니다:
- 일관성: 개발, 스테이징, 프로덕션 환경이 동일합니다
- 격리: 각 애플리케이션은 충돌 없이 자체 컨테이너에서 실행됩니다
- 이식성: 컨테이너는 Docker가 설치된 곳이면 어디서나 실행됩니다
- 효율성: 컨테이너는 호스트 OS 커널을 공유하여 가상 머신보다 훨씬 적은 리소스를 사용합니다
- 속도: 컨테이너는 몇 분이 아닌 몇 초 만에 시작됩니다
개발자에게 Docker는 단일 명령으로 완전한 애플리케이션 스택—웹 서버, 데이터베이스, 캐시, 메시지 큐—을 구동할 수 있다는 것을 의미합니다. 더 이상 PostgreSQL을 설치하거나 Redis 구성 문제를 디버깅하는 데 몇 시간을 소비할 필요가 없습니다.
핵심 개념 설명
실제 사용에 뛰어들기 전에 Docker의 핵심 개념을 이해하는 것이 필수적입니다. 이러한 구성 요소들이 함께 작동하여 Docker 생태계를 만듭니다.
| 개념 | 정의 | 비유 | 주요 특성 |
|---|---|---|---|
| 이미지 | 앱 + 의존성이 포함된 읽기 전용 템플릿 | OOP의 클래스 정의 | 불변, 계층화, 공유 가능 |
| 컨테이너 | 이미지의 실행 중인 인스턴스 | 객체 (클래스의 인스턴스) | 격리됨, 임시적, 무상태 |
| Dockerfile | 이미지를 빌드하기 위한 지침 | 레시피 또는 청사진 | 텍스트 파일, 버전 관리됨 |
| 볼륨 | 컨테이너 외부의 영구 스토리지 | 외장 하드 드라이브 | 컨테이너 삭제 후에도 유지됨 |
| 네트워크 | 컨테이너 간 통신 | 근거리 통신망 (LAN) | 격리됨, 구성 가능, 안전함 |
| 레지스트리 | 이미지 저장소 (Docker Hub) | 컨테이너용 npm/PyPI | 공개 또는 비공개, 버전 관리됨 |
이미지 vs 컨테이너: 중요한 구분
이것은 많은 초보자들이 혼란스러워하는 부분입니다. 이미지는 정적 스냅샷입니다—동결된 템플릿이라고 생각하세요. 컨테이너는 그 이미지를 실행할 때 얻는 것입니다—살아있고 실행 중인 프로세스입니다.
하나의 클래스에서 여러 객체를 생성할 수 있는 것처럼, 단일 이미지에서 무제한의 컨테이너를 생성할 수 있습니다. 각 컨테이너는 모두 동일한 이미지를 기반으로 하더라도 서로 격리되어 있습니다.
프로 팁: 이미지는 레이어로 빌드됩니다. Dockerfile의 각 명령어는 새로운 레이어를 생성합니다. Docker는 이러한 레이어를 캐시하므로 이미지를 재빌드할 때 변경된 레이어만 재빌드합니다. 이것이 빌드를 믿을 수 없을 만큼 빠르게 만듭니다.
볼륨: 영속성 문제 해결
컨테이너는 설계상 임시적입니다—컨테이너를 삭제하면 그 안의 모든 것이 사라집니다. 이것은 무상태 애플리케이션에는 좋지만 데이터베이스나 보관해야 할 데이터에는 문제가 됩니다.
볼륨은 컨테이너의 파일 시스템 외부에 데이터를 저장하여 이 문제를 해결합니다. 컨테이너가 삭제되고 재생성되어도 데이터는 유지됩니다. 세 가지 유형의 마운트가 있습니다:
- 볼륨: Docker가 관리하며 Docker의 스토리지 영역에 저장됨 (권장)
- 바인드 마운트: 호스트 디렉토리를 컨테이너에 직접 매핑 (개발에 유용)
- tmpfs 마운트: 호스트 메모리에만 저장되며 디스크에 기록되지 않음 (민감한 데이터용)
Dockerfile 구조와 해부
Dockerfile은 Docker 이미지를 빌드하기 위한 지침이 포함된 텍스트 문서입니다. 각 명령어는 이미지에 레이어를 생성하며, Docker는 효율성을 위해 이러한 레이어를 캐시합니다.
가장 일반적인 Dockerfile 명령어에 대한 분석입니다:
| 명령어 | 목적 | 예시 | 모범 사례 |
|---|---|---|---|
FROM |
빌드할 기본 이미지 | FROM node:22-alpine |
특정 버전 사용, alpine 선호 |
WORKDIR |
작업 디렉토리 설정 | WORKDIR /app |
절대 경로 사용 |
COPY |
호스트에서 이미지로 파일 복사 | COPY package.json . |
캐싱을 위해 의존성을 먼저 복사 |
RUN |
빌드 중 명령어 실행 | RUN npm install |
레이어를 줄이기 위해 &&로 명령어 연결 |
EXPOSE |
앱이 수신하는 포트 문서화 | EXPOSE 3000 |
문서화 전용, 포트를 게시하지 않음 |
ENV |
환경 변수 설정 | ENV NODE_ENV=production |
구성에 사용 |
USER |
후속 명령어의 사용자 설정 | USER node |
프로덕션에서 절대 root로 실행하지 않음 |
CMD |
컨테이너 시작 시 기본 명령어 | CMD ["node", "server.js"] |
JSON 배열 형식 사용 |
레이어 캐싱 이해하기
Docker는 위에서 아래로 레이어별로 이미지를 빌드합니다. 각 명령어는 새로운 레이어를 생성합니다. 레이어가 변경되지 않았다면 Docker는 재빌드하는 대신 캐시된 버전을 재사용합니다.
이것이 바로 거의 변경되지 않는 명령어를 상단에, 자주 변경되는 명령어를 하단에 배치하도록 Dockerfile을 구조화해야 하는 이유입니다. 예를 들어, 의존성은 소스 코드보다 덜 자주 변경되므로 소스 파일을 복사하기 전에 의존성을 설치하세요.
# 나쁨: 모든 것을 먼저 복사한 다음 설치
COPY . .
RUN npm install
# 좋음: 의존성을 먼저 설치하여 캐시 활용
COPY package*.json ./
RUN npm install
COPY . .
Dockerfile 모범 사례
효율적인 Dockerfile을 작성하는 것은 예술입니다. 다음은 여러 모범 사례를 보여주는 프로덕션 준비 예제입니다:
# 멀티 스테이지 빌드: 빌더 스테이지
FROM node:22-alpine AS builder
WORKDIR /app
# 더 나은 캐싱을 위해 의존성 파일을 먼저 복사
COPY package*.json ./
RUN npm ci --production
# 소스 코드를 복사하고 빌드
COPY . .
RUN npm run build
# 멀티 스테이지 빌드: 프로덕션 스테이지
FROM node:22-alpine
WORKDIR /app
# 빌더에서 필요한 것만 복사
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
# 보안: 비root 사용자로 실행
USER node
# 포트 문서화 (실제로 게시하지는 않음)
EXPOSE 3000
# 컨테이너 오케스트레이션을 위한 헬스 체크
HEALTHCHECK --interval=30s --timeout=3s \
CMD node healthcheck.js || exit 1
# 애플리케이션 시작
CMD ["node", "dist/server.js"]
멀티 스테이지 빌드: 극적인 크기 감소
멀티 스테이지 빌드를 사용하면 하나의 Dockerfile에서 여러 FROM 문을 사용할 수 있습니다. 각 FROM은 새로운 스테이지를 시작하며, 이전 스테이지에서 아티팩트를 복사할 수 있습니다.
마법은 최종 스테이지만 이미지가 된다는 점에서 발생합니다. 빌드 도구, 컴파일러, 중간 파일은 이전 스테이지에 남아 있으며 프로덕션에 포함되지 않습니다. 이것은 이미지 크기를 10배 이상 줄일 수 있습니다.
빠른 팁: AS builder로 스테이지에 이름을 지정하면 나중에 COPY --from=builder로 참조할 수 있습니다. 이렇게 하면 Dockerfile이 더 읽기 쉽고 유지 관리하기 쉬워집니다.
Alpine 이미지: 작지만 강력함
Alpine Linux는 크기가 5MB에 불과한 최소한의 Linux 배포판입니다. 900MB 이상인 Ubuntu 기반 이미지와 비교해보세요. 대부분의 애플리케이션에서 Alpine은 필요한 모든 것을 제공합니다.
트레이드오프는 Alpine이 glibc 대신 musl libc를 사용한다는 것인데, 이는 때때로 미리 컴파일된 바이너리와의 호환성 문제를 일으킬 수 있습니다. 95%의 사용 사례에서 Alpine은 완벽하게 작동하며 이미지 크기, 다운로드 시간, 공격 표면을 극적으로 줄입니다.
.dockerignore 파일
.gitignore와 마찬가지로 .dockerignore 파일은 이미지를 빌드할 때 제외할 파일을 Docker에 알려줍니다. 이것은 빌드 속도를 높이고 이미지 크기를 줄입니다.
# .dockerignore 예제
node_modules
npm-debug.log
.git
.gitignore
README.md
.env
.env.local
dist
coverage
.vscode
.idea
*.log
.DS_Store
보안 모범 사례
보안은 처음부터 Dockerfile에 내장되어야 합니다:
- 절대 root로 실행하지 않기:
USER node를 사용하거나 전용 사용자 생성 - 특정 버전 고정:
node:latest가 아닌node:22.1.0-alpine사용 - 취약점 스캔:
docker scan myimage를 사용하여 알려진 CVE 확인 - 설치된 패키지 최소화: 소프트웨어가 적을수록 취약점도 적음
- 공식 기본 이미지 사용: 유지 관리되고 정기적으로 업데이트됨
- 이미지에 비밀 저장하지 않기: 환경 변수 또는 비밀 관리 도구 사용
최적화된 Dockerfile 생성에 도움이 필요하신가요? Dockerfile 생성기 도구를 사용해보세요.
필수 Docker 명령어
이러한 명령어를 마스터하면 일상적인 Docker 사용의 90%를 커버할 수 있습니다. 각 명령어에는 실용적인 예제와 일반적인 플래그가 포함되어 있습니다.
빌드 및 실행
# 현재 디렉토리의 Dockerfile에서 이미지 빌드
docker build -t myapp:1.0 .
# 빌드 인수와 함께 빌드
docker build --build-arg NODE_ENV=production -t myapp:1.0 .
# 캐시 없이 빌드 (강제 재빌드)
docker build --no-cache -t myapp:1.0 .
# 분리 모드로 컨테이너 실행
docker run -d -p 3000:3000 --name myapp myapp:1.0
# 환경 변수와 함께 실행
docker run -d -p 3000:3000 -e NODE_ENV=production --name myapp myapp:1.0
# 볼륨 마운트와 함께 실행
docker run -d -p 3000:3000 -v $(pwd)/data:/app/data --name myapp myapp:1.0
# 셸 액세스로 대화형 실행
docker run -it --rm myapp:1.0 sh
컨테이너 관리
# 실행 중인 컨테이너 나열
docker ps
# 모든 컨테이너 나열 (중지된 것 포함)
docker ps -a
# 컨테이너 로그 보기
docker logs myapp
# 실시간으로 로그 팔로우
docker logs -f myapp
# 로그의 마지막 100줄 보기
docker logs --tail 100 myapp
# 실행 중인 컨테이너에서 명령어 실행
docker exec -it myapp sh
# 일회성 명령어 실행
docker exec myapp npm test
# 컨테이너를 정상적으로 중지
docker stop myapp
# 컨테이너를 즉시 종료
docker kill myapp
# 중지된 컨테이너 제거
docker rm myapp
# 한 명령으로 중지 및 제거
docker stop myapp && docker rm myapp
# 모든 중지된 컨테이너 제거
docker container prune
이미지 작업
# 모든 이미지 나열
docker images
# 사용자 정의 형식으로 이미지 나열
docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"
# 이미지 제거
docker rmi myapp:1.0
# 사용하지 않는 모든 이미지 제거
docker image prune -a
# 이미지 태그 지정
docker tag myapp:1.0 myapp:latest
# 레지스트리에 푸시
docker push myregistry.com/myapp:1.0
# 레지스트리에서 풀
docker pull myregistry.com/myapp:1.0
# 이미지를 tar 파일로 저장
docker save myapp:1.0 > myapp.tar
# tar 파일에서 이미지 로드
docker load < myapp.tar
시스템 관리
# 디스크 사용량 보기
docker system df
# 사용하지 않는 모든 데이터 제거 (컨테이너, 네트워크, 이미지, 캐시)
docker system prune -a
# 실시간 컨테이너 통계 보기
docker stats
# 컨테이너 세부 정보 검사 (JSON 출력)
docker inspect myapp
# 컨테이너 프로세스 보기
docker top myapp
프로 팁: 테스트용 컨테이너를 실행할 때 --rm 플래그를 추가하세요. 이렇게 하면 컨테이너가 중지될 때 자동으로 제거되어 시스템을 깨끗하게 유지합니다: docker run --rm -it myapp:1.0 sh
멀티 컨테이너 앱을 위한 Docker Compose
Docker Compose는 멀티 컨테이너 애플리케이션을 정의하고 실행하기 위한 도구입니다. 여러 docker run 명령을 실행하는 대신 단일 YAML 파일에 모든 것을 정의합니다.
이것은 일반적으로 여러 서비스로 구성된 현대 애플리케이션에 필수적입니다: 웹 서버, 데이터베이스, 캐시, 메시지 큐 등.
완전한 Docker Compose 예제
# docker-compose.yml
version: '3.8'
services:
# W