Developer.

[멋사 백엔드 19기] TIL 60일차 Docker(1)

📂 목차


📚 본문

가상머신과 컨테이너

가상머신

VM 이라고도 불리며, 하이퍼바이저를 통해 물리 서버 위에 여러 독립된 운영체제를 구동하는 방식이다. 따라서 Linux VM, Window VM 등등은 운영체제가 각각 필요하게 된다.

┌──────────────────────────────────────────┐
│       App A    │    App B    │  App C    │
├────────────────┼─────────────┼───────────┤
│   Guest OS 1   │ Guest OS 2  │ Guest OS 3│
├────────────────┴─────────────┴───────────┤
│          Hypervisor (VMware, VirtualBox) │
├──────────────────────────────────────────┤
│            Host Operating System         │
├──────────────────────────────────────────┤
│          Physical Infrastructure         │
└──────────────────────────────────────────┘

장점

  • 커널까지 완전히 분리되어 있어 컨테이너보다 격리가 강함
  • 일반 서버 처럼 다루면 되므로 레거시 시스템 운영에 적합하다
  • 하이퍼바이저 기능을 활용하여 CPU/메모리 분리 배정이 가능하다
  • 서로 다른 OS 를 병렬로 실행 가능하다.

단점

  • 게스트 OS 까지 포함하기 때문에 컨테이너 대비 CPU/RAM 오버헤드가 크다
  • VM 이미지가 GB 단위로 크고 배포/확장이 부담이 간다.
  • OS 전체가 올라가야 해서 올리는데 오래 걸린다.

컨테이너

컨테이너는 호스트 OS의 커널을 공유하면서, 애플리케이션 실행에 필요한 프로세스와 라이브러리를 격리해 독립된 실행 환경을 제공하는 방식이다.

┌─────────────────────────────────────────┐
│   App A    │    App B    │    App C     │
├────────────┼─────────────┼──────────────┤
│  Libs/Bins │  Libs/Bins  │  Libs/Bins   │
├────────────┴─────────────┴──────────────┤
│      Docker Engine (Container Runtime)  │
├─────────────────────────────────────────┤
│        Host Operating System (Kernel)   │
├─────────────────────────────────────────┤
│         Physical Infrastructure         │
└─────────────────────────────────────────┘

위를 보면, 호스트 커널을 공유하는 것을 볼 수 있고, 이는 Linux Namespaces, cgroups 기반으로 커널을 공유하며 격리를 수행하게 된다. 그 아래의 Docker Engine 이 및 기타 컨테이너 런타임이 프로세스 격리와 자원 관리를 담당한다.

장점

  • 게스트 OS 가 없어 이미지가 작고 배포가 빠르다.
  • 커널 공유 구조로 메모리와 CPU 오버헤드가 적다.
  • 빌드, 테스트, 배포 속도가 매우 빠르다
  • 하나의 서버에 많은 컨테이너를 올리기 쉽다.

단점

  • 동일한 커널 계열에서만 실행 가능
  • 커널을 공유하므로 취약점이 있을 시 영향 범위 증가
  • 커널 기능이 필요한 경우 VM 이 필요할 수도 있다.

도커는 이런 컨테이너 아키텍처를 따라서 만들어졌다.

Layer 구조와 Caching 개념

도커 이미지는 여러 개의 레이어로 구성된다. Dockerfile 의 각 명령어가 한 레이어를 생성하며, 레이어 캐싱을 통해 이전과 동일한 명령어 / 컨텍스트면 다시 빌드하지 않고 재사용할 수 있다. 위에서 아래로 평가되기 때문에 자주 변경되는 명령은 아래로, 변경이 적은 명령은 위로 배치하는 것이 효율적이다.

캐싱이 깨지는 케이스

  • COPY 로 가져오는 소스가 변경됨
  • RUN 명령어 내부 내용이 변경됨
  • 순서 변경

Docker 아키텍처

