Docker

Docker 정리

승주우에요 2026. 4. 1. 07:38

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은 실제 작업을 수행한다.
  • containerdrunc는 컨테이너 실행의 하위 런타임 계층이다.
  • 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    # 이미지 삭제

run vs start 차이:
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_healthydb쪽에 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로 넘어갈 때도 훨씬 수월하다.