1. 도커란?


도커는 애플리케이션을 컨테이너화 하여 Host OS와 독립적으로 실행하는 기술입니다. 
Linux, Windows 등 어떤 OS든 상관없이 컨테이너화된 소프트웨어는 항상 동일하게 실행됩니다. (SW를 환경으로부터 격리)

2. 도커와 기존의 가상화 기술과의 차이


혹시 도커 이전에도 이런 SW 가상화 기술이 존재했었단 사실을 알고 계신가요?
도커 이전에 가장 많이 쓰였던 방식은 VirtureBox와 같은 가상 머신 위에 VM을 띄우는 하이퍼 바이저 방식입니다.
두 방식의 특징은 다음과 같습니다.

  • 도커 (컨테이너 방식)
    도커 컨테이너에서 돌아가는 애플리케이션은 컨테이너가 제공하는 격리 기능 내부에 샌드박스가 있지만, 여전히 Host와 OS 커널을 공유합니다. 따라서, 컨테이너 내부에서 실행되는 프로세스를 Host 시스템에서 볼 수 있습니다. (권한이 있다는 전제 하에)
  • 가상 머신 (하이퍼 바이저 방식)
    가상 머신과 함께 VM 내부에서 실행되는 모든 것은 Host 시스템과 독립적입니다.
    Host 시스템은 하드웨어 자원 일부를 VM에 할당하고, VirtureBox와 같은 가상머신 플랫폼이 VM에서 돌아가는 프로세스를 관리합니다. 각각의 VM마다 OS를 구동한 후, 애플리케이션을 실행시킵니다. 사용법이 간단하지만 굉장히 느리죠..!

 

아래 보시는 것처럼 하이퍼 바이저는 OS를 각각 띄워야하기 때문에 굉장히 무거운 반면, 컨테이너는 OS를 구동할 필요가 없어 가볍습니다.

[그림 1] 컨테이너 방식과 하이퍼 바이저 방식 비교

뭔가 엄청나다는 것은 이제 알겠는데.. 컨테이너를 어떻게 격리시키는 것일까요?
컨테이너 격리를 위해 Linux 커널의 기능 중에 Cgroup네임스페이스라는 격리 기술이 있는데 이를 이용합니다.

  • C Group
    CPU, 메모리, Network Bandwith, HDD 입출력 등 프로세스의 시스템 리소스 사용량을 관리합니다.
    어떤 애플리케이션이 사용하는 시스템 리소스양이 너무 많다면 C group에 넣어 리소스 사용량을 제한시킵니다.
  • 네임스페이스
    Linux에서 사용되는 하나의 시스템에서 프로세스를 격리시키는 경량 프로세스 가상화 기술입니다.

 

즉, 컨테이너 방식도 크게보면 하나의 Linux VM을 사용하는 것이라 할 수 있습니다.

[그림 2] 컨테이너 방식

3. 도커의 생명주기


도커 컨테이너는 다음과 같은 생명주기를 가집니다.

[그림 1] 컨테이너의 생명주기

각 생명주기에 해당하는 명령어를 살펴보겠습니다.

# Docker Image를 통해 컨테이너를 생성합니다. 
# 파일 스냅샷만 컨테이너의 저장 공간에 적재되고, 커맨드는 실행되지 않습니다.
$ docker create {컨테이너ID 또는 이미지명} 

# 컨테이너를 실행시킵니다. (Docker Image에 있는 커맨드를 실행합니다.)
$ docker start {컨테이너ID 또는 이미지명}

# 위 create와 start를 한 번에 진행합니다. (주로 run을 이용해 실행합니다.)
$ docker run {컨테이너ID 또는 이미지명}

# 실행 중인 컨테이너가 하던 작업을 마저 다 할 수 있도록 텀을 두고 안전하게 중지시킵니다.
# SIGTERM 후에 SIGKILL 시그널을 보냅니다.
$ docker stop {컨테이너ID 또는 이미지명}

# 실행 중인 컨테이너를 즉시 중지시킵니다.
# 곧바로 SIGKILL 시그널을 보냅니다.
$ docker kill {컨테이너ID 또는 이미지명}

# 중지된 컨테이너를 삭제합니다. (중지된 컨테이너는 `docker ps -a`로 확인할 수 있습니다.)
$ docker rm {컨테이너ID 또는 이미지명}
$ docker rm `docker ps -a -q` # 모든 컨테이너를 삭제합니다.

4. Docker Image 생성하고 실행하기


Docker Image를 생성하기 위해선 Dockerfile을 작성해야 합니다. 

Dockerfile에 Docker Image를 만들기위한 여러가지 내용을 적어줄 수 있지만, 필수적으로 다음 두 가지 내용이 들어가야 합니다.

  1. Base Image와 dependency 설치를 위한 명령어 (파일 스냅샷)
  2. Docker Image가 실행되어 컨테이너로 시작될 때 실행할 명령어 (컨테이너 시작시 실행될 명령어)

 

