프로필 로고
2026-04-15

Docker

호스트 OS 커널을 공유하는 컨테이너 기반 플랫폼 Docker의 동작 원리. VM과의 차이, Namespace·cgroups·chroot·Union File System 격리, 이미지 레이어 구조와 pull, 컨테이너 생명주기, 포트 포워딩과 볼륨 마운트를 다룬다.

  • Docker
  • 컨테이너
  • DevOps
  • 인프라
  • 배포

도커는 호스트 OS의 커널을 공유하는 컨테이너 기반 플랫폼으로, 가상 머신보다 가볍고 빠르게 동일한 실행 환경을 어디서든 재현하기 위한 도구이다.

도커의 동작 원리

도커 등장 배경

  1. 소프트웨어 개발에서는 "내 컴퓨터에서는 잘 되는데 서버에서는 안 된다"는 문제가 자주 발생한다.

  2. 이는 개발 환경과 운영 환경의 OS, 라이브러리 버전, 설정값 등이 서로 다르기 때문이다.

  3. 과거에는 이 문제를 해결하기 위해 가상 머신(VM)을 사용했다.

  4. 그러나 VM은 OS 전체를 가상화하기 때문에 무겁고 실행 속도가 느리다는 한계가 있다.

  5. 이런 한계를 보완하기 위해 2013년 컨테이너 기반 플랫폼인 도커가 등장했다.

도커란 무엇인가

  1. 도커는 애플리케이션과 그 실행에 필요한 환경을 하나로 묶어, 어디서든 동일하게 실행할 수 있게 해주는 컨테이너 기반 플랫폼이다.

  2. VM이 OS 전체를 복제하는 것과 달리, 도커는 호스트 OS의 커널을 공유하는 방식으로 동작한다.

  3. 이 차이 덕분에 VM보다 가볍고 시작 속도가 빠르다.

  4. 즉, 도커는 환경 차이로 인한 문제를 환경 자체를 함께 배포함으로써 해결하는 도구이다.

호스트 OS의 커널을 공유한다는 것의 의미

  1. 커널은 OS의 핵심으로, 하드웨어(CPU, 메모리, 디스크)와 프로그램 사이에서 자원을 관리하는 역할을 한다.

  2. 프로그램이 파일을 읽거나 네트워크를 사용할 때, 실제로는 커널에 요청(시스템 콜)을 보내는 방식으로 동작한다.

  3. VM은 가상 머신마다 커널을 따로 가지기 때문에, OS 전체를 새로 띄우는 것과 같은 비용이 든다.

  4. 반면 도커 컨테이너는 커널을 별도로 갖지 않고, 호스트 OS의 커널을 그대로 빌려 쓴다.

  5. 컨테이너 안의 프로그램은 중간 OS 없이 호스트 커널에 직접 시스템 콜을 보낸다.

  6. 다만 컨테이너는 자신만의 파일 시스템, 프로세스 목록, 네트워크 공간을 가지도록 격리되어 있다.

  7. 정리하면 커널은 공유하되, 각 컨테이너는 서로를 볼 수 없도록 분리된 구조이다.

호스트 커널 공유의 트레이드오프

  1. 커널을 공유하기 때문에 컨테이너는 호스트 OS와 동일한 커널 위에서만 동작할 수 있다.

  2. 예를 들어 호스트가 Linux라면 컨테이너도 Linux 커널 기반 환경만 실행할 수 있다.

  3. Windows나 macOS에서 도커를 쓸 때 내부적으로 경량 Linux VM이 먼저 뜨는 이유가 이 때문이다.

  4. Windows에서는 과거의 무거운 Hyper-V 기반 VM 대신 현재 WSL 2를 주로 활용하며, 호스트 윈도우 커널과 분리된 경량 리눅스 유틸리티 VM 위에서 도커를 구동한다.

  5. macOS에서는 LinuxKit으로 구성된 경량 리눅스 VM을 HyperKit 또는 Apple Virtualization.framework 위에서 실행한다.

  6. 또한 커널을 공유하므로, 커널 자체에 보안 취약점이 생기면 모든 컨테이너가 동시에 영향을 받는다.

  7. VM은 커널이 완전히 분리되어 있어 한 VM이 뚫려도 다른 VM은 안전하지만, 컨테이너는 그 격리 수준이 더 얕다.

  8. 컨테이너는 커널 파라미터나 커널 모듈을 자유롭게 변경하기 어렵고, 호스트 커널 버전에 종속된다.

  9. 정리하면 커널 공유 덕분에 가볍고 빠르지만, 그 대가로 OS 수준의 격리성과 이식성 일부를 포기한 구조이다.

