목록으로 가기

Docker 컨테이너 개념부터 Compose까지 한 번에 정리

컨테이너가 뭔지부터 Dockerfile 작성, 네트워크, 볼륨, Compose로 여러 서비스 띄우기까지. Docker를 처음 접해도 따라갈 수 있게 정리했습니다.

Docker 컨테이너 개념부터 Compose까지 한 번에 정리 ko posts docker 컨테이너가 뭔지부터 Dockerfile 작성, 네트워크, 볼륨, Compose로 여러 서비스 띄우기까지. Docker를 처음 접해도 따라갈 수 있게 정리했습니다.

개요

Docker는 애플리케이션을 컨테이너라는 격리된 환경에서 실행할 수 있게 해주는 도구입니다. 이미지에 애플리케이션과 의존성을 함께 담아 실행 환경을 고정합니다.

이 글에서는 컨테이너 개념, Dockerfile, 네트워크, 볼륨, Compose를 정리합니다.

1. 컨테이너 vs 가상머신

정리 보기

가상머신 (VM)

┌─────────────┐  ┌─────────────┐
│    App A    │  │    App B    │
│   Libs/Bins │  │   Libs/Bins │
│  Guest OS   │  │  Guest OS   │
├─────────────┴──┴─────────────┤
│         Hypervisor           │
├──────────────────────────────┤
│          Host OS             │
└──────────────────────────────┘
  • 각 VM마다 완전한 게스트 OS가 올라감
  • 실행하려면 게스트 OS 부팅 과정이 필요함
  • 완전한 격리

컨테이너

┌─────────────┐  ┌─────────────┐
│    App A    │  │    App B    │
│   Libs/Bins │  │   Libs/Bins │
├─────────────┴──┴─────────────┤
│       Docker Engine          │
├──────────────────────────────┤
│          Host OS             │
└──────────────────────────────┘
  • 호스트 OS의 커널을 공유
  • 게스트 OS가 없어 별도 부팅 과정이 필요하지 않음
  • 프로세스 수준 격리 (namespace, cgroup)

비교

항목 가상머신 컨테이너
구성 게스트 OS 포함 게스트 OS 없음 (호스트 커널 공유)
시작 게스트 OS 부팅 필요 호스트 커널 공유로 부팅 불필요
격리 수준 완전 (OS 분리) 프로세스 수준
실행 경로 하이퍼바이저 계층을 거침 호스트 커널을 직접 사용
용도 다른 OS 필요할 때 앱 배포, 개발 환경 통일

컨테이너가 가상머신을 대체하는 건 아닙니다. 용도가 다릅니다.

2. Docker 핵심 개념

정리 보기

이미지 (Image)

  • 컨테이너를 만들기 위한 읽기 전용 템플릿
  • 여러 레이어로 구성 (각 레이어는 Dockerfile의 명령어 하나)
  • 같은 이미지를 여러 호스트에서 동일한 구성으로 실행 가능 (호스트 커널·아키텍처 호환 범위 내)

컨테이너 (Container)

  • 이미지를 기반으로 실행된 인스턴스
  • 읽기/쓰기 가능한 레이어가 추가됨
  • 삭제하면 내부 데이터도 사라짐 (볼륨 제외)

레지스트리 (Registry)

  • 이미지를 저장하고 공유하는 저장소
  • Docker Hub (공개), ECR (AWS), GCR (GCP), GHCR (GitHub)

흐름

Dockerfile → (빌드) → Image → (실행) → Container
                    Registry에 push
                    서버에서 pull → Container

3. 기본 명령어

정리 보기

이미지 관련

# 이미지 빌드 (-t: 태그 지정, .: 현재 디렉터리의 Dockerfile 사용)
docker build -t myapp:1.0 .

# 이미지 목록
docker images

# 이미지 다운로드
docker pull nginx:latest

# 이미지 삭제
docker rmi myapp:1.0

# 사용하지 않는 이미지 정리
docker image prune

컨테이너 관련

# 컨테이너 실행
# -d: 백그라운드, -p: 포트 매핑 (호스트:컨테이너), --name: 이름 지정
docker run -d -p 8080:80 --name web nginx

# 실행 중인 컨테이너 목록
docker ps

# 모든 컨테이너 (중지 포함)
docker ps -a

# 컨테이너 중지 / 시작 / 재시작
docker stop web
docker start web
docker restart web

# 컨테이너 삭제
docker rm web

