raw
DevOps

Udemy Docker 완전정복 강의 학습 정리 — 컨테이너부터 Compose까지

2026.02.28·32분

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... 직접 만들 필요 없이 가져다 쓸 수 있어요.

bash
1# Node.js 공식 이미지를 기반으로 컨테이너 실행
2docker run node

이 명령어 하나로 Docker Hub에서 node 이미지를 자동으로 내려받아서 컨테이너를 실행해요. 놀랍죠?

나만의 이미지 만들기: Dockerfile

내 앱을 Docker로 실행하려면 Dockerfile이 필요하다. Dockerfile은 이미지를 어떻게 만들지 정의하는 레시피다.

간단한 Node.js 앱을 예시로 볼게요.

dockerfile
1# 베이스 이미지 지정
2FROM node:18
3
4# 컨테이너 내 작업 디렉토리 설정
5WORKDIR /app
6
7# package.json을 먼저 복사 (레이어 캐싱 최적화)
8COPY package.json .
9
10# 의존성 설치
11RUN npm install
12
13# 나머지 소스코드 복사
14COPY . .
15
16# 컨테이너 외부에 노출할 포트 (문서화 용도)
17EXPOSE 3000
18
19# 컨테이너 시작 시 실행할 명령어
20CMD ["node", "server.js"]

작성 후 이미지를 빌드해요.

bash
1# 현재 디렉토리의 Dockerfile로 이미지 빌드
2# -t: 이미지에 이름(태그) 붙이기
3docker build -t my-node-app .

빌드된 이미지로 컨테이너를 실행해요.

bash
1# -p: 호스트포트:컨테이너포트 매핑
2docker run -p 3000:3000 my-node-app

이제 localhost:3000으로 접속하면 앱이 뜨죠.

EXPOSE의 진실

EXPOSE 3000은 사실 포트를 열어주는 명령어가 아니다. 진짜 포트 오픈은 docker run -p 옵션이 한다.
EXPOSE는 순전히 문서화 목적이에요. "이 컨테이너는 3000번 포트를 사용할 예정이야"라고 알려주는 주석 같은 거죠. 근데 관례적으로 써두는 게 좋아요.


이미지 레이어 이해하기

Docker 이미지는 레이어(Layer) 구조로 이루어져 있다. 이게 Docker의 핵심 최적화 기법이에요.

text
1[Layer 4] COPY . . ← 소스코드
2[Layer 3] RUN npm install ← node_modules
3[Layer 2] COPY package.json .
4[Layer 1] FROM node:18 ← 베이스 이미지

각 Dockerfile 명령어는 새 레이어를 만든다. 빌드 시 변경된 레이어 이후만 다시 빌드한다. 변경 안 된 레이어는 캐시를 재사용한다.
그래서 package.json을 먼저 복사하고 npm install을 하는 거예요. 소스코드만 바뀌면 npm install 레이어를 캐시에서 가져와서 빌드가 훨씬 빨라지죠.

dockerfile
1FROM node:18
2WORKDIR /app
3COPY . . # 소스코드가 바뀌면
4RUN npm install # 여기도 매번 다시 실행됨 (느림)
5CMD ["node", "server.js"]
dockerfile
1FROM node:18
2WORKDIR /app
3COPY package.json . # package.json만 먼저 복사
4RUN npm install # package.json 안 바뀌면 캐시 사용
5COPY . . # 소스코드 나중에 복사
6CMD ["node", "server.js"]

소스코드 수정할 때마다 npm install이 통째로 다시 돌아가는 건 진짜 고통이에요. 이 패턴을 꼭 적용하세요.


컨테이너 관리

컨테이너 기본 명령어

bash
1# 실행 중인 컨테이너 목록
2docker ps
3
4# 모든 컨테이너 목록 (중지된 것 포함)
5docker ps -a
6
7# 컨테이너 중지
8docker stop <컨테이너이름 또는 ID>
9
10# 중지된 컨테이너 재시작
11docker start <컨테이너이름 또는 ID>
12
13# 컨테이너 삭제
14docker rm <컨테이너이름 또는 ID>
15
16# 실행 중인 컨테이너 강제 삭제
17docker rm -f <컨테이너이름 또는 ID>

Attached vs Detached 모드

컨테이너를 실행할 때 두 가지 모드가 있어요.

Attached 모드 (기본): 터미널이 컨테이너 출력에 연결된다. 컨테이너 로그가 터미널에 바로 보이지만, 그 터미널로 다른 걸 못 한다.