Node.js 애플리케이션을 Docker Imager로 만드는 경우 Dockerfile은 다음과 같이 구성될 수 있습니다.

# Base Image를 구성합니다.
FROM node:alpine 

# 컨테이너 내부의 워킹 디렉토리를 설정합니다.
# 이 경우 컨테이너 내부에 /usr/src/app/ 디렉토리 안에 관련 파일이 위치합니다.
WORKDIR /usr/src/app 

# 로컬에 있는 package.json 파일을 컨테이너 내부 ./ 경로에 복사합니다.
COPY package.json ./

# 컨테이너 내부에서 해당 명령어를 실행하여 dependency를 다운받습니다.
RUN npm install

# 로컬 경로에 있는 모든 프로젝트 파일을 컨테이너 내부 ./ 경로에 복사합니다.
# 위에서 package.json만 미리 COPY한 이유는 해당 Dockerfile로 Docker Image를 생성할 때마다
# package.json 파일에 변경이 없어도 매번 dependency를 설치받기 때문입니다. 
# 이렇게 하면 package.json 파일에 변경사항이 없으면 npm install 부분까지 캐시를 사용합니다.
COPY ./ ./

CMD ["npm", "run", "start"]

위와 같이 Dockerfile을 만들었다면, 터미널에서 다음 명령어로 빌드 및 실행합니다.

# Dockerfile을 이용해 Docker Image 생성하기 위한 build 명령어
$ docker build -t {내 도커ID}/{저장소명} ./

# 생성한 Docker Image를 실행하여 컨테이너로 돌아가게 만들기
$ docker run -d -p 8080:8080 {내 도커ID}/{저장소명}

5. Docker Volume


위 방법으로 Docker Image를 생성해 실행하면 애플리케이션이 컨테이너로 동작하게 됩니다.
그런데 만약 소스 코드를 수정하게되면 어떻게 될까요? 다시 Docker Image를 생성하고 실행해야 변경된 소스가 반영됩니다.
이때 Docker Volume 기능을 이용해 컨테이너 내부의 파일과 로컬에 있는 파일을 매핑(참조)하여 문제를 해결할 수 있습니다.
다음과 같이 Docker Image를 실행할 때 volume을 명시합니다.

# 처음 -v 옵션으로 준 값은 매핑하지 않을 파일이고,
# 두 번째 -v 옵션으로 준 값은 매핑할 파일입니다.
# Node.js 앱을 컨테이너로 실행시킬 경우, 굳이 로컬에 npm install로 node_modules을 둘 필요가 없습니다.
$ docker run -d -p 8080:8080 -v /usr/src/app/node_modules -v $(pwd):/usr/src/app {내 도커ID}/{저장소명}

6. Docker Compose


Docker Image를 생성하고 실행하려면 터미널에 빌드 명령어와 실행 명령어를 직접 타이핑해야합니다.
더구나 위에서 봤듯이, Docker Image를 실행할 때 volume 설정만 더해도 타이핑이 상당히 길어집니다.
docker-compose.yml 파일에 터미널에 타이핑해야할 것들을 명시해두고 사용하면 타이핑 양도 적어지고, 재사용할 수도 있습니다.
다음과 같이 docker-compose.yml 파일을 작성할 수 있습니다.

version: '3'
services:
  react: # 컨테이너명 
    build: # 해당 디렉토리에 있는 Dockerfile을 이용해 빌드합니다. (Docker Image 생성용)
      context: . # Dockerfile이 있는 위치를 명시합니다.
      dockerfile: Dockerfile.dev # Dockerfile명을 명시합니다. (디폴트는 Dockerfile)
    ports:
      - "3000:3000"
    volumes:
      - /usr/src/app/node_modules
      - ./:/usr/src/app # :(콜론)을 기준으로 좌측이 매핑할 로컬 경로이고, 우측이 컨테이너 경로입니다.
      stdin_open: true
  
  tests: # 컨테이너명
    build:
      context: .
      dockerfile: Dockerfile.dev
    volumes:
      - /usr/src/app/node_modules
      - ./:/usr/src/app
    command: ["npm", "run", "test"]

위에서 보시는것처럼 docker-compose.yml에는 여러 컨테이너에 대한 작업을 적어줄 수 있는데요, docker-compose의 또 다른 역할은 멀티 컨테이너 상황에서 네트워크 연결을 쉽게 하는 것입니다. 만약 클라이언트 애플리케이션을 위한 컨테이너를 띄우고, 서버 애플리케이션을 위한 컨테이너를 띄웠다고 가정해보겠습니다. 이때 클라이언트와 서버 컨테이너는 서로 통신할 수 있어야하는데, 이때 docker-compose를 이용하면 쉽게 설정해줄 수 있습니다.