# 실행 중인 컨테이너에 접속
docker exec -it web /bin/bash
# -i: interactive, -t: tty (터미널)

# 컨테이너 로그
docker logs web
docker logs -f web  # -f: 실시간 (follow)

정리 명령어

# 중지된 컨테이너, 사용 안 하는 이미지/네트워크 전부 정리 (볼륨 제외)
docker system prune -a

# 현재 디스크 사용량 확인
docker system df

4. Dockerfile 작성법

정리 보기

기본 구조

# 베이스 이미지 지정
FROM node:20-alpine

# 작업 디렉터리 설정
WORKDIR /app

# 의존성 파일 복사 (캐시 활용을 위해 먼저 복사)
COPY package.json package-lock.json ./

# 의존성 설치
RUN npm ci

# 소스 코드 복사
COPY . .

# 빌드
RUN npm run build

# 포트 선언 (문서화 용도)
EXPOSE 3000

# 실행 명령어
CMD ["node", "dist/server.js"]

EXPOSE는 포트를 발행(publish)하지 않습니다. 이미지를 만든 사람과 실행하는 사람 사이의 문서 역할이고, 실제로 포트를 열려면 실행할 때 -p(특정 포트 매핑)나 -P(EXPOSE된 포트를 호스트의 임의 포트에 매핑)를 써야 합니다. 프로토콜을 안 적으면 TCP가 기본값입니다.

명령어

명령어 용도
FROM 베이스 이미지 지정
WORKDIR 작업 디렉터리 설정
COPY 파일/디렉터리 복사
RUN 빌드 시 실행할 명령어
CMD 컨테이너 시작 시 실행할 명령어
ENV 환경 변수 설정
EXPOSE 포트 문서화
ARG 빌드 시 전달할 인자 (이미지에 남지 않음)
ENTRYPOINT 컨테이너의 기본 실행 파일 지정

CMD vs ENTRYPOINT

# CMD: 기본 명령어 (docker run에 명령을 주면 대체됨)
CMD ["npm", "start"]

# ENTRYPOINT: 기본 실행 파일
ENTRYPOINT ["node", "server.js"]

# 조합: ENTRYPOINT가 실행, CMD가 기본 인자
ENTRYPOINT ["node"]
CMD ["server.js"]

ENTRYPOINT는 무엇을 실행할지, CMD는 그 뒤에 붙는 기본 인자입니다.

exec form ENTRYPOINT가 있으면 docker run <image> 뒤에 붙인 인자가 ENTRYPOINT 뒤에 이어 붙고, CMD에 적어둔 값은 그 인자로 대체됩니다. ENTRYPOINTdocker run --entrypoint로 덮어쓸 수 있습니다.

5. 레이어 캐시와 Multi-stage Build

정리 보기

레이어 캐시

Dockerfile의 각 명령어는 하나의 레이어를 만듭니다. 변경되지 않은 레이어는 캐시에서 재사용됩니다.

# 의존성 파일을 먼저 복사한 경우
COPY package.json package-lock.json ./  ← 자주 안 바뀜 (캐시 활용)
RUN npm ci
COPY . .                                ← 자주 바뀜 (여기부터 다시 빌드)
# 소스를 먼저 복사한 경우: 소스가 바뀌면 npm ci 레이어도 다시 실행됨
COPY . .
RUN npm ci

캐시 원리: 위에서부터 변경이 감지되면 그 아래 모든 레이어가 다시 빌드됩니다.

비교 방식은 명령어 종류에 따라 다릅니다. ADDCOPY(그리고 RUN --mount=type=bind)는 파일 메타데이터로 캐시 체크섬을 계산해서 파일이 바뀌었는지 봅니다. 그 외 RUN은 컨테이너 안의 파일을 들여다보지 않고 명령어 문자열만 비교합니다. 그래서 RUN apt-get update는 시간이 지나도 캐시가 자동으로 무효화되지 않고, 강제로 다시 실행하려면 앞 레이어를 바꾸거나 --no-cache 계열 옵션을 써야 합니다.

Multi-stage Build

빌드 환경과 실행 환경을 분리해서 최종 이미지 크기를 줄입니다.

# 1단계: 빌드
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

# 2단계: 실행 (빌드 도구 없이 결과물만)
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/server.js"]

효과

최종 이미지에 빌드 도구와 개발 의존성이 포함되지 않아 이미지 크기가 줄어듭니다. 실제 크기는 베이스 이미지와 애플리케이션에 따라 달라집니다.