bash
1# attached 모드 (기본)
2docker run -p 3000:3000 my-node-app

Detached 모드 (-d 옵션): 컨테이너가 백그라운드에서 실행된다. 터미널은 자유롭게 쓸 수 있다.

bash
1# detached 모드
2docker run -d -p 3000:3000 my-node-app
3
4# 나중에 로그 보고 싶으면
5docker logs <컨테이너이름>
6
7# 실시간으로 로그 따라가기
8docker logs -f <컨테이너이름>
9
10# 실행 중인 컨테이너에 다시 붙기
11docker attach <컨테이너이름>

인터랙티브 모드

Python 인터프리터나 Node REPL처럼 입력을 받아야 하는 앱은 -it 옵션을 써요.

  • -i: 입력 연결 유지 (interactive)
  • -t: 터미널 모드 (pseudo-TTY)
bash
1# Python 인터프리터를 컨테이너에서 실행
2docker run -it python
3
4# 이미 실행 중인 컨테이너에 새 터미널 세션 열기
5docker exec -it <컨테이너이름> /bin/bash

유용한 옵션들

bash
1# --rm: 컨테이너 종료 시 자동 삭제
2docker run --rm my-node-app
3
4# --name: 컨테이너에 이름 붙이기
5docker run --name my-server -p 3000:3000 my-node-app
6
7# 중지된 컨테이너 한 번에 삭제
8docker container prune

이미지 삭제

bash
1# 이미지 목록
2docker images
3
4# 이미지 삭제
5docker rmi <이미지ID 또는 이름>
6
7# 사용하지 않는 이미지 전부 삭제
8docker image prune
9
10# 태그가 없는 이미지(댕글링)도 포함해서 삭제
11docker image prune -a

이미지 검사하기

이미지 내부를 들여다보고 싶을 때 써요.

bash
1docker image inspect <이미지이름>

레이어 구조, 환경변수, 노출 포트, 진입점(Entrypoint) 등 이미지에 대한 모든 정보를 JSON으로 보여줘요.

컨테이너와 파일 복사

bash
1# 로컬 → 컨테이너로 파일 복사
2docker cp ./local-file.txt <컨테이너이름>:/app/
3
4# 컨테이너 → 로컬로 파일 복사
5docker cp <컨테이너이름>:/app/logs/. ./logs

실시간 코드 변경 반영에는 볼륨을 써야 하지만, 가끔 로그 파일이나 설정 파일을 꺼내올 때 유용해요.


이미지 공유하기

이미지를 팀원과 공유하거나 서버에 배포하는 방법이에요.

이미지 이름과 태그

text
1이미지이름:태그
2예시) node:18, node:18-alpine, my-app:1.0.0

태그를 붙여서 버전 관리를 해요.

bash
1# 이미지 빌드 시 태그 지정
2docker build -t my-node-app:1.0 .
3
4# 기존 이미지에 새 태그 추가
5docker tag my-node-app:1.0 username/my-node-app:1.0

Docker Hub에 푸시하기

bash
1# Docker Hub 로그인
2docker login
3
4# 이미지 푸시 (이름이 username/이미지이름 형식이어야 함)
5docker push username/my-node-app:1.0
6
7# 이미지 가져오기
8docker pull username/my-node-app:1.0
9
10# 이미지로 컨테이너 실행 (없으면 자동으로 pull)
11docker run username/my-node-app:1.0

[!tip] 이미지 푸시 전 확인사항 Docker Hub에서 이미지 이름이 username/이미지이름 형식이 되도록 태그를 맞춰야 해요. docker tag로 새 태그를 붙이면 원본은 그대로 있고 새 이름이 생겨요.


데이터 관리: 볼륨과 바인드 마운트

Docker에서 꽤 중요한 부분인데, 처음엔 좀 헷갈렸어요. (집중 필요)

컨테이너 데이터의 문제점

컨테이너를 삭제하면 그 안의 데이터도 같이 사라진다. 데이터베이스 컨테이너를 삭제했더니 DB가 날아가는 경험... 생각만 해도 아찔하죠.
강의에서는 세 가지 데이터 카테고리를 구분했어요.

  1. 애플리케이션 코드/환경: 이미지에 포함. 읽기 전용.
  2. 임시 데이터: 컨테이너 레이어에 저장. 컨테이너 삭제 시 사라져도 됨.
  3. 영구 데이터: 컨테이너와 독립적으로 유지돼야 하는 데이터. 볼륨이 필요하다.