컨테이너 격리를 가능하게 하는 리눅스 커널 기능

  1. 컨테이너는 하나의 리눅스 커널을 여러 프로세스가 안전하게 공유하는 방식으로 동작한다.

  2. 이를 위해 각 프로세스가 자신만의 독립된 환경을 가진다고 인식하도록 만들어야 한다.

  3. 이를 가능하게 하는 리눅스 커널 기능이 Namespace, cgroups, chroot, Union File System이다.

  4. Namespace는 프로세스가 볼 수 있는 시스템 자원의 범위를 분리하는 기능으로, PID·네트워크·마운트·사용자 등을 격리한다.

  5. 예를 들어 컨테이너 안에서 ps aux를 실행하면 호스트의 다른 프로세스는 보이지 않고, 컨테이너 내부 프로세스만 PID 1번부터 보이는 것처럼 동작한다.

  6. cgroups는 컨테이너가 사용할 수 있는 CPU, 메모리, 디스크 I/O 등의 자원량을 제한하는 기능이다.

  7. chroot는 프로세스가 볼 수 있는 루트 디렉토리(/)를 특정 경로로 한정해, 그 바깥의 파일 시스템에 접근하지 못하게 한다.

  8. Union File System은 여러 파일 시스템 레이어를 하나로 합쳐 보이게 하는 기술로, 도커 이미지의 레이어 구조를 가능하게 하는 기반이다.

  9. 즉, 컨테이너는 별도의 OS가 아니라 이 네 가지 커널 기능이 만들어내는 격리된 프로세스이다.

도커 이미지

이미지란 무엇인가

  1. 도커 이미지는 컨테이너를 만들기 위한 읽기 전용 패키지이다.

  2. 애플리케이션 코드, 런타임, 라이브러리, 환경 변수 등 실행에 필요한 모든 것이 이미지 안에 포함되어 있다.

  3. 이미지는 흔히 설계도로 비유되지만, 실제로는 실행 준비가 끝난 완성된 스냅샷에 더 가깝다.

  4. 이미지 안에는 파일 시스템, 라이브러리, 실행 파일 등 실체가 있는 데이터가 들어 있기 때문이다.

  5. 다만 이미지 자체는 실행되지 않고 읽기 전용으로 존재하므로, 실행에 필요한 모든 것이 담긴 읽기 전용 패키지로 이해하는 것이 정확하다.

  6. 이미지는 Dockerfile이라는 파일에 명령어를 작성해 빌드한다.

    FROM node:18              # 베이스 이미지 지정
    WORKDIR /app              # 작업 디렉토리 설정
    COPY . .                  # 호스트의 파일을 이미지로 복사
    RUN npm install           # 의존성 설치
    CMD ["node", "index.js"]  # 컨테이너 실행 시 동작할 명령

이미지의 레이어 구조

  1. 도커 이미지는 하나의 덩어리가 아니라, 변경 사항을 층층이 쌓은 레이어들의 집합으로 이루어진다.

  2. Dockerfile의 각 명령어가 실행될 때마다 새로운 레이어가 하나씩 추가된다.

    FROM node:18        # 레이어 1: node 베이스 이미지
    WORKDIR /app        # 레이어 2: 작업 디렉토리 설정
    COPY . .            # 레이어 3: 파일 복사
    RUN npm install     # 레이어 4: 의존성 설치
  3. 각 레이어는 이전 레이어와의 차이점(변경된 파일)만 저장한다.

  4. 이미지를 다시 빌드할 때, 변경되지 않은 레이어는 캐시에서 그대로 재사용된다.

  5. 예를 들어 코드만 수정했다면 COPY . . 이후의 레이어만 다시 빌드되고, 그 아래의 node:18 레이어는 다시 받지 않는다.

  6. 여러 이미지가 동일한 베이스 레이어를 공유할 수 있어, 디스크 공간도 절약된다.

이미지 pull의 동작 방식

  1. docker pull은 폴더 하나를 통째로 다운로드하는 것이 아니라, 이미지를 구성하는 레이어들을 하나씩 내려받는 동작이다.

  2. 이미 로컬에 같은 레이어가 있다면 그 레이어는 건너뛰고, 없는 레이어만 새로 받는다.

  3. 내려받은 레이어들은 도커가 관리하는 로컬 저장소에 쌓이며, 도커가 이를 조합해 하나의 이미지로 인식한다.

  4. 사용자에게는 폴더처럼 노출되지 않고, docker images 명령어로만 목록을 확인할 수 있다.

  5. 즉, 개념적으로는 패키지 다운로드와 비슷하지만, 실제로는 레이어 단위로 관리되는 도커 전용 저장 방식이다.

