도커를 사용하면 서버 구축 환경 설정과 서버 자원 관리를 자동화할 수 있습니다. 도커는 이미지와 컨테이너 개념을 활용하는데, Dockerfile을 통해 서버 환경 설정 과정을 코드로 관리하여 서버 구축 과정을 자동화할 수 있고, 이미지와 컨테이너를 통해 서버 자원을 자동으로 효율적으로 분배할 수 있습니다. 이처럼 코드로 인프라를 관리하는 개념을 IaC(Infrastructure as Code)라고 하는데, IaC 방식으로 서버를 운영하면 서버 관리에 드는 시간이 많이 절약돼서 지금 구글은 매주 10억 개의 컨테이너를 생성·삭제할 수 있는 여유를 가지고 있습니다.
도커는 2013년 3월 Docker, Inc에서 오픈 소스 컨테이너 프로젝트에서 처음 등장했습니다. 도커를 개발한 회사는 dotCloud인데 그 회사의 리눅스 엔지니어가 도커를 개발했습니다.
근데 컨테이너 개념은 1991년 리눅스가 처음 등장했을 때 프로세스를 격리하는 기술인 LXC(LinuX Container) 형태로도 존재했습니다. LXC는 리눅스 커널 안에 컨테이너 기능을 제공하는 라이브러리로서, 우리는 LXC를 이용해 프로세스를 리눅스 커널 환경으로부터 격리시킬 수 있었습니다.
그럼 기존에 컨테이너 개념이 있었는데 도커는 왜 등장했을까요? 바로 LXC로 서비스를 개발하고 관리하기가 어렵고 복잡했기 때문입니다. LXC는 컴퓨터 자원(CPU, 메모리, 디스크, 네트워크 등)를 일일이 각 컨테이너에 할당해야 했습니다. 그래서 LXC에서 설정해야 하는 여러 과정을 자동화해서 CLI(Command Line Interface)로 제공한 도커가 등장했습니다.
도커는 컨테이너 실행 환경을 제공하는 프로그램입니다. 우리가 도커 클라이언트를 통해 docker run
명령어를 실행하면 dockerd가 이 명령어를 해석해서 gRPC를 통해 containerd에 요구 사항을 전달합니다. 그럼 containerd는 containerd-shim을 거쳐 runC에 요청하고, runC는 컨테이너를 생성하기 위해 /var/run/docker.sock
파일에 정의된 소켓 통신으로 호스트OS에 있는 LXC에 컨테이너 생성을 요청합니다.
LXC는 내부적으로 namespace, cgroup, chroot 리눅스 라이브러리를 사용합니다. 각 라이브러리의 역할은 아래와 같습니다.
namespace는 각 컨테이너에 대해 파일 시스템이나 네트워킹과 같은 시스템 리소스를 가상화합니다. 컨테이너에 IP주소, 포트번호, 라우팅 테이블 등을 할당해주고, 컨테이너 내부적으로 PID=1을 할당해서 컨테이너를 호스트 OS INIT PROCESS인 systemd
로부터 분리시킵니다.
cgroup은 각 컨테이너가 사용할 수 있는 CPU 및 메모리와 같은 리소스 양을 제한하는 역할을 합니다. cgroup은 control group의 줄임말로서 개별 컨테이너에 자동으로 자원을 분배하고 관리하는 백그라운드 프로세스입니다. 기본적으로 컨테이너에 자원을 무제한 할당해줄 수 있지만, 일반적으로 컴퓨터 자원은 유한하기 때문에 개별 컨테이너마다 할당할 수 있는 자원량을 제한하기도 합니다. 예를 들어 docker run
옵션 중엔 --cpu-share
등과 같이 자원에 관련된 옵션이 있는데 이런 옵션을 처리하기 위해 도커 내부적으로 cgroup을 사용합니다.
chroot를 사용해 컨테이너 파일 시스템을 호스트 OS로부터 격리할 수 있습니다. 컨테이너가 격리되면 컨테이너 프로세스는 호스트 OS의 파일 시스템에 접근할 수 없게 됩니다.
이렇게 runC가 LXC에 컨테이너 생성을 요청하면, LXC가 호스트OS(리눅스)에 하나의 프로세스로서 컨테이너를 생성해주고 호스트OS의 리눅스 커널을 컨테이너에 공유해줍니다. 컨테이너는 호스트OS 커널을 공유받기 때문에 내부적으로 게스트OS를 가지고 있지 않아도 리눅스 기능을 사용할 수 있습니다. 그리고 이렇게 생성된 컨테이너의 실행·중지·삭제 등은 도커를 통해 관리됩니다.
# Docker는 64bit Linux kernel 3.1 이상에서만 설치 가능
$ uname -a
$ curl -fsSL https://get.docker.com -o get-docker.sh
$ chmod +x get-docker.sh
$ sh get-docker.sh
uname
명령어로 현재 운영체제의 버전에서 도커를 설치할 수 있는지 확인합니다. 도커 설치 중간에 권한 문제가 발생하면 명령어 앞에 sudo
를 입력해줍니다. sudo
명령어를 찾을 수 없다고 나오면 apt install sudo -y
로 설치해줍니다.
$ docker info
docker info
를 입력하면 도커에 관련된 여러 정보가 출력됩니다. 도커 클라이언트 정보보단 도커 서버 관련 정보가 더 중요합니다. 현재 컨테이너의 상태나 이미지 개수, 도커 서버 버전과 같이 기본 정보도 보여주고, 스토리지 파일 시스템(extfs), cgroup 드라이버, 로깅 드라이버도 보여줍니다. 그리고 도커에서 4대 핵심 관리 자원인 CPU, 메모리, 네트워크, 스토리지에 대한 정보도 보여줍니다.
기본적으로 도커 네트워크는 위 그림과 같이 docker0
브릿지(172.17.0.1/16)가 여러 컨테이너의 기본 게이트웨이로 설정되어 있습니다. 브릿지는 서로 다른 네트워크를 하나로 묶어 주는 네트워크 L2 장치로서 docker0
브릿지는 컨테이너의 기본 게이트웨이가 됩니다. 물론 새로운 사용자 지정 브릿지를 생성할 수 있는데 서브넷 마스크가 16이기 때문에 최대 65536개의 네트워크 브릿지를 생성할 수 있습니다.
도커 네트워크는 호스트 OS끼리도 연결할 수 있는데 이를 overlay라고 합니다. 네트워크 overlay를 사용해 여러 호스트 OS를 Docker Swarm으로 관리할 수 있습니다.
스토리지는 컨테이너 데이터를 저장하는 공간으로 다른 컨테이너와 공유하거나, 호스트OS와 공유하거나, NFS로 사용할 수도 있습니다.
로그는 호스트OS 로그, 도커 로그, 컨테이너 로그, 클라우드 공급자(AWS, GCP 등) 로그 등 다양한 로그를 관리할 수 있습니다.
도커 이미지는 서비스 인프라를 설정하기 위해 필요한 모든 정보가 담긴 파일입니다. 도커 이미지는 일반적으로 읽기 전용 파일이고 여러 레이어로 구성됩니다. 도커 이미지는 비유하자면 하드웨어 장비, OS, 네트워크, 저장 장치 등 시스템 구성 요소의 설정 방법이 담겨 있는 시스템 설계도와 비슷합니다.
$ docker pull 이미지이름:태그이름
도커를 구동하는데 핵심적인 파일은 /var/lib/docker
폴더에 들어있습니다. docker pull
명령어로 docker.io에 도커 이미지를 요청하면 이미지가 개별 레이어로 나뉘어서 다운로드됩니다. 그리고 그렇게 받은 여러 레이어가 하나의 파일에 담겨 하나의 이미지로 관리됩니다. 각각의 레이어는 /var/lib/docker/image/overlay2/layer/sha256
에 하나씩 저장되고, 개별 레이어가 모여 읽기 전용의 도커 이미지가 생성됩니다.
리눅스에 기반한 이미지는 alpine과 buster가 있는데 둘의 차이는 아래와 같습니다.
$ docker build --tag 이미지이름:태그이름 .
Dockerfile로 부터 도커 이미지를 생성합니다. 위와 같이 .
을 통해 현재 디렉토리의 Dockerfile을 불러올 수 있고, 외부 git 저장소에 있는 Dockerfile도 불러올 수 있습니다.
$ docker image ls
$ docker images
현재 컴퓨터에 있는 도커 이미지 목록을 출력합니다. 위 두 명령어의 결과는 동일합니다.
도커 컨테이너는 이미지의 런타임입니다.
$ docker run \
--name 컨테이너이름 \
--publish 호스트OS포트:컨테이너포트 \
이미지이름:태그이름
도커 이미지를 기반으로 컨테이너를 생성하고 실행합니다. --name
옵션으로 컨테이너 이름을 설정할 수 있고, --publish
옵션으로 외부 포트와 컨테이너 포트를 맵핑할 수 있습니다. 하나의 도커 이미지로부터 여러 개의 동일한 컨테이너를 생성할 수 있습니다. 즉, 이미지는 클래스에 비유할 수 있고, 컨테이너는 객체에 비유할 수 있습니다.
docker run: 새로운 컨테이너를 생성 후 실행
docker exec: 기존 실행 중인 컨테이너에 명령어를 전달
$ docker container ls
$ docker ps
현재 실행 중인 컨테이너 목록을 볼 수 있습니다. --all
옵션을 붙이면 일시정지, 중지 등 모든 상태의 컨테이너 목록을 볼 수 있습니다.
0.0.0.0:8001 -> 80/tcp # IPv4
:::8001 -> 80/tcp # IPv6
거기엔 PORTS
항목도 출력되는데, 만약 위와 같이 출력됐으면 호스트 OS의 8001번 포트로 패킷을 보내면 컨테이너 80번 포트로 패킷을 전달하겠다는 뜻입니다.
$ docker stop 컨테이너이름
$ docker kill 컨테이너이름
docker stop
: Stop a running container (send SIGTERM, and then SIGKILL after grace period) The main process inside the container will receive SIGTERM, and after a grace period, SIGKILL.docker kill
: Kill a running container (send SIGKILL, or specified signal) The main process inside the container will be sent SIGKILL, or any signal specified with option --signal
.$ docker rm 컨테이너이름
현재 실행 중인 컨테이너는 바로 삭제할 수 없습니다. 기본적으로 컨테이너를 중지하고 삭제해야 하지만, --force
옵션을 주면 실행 중인 컨테이너도 강제로 삭제할 수 있습니다. 띄어쓰기로 컨테이너 ID 또는 이름을 구분하여 여러 컨테이너를 한번에 삭제할 수도 있습니다.