볼륨(Volume): Docker가 관리하는 스토리지

볼륨은 Docker가 호스트 파일시스템의 특정 위치에 데이터를 저장하고 관리하는 방식이다. 컨테이너가 삭제되어도 볼륨은 살아있다.

익명 볼륨 (Anonymous Volume)

dockerfile
1VOLUME ["/app/data"]

또는

bash
1docker run -v /app/data my-app

Docker가 자동으로 이름을 생성한다. --rm 옵션으로 컨테이너 삭제 시 같이 삭제된다. 주로 컨테이너 내 특정 디렉토리를 외부 마운트로 보호하는 데 쓰인다.

명명된 볼륨 (Named Volume)

bash
1# -v 볼륨이름:컨테이너경로
2docker run -v mydata:/app/data my-app

이름을 직접 지정해서 컨테이너가 삭제된 후에도 유지된다. 데이터베이스 파일, 업로드 파일처럼 영구 보존이 필요한 데이터에 쓴다.

bash
1# 볼륨 목록 확인
2docker volume ls
3
4# 볼륨 상세 정보
5docker volume inspect mydata
6
7# 사용하지 않는 볼륨 삭제
8docker volume prune
9
10# 특정 볼륨 삭제
11docker volume rm mydata

바인드 마운트(Bind Mount): 개발 시 코드 핫리로드

바인드 마운트는 호스트 파일시스템의 특정 경로를 컨테이너에 직접 연결한다. 볼륨과 달리 개발자가 호스트 경로를 직접 지정한다.

bash
1# -v 절대경로:컨테이너경로
2docker run -v /Users/me/project:/app my-app
3
4# macOS/Linux 단축키
5docker run -v $(pwd):/app my-app

로컬에서 코드를 수정하면 컨테이너 안에도 즉시 반영되죠. 개발 환경에서 핫리로드에 필수적이에요.

[!warning] 바인드 마운트 주의사항 호스트 디렉토리가 컨테이너 내 경로를 덮어씌운다. 그래서 npm install로 만든 node_modules가 사라질 수 있어요. 이걸 막으려면 익명 볼륨을 같이 써야 해요.

볼륨 결합하기

bash
1docker run \
2 -v $(pwd):/app \ # 바인드 마운트: 소스코드
3 -v /app/node_modules \ # 익명 볼륨: node_modules 보호
4 -p 3000:3000 \
5 my-node-app

Docker는 더 구체적인 경로의 볼륨을 우선시한다. /app/node_modules/app보다 구체적이라서 node_modules는 바인드 마운트에 덮어쓰이지 않는다.

Nodemon으로 개발 서버 핫리로드

바인드 마운트로 코드는 바뀌는데, Node.js 서버가 자동으로 재시작이 안 되면 의미가 없겠죠. nodemon을 쓰면 파일 변경 시 자동으로 서버를 재시작해줘요.

json
1{
2 "scripts": {
3 "start": "node server.js",
4 "dev": "nodemon server.js"
5 },
6 "devDependencies": {
7 "nodemon": "^3.0.0"
8 }
9}
dockerfile
1FROM node:18
2WORKDIR /app
3COPY package.json .
4RUN npm install
5COPY . .
6EXPOSE 3000
7CMD ["npm", "run", "dev"]

읽기 전용 바인드 마운트

컨테이너가 소스코드를 읽기만 하고 수정 못 하게 하려면 :ro를 붙이면 돼요.

bash
1docker run -v $(pwd):/app:ro my-app

.dockerignore

.gitignore처럼 .dockerignore는 이미지 빌드 시 복사하지 않을 파일/디렉토리를 지정한다.

text
1node_modules
2.git
3.gitignore
4*.md
5Dockerfile
6.dockerignore
7npm-debug.log

node_modules를 제외하지 않으면 로컬 node_modules가 이미지 안으로 복사돼서 이미지가 뚱뚱해져요. 꼭 넣어두세요.
또는 해당하는 파일이나 디렉토리를 명시적으로 넣어두면 제외할 수 있습니다.

환경 변수

dockerfile
1ENV PORT=3000
2EXPOSE $PORT
3CMD ["node", "server.js"]
bash
1# 실행 시 환경 변수 오버라이드
2docker run -e PORT=8080 -p 8080:8080 my-app
3
4# .env 파일 사용
5docker run --env-file ./.env -p 3000:3000 my-app

