Udemy Docker 완전정복 강의 학습 정리 — 컨테이너부터 Compose까지
Udemy Docker 강의를 처음부터 끝까지 수강하며 정리한 학습 노트입니다. Docker가 뭔지, 왜 써야 하는지부터 시작해서 이미지/컨테이너 관리, 볼륨, 네트워크, Docker Compose, 유틸리티 컨테이너까지 — 실제 코드와 함께 전부 담았어요.
Udemy에서 Docker 강의를 다 봤어요. (사실 다 안봄)
솔직히 처음에는 "Docker? 대충 알지 뭐" 하는 마음으로 시작했는데, 막상 다 듣고 나니 제가 알고 있던 건 겉핥기도 아니었더라고요. 이 글은 강의를 들으며 정리한 학습 노트예요. 강의 순서를 따라가면서 중요한 개념은 제가 이해한 방식대로 다시 설명해볼게요.
글이 상당히 긴데, 그게 의도예요. 나중에 제가 다시 꺼내볼 레퍼런스로 쓸 생각이거든요 ㅎㅎ.
Docker가 뭔가요? 그리고 왜 써야 하죠?
Docker란?
Docker는 애플리케이션을 컨테이너(Container) 라는 격리된 환경에서 실행할 수 있게 해주는 플랫폼이다.
컨테이너를 이렇게 생각해보세요. 이사할 때 짐을 박스에 담잖아요? 그 박스 안에 필요한 거 다 넣어두면 어디서 열어도 똑같이 나오죠. Docker 컨테이너도 마찬가지다. 앱 실행에 필요한 코드, 런타임, 라이브러리, 설정을 전부 담아서 어디서든 동일하게 실행된다.
가상 머신(VM)이랑 뭐가 달라요?
이 둘을 헷갈리는 분이 많은데, 차이가 꽤 커요.
| 가상 머신 (VM) | Docker 컨테이너 | |
|---|---|---|
| OS | 각 VM마다 별도 OS 설치 | 호스트 OS 커널 공유 |
| 용량 | GB 단위 | MB 단위 |
| 시작 시간 | 분 단위 | 초 단위 |
| 격리 수준 | 완전 격리 | 프로세스 수준 격리 |
| 성능 | 오버헤드 있음 | 거의 네이티브 수준 |
VM은 각각이 완전한 운영체제를 가진다. 반면 컨테이너는 호스트 OS의 커널을 공유하면서 프로세스만 격리한다. 그래서 훨씬 가볍고 빠르다.
왜 Docker를 써야 하나요?
"내 컴퓨터에서는 됐는데요?" — 이 말, 개발하다 보면 한 번쯤은 해보셨을 거예요. 동료한테도 들어봤을 거고요.
Docker가 해결하는 게 바로 이거예요. 환경 불일치 문제를 없애준다.
- 개발 환경과 운영 환경이 완전히 동일해진다
- Node.js 18을 쓰는 프로젝트와 Node.js 20을 쓰는 프로젝트를 같은 머신에서 충돌 없이 실행할 수 있다
- 새 팀원이 합류해도
docker run한 방으로 개발환경 세팅이 끝난다 - CI/CD 파이프라인에서 재현 가능한 빌드가 보장된다
핵심 개념: 이미지와 컨테이너
Docker를 이해하는 데 가장 중요한 두 개념이 이미지(Image) 와 컨테이너(Container) 예요.
이미지(Image)란?
이미지는 컨테이너를 만드는 청사진(Blueprint) 이다. 실행 가능한 코드가 담긴 패키지인데, 클래스와 인스턴스 관계로 이해하면 딱 맞아요.
- 이미지 = 클래스 (설계도)
- 컨테이너 = 인스턴스 (실행 중인 실체)
이미지 하나로 컨테이너를 여러 개 만들 수 있어요.
이미지는 읽기 전용(Read-Only)이다.
이미지를 한 번 만들면 변경할 수 없다. 컨테이너가 실행될 때 이미지 위에 쓰기 가능한 레이어가 올라가는 구조다.
컨테이너(Container)란?
컨테이너는 이미지의 실행 중인 인스턴스다. 이미지를 기반으로 생성되고, 격리된 환경에서 독립적으로 실행된다.
컨테이너는 이미지와 달리 상태를 가진다. 파일을 쓰거나, 데이터를 저장하거나, 네트워크 연결을 맺을 수 있다.
첫 번째 Docker 실행
사전 빌드된 이미지 사용하기
Docker Hub에는 이미 수많은 공식 이미지들이 올라와 있어요. Node.js, Python, Nginx, PostgreSQL... 직접 만들 필요 없이 가져다 쓸 수 있어요.
# Node.js 공식 이미지를 기반으로 컨테이너 실행docker run node이 명령어 하나로 Docker Hub에서 node 이미지를 자동으로 내려받아서 컨테이너를 실행해요. 놀랍죠?
나만의 이미지 만들기: Dockerfile
내 앱을 Docker로 실행하려면 Dockerfile이 필요하다. Dockerfile은 이미지를 어떻게 만들지 정의하는 레시피다.
간단한 Node.js 앱을 예시로 볼게요.
# 베이스 이미지 지정FROM node:18
# 컨테이너 내 작업 디렉토리 설정WORKDIR /app
# package.json을 먼저 복사 (레이어 캐싱 최적화)COPY package.json .
# 의존성 설치RUN npm install
# 나머지 소스코드 복사COPY . .
# 컨테이너 외부에 노출할 포트 (문서화 용도)EXPOSE 3000
# 컨테이너 시작 시 실행할 명령어CMD ["node", "server.js"]작성 후 이미지를 빌드해요.
# 현재 디렉토리의 Dockerfile로 이미지 빌드# -t: 이미지에 이름(태그) 붙이기docker build -t my-node-app .빌드된 이미지로 컨테이너를 실행해요.
# -p: 호스트포트:컨테이너포트 매핑docker run -p 3000:3000 my-node-app이제 localhost:3000으로 접속하면 앱이 뜨죠.
EXPOSE의 진실
EXPOSE 3000은 사실 포트를 열어주는 명령어가 아니다. 진짜 포트 오픈은 docker run -p 옵션이 한다.
EXPOSE는 순전히 문서화 목적이에요. "이 컨테이너는 3000번 포트를 사용할 예정이야"라고 알려주는 주석 같은 거죠. 근데 관례적으로 써두는 게 좋아요.
이미지 레이어 이해하기
Docker 이미지는 레이어(Layer) 구조로 이루어져 있다. 이게 Docker의 핵심 최적화 기법이에요.
[Layer 4] COPY . . ← 소스코드[Layer 3] RUN npm install ← node_modules[Layer 2] COPY package.json .[Layer 1] FROM node:18 ← 베이스 이미지각 Dockerfile 명령어는 새 레이어를 만든다. 빌드 시 변경된 레이어 이후만 다시 빌드한다. 변경 안 된 레이어는 캐시를 재사용한다.
그래서 package.json을 먼저 복사하고 npm install을 하는 거예요. 소스코드만 바뀌면 npm install 레이어를 캐시에서 가져와서 빌드가 훨씬 빨라지죠.
FROM node:18WORKDIR /appCOPY . . # 소스코드가 바뀌면RUN npm install # 여기도 매번 다시 실행됨 (느림)CMD ["node", "server.js"]FROM node:18WORKDIR /appCOPY package.json . # package.json만 먼저 복사RUN npm install # package.json 안 바뀌면 캐시 사용COPY . . # 소스코드 나중에 복사CMD ["node", "server.js"]소스코드 수정할 때마다 npm install이 통째로 다시 돌아가는 건 진짜 고통이에요. 이 패턴을 꼭 적용하세요.
컨테이너 관리
컨테이너 기본 명령어
# 실행 중인 컨테이너 목록docker ps
# 모든 컨테이너 목록 (중지된 것 포함)docker ps -a
# 컨테이너 중지docker stop <컨테이너이름 또는 ID>
# 중지된 컨테이너 재시작docker start <컨테이너이름 또는 ID>
# 컨테이너 삭제docker rm <컨테이너이름 또는 ID>
# 실행 중인 컨테이너 강제 삭제docker rm -f <컨테 이너이름 또는 ID>Attached vs Detached 모드
컨테이너를 실행할 때 두 가지 모드가 있어요.
Attached 모드 (기본): 터미널이 컨테이너 출력에 연결된다. 컨테이너 로그가 터미널에 바로 보이지만, 그 터미널로 다른 걸 못 한다.
# attached 모드 (기본)docker run -p 3000:3000 my-node-appDetached 모드 (-d 옵션): 컨테이너가 백그라운드에서 실행된다. 터미널은 자유롭게 쓸 수 있다.
# detached 모드docker run -d -p 3000:3000 my-node-app
# 나중에 로그 보고 싶으면docker logs <컨테이너이름>
# 실시간으로 로그 따라가기docker logs -f <컨테이너이름>
# 실행 중인 컨테이너에 다시 붙기docker attach <컨테이너이름>인터랙티브 모드
Python 인터프리터나 Node REPL처럼 입력을 받아야 하는 앱은 -it 옵션을 써요.
-i: 입력 연결 유지 (interactive)-t: 터미널 모드 (pseudo-TTY)
# Python 인터프리터를 컨테이너에서 실행docker run -it python
# 이미 실행 중인 컨테이너에 새 터미널 세션 열기docker exec -it <컨테이너이름> /bin/bash유용한 옵션들
# --rm: 컨테이너 종료 시 자동 삭제docker run --rm my-node-app
# --name: 컨테이너에 이름 붙이기docker run --name my-server -p 3000:3000 my-node-app
# 중지된 컨테이너 한 번에 삭제docker container prune이미지 삭제
# 이미지 목록docker images
# 이미지 삭제docker rmi <이미지ID 또는 이름>
# 사용하지 않는 이미지 전부 삭제docker image prune
# 태그가 없는 이미지(댕글링)도 포함해서 삭제docker image prune -a이미지 검사하기
이미지 내부를 들여다보고 싶을 때 써요.
docker image inspect <이미지이름>레이어 구조, 환경변수, 노출 포트, 진입점(Entrypoint) 등 이미지에 대한 모든 정보를 JSON으로 보여줘요.
컨테이너와 파일 복사
# 로컬 → 컨테이너로 파일 복사docker cp ./local-file.txt <컨테이너이름>:/app/
# 컨테이너 → 로컬로 파일 복사docker cp <컨테이너이름>:/app/logs/. ./logs실시간 코드 변경 반영에는 볼륨을 써야 하지만, 가끔 로그 파일이나 설정 파일을 꺼내올 때 유용해요.
이미지 공유하기
이미지를 팀원과 공유하거나 서버에 배포하는 방법이에요.
이미지 이름과 태그
이미지이름:태그예시) node:18, node:18-alpine, my-app:1.0.0태그를 붙여서 버전 관리를 해요.
# 이미지 빌드 시 태그 지정docker build -t my-node-app:1.0 .
# 기존 이미지에 새 태그 추가docker tag my-node-app:1.0 username/my-node-app:1.0Docker Hub에 푸시하기
# Docker Hub 로그인docker login
# 이미지 푸시 (이름이 username/이미지이름 형식이어야 함)docker push username/my-node-app:1.0
# 이미지 가져오기docker pull username/my-node-app:1.0
# 이미지로 컨테이너 실행 (없으면 자동으로 pull)docker run username/my-node-app:1.0데이터 관리: 볼륨과 바인드 마운트
Docker에서 꽤 중요한 부분인데, 처음엔 좀 헷갈렸어요. (집중 필요)
컨테이너 데이터의 문제점
컨테이너를 삭제하면 그 안의 데이터도 같이 사라진다. 데이터베이스 컨테이너를 삭제했더니 DB가 날아가는 경험... 생각만 해도 아찔하죠.
강의에서는 세 가지 데이터 카테고리를 구분했어요.
- 애플리케이션 코드/환경: 이미지에 포함. 읽기 전용.
- 임시 데이터: 컨테이너 레이어에 저장. 컨테이너 삭제 시 사라져도 됨.
- 영구 데이터: 컨테이너와 독립적으로 유지돼야 하는 데이터. 볼륨이 필요하다.
볼륨(Volume): Docker가 관리하는 스토리지
볼륨은 Docker가 호스트 파일시스템의 특정 위치에 데이터를 저장하고 관리하는 방식이다. 컨테이너가 삭제되어도 볼륨은 살아있다.
익명 볼륨 (Anonymous Volume)
VOLUME ["/app/data"]또는
docker run -v /app/data my-appDocker가 자동으로 이름을 생성한다. --rm 옵션으로 컨테이너 삭제 시 같이 삭제된다. 주로 컨테이너 내 특정 디렉토리를 외부 마운트로 보호하는 데 쓰인다.
명명된 볼륨 (Named Volume)
# -v 볼륨이름:컨테이너경로docker run -v mydata:/app/data my-app이름을 직접 지정해서 컨테이너가 삭제된 후에도 유지된다. 데이터베 이스 파일, 업로드 파일처럼 영구 보존이 필요한 데이터에 쓴다.
# 볼륨 목록 확인docker volume ls
# 볼륨 상세 정보docker volume inspect mydata
# 사용하지 않는 볼륨 삭제docker volume prune
# 특정 볼륨 삭제docker volume rm mydata바인드 마운트(Bind Mount): 개발 시 코드 핫리로드
바인드 마운트는 호스트 파일시스템의 특정 경로를 컨테이너에 직접 연결한다. 볼륨과 달리 개발자가 호스트 경로를 직접 지정한다.
# -v 절대경로:컨테이너경로docker run -v /Users/me/project:/app my-app
# macOS/Linux 단축키docker run -v $(pwd):/app my-app로컬에서 코드를 수정하면 컨테이너 안에도 즉시 반영되죠. 개발 환경에서 핫리로드에 필수적이에요.