이미지가 작아지면 전송할 데이터가 줄고, 포함되는 패키지가 적어져 점검 대상이 줄어듭니다.

6. 볼륨과 데이터 관리

정리 보기

왜 필요한가

컨테이너는 삭제하면 내부 데이터도 사라집니다. DB 데이터, 로그, 설정 파일처럼 유지돼야 하는 데이터는 볼륨으로 분리합니다.

볼륨 종류

# Named Volume (Docker가 생성·관리하는 저장 위치 사용)
docker run -d -v mydata:/var/lib/mysql mysql

# Bind Mount (호스트 경로 직접 지정)
docker run -d -v /host/path:/container/path nginx

# 읽기 전용 (:ro)
docker run -d -v "$(pwd)"/config:/app/config:ro nginx

-v의 세 번째 필드가 옵션 자리이고, 여기에 ro(= readonly)를 주면 컨테이너가 해당 마운트에 쓸 수 없습니다. bind mount에서 호스트 경로는 $(pwd)처럼 절대 경로로 넘깁니다. 상대 경로를 그대로 쓰려면 절대·상대 경로를 모두 받는 --mount type=bind,src=...,dst=...를 씁니다.

차이

종류 관리 용도
Named Volume Docker가 관리 DB 데이터, 영구 저장
Bind Mount 호스트 경로 직접 개발 중 소스 코드 마운트

bind mount는 기본적으로 호스트 파일에 쓰기 권한을 가집니다. 컨테이너 안 프로세스가 호스트 파일을 지우거나 바꿀 수 있다는 뜻입니다. 읽기 전용으로 마운트하면 컨테이너에서 해당 경로에 쓰기가 차단됩니다.

볼륨 명령어

# 볼륨 목록
docker volume ls

# 볼륨 생성
docker volume create mydata

# 볼륨 삭제
docker volume rm mydata

# 컨테이너가 참조하지 않는 볼륨 정리 (기본값: 익명 볼륨만)
docker volume prune

# 이름 붙인 볼륨까지 정리
docker volume prune -a

docker volume prune은 기본값으로 익명 볼륨만 지웁니다. 이름을 붙인 볼륨까지 지우려면 -a(--all)가 필요합니다.

7. Docker 네트워크

정리 보기

기본 네트워크 종류

네트워크 설명
bridge 기본값. 같은 bridge 안의 컨테이너끼리 통신 가능
host 호스트 네트워크 그대로 사용 (포트 매핑 불필요)
none 네트워크 없음

커스텀 네트워크

# 네트워크 생성
docker network create app-net

# 네트워크에 연결하면서 실행
docker run -d --name api --network app-net myapi
docker run -d --name db --network app-net mysql

# 같은 네트워크 안에서는 컨테이너 이름으로 통신 가능
# api 컨테이너에서: mysql -h db -u root

왜 커스텀 네트워크를 쓰는가

같은 커스텀 네트워크에 넣으면 Docker가 내장 DNS를 제공합니다. 컨테이너 이름이 곧 호스트명이 됩니다.

api 컨테이너 안에서:
  mysql -h db -u root
    "db"라는 컨테이너 이름이 자동으로 해당 컨테이너의 IP로 해석됨
항목 기본 bridge 커스텀(사용자 정의) bridge
이름으로 통신 안 됨 (IP로만 가능, 레거시 --link 제외) 가능 (컨테이너 이름 또는 alias)
격리 --network를 안 주면 전부 같은 bridge에 붙음 그 네트워크에 붙은 컨테이너끼리만 통신
컨테이너 이름 DNS 해석 미제공 자동 제공

기본 bridge에서는 mysql -h db가 안 됩니다. 기본 bridge의 컨테이너는 레거시 --link를 쓰지 않으면 IP로만 서로 접근할 수 있습니다. 그런데 컨테이너 IP는 시작할 때마다 서브넷에서 새로 할당되고 재시작·재생성 사이에 유지되지 않습니다. 사용자 정의 네트워크에서는 컨테이너 이름이 DNS로 해석되므로 IP가 바뀌어도 같은 이름으로 접근합니다.

docker-compose에서 서비스끼리 이름으로 통신할 수 있는 이유도 이 원리입니다. Compose는 docker compose up을 하면 <프로젝트명>_default라는 bridge 네트워크를 만들고 모든 서비스를 여기에 붙입니다. 각 서비스는 자기 이름을 내부 DNS에 등록합니다.