[!warning] 환경 변수 보안 .env 파일을 절대 이미지에 복사하면 안 된다. COPY . . 할 때 .dockerignore.env를 추가하고, 실행 시 --env-file로 주입하는 게 안전하다.

빌드 인수(ARG)

빌드 시에만 쓰는 값을 전달할 때 써요. 런타임에는 사용할 수 없어요.

dockerfile
1ARG DEFAULT_PORT=3000
2ENV PORT=$DEFAULT_PORT
3EXPOSE $PORT
bash
1docker build --build-arg DEFAULT_PORT=8080 -t my-app .

네트워크: 컨테이너 간 통신

컨테이너는 기본적으로 격리되어 있어서 서로 통신하려면 명시적으로 연결해야 해요.

세 가지 통신 시나리오

강의에서 세 가지 케이스를 다뤘어요.

Case 1: 컨테이너 → 인터넷(WWW)
컨테이너는 기본적으로 외부 인터넷에 접근할 수 있다. 코드에서 fetch('https://api.example.com') 같이 HTTP 요청을 그냥 날리면 된다. 특별한 설정이 필요 없다.

Case 2: 컨테이너 → 호스트 머신
로컬 MongoDB나 MySQL에 컨테이너가 접근하려면 localhost 대신 특별한 도메인을 써야 한다.

javascript
1// ❌ 이렇게 하면 안 돼요 (컨테이너 내부 자기 자신을 가리킴)
2mongoose.connect('mongodb://localhost:27017/mydb')
3
4// ✅ 이렇게 해야 해요
5mongoose.connect('mongodb://host.docker.internal:27017/mydb')

host.docker.internal은 Docker가 제공하는 특별한 호스트명이다. 컨테이너 내에서 호스트 머신을 가리킨다.

Case 3: 컨테이너 ↔ 컨테이너
이게 가장 자주 쓰는 케이스죠. 백엔드 컨테이너가 DB 컨테이너에 연결하는 경우요.

방법 1: IP 주소로 연결 (권장하지 않음)

bash
1docker inspect <컨테이너이름> | grep IPAddress

IP를 확인하고 하드코딩하면 동작은 해요. 근데 컨테이너를 재시작하면 IP가 바뀔 수 있어서 불안정하다. 쓰지 않는 게 낫다.

방법 2: Docker Network 사용 (권장)

같은 네트워크에 속한 컨테이너끼리는 컨테이너 이름으로 통신할 수 있다.

bash
1# 네트워크 생성
2docker network create my-network
3
4# 네트워크에 컨테이너 연결해서 실행
5docker run -d --name mongodb --network my-network mongo
6docker run -d --name backend --network my-network my-backend-app
javascript
1// 컨테이너 이름을 호스트명으로 사용
2mongoose.connect('mongodb://mongodb:27017/mydb')

localhost 대신 컨테이너 이름 mongodb를 쓰면 된다. 같은 네트워크에 있으면 Docker가 알아서 IP를 해석해준다.

[!info] Docker 네트워크 드라이버

  • bridge: 기본값. 같은 호스트의 컨테이너들이 통신하는 사설 네트워크.
  • host: 컨테이너가 호스트 네트워크를 직접 사용. 격리 없음.
  • none: 네트워크 연결 없음. 완전 격리.
  • overlay: 여러 Docker 호스트에 걸친 네트워크 (Swarm에서 사용).

실전 프로젝트: MERN 스택 도커화

강의에서 MongoDB + Node.js + React 앱을 전부 도커화하는 실전 모듈이 있었어요.

MongoDB 컨테이너

bash
1docker run -d \
2 --name mongodb \
3 --network goals-net \
4 -v data:/data/db \
5 -e MONGO_INITDB_ROOT_USERNAME=admin \
6 -e MONGO_INITDB_ROOT_PASSWORD=secret \
7 mongo

Node.js 백엔드 Dockerfile

dockerfile
1FROM node:18
2WORKDIR /app
3COPY package.json .
4RUN npm install
5COPY . .
6EXPOSE 80
7ENV MONGODB_USERNAME=root
8ENV MONGODB_PASSWORD=secret
9CMD ["node", "app.js"]
bash
1docker run -d \
2 --name backend \
3 --network goals-net \
4 -p 80:80 \
5 -v $(pwd):/app \
6 -v /app/node_modules \
7 -v logs:/app/logs \
8 -e MONGODB_USERNAME=admin \
9 -e MONGODB_PASSWORD=secret \
10 my-backend
javascript
1mongoose.connect(
2 `mongodb://${process.env.MONGODB_USERNAME}:${process.env.MONGODB_PASSWORD}@mongodb:27017/goals?authSource=admin`
3)