┌─────────────┐     REST API      ┌──────────────┐
│   Docker    │ ←───────────────→ │   Docker     │
│   Client    │                   │   Daemon     │
│  (docker)   │                   │  (dockerd)   │
└─────────────┘                   └──────────────┘
                                         │
                       ┌─────────────────┼─────────────────┐
                       │                 │                 │
                  ┌────▼────┐       ┌────▼────┐       ┌────▼────┐
                  │ Images  │       │Container│       │ Network │
                  └─────────┘       └─────────┘       └─────────┘
                       │
                  ┌────▼────┐
                  │Registry │
                  │(Hub/ECR)│
                  └─────────┘

우리는 도커 틀라이언트(도커 앱, 도커 CLI 등등)를 통해 dockerd 와 통신을 하고 직접적인 작업은 전부 dockerd 가 맡게 된다. dockerd 와 클라이언트 사이에서는 rest api 통신을 통해 주고 받게 된다(json 보냄).

  • Docker Client: Docker CLI, Docker Desktop, Code Docker 확장 등이 모두 클라이언트에 속한다. 단지 명령어 전달만 수행한다.

Docker CLI는 REST API로 Docker 데몬과 통신
Unix 소켓(/var/run/docker.sock) 또는 TCP 사용
원격 Docker 호스트에도 연결 가능:

docker -H tcp://192.168.1.100:2375 ps

  • Docker Daemon: 컨테이너 관리, 이미지 관리, 네트워크 관리, 볼륨 관리, 로컬 이미지 저장, Registry 와 통신하여 push/pull 수행을 담당한다.

    • Containers: 이미지로부터 실행되는 실제 프로세스이며 네임스페이스와 cgroups 을 통해 독립된 실행 환경을 제공한다.

    • Network: 컨테이너간 통신 구조(bridge, host, overlay 등)를 통해 컨테이너끼리 혹은 외부와 연결될 수 있도록 관리한다.

    • Registry: 이미지 저장소이며 기본적으로 우리는 Image 를 Docker Hub 에서 가져오지만 이 외에도 AWS ECR, Github Registry 등등 사설 CR(Container Registry) 에게서도 얻을 수 있다.

Public Registry 사용 시 주의사항:
신뢰할 수 있는 이미지만 사용 (Official Images 우선)
이미지 스캔 도구로 취약점 검사 (Trivy, Clair)
최신 버전 유지 및 정기적 업데이트

docker hub 에서 image 는 검색하여 사용하자.

Dockerfile 베스트 프랙티스

  • 불필요 레이어 최소화(RUN 명령어들은 && 로 묶기)
  • 멀티 스테이지 빌드로 최종 이미지 크기 최소화
  • 특정 태그 고정(latest 보다 버전 명시)
  • 캐시 최적화를 고려한 명령 순서 구성
  • .dockerignore 파일 적극 활용
  • root 실행 지양 -> USER 로 앱 실행
  • 가능한 슬림 이미지 사용(alpine, distroless, slim)
  • 빌드 아티팩트(node_modules, …)를 직접 COPY 하지 말고 Docker 내부에서 빌드

컨테이너 볼륨

컨테이너는 기본적으로 휘발성이기 때문에, 컨테이너를 지우면 내부 파일도 같이 삭제된다. 따라서 DB 데이터를 보존한다거나 로그를 저장한다거나 업로드 파일을 유지하거나 여러 컨테이너 간 데이터 공유 등등은 볼륨이 없이는 불가능한 작업이다.

이를 해결하기 위해 Docker Volume 이라는 기능이 있다. 컨테이너 외부에 존재하면서 컨테이너와 연결되는 독립된 영구 저장소이다.

  • 컨테이너 삭제해도 데이터 유지
  • 여러 컨테이너가 같은 공간 공유 가능
  • OS 의 커널 기능을 이용해 빠르고 안전하게 마운트

Named Volume

도커가 관리하는 따로 분리된 저장 공간이다.

docker volume create mydata
docker run -v mydata:/var/lib/mysql ...

실제 호스트 물리 저장소 내의 /var/lib/mysql 에 존재하며, 도커가 알아서 관리를 하고, 별칭을 통하여 접근할 수 있다. 가장 많이 쓰이는 방식이다.

Bind Mount