네트워크 명령어

# 네트워크 목록
docker network ls

# 네트워크 상세 정보
docker network inspect app-net

# 실행 중인 컨테이너를 네트워크에 연결
docker network connect app-net web

# 네트워크 삭제
docker network rm app-net

8. Docker Compose

정리 보기

왜 쓰는가

여러 컨테이너를 한 번에 정의하고 실행할 때 사용합니다. 앱 서버 + DB + Redis 같은 조합을 한 파일로 관리합니다.

docker-compose.yml 예시

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DB_HOST=db
      - REDIS_HOST=redis
    depends_on:
      - db
      - redis

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: admin
      POSTGRES_PASSWORD: secret
    volumes:
      - db-data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

volumes:
  db-data:

명령어

# 전체 실행 (-d: 백그라운드)
docker compose up -d

# 전체 중지 + 삭제
docker compose down

# 로그 확인
docker compose logs -f app

# 특정 서비스만 재빌드
docker compose up -d --build app

# 상태 확인
docker compose ps

depends_on 동작

  • depends_on은 시작 순서만 보장하고 서비스가 준비됐는지는 보장하지 않습니다
  • 준비 상태까지 기다리려면 healthcheck와 depends_on.condition: service_healthy를 함께 씁니다

9. .dockerignore

정리 보기

이미지 빌드 시 불필요한 파일을 제외합니다. .gitignore와 같은 원리입니다.

# .dockerignore
node_modules
dist
.git
.env
*.log
.DS_Store
docker-compose.yml
README.md

제외했을 때의 동작

  • COPY . . 할 때 불필요한 파일이 이미지에 들어가는 것 방지
  • 호스트에서 설치한 node_modules가 복사되면 네이티브 모듈이 컨테이너 환경과 맞지 않을 수 있음
  • 빌드 속도 향상 (빌드 컨텍스트가 작아짐)
  • 민감한 파일 (.env, 키 파일) 이미지에 포함되는 것 방지

10. 디버깅과 문제 해결

정리 보기

컨테이너가 바로 죽을 때

# 컨테이너 표준 출력·표준 에러 확인
docker logs <container-name>

# 종료 코드 확인
docker inspect <container-name> --format='{{.State.ExitCode}}'
# 0: 정상 종료

# OOM Kill로 죽었는지 확인
docker inspect <container-name> --format='{{.State.OOMKilled}}'

# 이미지로 셸 띄우기 (ENTRYPOINT를 셸로 바꿔서 디버깅)
docker run -it --entrypoint /bin/sh myapp:1.0

종료 코드가 128보다 크면 신호로 죽은 경우입니다. 셸은 신호 N으로 종료된 명령에 128 + N을 종료 상태로 사용하기 때문에, 137은 SIGKILL(9), 143은 SIGTERM(15)에 대응합니다. 137은 OOM으로만 발생하지 않습니다. docker stop의 타임아웃이 지나 SIGKILL이 나간 경우도 같은 값이 됩니다. OOM 여부는 docker inspectState.OOMKilled 필드에 기록됩니다.

이미지 빌드 실패 시

# 빌드 로그 자세히 보기
docker build --no-cache --progress=plain -t myapp .

# 특정 스테이지까지만 빌드
docker build --target builder -t myapp-debug .

네트워크 문제

# 컨테이너 내부에서 네트워크 확인
docker exec -it web sh -c "curl http://api:3000/health"

# 컨테이너의 IP 확인
docker inspect web --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'

# DNS 확인
docker exec -it web nslookup db

디스크 공간 문제

# Docker 디스크 사용량
docker system df

# 정리 (중지된 컨테이너, 사용 안 하는 이미지/네트워크 + 익명 볼륨)
docker system prune -a --volumes

docker system prune은 기본적으로 볼륨을 지우지 않습니다. 볼륨을 쓰는 컨테이너가 없다는 이유로 중요한 데이터가 삭제되는 걸 막기 위한 동작이고, --volumes를 줘야 정리 대상에 포함됩니다. 이때도 대상은 익명 볼륨입니다.

관련 포스트

Docker 보안, 리소스 제한, BuildKit — 운영할 때 알아야 할 것들 컨테이너 보안 설정부터 PID 1 문제, 메모리/CPU 제한, BuildKit 캐시 최적화까지. Docker …