React 프론트엔드 Dockerfile

dockerfile
1FROM node:18
2WORKDIR /app
3COPY package.json .
4RUN npm install
5COPY . .
6EXPOSE 3000
7CMD ["npm", "start"]
bash
1docker run -d \
2 --name frontend \
3 --network goals-net \
4 -p 3000:3000 \
5 -v $(pwd)/src:/app/src \
6 -it \
7 my-frontend

[!tip] React 개발 서버와 -it Create React App의 개발 서버는 인터랙티브 모드(-it)가 없으면 즉시 종료된다. stdin을 연결해줘야 계속 실행된다.


Docker Compose: 멀티 컨테이너 앱 한 번에 관리

위에서 보셨다시피 docker run 명령어가 길어지고 여러 컨테이너를 관리하다 보면 엄청 복잡해져요. Docker Compose는 이걸 YAML 파일 하나로 정의하고 한 번에 실행할 수 있게 해준다.

Docker Compose란?

  • 단일 파일(docker-compose.yml)로 여러 컨테이너를 정의한다
  • docker compose up 한 번으로 모든 컨테이너가 시작된다
  • docker compose down 한 번으로 모든 컨테이너가 종료된다
  • 서비스 간 네트워크는 자동으로 구성된다

docker-compose.yml 기본 구조

위의 MERN 앱을 Docker Compose로 변환해볼게요.

yaml
1version: "3.8"
2
3services:
4 # MongoDB 서비스
5 mongodb:
6 image: mongo
7 volumes:
8 - data:/data/db
9 environment:
10 MONGO_INITDB_ROOT_USERNAME: admin
11 MONGO_INITDB_ROOT_PASSWORD: secret
12
13 # Node.js 백엔드
14 backend:
15 build: ./backend
16 ports:
17 - "80:80"
18 volumes:
19 - logs:/app/logs
20 - ./backend:/app
21 - /app/node_modules
22 environment:
23 MONGODB_USERNAME: admin
24 MONGODB_PASSWORD: secret
25 depends_on:
26 - mongodb
27
28 # React 프론트엔드
29 frontend:
30 build: ./frontend
31 ports:
32 - "3000:3000"
33 volumes:
34 - ./frontend/src:/app/src
35 stdin_open: true
36 tty: true
37 depends_on:
38 - backend
39
40volumes:
41 data:
42 logs:

수십 줄의 docker run 명령어가 이 파일 하나로 정리됐어요.

Docker Compose 주요 명령어

bash
1# 모든 서비스 빌드 + 시작
2docker compose up -d
3
4# 이미지 강제 재빌드 후 시작
5docker compose up -d --build
6
7# 모든 서비스 중지 + 컨테이너 삭제
8docker compose down
9
10# 볼륨까지 삭제
11docker compose down -v
12
13# 로그 확인
14docker compose logs
15
16# 실시간 로그
17docker compose logs -f
18
19# 서비스 상태 확인
20docker compose ps
21
22# 특정 서비스만 시작
23docker compose up -d mongodb

이미지 빌드 설정 커스터마이징

yaml
1services:
2 backend:
3 build:
4 context: ./backend
5 dockerfile: Dockerfile.dev
6 image: my-backend:dev
7 container_name: my-backend

실전 예시: PostgreSQL + Redis + Nginx

yaml
1version: "3.8"
2
3services:
4 db:
5 image: postgres:15
6 restart: unless-stopped
7 volumes:
8 - postgres_data:/var/lib/postgresql/data
9 environment:
10 POSTGRES_DB: myapp
11 POSTGRES_USER: user
12 POSTGRES_PASSWORD: password
13
14 redis:
15 image: redis:7-alpine
16 restart: unless-stopped
17
18 api:
19 build:
20 context: .
21 dockerfile: Dockerfile
22 restart: unless-stopped
23 ports:
24 - "8080:8080"
25 depends_on:
26 - db
27 - redis
28 environment:
29 DATABASE_URL: postgresql://user:password@db:5432/myapp
30 REDIS_URL: redis://redis:6379
31 volumes:
32 - ./src:/app/src
33
34 nginx:
35 image: nginx:alpine
36 ports:
37 - "80:80"
38 - "443:443"
39 volumes:
40 - ./nginx.conf:/etc/nginx/nginx.conf:ro
41 depends_on:
42 - api
43
44volumes:
45 postgres_data:

