초보자를 위한 Docker: 컨테이너를 쉽게 이해하기
· 12분 읽기
목차
Docker란 무엇인가?
Docker는 애플리케이션과 그 종속성을 가볍고 이식 가능한 컨테이너로 패키징할 수 있게 해주는 플랫폼입니다. 컨테이너를 앱이 실행하는 데 필요한 모든 것(코드, 런타임, 라이브러리, 시스템 도구)을 담고 있는 작고 독립적인 상자라고 생각하세요. 더 이상 "내 컴퓨터에서는 작동하는데" 문제가 없습니다.
Docker 이전에는 소프트웨어를 배포하는 것이 수동으로 종속성을 설치하고, 서버를 구성하고, 충돌이 없기를 바라는 것을 의미했습니다. Python 버전, Node.js 패키지, 데이터베이스 드라이버, 시스템 라이브러리를 설정하는 데 몇 시간을 보냈습니다. 그런 다음 스테이징과 프로덕션 서버에서 모든 것을 다시 해야 했고, 구성이 일치하기를 기도했습니다.
Docker는 앱이 어디서나 동일하게 실행되도록 보장함으로써 이러한 혼란을 제거합니다 — 노트북, 팀원의 컴퓨터, 스테이징 서버 또는 프로덕션. 컨테이너가 애플리케이션 코드만이 아닌 배포의 단위가 됩니다.
Docker는 현대 소프트웨어 개발의 표준 도구가 되었습니다. 마이크로서비스를 구축하든, CI/CD 파이프라인을 설정하든, 아니면 단지 일관된 개발 환경을 원하든, Docker는 최소한의 오버헤드로 이를 가능하게 합니다. Netflix, Spotify, PayPal과 같은 회사들은 매일 수백만 개의 컨테이너를 프로덕션에서 실행합니다.
빠른 팁: Docker는 프로덕션 배포만을 위한 것이 아닙니다. 많은 개발자들이 로컬 머신을 다양한 언어 버전, 데이터베이스, 도구로 어지럽히지 않기 위해 사용합니다. 한 프로젝트에는 PostgreSQL이 필요하고 다른 프로젝트에는 MySQL이 필요한가요? 충돌 없이 둘 다 컨테이너에서 실행하세요.
컨테이너 vs 가상 머신
컨테이너와 가상 머신은 모두 격리를 제공하지만, 내부적으로는 매우 다르게 작동합니다. 이 차이를 이해하는 것은 컨테이너가 왜 그렇게 인기를 얻게 되었는지 이해하는 데 중요합니다.
가상 머신은 하이퍼바이저 위에 자체 커널을 가진 완전한 운영 체제를 실행합니다. 각 VM은 자체 OS 설치가 필요하며, 기가바이트의 디스크 공간과 상당한 메모리를 소비합니다. 부팅 시간은 분 단위로 측정됩니다. 세 개의 VM을 실행하면 세 개의 완전한 운영 체제를 동시에 실행하는 것입니다.
컨테이너는 호스트 OS 커널을 공유하고 애플리케이션 계층만 패키징합니다. 크기는 메가바이트(기가바이트가 아님)이고, 초 단위로 시작하며(분이 아님), 단일 머신에서 수십 개를 무리 없이 실행할 수 있습니다.
# VM 방식: 각 앱이 완전한 OS를 가짐
앱 A → 게스트 OS → 하이퍼바이저 → 호스트 OS → 하드웨어
앱 B → 게스트 OS → 하이퍼바이저 → 호스트 OS → 하드웨어
# 컨테이너 방식: 앱들이 커널을 공유
앱 A → 컨테이너 런타임 → 호스트 OS → 하드웨어
앱 B → 컨테이너 런타임 → 호스트 OS → 하드웨어
이 경량 아키텍처는 하나의 모놀리식 애플리케이션 대신 수백 개의 작은 서비스를 실행할 수 있는 마이크로서비스에 컨테이너를 이상적으로 만듭니다. 리소스 효율성은 놀랍습니다 — 10개의 VM을 실행하는 서버가 100개의 컨테이너를 편안하게 실행할 수 있습니다.
| 기능 | 가상 머신 | 컨테이너 |
|---|---|---|
| 시작 시간 | 분 | 초 |
| 디스크 공간 | 기가바이트 (전체 OS) | 메가바이트 (앱 계층만) |
| 성능 | 네이티브에 가까움 | 네이티브 (하이퍼바이저 오버헤드 없음) |
| 격리 | 완전함 (별도 커널) | 프로세스 수준 (공유 커널) |
| 이식성 | 제한적 (하이퍼바이저 의존) | 높음 (Docker가 실행되는 곳이면 어디서나) |
| 리소스 사용 | 무거움 | 경량 |
그렇다고 VM이 구식이 된 것은 아닙니다. 각 VM이 자체 커널을 가지고 있기 때문에 더 강력한 격리를 제공합니다. 보안이 중요한 워크로드나 다른 운영 체제를 실행해야 할 때 VM이 여전히 더 나은 선택입니다. 많은 조직이 둘 다 사용합니다: 인프라 격리를 위한 VM과 애플리케이션 배포를 위한 컨테이너.
Docker 핵심 개념
명령어와 Dockerfile을 다루기 전에 이해해야 할 몇 가지 핵심 개념이 있습니다. 이러한 구성 요소는 Docker가 작동하는 방식의 기초를 형성합니다.
이미지
Docker 이미지는 애플리케이션 코드, 런타임, 라이브러리 및 종속성을 포함하는 읽기 전용 템플릿입니다. 스냅샷이나 청사진으로 생각하세요. 이미지는 Dockerfile의 지침으로부터 빌드되고 Docker Hub와 같은 레지스트리에 저장됩니다.
이미지는 레이어로 구성됩니다. Dockerfile의 각 지침은 새로운 레이어를 생성합니다. Docker는 이러한 레이어를 캐시하므로, 이미지를 다시 빌드하고 마지막 레이어만 변경된 경우 Docker는 캐시된 레이어를 재사용합니다. 이것이 빌드를 믿을 수 없을 만큼 빠르게 만듭니다.
컨테이너
컨테이너는 이미지의 실행 중인 인스턴스입니다. 동일한 이미지에서 여러 컨테이너를 생성할 수 있으며, 각각은 격리되어 실행됩니다. 컨테이너를 중지하면 내부에서 수행된 모든 변경 사항은 명시적으로 저장하거나 볼륨을 사용하지 않는 한 손실됩니다.
컨테이너는 설계상 임시적입니다. 이 불변성은 버그가 아니라 기능입니다 — 일관성을 보장하고 확장을 간단하게 만듭니다.
Dockerfile
Dockerfile은 Docker 이미지를 빌드하기 위한 지침을 포함하는 텍스트 파일입니다. 기본 이미지를 지정하고, 코드를 복사하고, 종속성을 설치하고, 애플리케이션을 실행하는 방법을 정의합니다. Dockerfile에 대해서는 이후 섹션에서 더 자세히 다룰 것입니다.
Docker 레지스트리
레지스트리는 Docker 이미지를 위한 저장 및 배포 시스템입니다. Docker Hub는 수백만 개의 이미지를 호스팅하는 기본 공개 레지스트리입니다. 독점 애플리케이션을 위해 프라이빗 레지스트리를 실행할 수도 있습니다. docker pull nginx를 실행하면 Docker는 Docker Hub에서 nginx 이미지를 다운로드합니다.
볼륨
볼륨은 데이터를 유지하기 위한 Docker의 메커니즘입니다. 컨테이너는 임시적이므로 컨테이너 내부에 작성된 모든 데이터는 중지되면 사라집니다. 볼륨을 사용하면 컨테이너 파일 시스템 외부에 데이터를 저장하여 컨테이너 재시작 및 삭제에도 유지됩니다.
프로 팁: 모든 플래그와 옵션을 외우지 않고도 복잡한 Docker 명령을 빠르게 생성하려면 Docker 명령 생성기를 사용하세요.
필수 Docker 명령어
매일 사용하게 될 Docker 명령어를 살펴보겠습니다. 이것들은 이미지 가져오기부터 리소스 정리까지 전체 컨테이너 수명 주기를 다룹니다.
이미지 작업
# Docker Hub에서 이미지 가져오기
docker pull ubuntu:22.04
# 모든 로컬 이미지 나열
docker images
# 이미지 제거
docker rmi ubuntu:22.04
# Dockerfile에서 이미지 빌드
docker build -t myapp:1.0 .
# 레지스트리에 푸시하기 위해 이미지 태그 지정
docker tag myapp:1.0 username/myapp:1.0
# 레지스트리에 이미지 푸시
docker push username/myapp:1.0
컨테이너 실행
# 포그라운드에서 컨테이너 실행
docker run ubuntu:22.04 echo "Hello Docker"
# 분리 모드(백그라운드)에서 컨테이너 실행
docker run -d nginx
# 사용자 지정 이름으로 실행
docker run -d --name my-nginx nginx
# 포트 매핑으로 실행 (호스트:컨테이너)
docker run -d -p 8080:80 nginx
# 환경 변수와 함께 실행
docker run -d -e POSTGRES_PASSWORD=secret postgres
# 볼륨 마운트와 함께 실행
docker run -d -v /host/path:/container/path nginx
# 셸과 함께 대화형으로 실행
docker run -it ubuntu:22.04 /bin/bash
컨테이너 관리
# 실행 중인 컨테이너 나열
docker ps
# 모든 컨테이너 나열 (중지된 것 포함)
docker ps -a
# 컨테이너 중지
docker stop my-nginx
# 중지된 컨테이너 시작
docker start my-nginx
# 컨테이너 재시작
docker restart my-nginx
# 컨테이너 제거
docker rm my-nginx
# 실행 중인 컨테이너 제거 (강제)
docker rm -f my-nginx
# 컨테이너 로그 보기
docker logs my-nginx
# 실시간으로 로그 팔로우
docker logs -f my-nginx
# 실행 중인 컨테이너에서 명령 실행
docker exec my-nginx ls /usr/share/nginx/html
# 실행 중인 컨테이너에서 셸 열기
docker exec -it my-nginx /bin/bash
# 컨테이너 리소스 사용량 보기
docker stats
# 컨테이너 세부 정보 검사
docker inspect my-nginx
정리 명령어
# 중지된 모든 컨테이너 제거
docker container prune
# 사용하지 않는 이미지 제거
docker image prune
# 사용하지 않는 볼륨 제거
docker volume prune
# 사용하지 않는 모든 것 제거 (컨테이너, 이미지, 네트워크, 볼륨)
docker system prune -a
-it 플래그는 특별히 언급할 가치가 있습니다. -i는 STDIN을 열어 둡니다(대화형), -t는 의사 TTY(터미널)를 할당합니다. 함께 사용하면 일반 셸 세션처럼 컨테이너와 상호 작용할 수 있습니다.
빠른 팁: 스크립팅에 유용한 컨테이너 ID만 가져오려면 docker ps -q를 사용하세요. 예를 들어, docker stop $(docker ps -q)는 실행 중인 모든 컨테이너를 중지합니다.
Dockerfile 작성하기
Dockerfile은 마법이 일어나는 곳입니다. Docker 이미지를 빌드하기 위한 레시피로, 컨테이너에 무엇이 들어가는지 정확히 지정합니다. 가장 중요한 지침과 모범 사례를 살펴보겠습니다.
기본 Dockerfile 구조
# 기본 이미지에서 시작
FROM node:18-alpine
# 작업 디렉토리 설정
WORKDIR /app
# 패키지 파일 복사
COPY package*.json ./
# 종속성 설치
RUN npm install
# 애플리케이션 코드 복사
COPY . .
# 앱이 실행되는 포트 노출
EXPOSE 3000
# 앱을 실행할 명령 정의
CMD ["node", "server.js"]
주요 Dockerfile 지침
FROM은 기본 이미지를 지정합니다. 재현 가능한 빌드를 보장하기 위해 latest 대신 항상 특정 버전 태그(예: node:18-alpine)를 사용하세요. Alpine 변형은 더 작고 안전합니다.
WORKDIR은 후속 지침의 작업 디렉토리를 설정합니다. 존재하지 않으면 디렉토리를 생성합니다. 이것은 RUN cd /app을 사용하는 것보다 깔끔합니다.
COPY는 호스트 머신에서 이미지로 파일을 복사합니다. 구문은 COPY 소스 대상입니다. 모든 것을 복사하려면 COPY . .를 사용하되, 포함하는 내용에 주의하세요(.dockerignore 사용).
RUN은 빌드 프로세스 중에 명령을 실행합니다. 각 RUN 지침은 새로운 레이어를 생성합니다. 레이어를 줄이기 위해 &&로 명령을 연결하세요:
# 나쁨: 3개의 레이어 생성
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get clean
# 좋음: 1개의 레이어 생성
RUN apt-get update && \
apt-get install -y curl && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
EXPOSE는 애플리케이션이 수신하는 포트를 문서화합니다. 실제로 포트를 게시하지는 않습니다 — 그것은 docker run -p로 발생합니다.
CMD는 컨테이너가 시작될 때 실행할 기본 명령을 지정합니다. 셸 처리 문제를 피하기 위해 JSON 배열 형식(["실행파일", "매개변수1"])을 사용하세요.
ENTRYPOINT는 CMD와 유사하지만 재정의하기 더 어렵습니다. 컨테이너가 실행 파일처럼 동작하기를 원할 때 사용하세요. 유연한 기본값을 위해 ENTRYPOINT와 CMD를 결합할 수 있습니다.
멀티 스테이지 빌드
멀티 스테이지 빌드를 사용하면 하나의 Dockerfile에서 여러 FROM 문을 사용할 수 있습니다. 이것은 빌드 도구를 별도로 유지하면서 작은 프로덕션 이미지를 만드는 데 매우 강력합니다.
# 빌드 스테이지
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# 프로덕션 스테이지
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
EXPOSE 3000
CMD ["node", "dist/server.js"]
프로덕션 이미지는 빌드 도구가 아닌 컴파일된 코드와 런타임 종속성만 포함합니다. 이것은 이미지 크기를 70% 이상 줄일 수 있습니다.
.dockerignore 파일
빌드 컨텍스트에서 파일을 제외하려면 .dockerignore 파일을 만드세요. 이것은 빌드 속도를 높이고 이미지 크기를 줄입니다.
node_modules
npm-debug.log
.git
.env
*.md
.DS_Store
coverage
.vscode