개발자를 위한 Docker: 이미지, 컨테이너, Compose
프로젝트를 클론하고 설정 지침을 실행했는데 벽에 부딪혔나요? 시스템 라이브러리 누락, 시작되지 않는 데이터베이스, 또는 Node 버전 불일치 같은 문제 말이죠. Docker는 바로 이런 종류의 문제를 없애기 위해 존재합니다. 서비스를 직접 머신에 설치하는 대신, 한 번 설명해두고 어디서나 동일하게 동작하는 격리된 컨테이너에서 실행하면 됩니다.
이 가이드에서는 실제로 필요한 세 가지 개념인 이미지, 컨테이너, Compose를 설명합니다. 읽으면서 바로 실행할 수 있는 명령어와 함께 실용적으로 다룹니다.
이미지 vs 컨테이너: 멘탈 모델
이미지는 읽기 전용 템플릿입니다. 파일시스템 스냅샷과 기본 명령어, 노출된 포트 같은 메타데이터를 포함합니다. 컨테이너는 그 이미지의 실행 중(또는 중지된) 인스턴스로, 위에 얇은 쓰기 가능 레이어가 추가됩니다.
객체지향 프로그래밍에 익숙하다면 비유가 거의 맞습니다. 이미지는 클래스, 컨테이너는 객체입니다. 하나의 이미지에서 여러 컨테이너를 시작할 수 있고, 컨테이너 내부의 변경은 이미지를 수정하지 않습니다.
| 개념 | 설명 | 대표 명령어 |
|---|---|---|
| 이미지 | 불변 템플릿 | docker build, docker pull |
| 컨테이너 | 실행 중 인스턴스 | docker run, docker ps |
| 볼륨 | 영구 저장소 | docker volume create |
| Compose | 멀티 컨테이너 정의 | docker compose up |
Dockerfile로 첫 이미지 만들기
Dockerfile은 레시피입니다. 각 명령어는 레이어를 생성하고, Docker는 레이어를 캐시하여 변경 사항이 없을 때 빠르게 재빌드합니다. 다음은 작은 Node.js 예제입니다:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]여기서 두 가지 습관이 중요합니다. 첫째, 소스 코드보다 의존성 매니페스트를 먼저 복사하여 애플리케이션 코드만 변경될 때 npm ci 레이어가 캐시되도록 합니다. 둘째, 크기와 공격 표면을 줄이기 위해 alpine과 같은 슬림 베이스 이미지를 선호합니다.
빌드하고 실행하세요:
docker build -t myapp:1.0 .
docker run --rm -p 3000:3000 myapp:1.0-p 3000:3000 플래그는 호스트 포트 3000을 컨테이너 포트 3000에 매핑합니다. --rm은 컨테이너 종료 시 삭제하여 개발 중 머신을 깔끔하게 유지합니다.
일상적인 컨테이너 명령어
대부분의 일상 작업을 커버합니다:
docker ps— 실행 중인 컨테이너 목록;-a를 추가하면 중지된 것도 포함.docker logs -f <container>— 애플리케이션 출력 스트리밍.docker exec -it <container> sh— 실행 중인 컨테이너 내부에서 셸 열기.docker stop <container>— 정상 종료.docker images— 로컬 이미지와 크기 목록.docker system prune— 사용하지 않는 컨테이너, 네트워크, 댕글링 이미지 제거.
컨테이너가 즉시 종료되면 먼저 로그를 확인하세요. 가장 흔한 원인은 잘못된 CMD, 누락된 환경 변수, 또는 컨테이너 내부에서 0.0.0.0 대신 127.0.0.1에 바인딩하는 프로세스입니다.
Compose가 존재하는 이유
실제 애플리케이션은 보통 하나 이상의 프로세스가 필요합니다: API, 데이터베이스, 어쩌면 캐시까지. 긴 플래그 목록과 함께 세 개의 docker run 명령어를 실행하는 것은 오류가 발생하기 쉽습니다. Docker Compose를 사용하면 모든 서비스를 하나의 YAML 파일에 선언하고 함께 시작할 수 있습니다.
services:
api:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://app:secret@db:5432/app
depends_on:
- db
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: app
volumes:
- db_data:/var/lib/postgresql/data
volumes:
db_data:API가 localhost가 아닌 db에 연결된다는 점에 주목하세요. Compose는 각 서비스가 서비스 이름으로 접근 가능한 사설 네트워크를 생성합니다. 이것이 초보자에게 가장 흔한 혼란의 원인입니다.
docker compose up으로 모두 시작하거나, 백그라운드에서 실행하려면 docker compose up -d를 사용하세요. 코드 변경 후에는 docker compose up --build로 재빌드합니다. docker compose down으로 정리하고, 명명된 볼륨도 삭제하려면 -v를 추가하세요.
볼륨, 바인드 마운트, 라이브 리로드
컨테이너는 임시적입니다. 내부의 데이터는 제거 시 사라집니다. 두 가지 메커니즘이 이를 해결합니다:
- 명명된 볼륨 — Docker가 관리하며 데이터베이스 파일에 이상적입니다. 컨테이너 재시작 간에도 유지되며
volumes:섹션에 선언됩니다. - 바인드 마운트 — 호스트 디렉토리를 컨테이너에 매핑하며, 개발 중 소스 코드에 이상적입니다.
services:
api:
build: .
volumes:
- ./src:/app/src바인드 마운트를 사용하면 호스트에서 파일을 편집하면 컨테이너 내부에 반영됩니다. 파일 감시자(nodemon, uvicorn --reload, air)와 결합하면 이미지를 재빌드하지 않고도 라이브 리로드를 얻을 수 있습니다.
실용적인 개발 워크플로
- 앱을 위한 Dockerfile을 작성하고 최소한으로 유지하세요.
- 앱과 의존성을 포함하는
docker-compose.yml을 추가하세요. - 소스 코드에는 바인드 마운트를, 데이터에는 명명된 볼륨을 사용하세요.
- 비밀 정보는 파일에 넣지 말고
.env파일을 사용하고${VAR}로 변수를 참조하세요. node_modules,.git, 빌드 출력을 포함하는.dockerignore를 추가하여 빌드 속도를 높이세요.docker compose up --build를 실행하고 반복하세요.
나중에 검사해야 할 로그를 서비스가 생성한다면, 컨테이너 로그는 그저 텍스트임을 기억하세요. nginx 로그 분석기와 같이 서버 로그를 파싱하는 도구는 호스트에 아무것도 설치하지 않고 로그를 파이프하거나 복사할 수 있기 때문에 컨테이너와 잘 어울립니다.
흔한 함정
- 뚱뚱한 이미지: 멀티 스테이지 빌드를 사용하여 한 단계에서 컴파일하고 슬림 런타임 이미지에 아티팩트만 복사하세요.
- 모든 것 재빌드: Dockerfile 명령어를 변경 빈도가 낮은 것부터 높은 것 순으로 정렬하세요.
- 하드코딩된 호스트: Compose 내부에서는 서비스 이름을, 외부에서는 환경 변수를 사용하세요.
- 정리 무시: 댕글링 이미지와 중지된 컨테이너가 쌓입니다. 주기적으로
docker system prune을 실행하세요. - 루트로 실행: 프로덕션 이미지에 비루트
USER를 추가하세요.
FAQ
이미지와 컨테이너의 차이는 무엇인가요?
이미지는 파일시스템과 메타데이터를 포함하는 불변 템플릿입니다. 컨테이너는 쓰기 가능 레이어가 추가된 그 이미지의 실행 중 인스턴스입니다. 하나의 이미지에서 여러 컨테이너를 시작할 수 있습니다.
단일 서비스에 Docker Compose가 필요한가요?
아니요. 하나의 컨테이너에는 docker run 명령어 하나로 충분합니다. Compose는 여러 서비스, 공유 네트워크, 또는 한 곳에 선언하고 싶은 환경 변수가 있을 때 가치가 있습니다.
컨테이너끼리 어떻게 통신하나요?
Compose는 모든 서비스를 각 서비스가 서비스 이름으로 접근 가능한 공유 네트워크에 배치합니다. localhost 대신 그 이름을 호스트명으로 사용하세요. 예를 들어 db:5432처럼요.
Docker는 작은 초기 투자에 보답합니다. 이미지, 컨테이너, Compose가 이해되면, 팀원 온보딩이나 머신 전환이 더 이상 설정 의식이 아니라 단일 명령어가 됩니다.