[!warning] depends_on의 한계 depends_on으로 시작 순서를 제어하지만, 서비스가 완전히 준비됐는지는 보장하지 않는다. DB가 실제로 요청을 받을 준비가 될 때까지 기다리려면 헬스체크나 재시도 로직이 별도로 필요하다.


유틸리티 컨테이너

이 개념이 처음에는 좀 낯설었는데, 생각보다 많이 쓰게 되더라고요.

유틸리티 컨테이너란?

유틸리티 컨테이너는 애플리케이션을 실행하는 게 아니라, 특정 명령어를 실행하는 목적으로만 쓰는 컨테이너다.

예를 들어 Node.js 프로젝트를 새로 시작할 때 npm init이 필요한데, 로컬에 Node.js를 설치하고 싶지 않을 때 유틸리티 컨테이너를 쓸 수 있어요.

bash
1# 로컬에 Node 없어도 npm init 실행
2docker run -it --rm -v $(pwd):/app node npm init

결과물이 바인드 마운트를 통해 로컬에 생성된다.

ENTRYPOINT 활용

CMDENTRYPOINT의 차이를 여기서 제대로 이해했어요.

  • CMD: docker run 뒤에 오는 명령어가 있으면 완전히 대체된다
  • ENTRYPOINT: docker run 뒤의 내용이 ENTRYPOINT 뒤에 추가된다
dockerfile
1FROM node:18
2WORKDIR /app
3ENTRYPOINT ["npm"]
bash
1# npm install 이 실행됨
2docker run --rm -v $(pwd):/app my-util install
3
4# npm run dev 가 실행됨
5docker run --rm -v $(pwd):/app my-util run dev
6
7# npm init -y 가 실행됨
8docker run --rm -v $(pwd):/app my-util init -y

Docker Compose로 유틸리티 컨테이너 관리

yaml
1version: "3.8"
2
3services:
4 npm:
5 build: ./utility
6 volumes:
7 - ./:/app
8
9 app:
10 build: .
11 ports:
12 - "3000:3000"
bash
1# --no-deps: 의존 서비스 시작하지 않음
2# run: 새 컨테이너로 일회성 명령 실행
3docker compose run --rm npm install
4docker compose run --rm npm run build

docker compose up 없이도 특정 서비스만 실행할 수 있어요.


전체 흐름 정리

text
11. Dockerfile 작성
2 └─ FROM, WORKDIR, COPY, RUN, EXPOSE, CMD/ENTRYPOINT
3
42. 이미지 빌드
5 └─ docker build -t <이름>:<태그> .
6
73. 컨테이너 실행
8 └─ docker run [옵션] <이미지>
9 -d: 백그라운드
10 -p: 포트 매핑
11 -v: 볼륨/바인드 마운트
12 -e: 환경 변수
13 --name: 이름 지정
14 --network: 네트워크 연결
15 --rm: 종료 시 자동 삭제
16
174. 데이터 관리
18 ├─ 익명 볼륨: 특정 경로 보호
19 ├─ 명명된 볼륨: 영구 데이터 보존
20 └─ 바인드 마운트: 개발 시 코드 공유
21
225. 네트워크
23 └─ docker network create <이름>
24 같은 네트워크의 컨테이너는 이름으로 통신한다
25
266. Docker Compose
27 └─ docker-compose.yml로 전체 환경 정의
28 docker compose up -d
29 docker compose down

마치며

솔직히 처음에 Docker를 "그냥 배포 도구 아니야?" 하고 가볍게 봤는데, 강의를 다 듣고 나니 생각이 바뀌었어요.

특히 이미지 레이어 캐싱이랑 볼륨 결합하는 부분이 처음에 직관적으로 안 와닿았는데, 실제로 손으로 쳐보면서 이해하는 데 좀 걸렸어요. 역시 개발은 눈으로만 보면 안 된다는 걸 또 느꼈네요 ㅎㅎ.

Docker Compose는 쓰면 쓸수록 진짜 편하다. 기본기 없이 바로 쓰면 왜 이렇게 써야 하는지 모르고 copy-paste만 하게 되는데, 이 강의에서 기본기를 쌓고 나니 Compose 파일 보면 어떻게 돌아가는지 이해가 돼요.

다음 스텝은 Kubernetes인데... 언제 할지는 모르겠지만 ㅎㅎ. 일단 Docker를 실제 프로젝트에 제대로 적용해보고 나서 생각해봐야겠어요.