
"내 컴퓨터에서는 잘 되는데요..."
Docker를 사용하면 이 말을 더 이상 하지 않아도 됩니다.
개발자라면 한 번쯤 이런 경험이 있을 겁니다.
이 모든 문제를 해결해주는 것이 바로 Docker입니다.
이 글에서는 Docker가 무엇인지, 왜 필요한지, 그리고 실제로 어떻게 사용하는지까지 정리해보도록 하겠습니다.
Docker는 애플리케이션을 컨테이너라는 격리된 환경에서 실행할 수 있게 해주는 플랫폼입니다.
📅 2013년: Solomon Hykes가 PyCon에서 Docker 첫 공개
📅 2014년: Docker 1.0 출시
📅 2025년: Docker 28.x 버전, 클라우드 배포의 사실상 표준
Docker는 Go 언어로 개발되었으며, 현재 컨테이너 기반 개발과 배포의 업계 표준으로 자리잡았습니다.

| 해운업 | Docker |
|---|---|
| 물건을 표준 컨테이너에 담음 | 앱을 Docker 컨테이너에 담음 |
| 어떤 배에서든 운송 가능 | 어떤 서버에서든 실행 가능 |
| 내용물이 뭔지 몰라도 됨 | 어떤 언어/런타임인지 몰라도 됨 |

환경 불일치 문제:

Docker의 해결책:


| 항목 | 가상머신 (VM) | 컨테이너 (Docker) |
|---|---|---|
| 시작 시간 | 수 분 | 수 초 |
| 용량 | GB 단위 | MB 단위 |
| 성능 | 오버헤드 있음 | 네이티브에 가까움 |
| 격리 수준 | 완전 격리 (별도 OS) | 프로세스 수준 격리 |
| OS | 각각 별도 OS 필요 | 호스트 OS 커널 공유 |
| 밀도 | 서버당 수십 개 | 서버당 수백~수천 개 |
✅ 컨테이너 (Docker) 추천
- 마이크로서비스 아키텍처
- 빠른 배포와 확장이 필요할 때
- 개발 환경 통일
- CI/CD 파이프라인
✅ VM 추천
- 완전히 다른 OS가 필요할 때
- 강력한 격리가 필요할 때
- 레거시 시스템 운영
이미지 = 컨테이너를 만들기 위한 설계도 (읽기 전용)
특징:
비유:
컨테이너 = 이미지를 실행한 인스턴스

특징:
Dockerfile = 이미지를 만드는 레시피
# 베이스 이미지 지정
FROM node:18-alpine
# 작업 디렉토리 설정
WORKDIR /app
# 의존성 파일 복사 (캐싱 활용)
COPY package*.json ./
# 의존성 설치
RUN npm install
# 소스 코드 복사
COPY . .
# 포트 노출
EXPOSE 3000
# 실행 명령
CMD ["npm", "start"]
Docker Hub = 이미지 저장소 (GitHub 같은 것)

공식 이미지: nginx, node, python, mysql 등
커스텀 이미지: username/my-app 형태로 업로드
macOS:
# Homebrew로 설치
brew install --cask docker
# 또는 공식 사이트에서 다운로드
# https://www.docker.com/products/docker-desktop
Windows:
1. Docker Desktop for Windows 다운로드
2. WSL 2 활성화 필요
3. 설치 후 재시작
Linux (Ubuntu):
# 패키지 업데이트
sudo apt-get update
# 필요 패키지 설치
sudo apt-get install ca-certificates curl gnupg
# Docker GPG 키 추가
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
# Docker 설치
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# Docker 버전 확인
docker --version
# Docker version 28.x.x
# Docker 정상 동작 확인
docker run hello-world