호스트의 특정 경로를 그대로 컨테이너 내부에 연결한다.
여기서는 별칭이 없어 Named Volume 과는 차이가 있다.

docker run -v /host/path:/container/path
  • 개발에서 주로 사용
  • 호스트 파일 시스템 구조에 의존

하지만 운영환경에서는 관리가 어려워 잘 안씀

Docker Storage 구조

Docker 의 저장 구조는 크게 3가지 레이어로 나뉜다.

  1. 이미지 레이어
  2. 컨테이너 레이어
  3. 볼륨

3가지가 다 합쳐 컨테이너의 파일 시스템을 형성한다.

이미지 레이어

docker pull 하면 도커는 여러 개의 레이어로 분할된 파일 시스템을 저장한다.

ubuntu:latest
├─ layer1 (기본 리눅스 파일)
├─ layer2 (패키지 설치)
└─ layer3 (환경설정)

특징은 불변이라는 것이다.

컨테이너 레이어

컨테이너를 docker run 할 때 생성되는 레이어이며, Dockerfile 에 기록된 지시문들 하나하나와도 같다.

이미지 레이어 (RO)
     +
컨테이너 레이어 (RW)

볼륨

외부에 있는 스토리지이다. 설명은 생략한다.

Docker Logging 방식

[컨테이너 내부]
    │   stdout / stderr
    ▼
[containerd / dockerd]
    │   (로그 드라이버로 전달)
    ▼
[Log Driver]  ← docker engine 설정으로 선택
    ├─ json-file (기본)
    ├─ local
    ├─ journald
    ├─ syslog
    ├─ fluentd
    ├─ awslogs
    └─ gelf …

도커는 컨테이너 내부에서 출력되는 stdout, stderr를 기반으로 로그를 처리하게 된다. docker 는 단순히 컨테이너의 출력 스트림을 수집한 뒤, 지정된 로그 드라이버에게 던져주는 구조가 된다.

기본 로그 방식: json-file

대부분의 Linux Docker 환경에서는 기본적으로 json-file 로그 드라이버가 사용된다.

  • 컨테이너마다 로그 파일이 생성됨
  • 저장 위치: /var/lib/docker/containers/<container-id>/<container-id>-json.log

이 파일은 로그가 쌓일수록 디스크를 많이 차지하기 때문에 운영에서는 logrotate 또는 다른 로그 드라이버 사용을 고려함

local

도커가 자체적으로 로그를 더 효율적으로 관리하는 방식이다.

  • 바이너리 형태로 압축 및 저장
  • 메타데이터 구분 쉬움
  • json-file 보다 빠르고 공간 효율 좋음
  • Docker Desktop 에서는 default 로 쓰는 경우도 있다

운영환경에서는 json-file 대신 local 을 추천하는 경우가 많다.

journald

시스템 데몬 로그(systemd-journald)에게 직접 전달되는 방식이며 로그가 OS의 journalctl 명령에서 확인이 가능하다. 이는 리눅스에서만 사용할 수 있다.

syslog

컨테이너 로그를 로컬 또는 원격 syslog 서버로 바로 전송한다.

실무에서는 다음과 같은 환경에서 사용하게 된다:

  • On-premise 서버 클러스터
  • 기존 syslog 인프라가 있었을 때
  • 중앙화된 보안 로그 관리 필요

Fluentd / GELF / AWS Logs / GCP Logs

컨테이너 로그 -> 외부 로그 수집 시스템으로 바로 전송

  • Fluentd -> Elasticsearch, Loki, Splunk, Kafka
  • GELF -> Graylog, logstash
  • awslogs -> CloudWatch
  • gcp logging -> Google Cloud Logging

Kubernetes 는 stdout -> Fluentd -> Elastic/Loki 로 들어가는 구조다.

Docker Logging 동작 원리 Docker Engine은 다음을 한다:

  1. 컨테이너 프로세스의 stdout/stderr 파이프 FD를 잡는다
  2. containerd-shim이 그 스트림을 읽는다
  3. Docker Engine이 읽고 지정된 로그 드라이버에게 전달한다
  4. 따라서 컨테이너가 죽어도 로그는 이미 수집되어 저장됨