Overview
Docker는 애플리케이션과 실행 환경을 컨테이너 단위로 패키징하여, 어디서든 동일하게 실행할 수 있도록 해주는 컨테이너 플랫폼이다. 2013년 dotCloud(현 Docker Inc.)에서 공개되었고, Linux Container(LXC) 계열 기술을 바탕으로 발전했다.
Docker가 해결하려는 핵심 문제는 환경 불일치다.
"내 컴퓨터에서는 되는데요?"
이 문제는 보통 OS, 런타임, 라이브러리 버전, 시스템 설정 차이에서 발생한다. Docker는 코드뿐 아니라 실행 환경 전체를 이미지로 패키징하여, 개발 환경과 운영 환경의 차이를 크게 줄인다.
1. 왜 Docker를 쓰는가
환경 불일치 문제 해결
- 개발 환경과 운영 환경을 동일한 이미지로 통일할 수 있다.
- 팀원이 바뀌어도 같은 명령으로 동일한 환경을 재현할 수 있다.
배포 단순화
- 서버에 애플리케이션을 설치하는 대신 이미지를 실행하면 된다.
- 배포 절차를 표준화하기 쉽다.
빠른 실행과 낮은 오버헤드
- VM처럼 Guest OS 전체를 띄우지 않는다.
- Host OS 커널을 공유하므로 훨씬 가볍고 빠르다.
2. VM vs Container
| 구분 | VM | Container |
|---|---|---|
| 가상화 대상 | 하드웨어 수준 | 프로세스 수준 |
| OS | Guest OS 포함 | Host OS 커널 공유 |
| 무게 | 무거움 | 가벼움 |
| 실행 속도 | 상대적으로 느림 | 빠름 |
| 자원 사용량 | 큼 | 적음 |
핵심 차이: VM은 운영체제를 통째로 올린다. Container는 운영체제 커널을 공유한 채, 프로세스를 격리해서 실행한다. 즉, 컨테이너는 "작은 VM"이 아니라 격리된 프로세스 실행 환경에 더 가깝다.
3. Docker 구성 요소
Docker를 이해하려면 Engine, Daemon, CLI, Desktop을 구분해야 한다.
| 구성 요소 | 역할 |
|---|---|
| Docker Engine | 컨테이너를 생성하고 실행하는 핵심 엔진 전체 |
Docker Daemon (dockerd) |
백그라운드에서 이미지, 컨테이너, 네트워크, 볼륨을 실제 관리 |
| Docker CLI | 사용자가 명령어를 입력하는 클라이언트 |
| Docker Desktop | macOS/Windows에서 Docker를 쉽게 사용할 수 있게 해주는 GUI 포함 패키지 |
내부 동작 흐름:
사용자
↓ docker run ...
Docker CLI
↓ REST API (Unix socket / TCP)
Docker Daemon (dockerd)
↓
containerd
↓
runc
↓
컨테이너 실행
- CLI는 명령을 받는다.
- Daemon은 실제 작업을 수행한다.
containerd와runc는 컨테이너 실행의 하위 런타임 계층이다.- Docker Desktop은 주로 로컬 개발 편의성을 위한 도구다.
4. Image vs Container
Image
이미지는 애플리케이션 실행 환경 전체를 정의한 읽기 전용 템플릿이다.
OS 레이어, 런타임, 라이브러리, 애플리케이션 코드, 실행 설정이 레이어 구조로 쌓인다.
Container
컨테이너는 이미지를 기반으로 실행된 인스턴스다.
이미지 위에 Writable Layer가 추가되고, 실행 중 발생한 파일 변경은 이 레이어에 기록된다.
┌──────────────────────────┐
│ Writable Layer │ ← 컨테이너 실행 시 추가. 삭제 시 사라짐
├──────────────────────────┤
│ COPY app.jar │
├──────────────────────────┤ ← 읽기 전용 이미지 레이어
│ RUN apt-get install ... │
├──────────────────────────┤
│ FROM openjdk:17 │
└──────────────────────────┘
- 이미지 하나로 컨테이너 여러 개를 만들 수 있다.
docker rm으로 컨테이너를 삭제해도 이미지는 남는다.- 컨테이너를 삭제하면 Writable Layer에 저장된 변경 사항은 사라진다.
5. Dockerfile — 이미지 빌드 레시피
Dockerfile은 이미지를 어떻게 만들지 정의하는 선언형 파일이다.
FROM openjdk:17-jdk-slim
WORKDIR /app
COPY build/libs/my-app.jar app.jar
EXPOSE 8080
ENV SPRING_PROFILES_ACTIVE=prod
ENTRYPOINT ["java", "-jar", "app.jar"]
| 명령어 | 역할 |
|---|---|
FROM |
베이스 이미지 지정 |
WORKDIR |
작업 디렉토리 지정 |
COPY |
파일 복사 |
RUN |
빌드 중 명령 실행 |
ENV |
환경변수 설정 |
EXPOSE |
컨테이너가 사용할 포트 문서화 |
CMD |
기본 실행 명령 (덮어쓰기 가능) |
ENTRYPOINT |
컨테이너의 고정 실행 명령 |
6. ENTRYPOINT vs CMD
ENTRYPOINT |
CMD |
|
|---|---|---|
| 역할 | 실행 파일 고정 | 기본 인자 또는 명령 |
| 덮어쓰기 | --entrypoint 옵션으로만 가능 |
docker run 인자로 쉽게 가능 |
| ```dockerfile | ||
| ENTRYPOINT ["java", "-jar"] | ||
| CMD ["app.jar"] | ||
| ``` | ||
| ```bash | ||
| docker run my-app # java -jar app.jar | ||
| docker run my-app other.jar # java -jar other.jar | ||
| ``` |
실무 기준: 애플리케이션 실행 자체는
ENTRYPOINT, 기본 인자나 옵션은CMD. Spring Boot JAR 실행은ENTRYPOINT ["java", "-jar", "app.jar"]처럼 단순하게 가는 경우도 많다.
7. 레이어 캐시 최적화
Dockerfile의 각 명령은 레이어가 된다. 어떤 레이어가 바뀌면 그 아래 레이어 캐시는 모두 무효화된다.
# ❌ Bad — 소스 하나만 바뀌어도 의존성 설치와 빌드가 다시 일어남
COPY . .
RUN gradle build --no-daemon
# ✅ Good — 변경 빈도 낮은 것 먼저
COPY build.gradle settings.gradle ./
RUN gradle dependencies --no-daemon # 캐시 활용!
COPY src ./src
RUN gradle build --no-daemon
핵심 원칙: 변경 빈도가 낮은 것 먼저, 변경 빈도가 높은 것 나중에.
의존성 정의 파일을 먼저 복사하고, 소스 코드는 뒤에 복사하는 구조가 캐시 효율에 좋다.
8. .dockerignore
불필요한 파일이 빌드 컨텍스트에 포함되면 빌드 속도와 이미지 크기가 나빠진다.
.git
build/
*.log
node_modules
.idea
.gradle
- 빌드 컨텍스트 감소
- 빌드 속도 향상
- 불필요한 민감 정보 포함 방지
9. 멀티스테이지 빌드
빌드 환경과 실행 환경을 분리해서 최종 이미지를 가볍게 만드는 기법이다.
# Stage 1: Build
FROM gradle:8-jdk17 AS builder
WORKDIR /app
COPY . .
RUN gradle build --no-daemon
# Stage 2: Runtime — 빌드 도구 제거
FROM openjdk:17-jdk-slim
WORKDIR /app
COPY --from=builder /app/build/libs/*.jar app.jar
ENV SPRING_PROFILES_ACTIVE=prod
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
- 빌드 도구가 최종 이미지에 포함되지 않는다.
- 이미지 크기가 크게 줄어든다. (1GB+ → ~200MB)
- 공격 표면이 줄어든다.
10. DockerHub / Registry
DockerHub는 Docker에서 제공하는 대표적인 공개 이미지 레지스트리다.
GitHub가 소스코드 저장소라면, DockerHub는 이미지 저장소다.
| 종류 | 형식 | 예시 |
|---|---|---|
| Official Image | 이미지명:태그 |
postgres:15, nginx:1.27 |
| User Image | 유저명/이미지명:태그 |
myname/myapp:1.0 |
| ```bash | ||
| docker login # ~/.docker/config.json에 인증 정보 저장 | ||
| docker tag myapp myregistry/myapp:1.0.0 | ||
| docker push myregistry/myapp:1.0.0 | ||
| docker pull postgres:15 | ||
| ``` |
태그 전략
latest에만 의존하지 않는다.- 운영 환경에서는 명시적 버전을 사용한다.
- 베이스 이미지도 가급적 버전을 고정한다.
멀티 아키텍처 빌드 (buildx):
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t myregistry/myapp:1.0 \
--push .
buildx는 멀티 아키텍처(x86, ARM) 이미지가 필요할 때 사용한다.
11. Volume — 데이터 영속성
컨테이너 내부 데이터는 기본적으로 컨테이너 수명에 종속된다.
컨테이너를 삭제하면 Writable Layer 데이터도 사라진다.
| 방식 | 예시 | 용도 |
|---|---|---|
| Named Volume | -v pgdata:/var/lib/postgresql/data |
운영 환경 영속 데이터 |
| Bind Mount | -v $(pwd):/app |
개발 중 로컬 코드 연결 |
| tmpfs | --tmpfs /tmp |
메모리 기반 임시 데이터 |
| ```bash | ||
| docker run -d \ | ||
| --name pg-demo \ | ||
| -v pgdata:/var/lib/postgresql/data \ | ||
| postgres:15 |
docker rm pg-demo # 컨테이너만 삭제, 볼륨은 유지
docker volume rm pgdata # 볼륨까지 삭제
> **실무 기준:** 개발 시 Bind Mount, 운영 시 Named Volume 권장.
> DB 데이터는 컨테이너 내부에만 저장하지 않는다.
---
## 12. Network — 컨테이너 간 통신
Docker 네트워크는 컨테이너 간 통신과 외부 접근을 제어하는 가상 네트워크 계층이다.
| 드라이버 | 특징 | 주의점 |
|---------|------|--------|
| `bridge` (기본) | 가상 네트워크, 컨테이너 격리 | 기본 bridge는 이름 기반 DNS 미지원 |
| `host` | 호스트 네트워크 공유, IP 동일 | 포트 충돌, 격리 약화 |
| `none` | 네트워크 완전 차단 | 외부 통신 불가 |
**사용자 정의 네트워크 — 실무 필수:**
```bash
docker network create my-net
docker run -d --network my-net --name db postgres
docker run -d --network my-net --name app myapp
# app 컨테이너 내부에서 서비스명으로 접근 가능
curl http://db:5432
기본
bridge네트워크는 컨테이너 이름 기반 DNS를 지원하지 않는다.
반드시 사용자 정의 네트워크를 생성해야 서비스명으로 통신 가능하다.
13. Port Binding — 호스트와 컨테이너 연결
컨테이너는 격리된 네트워크 공간에서 실행되므로, 외부에서 접근하려면 포트 바인딩이 필요하다.
docker run -d \
--name my-spring-app \
-p 8080:8080 \
my-spring-image
# 왼쪽: 호스트 포트 / 오른쪽: 컨테이너 포트
localhost:8080 (호스트)
↓ 포트 바인딩
컨테이너:8080
14. EXPOSE vs -p
EXPOSE |
-p |
|
|---|---|---|
| 위치 | Dockerfile | docker run 옵션 |
| 역할 | 사용하는 포트 문서화 | 실제 포트 공개 |
| 외부 접근 가능 여부 | ❌ | ✅ |
EXPOSE는 설명이다. 실제 외부 공개는-p가 한다.EXPOSE만 써서는 외부에서 접근할 수 없다.
15. 환경변수와 시크릿
docker run -e SPRING_PROFILES_ACTIVE=prod myapp
# Compose
environment:
SPRING_PROFILES_ACTIVE: prod
주의: 비밀번호, 토큰 같은 민감 정보는 이미지에 직접 넣지 않는다.
운영 환경에서는.env, Secret Manager, Vault 같은 별도 방식이 더 적절하다.
# ❌ 안 좋은 예 — 이미지 히스토리에 남을 수 있음
ENV DB_PASSWORD=my-secret-password
16. Healthcheck와 Readiness
컨테이너가 "시작됨"과 "준비됨"은 다르다.
PostgreSQL 컨테이너가 시작되었다고 해서, 즉시 연결 가능한 것은 아니다.
# Compose
healthcheck:
test: ["CMD-SHELL", "pg_isready -U demo"]
interval: 10s
timeout: 5s
retries: 5
- 앱이 DB보다 먼저 올라와 연결 실패하는 문제를 방지한다.
- 운영 상태를 더 정확히 판단할 수 있다.
17. 컨테이너 라이프사이클과 기본 명령어
# 상태 확인
docker ps # 실행 중 컨테이너 목록
docker ps -a # 전체 컨테이너 목록 (종료된 것 포함)
docker images # 이미지 목록
# 로그 및 접근
docker logs -f app # 실시간 로그 확인
docker exec -it app sh # 컨테이너 내부 셸 진입
# 생명주기 관리
docker stop app # 중지
docker start app # 다시 시작
docker rm app # 컨테이너 삭제
docker rmi myapp:1.0 # 이미지 삭제
runvsstart차이:docker run은 새 컨테이너를 만든다.docker start는 기존 컨테이너를 다시 켠다.
18. Docker Compose — 멀티 컨테이너 관리
단일 컨테이너는 docker run으로 충분하지만, 실제 서비스는 보통 여러 컨테이너로 구성된다.
(Spring App + PostgreSQL + Redis + Nginx 등)
docker run 여러 개로 관리하면 명령어가 길고 실수하기 쉽다.
Compose는 이를 YAML 하나로 선언형 관리하게 해준다.
| 구분 | 역할 |
|---|---|
| Dockerfile | 이미지를 어떻게 빌드할지 정의 |
docker run |
컨테이너 하나를 어떻게 실행할지 정의 |
| Docker Compose | 여러 컨테이너를 어떻게 함께 실행하고 연결할지 정의 |
| ```yaml | |
| services: | |
| app: | |
| image: my-spring-app:1.0 | |
| ports: | |
| - "8080:8080" | |
| environment: | |
| SPRING_PROFILES_ACTIVE: prod | |
| SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/mydb | |
| depends_on: | |
| db: | |
| condition: service_healthy | |
| networks: | |
| - app-net |
db:
image: postgres:15
environment:
POSTGRES_DB: mydb
POSTGRES_USER: demo
POSTGRES_PASSWORD: secret
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U demo"]
interval: 10s
timeout: 5s
retries: 5
networks:
- app-net
networks:
app-net:
driver: bridge
volumes:
pgdata:
```bash
docker compose up -d # 전체 실행
docker compose down # 전체 종료 (볼륨 유지)
docker compose down -v # 볼륨까지 삭제
docker compose logs -f # 실시간 로그
docker compose ps # 서비스 상태 확인
docker compose exec app sh # 서비스 내부 접근
19. depends_on의 한계
# ❌ 단순 depends_on — 시작 순서만 보장, readiness는 보장 안 함
depends_on:
- db
# ✅ 권장 방식 — db healthcheck 통과 후 app 시작
depends_on:
db:
condition: service_healthy
condition: service_healthy는db쪽에healthcheck가 정의되어 있어야 의미가 있다.
단순depends_on만으로는 DB가 실제로 연결 가능한 상태인지 보장하지 않는다.
20. ports vs expose in Compose
ports |
expose |
|
|---|---|---|
| 공개 범위 | 호스트까지 공개 | Docker 네트워크 내부만 |
| 외부 접근 | ✅ | ❌ |
| 사용 예 | 브라우저에서 직접 접근 | 내부 서비스 간 통신만 필요 |
| ```yaml | ||
| ports: | ||
| - "8080:8080" # 호스트:컨테이너 — 외부 접근 가능 |
expose:
- "8080" # 내부 네트워크에만 노출
21. 개발/운영 관점에서의 실무 팁
개발 환경
- Bind Mount로 코드 실시간 반영
- Compose로 앱 + DB + 캐시 한 번에 실행
- 로그와 포트 노출을 쉽게 확인
운영 환경
- 버전 고정 이미지 사용
- Named Volume 사용
- 민감 정보는 외부 시크릿 관리
- 리버스 프록시(Nginx 등) 앞단 배치
- 헬스체크와 재시작 정책 설정
# 재시작 정책 예시 restart: unless-stopped
22. 자주 하는 오해 정리
1) EXPOSE하면 외부 접속 가능하다
아니다. EXPOSE는 문서화다. 실제 외부 공개는 -p 또는 ports가 담당한다.
2) 컨테이너는 VM과 완전히 같다
아니다. 컨테이너는 Host OS 커널을 공유하는 격리된 프로세스 환경이다.
3) depends_on만 있으면 DB 준비까지 보장된다
아니다. 시작 순서만 보장한다. readiness는 healthcheck로 보완해야 한다.
4) 컨테이너 안에 DB 데이터를 저장해도 괜찮다
위험하다. 컨테이너 삭제 시 데이터 손실 가능성이 있다. 운영 DB는 반드시 볼륨을 사용해야 한다.
5) latest 태그는 항상 최신이라 안전하다
편리하지만 안전하지 않다. 어떤 버전이 배포되는지 추적하기 어렵다.
23. 한 번에 보는 최소 명령 세트
# 이미지 빌드
docker build -t myapp:1.0 .
# 컨테이너 실행
docker run -d -p 8080:8080 --name myapp myapp:1.0
# 실행 중 컨테이너 확인
docker ps
# 로그 확인
docker logs -f myapp
# 컨테이너 내부 접속
docker exec -it myapp sh
# 중지 및 삭제
docker stop myapp
docker rm myapp
# Compose 실행
docker compose up -d
# Compose 종료
docker compose down
24. 핵심 요약
- Docker는 환경 불일치 문제를 해결하기 위한 컨테이너 플랫폼이다.
- 이미지는 설계도, 컨테이너는 실행 인스턴스다.
- Dockerfile은 이미지를 정의하고, Compose는 여러 컨테이너 실행을 정의한다.
- 볼륨은 데이터 영속성을 보장한다.
- 네트워크는 컨테이너 간 통신을 담당한다.
- 외부 접근에는 포트 바인딩(
-p)이 필요하다. - 운영 환경에서는 태그 고정, 시크릿 분리, healthcheck, volume 전략이 중요하다.
이미지와 컨테이너를 구분하고, Dockerfile은 빌드 / Compose는 실행 구조 관리라는 점을 명확히 잡으면
이후 Kubernetes나 CI/CD로 넘어갈 때도 훨씬 수월하다.