포스트

Docker 핵심 개념과 명령어 정리: 이미지, 컨테이너, Dockerfile

이미지와 컨테이너의 관계부터 run 명령의 핵심 옵션, Dockerfile의 레이어와 캐시 원리, 볼륨과 compose까지 Docker의 개념과 명령어를 정리한 스터디 노트입니다.

Docker 핵심 개념과 명령어 정리: 이미지, 컨테이너, Dockerfile

Docker의 핵심 개념과 실무에서 자주 쓰는 명령어를 정리한 스터디 노트입니다.

Docker가 해결하는 문제: “내 컴퓨터에선 됐는데”

같은 코드가 컴퓨터마다 다르게 동작하는 이유는 코드 바깥에 있습니다. OS 종류, 시스템 라이브러리(glibc, CUDA), 환경 변수, 설치된 도구들이 그것입니다. 코드는 같아도 이 “환경”이 다르면 결과가 달라집니다. 가상환경(venv 등)으로 Python 패키지 버전을 고정해도, 격리되는 것은 패키지까지이고 그 아래의 OS 레벨은 여전히 공유됩니다.

Docker는 격리의 범위를 한 단계 내립니다. 애플리케이션과 그것이 필요로 하는 OS 레벨 환경 전체를 하나의 패키지(이미지)로 묶고, 어디서 실행하든 같은 환경이 되게 만듭니다. 내 맥북에서 만든 이미지가 리눅스 서버에서도, 동료의 윈도우에서도 같은 모습으로 돕니다.

가상머신(VM)과의 차이도 여기서 갈립니다. VM은 OS 전체(커널 포함)를 통째로 띄우지만, 컨테이너는 호스트의 커널을 공유하고 그 위에 격리된 프로세스로 돕니다. 그래서 VM이 분 단위로 부팅하고 GB 단위 메모리를 먹는 동안, 컨테이너는 초 단위로 뜨고 프로세스 하나 수준의 비용만 듭니다. “가볍게 쓰고 버리는 격리 환경”이라는 Docker의 사용 방식이 여기서 나옵니다.

이미지와 컨테이너: 클래스와 인스턴스

Docker의 개념 축은 두 개뿐이고, 프로그래밍 비유가 정확히 통합니다.

  • 이미지(image): 환경의 설계도이자 스냅샷. 읽기 전용. 클래스에 해당.
  • 컨테이너(container): 이미지를 실제로 실행한 것. 인스턴스에 해당.

클래스 하나로 인스턴스를 여러 개 만들듯, 이미지 하나로 컨테이너를 여러 개 띄울 수 있습니다. 그리고 인스턴스를 지워도 클래스가 남듯, 컨테이너를 지워도 이미지는 남습니다.

여기서 Docker의 가장 중요한 성질이 나옵니다. 컨테이너는 일회용입니다. 컨테이너 안에서 파일을 만들거나 수정한 내용은 그 컨테이너가 삭제되면 함께 사라집니다. “지워도 아깝지 않은 것”이 컨테이너의 정체성이고, 남겨야 할 데이터는 뒤에서 다룰 볼륨으로 밖에 둡니다.

1
2
3
4
5
6
7
8
docker pull python:3.12-slim     # 레지스트리(Docker Hub)에서 이미지 받기
docker images                    # 로컬 이미지 목록
docker run -it python:3.12-slim  # 이미지로 컨테이너 실행 (-it: 터미널 연결)
docker ps                        # 실행 중인 컨테이너 목록
docker ps -a                     # 종료된 것 포함 전체
docker stop <이름|ID>            # 정지
docker rm <이름|ID>              # 삭제
docker rmi python:3.12-slim      # 이미지 삭제

이미지 이름의 python:3.12-slim에서 콜론 뒤가 태그(버전)입니다. 태그를 생략하면 latest가 붙는데, latest는 “최신”이라는 뜻일 뿐 특정 버전을 가리키지 않으므로 재현성이 필요한 곳에서는 항상 태그를 명시합니다.

docker run: 옵션 다섯 개가 실사용의 전부

run 명령은 옵션이 많아 보이지만, 실제로 매일 쓰는 것은 다섯 계열입니다.

