Docker 핵심 개념과 명령어 정리: 이미지, 컨테이너, Dockerfile
이미지와 컨테이너의 관계부터 run 명령의 핵심 옵션, Dockerfile의 레이어와 캐시 원리, 볼륨과 compose까지 Docker의 개념과 명령어를 정리한 스터디 노트입니다.
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 시작 시 실행 |
| 볼륨 | 이미지에는 코드를, 볼륨에는 상태를 |
| compose | run 옵션의 yaml화. 환경 실행법이 저장소에 커밋된다 |
Docker의 약속은 한 문장으로 요약됩니다. 환경을 코드로 적고, 어디서든 같은 모습으로 실행한다. 이미지, 컨테이너, 볼륨의 역할 분담만 잡히면 나머지 명령은 그 위에서 자연스럽게 따라옵니다.