# 이미지 검색
docker search nginx
# 이미지 다운로드 (pull)
docker pull nginx
docker pull nginx:1.25 # 특정 버전
# 이미지 목록 확인
docker images
# 이미지 삭제
docker rmi nginx
# 이미지 빌드 (Dockerfile 필요)
docker build -t my-app:1.0 .
# 컨테이너 실행
docker run nginx
# 백그라운드 실행 (-d)
docker run -d nginx
# 이름 지정 + 포트 매핑 (-p)
docker run -d --name my-nginx -p 8080:80 nginx
# 컨테이너 목록 (실행 중)
docker ps
# 컨테이너 목록 (전체)
docker ps -a
# 컨테이너 중지
docker stop my-nginx
# 컨테이너 시작
docker start my-nginx
# 컨테이너 재시작
docker restart my-nginx
# 컨테이너 삭제
docker rm my-nginx
# 컨테이너 강제 삭제 (실행 중이어도)
docker rm -f my-nginx
# 컨테이너 내부 셸 접속
docker exec -it my-nginx /bin/bash
# 컨테이너 로그 확인
docker logs my-nginx
# 실시간 로그 확인
docker logs -f my-nginx
# 컨테이너 상세 정보
docker inspect my-nginx
| 옵션 | 설명 | 예시 |
|---|---|---|
-d | 백그라운드 실행 | docker run -d nginx |
-p | 포트 매핑 | -p 8080:80 (호스트:컨테이너) |
--name | 컨테이너 이름 | --name my-app |
-e | 환경 변수 | -e NODE_ENV=production |
-v | 볼륨 마운트 | -v /host/path:/container/path |
--rm | 종료 시 자동 삭제 | docker run --rm nginx |
-it | 인터랙티브 + TTY | docker exec -it container bash |
# 1. 베이스 이미지
FROM node:18-alpine
# 2. 메타데이터
LABEL maintainer="your@email.com"
LABEL version="1.0"
# 3. 환경 변수
ENV NODE_ENV=production
# 4. 작업 디렉토리
WORKDIR /app
# 5. 파일 복사
COPY package*.json ./
RUN npm ci --only=production
COPY . .
# 6. 포트 노출
EXPOSE 3000
# 7. 실행 명령
CMD ["node", "server.js"]
| 명령어 | 설명 | 예시 |
|---|---|---|
FROM | 베이스 이미지 | FROM node:18-alpine |
WORKDIR | 작업 디렉토리 | WORKDIR /app |
COPY | 파일 복사 | COPY . . |
ADD | 파일 복사 (압축 해제 가능) | ADD app.tar.gz /app |
RUN | 빌드 시 명령 실행 | RUN npm install |
ENV | 환경 변수 설정 | ENV NODE_ENV=production |
EXPOSE | 포트 문서화 | EXPOSE 3000 |
CMD | 컨테이너 시작 시 실행 | CMD ["npm", "start"] |
ENTRYPOINT | 컨테이너 진입점 | ENTRYPOINT ["node"] |
프로젝트 구조:
my-app/
├── Dockerfile
├── .dockerignore
├── package.json
├── package-lock.json
└── src/
└── index.js
Dockerfile:
# 멀티 스테이지 빌드
# Stage 1: 빌드
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: 실행
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
EXPOSE 3000
USER node
CMD ["node", "dist/index.js"]
.dockerignore:
node_modules
npm-debug.log
Dockerfile
.dockerignore
.git
.gitignore
README.md
빌드 및 실행:
# 이미지 빌드
docker build -t my-app:1.0 .
# 컨테이너 실행
docker run -d -p 3000:3000 --name my-app my-app:1.0
# 확인
curl http://localhost:3000
실제 애플리케이션은 보통 여러 서비스로 구성됩니다:

이런 경우 각 컨테이너를 개별로 실행하면 번거롭습니다.
# 이렇게 하나하나 실행하면 힘듦...
docker run -d --name db postgres
docker run -d --name redis redis
docker run -d --name app --link db --link redis my-app
docker run -d --name nginx --link app nginx
docker-compose.yml:
version: '3.8'
services:
# 웹 애플리케이션
app:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgres://user:pass@db:5432/mydb
- REDIS_URL=redis://redis:6379
depends_on:
- db
- redis
restart: unless-stopped
# 데이터베이스
db:
image: postgres:15-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: mydb
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
# 캐시
redis:
image: redis:7-alpine
restart: unless-stopped
# 볼륨 정의
volumes:
postgres_data:
# 모든 서비스 시작
docker compose up
# 백그라운드 실행
docker compose up -d
# 서비스 중지
docker compose down
# 볼륨까지 삭제
docker compose down -v
# 로그 확인
docker compose logs
# 특정 서비스 로그
docker compose logs app
# 서비스 재시작
docker compose restart app
# 실행 중인 서비스 확인
docker compose ps