1
2
3
4
5
6
docker run -d \                          # 백그라운드 실행 (detached)
  --name pg \                            # 이름 붙이기 (없으면 랜덤 이름)
  -p 5432:5432 \                         # 포트 연결: 호스트:컨테이너
  -v pgdata:/var/lib/postgresql/data \   # 볼륨: 데이터를 컨테이너 밖에
  -e POSTGRES_PASSWORD=dev \             # 환경 변수 주입
  postgres:16
옵션역할이해의 핵심
-d백그라운드 실행서버류는 -d, 일회성 작업은 --rm(종료 시 자동 삭제)
-p 호스트:컨테이너포트 연결컨테이너는 기본적으로 밀폐 상태. 구멍은 명시적으로 뚫는다
-v볼륨/마운트남길 데이터는 컨테이너 밖으로
-e환경 변수설정과 비밀값은 이미지에 굽지 말고 실행 시 주입
-it터미널 연결쉘로 들어가 직접 조작할 때

이 예시가 Docker의 대표적인 실용 가치이기도 합니다. PostgreSQL을 설치 없이 명령 한 줄로 띄우고, 다 쓰면 흔적 없이 지운다. DB, Redis, 메시지 큐 같은 인프라를 로컬에 직접 설치하던 시대의 번거로움이 이것으로 사라졌습니다.

떠 있는 컨테이너를 다루는 명령 두 개도 세트로 기억합니다.

1
2
docker logs -f pg          # 컨테이너의 출력(로그) 보기, -f는 실시간
docker exec -it pg bash    # 실행 중인 컨테이너 안에 쉘로 들어가기

exec로 들어가 보는 것은 디버깅의 기본기입니다. “컨테이너 안에서 세상이 어떻게 보이는가”를 직접 확인하면, 경로 문제나 환경 변수 문제가 대부분 눈에 보입니다.

Dockerfile: 환경을 코드로 적는다

지금까지는 남이 만든 이미지를 썼습니다. 내 프로젝트의 이미지는 Dockerfile이라는 텍스트 파일로 정의합니다. 환경 구축 절차를 코드로 적어서 버전 관리한다. 이것이 “Infrastructure as Code”의 가장 작은 단위입니다.

1
2
3
4
5
6
7
8
9
10
FROM python:3.12-slim              # 베이스 이미지에서 시작

WORKDIR /app                       # 이후 명령의 작업 디렉토리

COPY requirements.txt .            # 의존성 목록을 먼저 복사
RUN pip install -r requirements.txt

COPY . .                           # 나머지 소스 코드 복사

CMD ["python", "-m", "src.train"]  # 컨테이너 시작 시 실행할 명령
1
2
docker build -t ml-study:0.1 .     # 현재 폴더의 Dockerfile로 이미지 빌드
docker run --rm ml-study:0.1

레이어와 캐시: COPY를 두 번 나눠 쓰는 이유

위 Dockerfile에서 가장 중요한 대목은 COPY가 두 번으로 나뉜 것입니다. 이유는 Docker의 빌드 방식에 있습니다.

Dockerfile의 각 명령은 레이어라는 스냅샷을 하나씩 쌓고, 다시 빌드할 때 바뀌지 않은 명령까지는 캐시를 재사용합니다. 단, 한 레이어가 바뀌면 그 이후의 캐시는 전부 무효화됩니다.

  • COPY . .을 먼저 쓰면: 소스 코드 한 줄만 고쳐도 그 이후인 pip install이 매번 다시 돕니다. 코드 수정마다 수 분짜리 설치를 반복하는 것입니다.
  • requirements.txt만 먼저 복사해 설치하면: 의존성이 안 바뀌는 한 설치 레이어는 캐시로 즉시 통과하고, 코드 수정은 마지막 COPY만 다시 돕니다.

“자주 바뀌는 것일수록 아래(뒤)에”. 이것이 Dockerfile 작성의 제1원칙이고, 빌드가 느리다는 불평의 대부분이 이 순서 문제입니다.

CMD는 컨테이너 시작 시의 기본 명령이며 docker run ml-study:0.1 python -m src.evaluate처럼 실행 시점에 덮어쓸 수 있습니다. 참고로 RUN빌드할 때 실행되고 CMD컨테이너가 시작될 때 실행됩니다. 이름이 비슷해서 자주 헷갈리는 지점입니다.