Docker Hub

  1. 도커 허브는 도커 이미지를 저장하고 공유하는 공식 레지스트리 서비스이다.

  2. GitHub이 코드를 올리고 받는 공간이라면, 도커 허브는 이미지를 올리고 받는 공간이다.

  3. node, nginx, mysql 같은 공식 이미지가 도커 허브에 공개되어 있어 바로 가져다 쓸 수 있다.

  4. 직접 만든 이미지도 도커 허브에 올려 팀원과 공유하거나 서버에 배포할 수 있다.

  5. 이미지를 올리고 내려받는 명령어는 다음과 같다.

    docker push myusername/my-app   # 이미지를 도커 허브에 업로드
    docker pull myusername/my-app   # 도커 허브에서 이미지 다운로드
  6. 이를 통해 개발자는 별도의 환경 설정 없이 이미지 하나로 어디서든 동일한 애플리케이션을 실행할 수 있다.

도커 컨테이너

컨테이너란 무엇인가

  1. 컨테이너는 이미지를 실제로 실행한 인스턴스이다.

  2. 이미지가 정적인 패키지라면, 컨테이너는 그 패키지를 메모리에 올려 동작시키고 있는 살아 있는 프로세스 단위이다.

  3. 하나의 이미지로 여러 개의 컨테이너를 동시에 실행할 수 있다.

  4. 각 컨테이너는 독립된 실행 환경을 가지므로, 한 컨테이너의 문제가 다른 컨테이너에 영향을 주지 않는다.

  5. 컨테이너는 docker run 명령으로 이미지를 기반으로 실행한다.

    docker run -p 3000:3000 my-app-image
    # 이미지를 컨테이너로 실행하고, 호스트의 3000번 포트와 연결
  6. 컨테이너를 삭제하면 내부에서 생긴 데이터는 기본적으로 사라지며, 이미지 자체는 변하지 않는다.

동일한 이미지로 여러 컨테이너를 실행하는 원리

  1. 이미지는 읽기 전용이고, 컨테이너는 그 이미지 위에 얇은 쓰기 가능 레이어를 추가해 실행된다.

  2. 따라서 같은 이미지를 기반으로 컨테이너를 여러 개 만들어도, 이미지 자체는 공유되며 복사되지 않는다.

  3. 각 컨테이너는 자신만의 쓰기 레이어를 가지므로, 서로 독립적으로 동작한다.

  4. 예를 들어 같은 웹 서버 이미지로 컨테이너 3개를 띄우면, 포트만 다르게 해서 3개의 독립된 서버를 동시에 운영할 수 있다.

    docker run -p 3000:80 my-web  # 컨테이너 A
    docker run -p 3001:80 my-web  # 컨테이너 B
    docker run -p 3002:80 my-web  # 컨테이너 C
  5. 이는 트래픽이 몰릴 때 동일한 이미지로 컨테이너를 빠르게 늘리는 수평 확장의 핵심 원리이다.

docker run vs docker start

  1. docker run은 이미지를 기반으로 새로운 컨테이너를 생성하고 즉시 실행하는 두 단계를 한 번에 수행한다.

  2. 내부적으로는 이미지 레이어 위에 새 쓰기 레이어를 할당하고, 네임스페이스와 cgroups를 새로 구성한 뒤 프로세스를 시작한다.

  3. 반면 docker start는 이미 존재하는 컨테이너, 즉 쓰기 레이어와 설정이 그대로 남아 있는 컨테이너를 다시 깨우는 명령이다.

  4. 새 레이어를 만들거나 네임스페이스를 새로 구성하지 않고, 멈춰 있던 프로세스를 다시 스케줄링하는 것에 가깝다.

  5. 따라서 docker run은 매번 새 컨테이너를 찍어내고, docker start는 기존 컨테이너를 재가동한다.

    docker run my-image       # 이미지 → 새 컨테이너 생성 + 실행
    docker stop my-container  # 컨테이너 중단 (컨테이너는 남아 있음)
    docker start my-container # 기존 컨테이너 재실행 (레이어 유지)

