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... 직접 만들 필요 없이 가져다 쓸 수 있어요.
1# Node.js 공식 이미지를 기반으로 컨테이너 실행2docker run node이 명령어 하나로 Docker Hub에서 node 이미지를 자동으로 내려받아서 컨테이너를 실행해요. 놀랍죠?
나만의 이미지 만들기: Dockerfile
내 앱을 Docker로 실행하려면 Dockerfile이 필요하다. Dockerfile은 이미지를 어떻게 만들지 정의하는 레시피다.
간단한 Node.js 앱을 예시로 볼게요.
1# 베이스 이미지 지정2FROM node:183
4# 컨테이너 내 작업 디렉토리 설정5WORKDIR /app6
7# package.json을 먼저 복사 (레이어 캐싱 최적화)8COPY package.json .9
10# 의존성 설치11RUN npm install12
13# 나머지 소스코드 복사14COPY . .15
16# 컨테이너 외부에 노출할 포트 (문서화 용도)17EXPOSE 300018
19# 컨테이너 시작 시 실행할 명령어20CMD ["node", "server.js"]작성 후 이미지를 빌드해요.
1# 현재 디렉토리의 Dockerfile로 이미지 빌드2# -t: 이미지에 이름(태그) 붙이기3docker build -t my-node-app .빌드된 이미지로 컨테이너를 실행해요.
1# -p: 호스트포트:컨테이너포트 매핑2docker run -p 3000:3000 my-node-app이제 localhost:3000으로 접속하면 앱이 뜨죠.
EXPOSE의 진실
EXPOSE 3000은 사실 포트를 열어주는 명령어가 아니다. 진짜 포트 오픈은 docker run -p 옵션이 한다.
EXPOSE는 순전히 문서화 목적이에요. "이 컨테이너는 3000번 포트를 사용할 예정이야"라고 알려주는 주석 같은 거죠. 근데 관례적으로 써두는 게 좋아요.
이미지 레이어 이해하기
Docker 이미지는 레이어(Layer) 구조로 이루어져 있다. 이게 Docker의 핵심 최적화 기법이에요.
1[Layer 4] COPY . . ← 소스코드2[Layer 3] RUN npm install ← node_modules3[Layer 2] COPY package.json .4[Layer 1] FROM node:18 ← 베이스 이미지각 Dockerfile 명령어는 새 레이어를 만든다. 빌드 시 변경된 레이어 이후만 다시 빌드한다. 변경 안 된 레이어는 캐시를 재사용한다.
그래서 package.json을 먼저 복사하고 npm install을 하는 거예요. 소스코드만 바뀌면 npm install 레이어를 캐시에서 가져와서 빌드가 훨씬 빨라지죠.
1FROM node:182WORKDIR /app3COPY . . # 소스코드가 바뀌면4RUN npm install # 여기도 매번 다시 실행됨 (느림)5CMD ["node", "server.js"]1FROM node:182WORKDIR /app3COPY package.json . # package.json만 먼저 복사4RUN npm install # package.json 안 바뀌면 캐시 사용5COPY . . # 소스코드 나중에 복사6CMD ["node", "server.js"]소스코드 수정할 때마다 npm install이 통째로 다시 돌아가는 건 진짜 고통이에요. 이 패턴을 꼭 적용하세요.
컨테이너 관리
컨테이너 기본 명령어
1# 실행 중인 컨테이너 목록2docker ps3
4# 모든 컨테이너 목록 (중지된 것 포함)5docker ps -a6
7# 컨테이너 중지8docker stop <컨테이너이름 또는 ID>9
10# 중지된 컨테이너 재시작11docker start <컨테이너이름 또는 ID>12
13# 컨테이너 삭제14docker rm <컨테이너이름 또는 ID>15
16# 실행 중인 컨테이너 강제 삭제17docker rm -f <컨테이너이름 또는 ID>Attached vs Detached 모드
컨테이너를 실행할 때 두 가지 모드가 있어요.
Attached 모드 (기본): 터미널이 컨테이너 출력에 연결된다. 컨테이너 로그가 터미널에 바로 보이지만, 그 터미널로 다른 걸 못 한다.
1# attached 모드 (기본)2docker run -p 3000:3000 my-node-appDetached 모드 (-d 옵션): 컨테이너가 백그라운드에서 실행된다. 터미널은 자유롭게 쓸 수 있다.
1# detached 모드2docker run -d -p 3000:3000 my-node-app3
4# 나중에 로그 보고 싶으면5docker logs <컨테이너이름>6
7# 실시간으로 로그 따라가기8docker logs -f <컨테이너이름>9
10# 실행 중인 컨테이너에 다시 붙기11docker attach <컨테이너이름>인터랙티브 모드
Python 인터프리터나 Node REPL처럼 입력을 받아야 하는 앱은 -it 옵션을 써요.
-i: 입력 연결 유지 (interactive)-t: 터미널 모드 (pseudo-TTY)
1# Python 인터프리터를 컨테이너에서 실행2docker run -it python3
4# 이미 실행 중인 컨테이너에 새 터미널 세션 열기5docker exec -it <컨테이너이름> /bin/bash유용한 옵션들
1# --rm: 컨테이너 종료 시 자동 삭제2docker run --rm my-node-app3
4# --name: 컨테이너에 이름 붙이기5docker run --name my-server -p 3000:3000 my-node-app6
7# 중지된 컨테이너 한 번에 삭제8docker container prune이미지 삭제
1# 이미지 목록2docker images3
4# 이미지 삭제5docker rmi <이미지ID 또는 이름>6
7# 사용하지 않는 이미지 전부 삭제8docker image prune9
10# 태그가 없는 이미지(댕글링)도 포함해서 삭제11docker image prune -a이미지 검사하기
이미지 내부를 들여다보고 싶을 때 써요.
1docker image inspect <이미지이름>레이어 구조, 환경변수, 노출 포트, 진입점(Entrypoint) 등 이미지에 대한 모든 정보를 JSON으로 보여줘요.
컨테이너와 파일 복사
1# 로컬 → 컨테이너로 파일 복사2docker cp ./local-file.txt <컨테이너이름>:/app/3
4# 컨테이너 → 로컬로 파일 복사5docker cp <컨테이너이름>:/app/logs/. ./logs실시간 코드 변경 반영에는 볼륨을 써야 하지만, 가끔 로그 파일이나 설정 파일을 꺼내올 때 유용해요.
이미지 공유하기
이미지를 팀원과 공유하거나 서버에 배포하는 방법이에요.
이미지 이름과 태그
1이미지이름:태그2예시) node:18, node:18-alpine, my-app:1.0.0태그를 붙여서 버전 관리를 해요.
1# 이미지 빌드 시 태그 지정2docker build -t my-node-app:1.0 .3
4# 기존 이미지에 새 태그 추가5docker tag my-node-app:1.0 username/my-node-app:1.0Docker Hub에 푸시하기
1# Docker Hub 로그인2docker login3
4# 이미지 푸시 (이름이 username/이미지이름 형식이어야 함)5docker push username/my-node-app:1.06
7# 이미지 가져오기8docker pull username/my-node-app:1.09
10# 이미지로 컨테이너 실행 (없으면 자동으로 pull)11docker run username/my-node-app:1.0[!tip] 이미지 푸시 전 확인사항 Docker Hub에서 이미지 이름이
username/이미지이름형식이 되도록 태그를 맞춰야 해요.docker tag로 새 태그를 붙이면 원본은 그대로 있고 새 이름이 생겨요.
데이터 관리: 볼륨과 바인드 마운트
Docker에서 꽤 중요한 부분인데, 처음엔 좀 헷갈렸어요. (집중 필요)
컨테이너 데이터의 문제점
컨테이너를 삭제하면 그 안의 데이터도 같이 사라진다. 데이터베이스 컨테이너를 삭제했더니 DB가 날아가는 경험... 생각만 해도 아찔하죠.
강의에서는 세 가지 데이터 카테고리를 구분했어요.
- 애플리케이션 코드/환경: 이미지에 포함. 읽기 전용.
- 임시 데이터: 컨테이너 레이어에 저장. 컨테이너 삭제 시 사라져도 됨.
- 영구 데이터: 컨테이너와 독립적으로 유지돼야 하는 데이터. 볼륨이 필요하다.
볼륨(Volume): Docker가 관리하는 스토리지
볼륨은 Docker가 호스트 파일시스템의 특정 위치에 데이터를 저장하고 관리하는 방식이다. 컨테이너가 삭제되어도 볼륨은 살아있다.
익명 볼륨 (Anonymous Volume)
1VOLUME ["/app/data"]또는
1docker run -v /app/data my-appDocker가 자동으로 이름을 생성한다. --rm 옵션으로 컨테이너 삭제 시 같이 삭제된다. 주로 컨테이너 내 특정 디렉토리를 외부 마운트로 보호하는 데 쓰인다.
명명된 볼륨 (Named Volume)
1# -v 볼륨이름:컨테이너경로2docker run -v mydata:/app/data my-app이름을 직접 지정해서 컨테이너가 삭제된 후에도 유지된다. 데이터베이스 파일, 업로드 파일처럼 영구 보존이 필요한 데이터에 쓴다.
1# 볼륨 목록 확인2docker volume ls3
4# 볼륨 상세 정보5docker volume inspect mydata6
7# 사용하지 않는 볼륨 삭제8docker volume prune9
10# 특정 볼륨 삭제11docker volume rm mydata바인드 마운트(Bind Mount): 개발 시 코드 핫리로드
바인드 마운트는 호스트 파일시스템의 특정 경로를 컨테이너에 직접 연결한다. 볼륨과 달리 개발자가 호스트 경로를 직접 지정한다.
1# -v 절대경로:컨테이너경로2docker run -v /Users/me/project:/app my-app3
4# macOS/Linux 단축키5docker run -v $(pwd):/app my-app로컬에서 코드를 수정하면 컨테이너 안에도 즉시 반영되죠. 개발 환경에서 핫리로드에 필수적이에요.
[!warning] 바인드 마운트 주의사 항 호스트 디렉토리가 컨테이너 내 경로를 덮어씌운다. 그래서
npm install로 만든node_modules가 사라질 수 있어요. 이걸 막으려면 익명 볼륨을 같이 써야 해요.
볼륨 결합하기
1docker run \2 -v $(pwd):/app \ # 바인드 마운트: 소스코드3 -v /app/node_modules \ # 익명 볼륨: node_modules 보호4 -p 3000:3000 \5 my-node-appDocker는 더 구체적인 경로의 볼륨을 우선시한다. /app/node_modules가 /app보다 구체적이라서 node_modules는 바인드 마운트에 덮어쓰이지 않는다.
Nodemon으로 개발 서버 핫리로드
바인드 마운트로 코드는 바뀌는데, Node.js 서버가 자동으로 재시작이 안 되면 의미가 없겠죠. nodemon을 쓰면 파일 변경 시 자동으로 서버를 재시작해줘요.
1{2 "scripts": {3 "start": "node server.js",4 "dev": "nodemon server.js"5 },6 "devDependencies": {7 "nodemon": "^3.0.0"8 }9}1FROM node:182WORKDIR /app3COPY package.json .4RUN npm install5COPY . .6EXPOSE 30007CMD ["npm", "run", "dev"]읽기 전용 바인드 마운트
컨테이너가 소스코드를 읽기만 하고 수정 못 하게 하려면 :ro를 붙이면 돼요.
1docker run -v $(pwd):/app:ro my-app.dockerignore
.gitignore처럼 .dockerignore는 이미지 빌드 시 복사하지 않을 파일/디렉토리를 지정한다.
1node_modules2.git3.gitignore4*.md5Dockerfile6.dockerignore7npm-debug.lognode_modules를 제외하지 않으면 로컬 node_modules가 이미지 안으로 복사돼서 이미지가 뚱뚱해져요. 꼭 넣어두세요.
또는 해당하는 파일이나 디렉토리를 명시적으로 넣어두면 제외할 수 있습니다.
환경 변수
1ENV PORT=30002EXPOSE $PORT3CMD ["node", "server.js"]1# 실행 시 환경 변수 오버라이드2docker run -e PORT=8080 -p 8080:8080 my-app3
4# .env 파일 사용5docker run --env-file ./.env -p 3000:3000 my-app[!warning] 환경 변수 보안
.env파일을 절대 이미지에 복사하면 안 된다.COPY . .할 때.dockerignore에.env를 추가하고, 실행 시--env-file로 주입하는 게 안전하다.
빌드 인수(ARG)
빌드 시에만 쓰는 값을 전달할 때 써요. 런타임에는 사용할 수 없어요.
1ARG DEFAULT_PORT=30002ENV PORT=$DEFAULT_PORT3EXPOSE $PORT1docker build --build-arg DEFAULT_PORT=8080 -t my-app .네트워크: 컨테이너 간 통신
컨테이너는 기본적으로 격리되어 있어서 서로 통신하려면 명시적으로 연결해야 해요.
세 가지 통신 시나리오
강의에서 세 가지 케이스를 다뤘어요.
Case 1: 컨테이너 → 인터넷(WWW)
컨테이너는 기본적으로 외부 인터넷에 접근할 수 있다. 코드에서 fetch('https://api.example.com') 같이 HTTP 요청을 그냥 날리면 된다. 특별한 설정이 필요 없다.
Case 2: 컨테이너 → 호스트 머신
로컬 MongoDB나 MySQL에 컨테이너가 접근하려면 localhost 대신 특별한 도메인을 써야 한다.
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 주소로 연결 (권장하지 않음)
1docker inspect <컨테이너이름> | grep IPAddressIP를 확인하고 하드코딩하면 동작은 해요. 근데 컨테이너를 재시작하면 IP가 바뀔 수 있어서 불안정하다. 쓰지 않는 게 낫다.
방법 2: Docker Network 사용 (권장)
같은 네트워크에 속한 컨테이너끼리는 컨테이너 이름으로 통신할 수 있다.
1# 네트워크 생성2docker network create my-network3
4# 네트워크에 컨테이너 연결해서 실행5docker run -d --name mongodb --network my-network mongo6docker run -d --name backend --network my-network my-backend-app1// 컨테이너 이름을 호스트명으로 사용2mongoose.connect('mongodb://mongodb:27017/mydb')localhost 대신 컨테이너 이름 mongodb를 쓰면 된다. 같은 네트워크에 있으면 Docker가 알아서 IP를 해석해준다.
[!info] Docker 네트워크 드라이버
bridge: 기본값. 같은 호스트의 컨테이너들이 통신하는 사설 네트워크.host: 컨테이너가 호스트 네트워크를 직접 사용. 격리 없음.none: 네트워크 연결 없음. 완전 격리.overlay: 여러 Docker 호스트에 걸친 네트워크 (Swarm에서 사용).
실전 프로젝트: MERN 스택 도커화
강의에서 MongoDB + Node.js + React 앱을 전부 도커화하는 실전 모듈이 있었어요.
MongoDB 컨테이너
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 mongoNode.js 백엔드 Dockerfile
1FROM node:182WORKDIR /app3COPY package.json .4RUN npm install5COPY . .6EXPOSE 807ENV MONGODB_USERNAME=root8ENV MONGODB_PASSWORD=secret9CMD ["node", "app.js"]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-backend1mongoose.connect(2 `mongodb://${process.env.MONGODB_USERNAME}:${process.env.MONGODB_PASSWORD}@mongodb:27017/goals?authSource=admin`3)React 프론트엔드 Dockerfile
1FROM node:182WORKDIR /app3COPY package.json .4RUN npm install5COPY . .6EXPOSE 30007CMD ["npm", "start"]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로 변환해볼게요.
1version: "3.8"2
3services:4 # MongoDB 서비스5 mongodb:6 image: mongo7 volumes:8 - data:/data/db9 environment:10 MONGO_INITDB_ROOT_USERNAME: admin11 MONGO_INITDB_ROOT_PASSWORD: secret12
13 # Node.js 백엔드14 backend:15 build: ./backend16 ports:17 - "80:80"18 volumes:19 - logs:/app/logs20 - ./backend:/app21 - /app/node_modules22 environment:23 MONGODB_USERNAME: admin24 MONGODB_PASSWORD: secret25 depends_on:26 - mongodb27
28 # React 프론트엔드29 frontend:30 build: ./frontend31 ports:32 - "3000:3000"33 volumes:34 - ./frontend/src:/app/src35 stdin_open: true36 tty: true37 depends_on:38 - backend39
40volumes:41 data:42 logs:수십 줄의 docker run 명령어가 이 파일 하나로 정리됐어요.
Docker Compose 주요 명령어
1# 모든 서비스 빌드 + 시작2docker compose up -d3
4# 이미지 강제 재빌드 후 시작5docker compose up -d --build6
7# 모든 서비스 중지 + 컨테이너 삭제8docker compose down9
10# 볼륨까지 삭제11docker compose down -v12
13# 로그 확인14docker compose logs15
16# 실시간 로그17docker compose logs -f18
19# 서비스 상태 확인20docker compose ps21
22# 특정 서비스만 시작23docker compose up -d mongodb이미지 빌드 설정 커스터마이징
1services:2 backend:3 build:4 context: ./backend5 dockerfile: Dockerfile.dev6 image: my-backend:dev7 container_name: my-backend실전 예시: PostgreSQL + Redis + Nginx
1version: "3.8"2
3services:4 db:5 image: postgres:156 restart: unless-stopped7 volumes:8 - postgres_data:/var/lib/postgresql/data9 environment:10 POSTGRES_DB: myapp11 POSTGRES_USER: user12 POSTGRES_PASSWORD: password13
14 redis:15 image: redis:7-alpine16 restart: unless-stopped17
18 api:19 build:20 context: .21 dockerfile: Dockerfile22 restart: unless-stopped23 ports:24 - "8080:8080"25 depends_on:26 - db27 - redis28 environment:29 DATABASE_URL: postgresql://user:password@db:5432/myapp30 REDIS_URL: redis://redis:637931 volumes:32 - ./src:/app/src33
34 nginx:35 image: nginx:alpine36 ports:37 - "80:80"38 - "443:443"39 volumes:40 - ./nginx.conf:/etc/nginx/nginx.conf:ro41 depends_on:42 - api43
44volumes:45 postgres_data:[!warning] depends_on의 한계
depends_on으로 시작 순서를 제어하지만, 서비스가 완전히 준비됐는지는 보장하지 않는다. DB가 실제로 요청을 받을 준비가 될 때까지 기다리려면 헬스체크나 재시도 로직이 별도로 필요하다.
유틸리티 컨테이너
이 개념이 처음에는 좀 낯설었는데, 생각보다 많이 쓰게 되더라고요.
유틸리티 컨테이너란?
유틸리티 컨테이너는 애플리케이션을 실행하는 게 아니라, 특정 명령어를 실행하는 목적으로만 쓰는 컨테이너다.
예를 들어 Node.js 프로젝트를 새로 시작할 때 npm init이 필요한데, 로컬에 Node.js를 설치하고 싶지 않을 때 유틸리티 컨 테이너를 쓸 수 있어요.
1# 로컬에 Node 없어도 npm init 실행2docker run -it --rm -v $(pwd):/app node npm init결과물이 바인드 마운트를 통해 로컬에 생성된다.
ENTRYPOINT 활용
CMD와 ENTRYPOINT의 차이를 여기서 제대로 이해했어요.
CMD:docker run뒤에 오는 명령어가 있으면 완전히 대체된다ENTRYPOINT:docker run뒤의 내용이 ENTRYPOINT 뒤에 추가된다
1FROM node:182WORKDIR /app3ENTRYPOINT ["npm"]1# npm install 이 실행됨2docker run --rm -v $(pwd):/app my-util install3
4# npm run dev 가 실행됨5docker run --rm -v $(pwd):/app my-util run dev6
7# npm init -y 가 실행됨8docker run --rm -v $(pwd):/app my-util init -yDocker Compose로 유틸리티 컨테이너 관리
1version: "3.8"2
3services:4 npm:5 build: ./utility6 volumes:7 - ./:/app8
9 app:10 build: .11 ports:12 - "3000:3000"1# --no-deps: 의존 서비스 시작하지 않음2# run: 새 컨테이너로 일회성 명령 실행3docker compose run --rm npm install4docker compose run --rm npm run builddocker compose up 없이도 특정 서비스만 실행할 수 있어요.
전체 흐름 정리
11. Dockerfile 작성2 └─ FROM, WORKDIR, COPY, RUN, EXPOSE, CMD/ENTRYPOINT3
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 Compose27 └─ docker-compose.yml로 전체 환경 정의28 docker compose up -d29 docker compose down마치며
솔직히 처음에 Docker를 "그냥 배포 도구 아니야?" 하고 가볍게 봤는데, 강의를 다 듣고 나니 생각이 바뀌었어요.
특히 이미지 레이어 캐싱이랑 볼륨 결합하는 부분이 처음에 직관적으로 안 와닿았는데, 실제로 손으로 쳐보면서 이해하는 데 좀 걸렸어요. 역시 개발은 눈으로만 보면 안 된다는 걸 또 느꼈네요 ㅎㅎ.
Docker Compose는 쓰면 쓸수록 진짜 편하다. 기본기 없이 바로 쓰면 왜 이렇게 써야 하는지 모르고 copy-paste만 하게 되는데, 이 강의에서 기본기를 쌓고 나니 Compose 파일 보면 어떻게 돌아가는지 이해가 돼요.
다음 스텝은 Kubernetes인데... 언제 할지는 모르겠지만 ㅎㅎ. 일단 Docker를 실제 프로젝트에 제대로 적용해보고 나서 생각해봐야겠어요.