# Named Volume 생성
docker volume create my-data
# 볼륨으로 컨테이너 실행
docker run -v my-data:/app/data my-app
# Bind Mount (호스트 경로 마운트)
docker run -v $(pwd)/data:/app/data my-app
# 볼륨 목록
docker volume ls
# 볼륨 삭제
docker volume rm my-data
# 사용하지 않는 볼륨 정리
docker volume prune
# docker-compose.yml
services:
db:
image: postgres:15
volumes:
- postgres_data:/var/lib/postgresql/data # 데이터 영구 저장
- ./init.sql:/docker-entrypoint-initdb.d/init.sql # 초기화 스크립트
environment:
POSTGRES_PASSWORD: secret
volumes:
postgres_data: # Named Volume 정의
| 타입 | 설명 | 용도 |
|---|---|---|
| bridge | 기본 네트워크, 컨테이너 간 통신 | 일반적인 경우 |
| host | 호스트 네트워크 직접 사용 | 성능 최적화 |
| none | 네트워크 없음 | 격리된 컨테이너 |
| overlay | 여러 호스트 간 네트워크 | Docker Swarm |

같은 네트워크 안에서는 컨테이너 이름으로 통신 가능!
# 네트워크 생성
docker network create my-network
# 네트워크에 연결하며 컨테이너 실행
docker run -d --name db --network my-network postgres
docker run -d --name app --network my-network my-app
# 네트워크 목록
docker network ls
# 네트워크 상세 정보
docker network inspect my-network
# ❌ 나쁜 예: 불필요하게 큰 이미지
FROM ubuntu:latest # ~29MB
# ✅ 좋은 예: 작은 이미지
FROM alpine:latest # ~3MB
FROM node:18-alpine # Node.js + Alpine
FROM python:3.11-slim # 슬림 버전
# ✅ 빌드 환경과 실행 환경 분리
# Stage 1: 빌드 (큰 이미지 OK)
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: 실행 (작은 이미지)
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]
# ❌ 나쁜 예: 코드 변경 시마다 npm install
COPY . .
RUN npm install
# ✅ 좋은 예: package.json 변경 시에만 npm install
COPY package*.json ./
RUN npm install
COPY . .

# .dockerignore
node_modules
.git
.gitignore
*.md
Dockerfile
docker-compose.yml
.env
.DS_Store
coverage
.nyc_output
# ✅ root가 아닌 사용자로 실행
FROM node:18-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
# ✅ 특정 버전 태그 사용 (latest 피하기)
FROM node:18.19.0-alpine3.19
# ✅ 불필요한 패키지 설치 안 함
RUN npm ci --only=production
# ✅ 민감 정보는 환경 변수로
ENV DATABASE_URL=${DATABASE_URL}
| 최적화 단계 | 이미지 크기 |
|---|---|
FROM node:18 | ~1GB |
FROM node:18-slim | ~200MB |
FROM node:18-alpine | ~120MB |
| 멀티 스테이지 + alpine | ~80MB |
| distroless | ~50MB |
my-fullstack-app/
├── frontend/
│ ├── Dockerfile
│ ├── package.json
│ └── src/
├── backend/
│ ├── Dockerfile
│ ├── package.json
│ └── src/
├── docker-compose.yml
└── .env
# backend/Dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:18-alpine
WORKDIR /app
RUN addgroup -S app && adduser -S app -G app
COPY --from=builder --chown=app:app /app/dist ./dist
COPY --from=builder --chown=app:app /app/node_modules ./node_modules
COPY --chown=app:app package*.json ./
USER app
EXPOSE 4000
CMD ["node", "dist/index.js"]
# frontend/Dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
version: '3.8'
services:
frontend:
build: ./frontend
ports:
- "80:80"
depends_on:
- backend
restart: unless-stopped
backend:
build: ./backend
ports:
- "4000:4000"
environment:
- DATABASE_URL=postgres://user:pass@db:5432/mydb
- REDIS_URL=redis://redis:6379
- JWT_SECRET=${JWT_SECRET}
depends_on:
- db
- redis
restart: unless-stopped
db:
image: postgres:15-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: mydb
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
redis:
image: redis:7-alpine
restart: unless-stopped
volumes:
postgres_data:
# 개발 환경 실행
docker compose up -d
# 로그 확인
docker compose logs -f
# 프로덕션 빌드
docker compose -f docker-compose.prod.yml up -d --build
| 작업 | 명령어 |
|---|---|
| 이미지 다운로드 | docker pull nginx |
| 이미지 빌드 | docker build -t my-app . |
| 컨테이너 실행 | docker run -d -p 8080:80 nginx |
| 컨테이너 목록 | docker ps |
| 컨테이너 중지 | docker stop <name> |
| 컨테이너 로그 | docker logs <name> |
| 컨테이너 접속 | docker exec -it <name> bash |
| Compose 실행 | docker compose up -d |
| Compose 중지 | docker compose down |