중단된 컨테이너의 자원 소모

  1. docker ps -a에서 보이는 중단된 컨테이너는 프로세스가 없는 상태이다.

  2. 프로세스가 없으므로 CPU와 메모리는 소모하지 않는다.

  3. 다만 컨테이너의 쓰기 레이어, 즉 컨테이너 안에서 생긴 파일 변경 사항은 디스크에 남아 있다.

  4. 컨테이너 수가 많아질수록 이 쓰기 레이어들이 디스크 공간을 조금씩 차지한다.

  5. 또한 컨테이너의 메타데이터(이름, 설정, 네트워크 정보 등)도 도커 데몬이 관리하며 소량의 메모리를 사용한다.

  6. 정리하면 중단된 컨테이너는 CPU·메모리는 소모하지 않지만 디스크를 소모하므로, 불필요한 컨테이너는 docker rm으로 정리하는 것이 좋다.

실행 중인 컨테이너 삭제가 기본적으로 거부되는 이유

  1. docker rm은 기본적으로 실행 중인 컨테이너에 대해서는 동작을 거부한다.

  2. 이는 리눅스 커널이 강제하는 제약이 아니라, 도커 엔진(Docker Daemon)이 직접 거는 소프트웨어 레벨의 보호 장치이다.

  3. 실제로 리눅스 커널은 실행 중인 프로세스의 바이너리나 마운트된 파일 시스템을 삭제·언마운트하는 것을 막지 않는다.

  4. 예를 들어 리눅스에서는 실행 중인 프로그램의 바이너리 파일을 지워도 프로세스는 메모리 위에서 정상적으로 계속 동작한다.

  5. 그러나 운영 관점에서, 실행 중인 컨테이너를 무심코 삭제하면 서비스가 갑자기 끊기거나 데이터가 의도치 않게 손실될 수 있다.

  6. 도커는 이런 실수를 방지하기 위해 컨테이너 상태(Running)를 추적하고, 실행 중인 컨테이너의 rm 요청을 거부한다.

  7. 따라서 정상적인 절차는 docker stop으로 프로세스를 안전하게 종료한 뒤 docker rm을 실행하는 것이다.

  8. 한 번에 강제로 제거해야 한다면 -f 옵션을 사용해 도커가 내부적으로 SIGKILL을 보낸 뒤 컨테이너를 삭제하도록 할 수 있다.

    docker stop my-container && docker rm my-container  # 권장: 안전한 정리
    docker rm -f my-container                            # 강제 종료 후 즉시 삭제
  9. 즉, 실행 중인 컨테이너의 삭제 금지는 커널 차원의 제약이 아니라 도커 엔진이 제공하는 안전 가드이다.

컨테이너의 독립적인 포트와 파일 시스템

  1. 포트는 네트워크에서 특정 프로세스를 구분하기 위한 번호로, 하나의 포트는 동시에 하나의 프로세스만 사용할 수 있다.

  2. 도커는 리눅스 Namespace 기술로 컨테이너마다 독립된 네트워크 공간을 만들어준다.

  3. 따라서 컨테이너 내부에서는 자신이 8080 포트를 쓴다고 인식하지만, 호스트 입장에서는 그 포트가 그대로 보이지 않는다.

  4. 호스트와 컨테이너의 포트를 연결하려면 명시적으로 포트 포워딩을 설정해야 한다.

    docker run -p 3000:8080 my-app
    # 호스트의 3000번 포트 → 컨테이너의 8080번 포트로 연결
  5. 파일 시스템도 마찬가지로 Namespace를 통해 컨테이너마다 독립된 루트 디렉토리(/)가 주어진다.

  6. 컨테이너 안에서 /app 폴더를 만들어도 호스트에는 그 폴더가 존재하지 않는다.

  7. 반대로 호스트의 /home 폴더도 컨테이너 안에서는 기본적으로 보이지 않는다.

  8. 호스트의 특정 폴더를 컨테이너와 공유하려면 볼륨 마운트를 명시적으로 설정해야 한다.

    docker run -v /host/data:/container/data my-app
    # 호스트의 /host/data 폴더를 컨테이너의 /container/data로 연결
  9. 정리하면 포트와 파일 시스템 모두 기본적으로 서로 보이지 않으며, 연결이 필요하면 명시적으로 뚫어 줘야 한다.

로컬 컴퓨터에서 컨테이너에 접속하는 방법

  1. 로컬에서 컨테이너에 접속한다는 것은, 포트 포워딩을 통해 localhost로 보낸 요청이 컨테이너 내부 프로세스에 전달되는 것을 의미한다.

  2. 컨테이너를 실행할 때 -p 옵션으로 호스트 포트와 컨테이너 포트를 연결한다.

    docker run -d -p 3000:8080 my-app
    # -d: 백그라운드 실행, 호스트 3000 → 컨테이너 8080
  3. 이후 브라우저나 curllocalhost:3000에 접속하면 컨테이너 안의 서버로 요청이 전달된다.

    curl http://localhost:3000
  4. 즉, 실제로 컨테이너 안에 들어가는 것이 아니라 포트를 통해 컨테이너 안의 프로세스와 통신하는 것이다.