.gitignore처럼 .dockerignore도 있습니다. 데이터 폴더나 .venv를 빌드에 끌고 들어가지 않도록 선언해 두면 빌드가 가볍고 빨라집니다.

볼륨: 데이터는 컨테이너 밖에

컨테이너가 일회용이라면 DB 데이터나 학습 결과는 어디에 두는가 하면, 컨테이너 밖의 볼륨입니다. 두 방식이 있습니다.

1
2
3
4
5
# 1) 네임드 볼륨: Docker가 관리하는 저장 공간. DB 데이터에 적합
docker run -v pgdata:/var/lib/postgresql/data postgres:16

# 2) 바인드 마운트: 호스트의 폴더를 그대로 연결. 개발 중 코드와 데이터에 적합
docker run -v $(pwd)/data:/app/data ml-study:0.1

바인드 마운트는 개발 중에 특히 유용합니다. 호스트에서 코드를 고치면 컨테이너 안에도 즉시 반영되므로, 수정할 때마다 이미지를 다시 빌드할 필요가 없습니다. 정리하면 이미지에는 코드와 의존성을, 볼륨에는 상태(데이터)를 담는 것입니다. 이 분리가 “컨테이너는 언제든 지우고 다시 만든다”를 가능하게 합니다.

docker compose: 여러 컨테이너를 파일 하나로

실제 프로젝트는 앱 + DB처럼 컨테이너 여러 개가 세트로 움직입니다. run 옵션을 매번 타이핑하는 대신 compose.yaml에 선언해 둡니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
services:
  app:
    build: .
    volumes:
      - ./data:/app/data
    depends_on: [db]
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: dev
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:
1
2
3
docker compose up -d      # 전부 띄우기
docker compose logs -f    # 모든 서비스 로그
docker compose down       # 전부 정리 (볼륨은 유지)

run 옵션으로 배운 것들(-p, -v, -e)이 yaml의 항목으로 그대로 옮겨간 것뿐이라 새로 배울 개념은 없습니다. 핵심 가치는 환경 실행 방법 자체가 저장소에 커밋되는 것입니다. 새 팀원이 저장소를 받아 docker compose up 한 줄로 전체 개발 환경을 얻습니다.

조용히 당하는 함정들

  • latest 태그의 배신: 어제의 latest와 오늘의 latest는 다른 이미지일 수 있습니다. 어느 날 갑자기 빌드가 깨졌다면 베이스 이미지가 바뀌었을 가능성부터 의심하고, 태그를 고정합니다.
  • 컨테이너 안에 데이터 저장: 볼륨 없이 DB 컨테이너를 쓰다가 docker rm 하는 순간 데이터가 통째로 사라집니다. 에러도 경고도 없습니다.
  • 캐시 때문에 반영이 안 됨: 바뀌었어야 할 것이 캐시로 통과해 옛 결과가 나오는 경우도 있습니다. docker build --no-cache로 전체 재빌드하면 확인됩니다.
  • 디스크 잠식: 이미지, 중단된 컨테이너, 안 쓰는 볼륨이 조용히 수십 GB를 먹습니다. docker system df로 확인하고 docker system prune으로 정리하는 습관이 필요합니다.
  • 아키텍처 불일치: 애플 실리콘 맥(ARM)에서 만든 이미지가 서버(x86)에서 안 도는 문제. docker build --platform linux/amd64로 대상 아키텍처를 명시해 해결합니다.

정리

개념한 줄 요약
격리 범위가상환경은 패키지까지, Docker는 OS 레벨 환경까지
이미지 / 컨테이너클래스 / 인스턴스. 컨테이너는 일회용
run 옵션-d, -p, -v, -e, -it 다섯이 실사용의 전부
Dockerfile환경을 코드로. 자주 바뀌는 것일수록 아래에 (캐시)
RUN vs CMD빌드 시 실행 vs 시작 시 실행
볼륨이미지에는 코드를, 볼륨에는 상태를
composerun 옵션의 yaml화. 환경 실행법이 저장소에 커밋된다

Docker의 약속은 한 문장으로 요약됩니다. 환경을 코드로 적고, 어디서든 같은 모습으로 실행한다. 이미지, 컨테이너, 볼륨의 역할 분담만 잡히면 나머지 명령은 그 위에서 자연스럽게 따라옵니다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.