도커는 호스트 OS의 커널을 공유하는 컨테이너 기반 플랫폼으로, 가상 머신보다 가볍고 빠르게 동일한 실행 환경을 어디서든 재현하기 위한 도구이다.
도커의 동작 원리
도커 등장 배경
-
소프트웨어 개발에서는 "내 컴퓨터에서는 잘 되는데 서버에서는 안 된다"는 문제가 자주 발생한다.
-
이는 개발 환경과 운영 환경의 OS, 라이브러리 버전, 설정값 등이 서로 다르기 때문이다.
-
과거에는 이 문제를 해결하기 위해 가상 머신(
VM)을 사용했다. -
그러나
VM은 OS 전체를 가상화하기 때문에 무겁고 실행 속도가 느리다는 한계가 있다. -
이런 한계를 보완하기 위해 2013년 컨테이너 기반 플랫폼인 도커가 등장했다.
도커란 무엇인가
-
도커는 애플리케이션과 그 실행에 필요한 환경을 하나로 묶어, 어디서든 동일하게 실행할 수 있게 해주는 컨테이너 기반 플랫폼이다.
-
VM이 OS 전체를 복제하는 것과 달리, 도커는 호스트 OS의 커널을 공유하는 방식으로 동작한다. -
이 차이 덕분에
VM보다 가볍고 시작 속도가 빠르다. -
즉, 도커는 환경 차이로 인한 문제를 환경 자체를 함께 배포함으로써 해결하는 도구이다.
호스트 OS의 커널을 공유한다는 것의 의미
-
커널은 OS의 핵심으로, 하드웨어(CPU, 메모리, 디스크)와 프로그램 사이에서 자원을 관리하는 역할을 한다.
-
프로그램이 파일을 읽거나 네트워크를 사용할 때, 실제로는 커널에 요청(시스템 콜)을 보내는 방식으로 동작한다.
-
VM은 가상 머신마다 커널을 따로 가지기 때문에, OS 전체를 새로 띄우는 것과 같은 비용이 든다. -
반면 도커 컨테이너는 커널을 별도로 갖지 않고, 호스트 OS의 커널을 그대로 빌려 쓴다.
-
컨테이너 안의 프로그램은 중간 OS 없이 호스트 커널에 직접 시스템 콜을 보낸다.
-
다만 컨테이너는 자신만의 파일 시스템, 프로세스 목록, 네트워크 공간을 가지도록 격리되어 있다.
-
정리하면 커널은 공유하되, 각 컨테이너는 서로를 볼 수 없도록 분리된 구조이다.
호스트 커널 공유의 트레이드오프
-
커널을 공유하기 때문에 컨테이너는 호스트 OS와 동일한 커널 위에서만 동작할 수 있다.
-
예를 들어 호스트가 Linux라면 컨테이너도 Linux 커널 기반 환경만 실행할 수 있다.
-
Windows나 macOS에서 도커를 쓸 때 내부적으로 경량 Linux
VM이 먼저 뜨는 이유가 이 때문이다. -
Windows에서는 과거의 무거운
Hyper-V기반 VM 대신 현재WSL 2를 주로 활용하며, 호스트 윈도우 커널과 분리된 경량 리눅스 유틸리티 VM 위에서 도커를 구동한다. -
macOS에서는
LinuxKit으로 구성된 경량 리눅스 VM을HyperKit또는 AppleVirtualization.framework위에서 실행한다. -
또한 커널을 공유하므로, 커널 자체에 보안 취약점이 생기면 모든 컨테이너가 동시에 영향을 받는다.
-
VM은 커널이 완전히 분리되어 있어 한VM이 뚫려도 다른VM은 안전하지만, 컨테이너는 그 격리 수준이 더 얕다. -
컨테이너는 커널 파라미터나 커널 모듈을 자유롭게 변경하기 어렵고, 호스트 커널 버전에 종속된다.
-
정리하면 커널 공유 덕분에 가볍고 빠르지만, 그 대가로 OS 수준의 격리성과 이식성 일부를 포기한 구조이다.
컨테이너 격리를 가능하게 하는 리눅스 커널 기능
-
컨테이너는 하나의 리눅스 커널을 여러 프로세스가 안전하게 공유하는 방식으로 동작한다.
-
이를 위해 각 프로세스가 자신만의 독립된 환경을 가진다고 인식하도록 만들어야 한다.
-
이를 가능하게 하는 리눅스 커널 기능이
Namespace,cgroups,chroot,Union File System이다. -
Namespace는 프로세스가 볼 수 있는 시스템 자원의 범위를 분리하는 기능으로,PID·네트워크·마운트·사용자 등을 격리한다. -
예를 들어 컨테이너 안에서
ps aux를 실행하면 호스트의 다른 프로세스는 보이지 않고, 컨테이너 내부 프로세스만PID1번부터 보이는 것처럼 동작한다. -
cgroups는 컨테이너가 사용할 수 있는 CPU, 메모리, 디스크 I/O 등의 자원량을 제한하는 기능이다. -
chroot는 프로세스가 볼 수 있는 루트 디렉토리(/)를 특정 경로로 한정해, 그 바깥의 파일 시스템에 접근하지 못하게 한다. -
Union File System은 여러 파일 시스템 레이어를 하나로 합쳐 보이게 하는 기술로, 도커 이미지의 레이어 구조를 가능하게 하는 기반이다. -
즉, 컨테이너는 별도의 OS가 아니라 이 네 가지 커널 기능이 만들어내는 격리된 프로세스이다.
도커 이미지
이미지란 무엇인가
-
도커 이미지는 컨테이너를 만들기 위한 읽기 전용 패키지이다.
-
애플리케이션 코드, 런타임, 라이브러리, 환경 변수 등 실행에 필요한 모든 것이 이미지 안에 포함되어 있다.
-
이미지는 흔히 설계도로 비유되지만, 실제로는 실행 준비가 끝난 완성된 스냅샷에 더 가깝다.
-
이미지 안에는 파일 시스템, 라이브러리, 실행 파일 등 실체가 있는 데이터가 들어 있기 때문이다.
-
다만 이미지 자체는 실행되지 않고 읽기 전용으로 존재하므로, 실행에 필요한 모든 것이 담긴 읽기 전용 패키지로 이해하는 것이 정확하다.
-
이미지는
Dockerfile이라는 파일에 명령어를 작성해 빌드한다.FROM node:18 # 베이스 이미지 지정 WORKDIR /app # 작업 디렉토리 설정 COPY . . # 호스트의 파일을 이미지로 복사 RUN npm install # 의존성 설치 CMD ["node", "index.js"] # 컨테이너 실행 시 동작할 명령
이미지의 레이어 구조
-
도커 이미지는 하나의 덩어리가 아니라, 변경 사항을 층층이 쌓은 레이어들의 집합으로 이루어진다.
-
Dockerfile의 각 명령어가 실행될 때마다 새로운 레이어가 하나씩 추가된다.FROM node:18 # 레이어 1: node 베이스 이미지 WORKDIR /app # 레이어 2: 작업 디렉토리 설정 COPY . . # 레이어 3: 파일 복사 RUN npm install # 레이어 4: 의존성 설치 -
각 레이어는 이전 레이어와의 차이점(변경된 파일)만 저장한다.
-
이미지를 다시 빌드할 때, 변경되지 않은 레이어는 캐시에서 그대로 재사용된다.
-
예를 들어 코드만 수정했다면
COPY . .이후의 레이어만 다시 빌드되고, 그 아래의node:18레이어는 다시 받지 않는다. -
여러 이미지가 동일한 베이스 레이어를 공유할 수 있어, 디스크 공간도 절약된다.
이미지 pull의 동작 방식
-
docker pull은 폴더 하나를 통째로 다운로드하는 것이 아니라, 이미지를 구성하는 레이어들을 하나씩 내려받는 동작이다. -
이미 로컬에 같은 레이어가 있다면 그 레이어는 건너뛰고, 없는 레이어만 새로 받는다.
-
내려받은 레이어들은 도커가 관리하는 로컬 저장소에 쌓이며, 도커가 이를 조합해 하나의 이미지로 인식한다.
-
사용자에게는 폴더처럼 노출되지 않고,
docker images명령어로만 목록을 확인할 수 있다. -
즉, 개념적으로는 패키지 다운로드와 비슷하지만, 실제로는 레이어 단위로 관리되는 도커 전용 저장 방식이다.
Docker Hub
-
도커 허브는 도커 이미지를 저장하고 공유하는 공식 레지스트리 서비스이다.
-
GitHub이 코드를 올리고 받는 공간이라면, 도커 허브는 이미지를 올리고 받는 공간이다. -
node,nginx,mysql같은 공식 이미지가 도커 허브에 공개되어 있어 바로 가져다 쓸 수 있다. -
직접 만든 이미지도 도커 허브에 올려 팀원과 공유하거나 서버에 배포할 수 있다.
-
이미지를 올리고 내려받는 명령어는 다음과 같다.
docker push myusername/my-app # 이미지를 도커 허브에 업로드 docker pull myusername/my-app # 도커 허브에서 이미지 다운로드 -
이를 통해 개발자는 별도의 환경 설정 없이 이미지 하나로 어디서든 동일한 애플리케이션을 실행할 수 있다.
도커 컨테이너
컨테이너란 무엇인가
-
컨테이너는 이미지를 실제로 실행한 인스턴스이다.
-
이미지가 정적인 패키지라면, 컨테이너는 그 패키지를 메모리에 올려 동작시키고 있는 살아 있는 프로세스 단위이다.
-
하나의 이미지로 여러 개의 컨테이너를 동시에 실행할 수 있다.
-
각 컨테이너는 독립된 실행 환경을 가지므로, 한 컨테이너의 문제가 다른 컨테이너에 영향을 주지 않는다.
-
컨테이너는
docker run명령으로 이미지를 기반으로 실행한다.docker run -p 3000:3000 my-app-image # 이미지를 컨테이너로 실행하고, 호스트의 3000번 포트와 연결 -
컨테이너를 삭제하면 내부에서 생긴 데이터는 기본적으로 사라지며, 이미지 자체는 변하지 않는다.
동일한 이미지로 여러 컨테이너를 실행하는 원리
-
이미지는 읽기 전용이고, 컨테이너는 그 이미지 위에 얇은 쓰기 가능 레이어를 추가해 실행된다.
-
따라서 같은 이미지를 기반으로 컨테이너를 여러 개 만들어도, 이미지 자체는 공유되며 복사되지 않는다.
-
각 컨테이너는 자신만의 쓰기 레이어를 가지므로, 서로 독립적으로 동작한다.
-
예를 들어 같은 웹 서버 이미지로 컨테이너 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 -
이는 트래픽이 몰릴 때 동일한 이미지로 컨테이너를 빠르게 늘리는 수평 확장의 핵심 원리이다.
docker run vs docker start
-
docker run은 이미지를 기반으로 새로운 컨테이너를 생성하고 즉시 실행하는 두 단계를 한 번에 수행한다. -
내부적으로는 이미지 레이어 위에 새 쓰기 레이어를 할당하고, 네임스페이스와
cgroups를 새로 구성한 뒤 프로세스를 시작한다. -
반면
docker start는 이미 존재하는 컨테이너, 즉 쓰기 레이어와 설정이 그대로 남아 있는 컨테이너를 다시 깨우는 명령이다. -
새 레이어를 만들거나 네임스페이스를 새로 구성하지 않고, 멈춰 있던 프로세스를 다시 스케줄링하는 것에 가깝다.
-
따라서
docker run은 매번 새 컨테이너를 찍어내고,docker start는 기존 컨테이너를 재가동한다.docker run my-image # 이미지 → 새 컨테이너 생성 + 실행 docker stop my-container # 컨테이너 중단 (컨테이너는 남아 있음) docker start my-container # 기존 컨테이너 재실행 (레이어 유지)
중단된 컨테이너의 자원 소모
-
docker ps -a에서 보이는 중단된 컨테이너는 프로세스가 없는 상태이다. -
프로세스가 없으므로 CPU와 메모리는 소모하지 않는다.
-
다만 컨테이너의 쓰기 레이어, 즉 컨테이너 안에서 생긴 파일 변경 사항은 디스크에 남아 있다.
-
컨테이너 수가 많아질수록 이 쓰기 레이어들이 디스크 공간을 조금씩 차지한다.
-
또한 컨테이너의 메타데이터(이름, 설정, 네트워크 정보 등)도 도커 데몬이 관리하며 소량의 메모리를 사용한다.
-
정리하면 중단된 컨테이너는 CPU·메모리는 소모하지 않지만 디스크를 소모하므로, 불필요한 컨테이너는
docker rm으로 정리하는 것이 좋다.
실행 중인 컨테이너 삭제가 기본적으로 거부되는 이유
-
docker rm은 기본적으로 실행 중인 컨테이너에 대해서는 동작을 거부한다. -
이는 리눅스 커널이 강제하는 제약이 아니라, 도커 엔진(
Docker Daemon)이 직접 거는 소프트웨어 레벨의 보호 장치이다. -
실제로 리눅스 커널은 실행 중인 프로세스의 바이너리나 마운트된 파일 시스템을 삭제·언마운트하는 것을 막지 않는다.
-
예를 들어 리눅스에서는 실행 중인 프로그램의 바이너리 파일을 지워도 프로세스는 메모리 위에서 정상적으로 계속 동작한다.
-
그러나 운영 관점에서, 실행 중인 컨테이너를 무심코 삭제하면 서비스가 갑자기 끊기거나 데이터가 의도치 않게 손실될 수 있다.
-
도커는 이런 실수를 방지하기 위해 컨테이너 상태(
Running)를 추적하고, 실행 중인 컨테이너의rm요청을 거부한다. -
따라서 정상적인 절차는
docker stop으로 프로세스를 안전하게 종료한 뒤docker rm을 실행하는 것이다. -
한 번에 강제로 제거해야 한다면
-f옵션을 사용해 도커가 내부적으로SIGKILL을 보낸 뒤 컨테이너를 삭제하도록 할 수 있다.docker stop my-container && docker rm my-container # 권장: 안전한 정리 docker rm -f my-container # 강제 종료 후 즉시 삭제 -
즉, 실행 중인 컨테이너의 삭제 금지는 커널 차원의 제약이 아니라 도커 엔진이 제공하는 안전 가드이다.
컨테이너의 독립적인 포트와 파일 시스템
-
포트는 네트워크에서 특정 프로세스를 구분하기 위한 번호로, 하나의 포트는 동시에 하나의 프로세스만 사용할 수 있다.
-
도커는 리눅스
Namespace기술로 컨테이너마다 독립된 네트워크 공간을 만들어준다. -
따라서 컨테이너 내부에서는 자신이 8080 포트를 쓴다고 인식하지만, 호스트 입장에서는 그 포트가 그대로 보이지 않는다.
-
호스트와 컨테이너의 포트를 연결하려면 명시적으로 포트 포워딩을 설정해야 한다.
docker run -p 3000:8080 my-app # 호스트의 3000번 포트 → 컨테이너의 8080번 포트로 연결 -
파일 시스템도 마찬가지로
Namespace를 통해 컨테이너마다 독립된 루트 디렉토리(/)가 주어진다. -
컨테이너 안에서
/app폴더를 만들어도 호스트에는 그 폴더가 존재하지 않는다. -
반대로 호스트의
/home폴더도 컨테이너 안에서는 기본적으로 보이지 않는다. -
호스트의 특정 폴더를 컨테이너와 공유하려면 볼륨 마운트를 명시적으로 설정해야 한다.
docker run -v /host/data:/container/data my-app # 호스트의 /host/data 폴더를 컨테이너의 /container/data로 연결 -
정리하면 포트와 파일 시스템 모두 기본적으로 서로 보이지 않으며, 연결이 필요하면 명시적으로 뚫어 줘야 한다.
로컬 컴퓨터에서 컨테이너에 접속하는 방법
-
로컬에서 컨테이너에 접속한다는 것은, 포트 포워딩을 통해
localhost로 보낸 요청이 컨테이너 내부 프로세스에 전달되는 것을 의미한다. -
컨테이너를 실행할 때
-p옵션으로 호스트 포트와 컨테이너 포트를 연결한다.docker run -d -p 3000:8080 my-app # -d: 백그라운드 실행, 호스트 3000 → 컨테이너 8080 -
이후 브라우저나
curl로localhost:3000에 접속하면 컨테이너 안의 서버로 요청이 전달된다.curl http://localhost:3000 -
즉, 실제로 컨테이너 안에 들어가는 것이 아니라 포트를 통해 컨테이너 안의 프로세스와 통신하는 것이다.
원격 서버의 컨테이너에 접속하는 방법
-
원리는 로컬 접속과 동일하고,
localhost대신 상대방 컴퓨터의 IP 주소를 사용한다. -
단, 상대방 컴퓨터가 해당 포트를 외부에 열어두었어야 하며, 방화벽이 그 포트를 허용해야 한다.
curl http://43.201.12.34:3000 # 상대방 서버 IP의 3000번 포트로 접속 -
클라우드 서버라면 AWS 보안 그룹이나 GCP 방화벽 규칙에서 해당 포트를 인바운드로 허용해야 한다.
-
서버 운영 시에는
SSH로 먼저 서버에 접속한 뒤 컨테이너 셸로 들어가는 방법도 자주 사용된다.ssh user@43.201.12.34 # 서버에 먼저 SSH 접속 docker exec -it my-app /bin/bash # 이후 컨테이너 셸 진입
docker exec -it 명령어의 의미
-
docker exec는 실행 중인 컨테이너 안에 새로운 프로세스를 추가로 실행하는 명령이다.docker exec -it my-container /bin/bash -
-i는interactive의 약자로, 표준 입력(stdin)을 열어두어 키보드 입력을 받을 수 있게 한다. -
-t는tty의 약자로, 터미널처럼 보이는 출력 환경을 만들어준다. -
둘을 합쳐
-it로 쓰면 일반 터미널처럼 양방향 입출력이 가능한 셸 환경이 된다. -
/bin/bash는 컨테이너 안에서 실행할 프로세스로,bash셸을 띄우라는 의미이다. -
이미지에
bash가 없는 경우/bin/sh를 대신 사용한다. -
exit를 입력하면bash프로세스만 종료되고, 컨테이너 자체는 계속 실행 상태를 유지한다.
컨테이너 내부 파일 시스템의 구조
-
컨테이너 안에서
pwd를 했을 때 보이는 경로는 컨테이너 자신의 파일 시스템 기준이다. -
이 파일 시스템은 이미지를 빌드할 때 레이어로 쌓인 내용 그대로이다.
-
즉,
Dockerfile을 작성한 이미지 제작자가 어떤 파일을 복사하고 어떤 폴더를 만들었느냐에 따라 결정된다.WORKDIR /app # /app 폴더를 만들고 기본 작업 위치로 설정 COPY . . # 제작자의 코드를 /app 안에 복사 -
공식 이미지라면 해당 소프트웨어 팀이 구성한 파일 시스템 구조가 들어 있고, 직접 만든 이미지라면 내가
Dockerfile에 정의한 구조가 들어 있다. -
공식 이미지의 경우 Docker Hub의 해당 이미지 페이지에 작업 경로가 문서로 명시되어 있다.
-
예를 들어 공식
nginx이미지는 웹 파일을/usr/share/nginx/html에 두도록 안내한다. -
컨테이너 실행 후 추가로 만든 파일은 컨테이너의 쓰기 레이어에 저장되며, 이미지 자체에는 반영되지 않는다.
컨테이너와 호스트 파일 시스템 연동 (볼륨 마운트)
-
기본적으로 컨테이너의 쓰기 레이어는 컨테이너가 삭제되면 함께 사라진다.
-
데이터를 영구적으로 유지하거나 호스트의 파일을 컨테이너에서 쓰려면 볼륨 마운트를 사용한다.
-
마운트란 호스트의 특정 폴더를 컨테이너 파일 시스템의 특정 경로에 연결하는 것이다.
docker run -v /Users/me/project:/app my-image # 호스트의 /Users/me/project 폴더를 컨테이너의 /app 경로에 연결 -
이렇게 하면 컨테이너 안에서
/app에 파일을 쓰면 실제로는 호스트의/Users/me/project에 저장된다. -
반대로 호스트에서 그 폴더의 파일을 수정하면 컨테이너 안에서도 즉시 반영된다.
-
컨테이너를 삭제해도 호스트 폴더의 데이터는 그대로 남는다.
-
이 방식은 개발 중 코드를 수정할 때마다 컨테이너를 재빌드하지 않아도 변경이 바로 반영되도록 하는 개발 환경 구성에 자주 쓰인다.