원격 서버의 컨테이너에 접속하는 방법

  1. 원리는 로컬 접속과 동일하고, localhost 대신 상대방 컴퓨터의 IP 주소를 사용한다.

  2. 단, 상대방 컴퓨터가 해당 포트를 외부에 열어두었어야 하며, 방화벽이 그 포트를 허용해야 한다.

    curl http://43.201.12.34:3000
    # 상대방 서버 IP의 3000번 포트로 접속
  3. 클라우드 서버라면 AWS 보안 그룹이나 GCP 방화벽 규칙에서 해당 포트를 인바운드로 허용해야 한다.

  4. 서버 운영 시에는 SSH로 먼저 서버에 접속한 뒤 컨테이너 셸로 들어가는 방법도 자주 사용된다.

    ssh user@43.201.12.34              # 서버에 먼저 SSH 접속
    docker exec -it my-app /bin/bash   # 이후 컨테이너 셸 진입

docker exec -it 명령어의 의미

  1. docker exec는 실행 중인 컨테이너 안에 새로운 프로세스를 추가로 실행하는 명령이다.

    docker exec -it my-container /bin/bash
  2. -iinteractive의 약자로, 표준 입력(stdin)을 열어두어 키보드 입력을 받을 수 있게 한다.

  3. -ttty의 약자로, 터미널처럼 보이는 출력 환경을 만들어준다.

  4. 둘을 합쳐 -it로 쓰면 일반 터미널처럼 양방향 입출력이 가능한 셸 환경이 된다.

  5. /bin/bash는 컨테이너 안에서 실행할 프로세스로, bash 셸을 띄우라는 의미이다.

  6. 이미지에 bash가 없는 경우 /bin/sh를 대신 사용한다.

  7. exit를 입력하면 bash 프로세스만 종료되고, 컨테이너 자체는 계속 실행 상태를 유지한다.

컨테이너 내부 파일 시스템의 구조

  1. 컨테이너 안에서 pwd를 했을 때 보이는 경로는 컨테이너 자신의 파일 시스템 기준이다.

  2. 이 파일 시스템은 이미지를 빌드할 때 레이어로 쌓인 내용 그대로이다.

  3. 즉, Dockerfile을 작성한 이미지 제작자가 어떤 파일을 복사하고 어떤 폴더를 만들었느냐에 따라 결정된다.

    WORKDIR /app   # /app 폴더를 만들고 기본 작업 위치로 설정
    COPY . .       # 제작자의 코드를 /app 안에 복사
  4. 공식 이미지라면 해당 소프트웨어 팀이 구성한 파일 시스템 구조가 들어 있고, 직접 만든 이미지라면 내가 Dockerfile에 정의한 구조가 들어 있다.

  5. 공식 이미지의 경우 Docker Hub의 해당 이미지 페이지에 작업 경로가 문서로 명시되어 있다.

  6. 예를 들어 공식 nginx 이미지는 웹 파일을 /usr/share/nginx/html에 두도록 안내한다.

  7. 컨테이너 실행 후 추가로 만든 파일은 컨테이너의 쓰기 레이어에 저장되며, 이미지 자체에는 반영되지 않는다.

컨테이너와 호스트 파일 시스템 연동 (볼륨 마운트)

  1. 기본적으로 컨테이너의 쓰기 레이어는 컨테이너가 삭제되면 함께 사라진다.

  2. 데이터를 영구적으로 유지하거나 호스트의 파일을 컨테이너에서 쓰려면 볼륨 마운트를 사용한다.

  3. 마운트란 호스트의 특정 폴더를 컨테이너 파일 시스템의 특정 경로에 연결하는 것이다.

    docker run -v /Users/me/project:/app my-image
    # 호스트의 /Users/me/project 폴더를 컨테이너의 /app 경로에 연결
  4. 이렇게 하면 컨테이너 안에서 /app에 파일을 쓰면 실제로는 호스트의 /Users/me/project에 저장된다.

  5. 반대로 호스트에서 그 폴더의 파일을 수정하면 컨테이너 안에서도 즉시 반영된다.

  6. 컨테이너를 삭제해도 호스트 폴더의 데이터는 그대로 남는다.

  7. 이 방식은 개발 중 코드를 수정할 때마다 컨테이너를 재빌드하지 않아도 변경이 바로 반영되도록 하는 개발 환경 구성에 자주 쓰인다.