Docker

컴퓨터 환경을 컨테이너로 만들어 사용할 수 있도록 하는 기술

도커는 컨테이너라는 가상머신을 구축, 배포, 복사하여 서로 다른 환경에서 사용할 수 있다. 컨테이너를 통해 프로세스를 분리/독립적으로 실행될 수 있도록하며, 이러한 독립성은 여러 프로세스를 개별적으로 실행하여 인프라를 더 효과적으로 활용하고, 개별 환경에서도 동일한 보안을 유지할 수 있다.

💡 Docker를 쓰는 이유

도커를 사용하지 않았을 경우

도커를 사용하지 않았을 경우

도커를 이용한 컨테이너화

도커를 이용한 컨테이너화

Docker Container


도커 컨테이너는 Linux 컨테이너 기반이다.

컨테이너는 인프라 또는 애플리케이션을 이루는 프로세스로, 이러한 프로세스를 실행하는데 필요한 모든 파일은 고유한 이미지로 저장된다. 따라서 전통적인 개발 환경을 구축하는데 의존하는 개발 파이프라인보다 사용 시점을 앞당길 수 있다.

Untitled

가상머신과 차이점

가상머신의 가상화는 단일 하드웨어 시스템에서 여러 OS가 동시에 실행될 수 있지만, 컨테이너는 동일한 OS 커널을 공유하고 애플리케이션을 격리한다.

Untitled

가상머신은 하이퍼바이저를 이용하여 하드웨어를 애뮬레이션(시스템 복제)한다. 이 경우 컨테이너를 사용하는 것만큼 경량화할 수 없다.

Linux 컨테이너는 운영 체제에서 실행되고 공유하므로 애플리케이션을 가볍게 유지할 수 있으며 빠른 속도로 동시에 실행할 수 있다.

가상머신 비유

가상머신 비유

도커 컨테이너 예

도커 컨테이너 예

Docker 시작하기

도커 설치 후 아래 명령어로 컨테이너를 만들 수 있다. 정상적으로 컨테이너가 실행되었다면 http://127.0.0.1로 접속할 수 있다.

$ docker run -d -p 80:80 docker/getting-started

run 명령어로 Docker Hub에 있는 이미지를 컨테이너로 만들어 실행시킬 수 있다.

  • -d 옵션은 분리모드로 백그라운드에서 도커 컨테이너를 실행한다.
  • -p 80:80 옵션은 호스트 포트 80번과 컨테이너 포트 80번을 매핑한다.
  • docker/getting-started 사용할 이미지

컨테이너 이미지란?

컨테이너를 실행할 때 격리된 파일 시스템을 사용하는데, 이 파일 시스템은 컨테이너 이미지에 의해 제공된다. 이미지에는 파일 시스템이 포함되있으며 애플리케이션을 실행하는데 필요한 모든 요소가 포함 되어야한다. 또한 이미지에는 환경변수, 실행할 기본 명령어, 메타데이터가 있다.

Dockerfile로 이미지 구성하기

Dockerfile은 이미지를 구성하기 위한 명령의 집합이다. 애플리케이션을 실행하기 위해 컨테이너에서 필요로 하는 소스코드, 종속성 패키지 설치를 수행한다. 또한 어느 시점에서 빌드와 실행을 할지 지정할 수 있다.

예제 파이썬 프로젝트를 컨테이너로 만들기 위한 Dockerfile 구성은 다음과 같다.

# 우분투OS 이미지를 가져옴
FROM ubuntu:18.04

# 작업할 디렉토리를 지정한다.
WORKDIR /src/usr/app

# 현재 디렉토리에 모든 파일을 컨테이너로 복사한다.
COPY . /app

# 컨테이너를 빌드하는 시점에서 앱 빌드
RUN make /app

# 컨테이너가 실행되는 시점에서 python 실행
CMD python /app/app.py

Dockerfile 명령어

  • FROM 하나의 Docker 이미지는 새로운 이미지를 중첩해 여러 단계의 Layer를 쌓아가며 만들어진다. FROM 명령문은 기본 이미지(base)를 지정해주기 위해 사용되는데, 보통 Dockerfile 최상단에 위치한다. base는 보통 Docker Hub와 같은 Docker repository에 올려놓은 잘 알려진 이미지를 가져온다.
  • WORKDIR 작업할 디렉토리를 지정한다. 지정된 디렉토리에서 COPY RUN CMD 등의 명령을 수행한다.
  • COPY 호스트(도커를 돌리는 컴퓨터)에 있는 디렉토리나 파일을 도커 이미지로 복사하기 위해서 사용된다.
  • RUN 쉘(Shell) 명령어를 실행하는 것 처럼 이미지 빌드 과정에서 필요한 커맨드를 실행하기 위해 사용된다. 보통 이미지 안에 특정 소프트웨어 또는 라이브러리를 설치하기 위해서 사용된다.

Dockerfile 이미지로 빌드

build 명령어로 컨테이너 이미지를 빌드할 수 있다. Dockerfile 내용을 토대로 많은 레이어가 설치되는데, ubuntu:18.14 이미지의 경우 컴퓨터에 없기 때문에 Docker Hub에서 이미지를 다운로드 받는다.

$ docker build -t getting-started .

-t 옵션은 이미지에 태그를 지정하는데, 여기서는 이미지의 이름정도로 생각하면 될 것 같다. 빌드가 완료되면 아래 명령어로 우리가 지정한 getting-started 라는 이미지가 생성된 것을 볼 수 있다.

$ docker images                                                                                                                       ✔  17:16:08 
REPOSITORY                 TAG       IMAGE ID       CREATED          SIZE
getting-started            latest    3c32b8d3e08d   21 minutes ago   430MB

컨테이너 시작

이전에 실행했던 run 명령어로 이미지를 컨테이너로 실행할 수 있다.

$ docker run -dp 3000:3000 getting-started

위 명령어로 컨테이너로 분리된 파이썬 애플리케이션을 백그라운드에서 실행한다. 호스트 3000 포트와 컨테이너 3000포트를 매핑하여 호스트에서 3000포트로 접속할 수 있다.

컨테이너 유지시키기

어떤 컨테이너에 저장된 데이터는 다른 컨테이너에서 사용할 수 없다.

예를 들어, 아래와 같이 우분투 이미지로 빌드된 컨테이너 A에 data.txt 파일을 만들고, 동일한 이미지로 빌드된 컨테이너 B로 접속할 경우 data.txt 파일을 찾을 수 없다.

$ docker run -d ubuntu bash -c "shuf -i 1-10000 -n 1 -o /data.txt && tail -f /dev/null"
$ docker exec -it <컨테이너-ID> cat /data.txt
...

$ docker run -it ubuntu ls /

이러한 데이터 유실 문제를 볼륨으로 해결할 수 있다.

컨테이너 볼륨(Volume)

볼륨은 컨테이너의 특정 경로를 호스트 시스템에 다시 연결하는 기능이다. 컨테이너의 디렉토리가 마운트(연결)되면 해당 디렉토리의 변경 사항은 호스트 시스템에서도 볼 수 있다. 컨테이너를 다시 시작할 때 동일한 디렉토리에 마운트 되면 동일한 파일이 표시된다.

경량화 데이터베이스인 SQLite를 이용하여 볼륨을 공유하고 컨테이너에 데이터를 유지해보자. 아래 명령어로 예제 node.js 프로젝트를 다운로드 받는다.

$ git clone https://github.com/docker/getting-started

app 디렉토리로 이동하면 Dockerfile을 확인할 수 있다. 컨테이너를 생성하여 http://127.0.0.1:3000 로 접속할 수 있다.

$ cd ./app
$ ls
Dockerfile   package.json spec         src          yarn.lock
$ docker run -dp 3000:3000 getting-started

아래 명령어로 도커 볼륨을 생성할 수 있다.

$ docker volume create todo-db

run 명령어로 컨테이너를 실행할 때, -v 옵션으로 볼륨 마운트(호스트와 연결)를 지정할 수 있다. 생성한 todo-db 볼륨과 컨테이너의 /etc/todos 경로에 생성된 모든 파일을 캡쳐하여 마운트한다.

$ docker run -dp 3000:3000 -v todo-db:/etc/todos getting-started

볼륨을 사용할 때 실제 데이터가 저장되는 경로는 아래 명령어로 확인할 수 있다.

$ docker volume inspect todo-db
[
    {
        "CreatedAt": "2022-03-19T11:42:11Z",
        "Driver": "local",
        "Labels": {},
        "Mountpoint": "/var/lib/docker/volumes/todo-db/_data",
        "Name": "todo-db",
        "Options": {},
        "Scope": "local"
    }
]

실제 데이터가 저장되는 위치를 마운트 포인트(Mountpoint)라고 한다.

원하는 경로에 볼륨 설정하기

생성된 볼륨을 컨테이너에 지정하는 명명된 볼륨의 경우 데이터 저장 위치를 걱정없이 도커가 알아서 처리해주어 단순 데이터 저장에 유용하다.

바인드 마운트(Bind Mount)를 사용하면 호스트의 마운트포인트를 지정할 수 있다. 바인드 마운트를 이용하면 컨테이너에 추가 데이터를 제공하는데 유용하다. 애플리케이션을 도커 컨테이너로 분리한 후 바인드 마운트를 통해 소스코드를 마운트하면 코드 변경을 즉시 볼 수 있다.

개발환경 컨테이너

바인드마운트를 이용하여 개발환경을 지원하는 컨테이너를 구축하기 위해서는 다음과 같다.

  • 소스코드를 컨테이너에 저장
  • 종속성 설치
  • 파일 시스템 변경을 감지하기 위해 *nodemon 설치

컨테이너에 바인드 마운트 및 종속성 설치

다음 명령어로 getting-started 컨테이너를 실행한다.

$ docker run -dp 3000:3000 \
     -w /app -v "$(pwd):/app" \
     getting-started \
     sh -c "yarn install && yarn run dev"

이전과 동일하게 백그라운드에서 컨테이너를 실행하고 3000포트를 매핑한다.

  • -w /app : 명령이 실행될 디렉토리를 지정한다.
  • -v "$(pwd):app" : 호스트와 컨테이너를 바인드 마운트(pwd 명령어는 현재 경로이다.)
  • node:12-alpine : 사용할 이미지(Dockerfile에서 가져온다.)
  • sh -c "yarn install && yarn run dev" : 애플리케이션의 종속성을 설치한다.

docker logs 명령어로 백그라운드에서 실행되는 컨테이너의 로그를 볼 수 있다.

$ docker logs -f <container-id>
...
Listening on port 3000

이후 바인드 마운트로 공유된 src/static/js/app.js 파일을 다음과 같이 수정하면, 컨테이너의 nodemon에서 수정된 내용을 확인하고 node.js 애플리케이션을 재시작한다.

- {submitting ? 'Adding...' : 'Add Item'}
+ {submitting ? 'Adding...' : 'Add'}

다중 컨테이너 애플리케이션


지금까지 단일 컨테이너에서 애플리케이션을 구축했지만, 하나의 작업은 하나의 컨테이너에서 이루어져야한다.

  • 현재 컨테이너 구조
    • Container: getting-started
      • Front-End
      • Back-End
      • Database
  • 각각의 작업을 컨테이너로 분리
    • Container: Front-End
    • Container: Back-End
    • Container: Database

컨테이너 네트워킹

컨테이너는 기본적으로 격리되어 있기 때문에 다른 프로세스나 컨테이너를 알 수 없다. 때문에 각각의 컨테이너는 동일한 네트워크를 사용해야 통신이 가능하다.

데이터베이스 분리

네트워크에 컨테이너를 설정하는 방법은 다음과 같다.

  1. 컨테이너 시작 시 네트워크 할당
  2. 기존 컨테이너에 네트워크 연결

아래 명령어로 네트워크를 생성할 수 있다.

$ docker network create todo-app

Docker Hub에서 MySQL 이미지를 가져와 컨테이너를 시작한다. —network 옵션으로 방금 생성한 네트워크로 설정해 준다.

$ docker run -d \
     --platform linux/amd64
     --network todo-app --network-alias mysql \
     -v todo-mysql-data:/var/lib/mysql \
     -e MYSQL_ROOT_PASSWORD=secret \
     -e MYSQL_DATABASE=todos \
     mysql:5.7

--network-alias mysql 옵션은 추후 다루어 보겠다.

MySQL 컨테이너가 제대로 생성되었는지 접속해 확인한다.

$ docker exec -it <mysql-container-id> mysql -u root -p

컨테이너 연결을 위한 네트워크 디버깅

실행된 MySQL 컨테이너를 연결하기 전 네트워크 디버깅을 통해 도커 네트워크에 대해 자세히 알아보자

Docker및 Kubernetes 네트워크 문제를 해결하기위해 많이 사용되는 디버깅 툴 nicolaka/netshoot 컨테이너를 사용해보자

nicolaka/netshoot 컨테이너를 실행한다.

$ docker run -it --network todo-app nicolaka/netshoot

DNS 도구인 dig 명령을 사용하여 MySQL 컨테이너의 호스트, IP등을 조회할 수 있다.

$ dig mysql

; <<>> DiG 9.16.22 <<>> mysql
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 59256
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;mysql.				IN	A

;; ANSWER SECTION:
mysql.			600	IN	A	172.24.0.2

;; Query time: 1 msec
;; SERVER: 127.0.0.11#53(127.0.0.11)
;; WHEN: Tue Mar 22 15:13:34 UTC 2022
;; MSG SIZE  rcvd: 44

“ANSWER SECTION”을 보면, A레코드가 호스트 이름 mysqlIP=172.24.0.2로 매핑되어 있다. mysql 이 유효한 호스트명은 아니지만, 이전에 설정했는 --network-alias 옵션의 별칭으로 설정되어 있다.

즉, 호스트명을 mysql 로 사용한다면 매핑된 IP주소(172.24.0.2)로 연결되어 컨테이너 통신을 할 수 있다.

기존의 컨테이너에 네트워크를 설정하려면 —network 옵션을 사용하면 된다.

$ docker run -dp 3000:3000 \
   -w /app -v "$(pwd):/app" \
   --network todo-app \
   -e MYSQL_HOST=mysql \
   -e MYSQL_USER=root \
   -e MYSQL_PASSWORD=secret \
   -e MYSQL_DB=todos \
   node:12-alpine \
   sh -c "yarn install && yarn run dev"

Docker Compose

다중 컨테이너 애플리케이션을 정의 및 공유하는데 사용되는 도구이다. 지금까지 진행했던 컨테이너 설정(볼륨 공유, 네트워크 설정 등)을 YAML 파일에 정의하여 사용된다. 정의를 통해 여러 컨테이너를 실행할 수 있다.

Docker Compose의 큰 장점은 애플리케이션을 이루는 스택을 파일에 정의한 후 컨테이너 설치만 하면 되기 때문에 다른 개발자가 쉽게 접근할 수 있다.

Docker Compose 설치

Docker Desktop/Toolbox가 있다면 기본적으로 Docker Compose가 설치되어 있다. Linux 환경의 경우 Docker Compose를 설치해야 한다.

설치 후 다음 명령어로 설치와 버전 정보를 확인한다.

docker-compose version

기본 구성

다중 애플리케이션을 구성하기 위해서 docker-compose.yml 이라는 파일에 컨테이너에 대한 정보를 정의한다. 앱 프로젝트의 루트에 docker-compose.yml 파일을 생성한다.

기본적으로 스키마 버전을 정의해야 한다. 대부분 최신 버전을 사용하는 것이 가장 좋다. 이후 실행하려는 서비스(컨테이너)를 정의한다.

version: "3.8"

services: 

서비스 정의하기

기존 CLI로 실행했던 MySQL 컨테이너는 다음과 같다.

$ docker run -dp 3000:3000 \
  -w /app -v "$(pwd):/app" \
  --network todo-app \
  -e MYSQL_HOST=mysql \
  -e MYSQL_USER=root \
  -e MYSQL_PASSWORD=secret \
  -e MYSQL_DB=todos \
  node:12-alpine \
  sh -c "yarn install && yarn run dev"

이를 서비스 항목에 정의하여 간편화 할 수 있다.

먼저 서비스 이름과 컨테이너의 이미지를 정의한다. 서비스 이름의 경우 원하는 이름으로 정의하며, 이름은 자동으로 Network Alias(도커 네트워크에서의 호스트명)로 설정된다.

version: "3.8"

 services:
   app:
     image: node:12-alpine

command 속성을 통해 명령어를 사용할 수 있다.(Dockerfile CMD와 유사하다. CMD 명령어가 없으면 docker-compose.yml에 정의된 command를 실행한다. 정의되어 있다면 overriding)

포트 매핑 -p 3000:3000 옵션의 경우 ports 를 정의하여 사용한다.

version: "3.8"

 services:
   app:
     image: node:12-alpine
     command: sh -c "yarn install && yarn run dev"
     ports:
       - 3000:3000

작업 디렉토리 지정 옵션인 -w /appworking_dir 를 통해 정의한다.

볼륨 공유 -v "$(pwd):/app" 의 경우 volumes 에 정의한다.

version: "3.8"

 services:
   app:
     image: node:12-alpine
     command: sh -c "yarn install && yarn run dev"
     ports:
       - 3000:3000
     working_dir: /app
     volumes:
       - ./:/app

마지막으로 enviroment 속성에 환경변수를 정의할 수 있다.

version: "3.8"

 services:
   app:
     image: node:12-alpine
     command: sh -c "yarn install && yarn run dev"
     ports:
       - 3000:3000
     working_dir: /app
     volumes:
       - ./:/app
     environment:
       MYSQL_HOST: mysql
       MYSQL_USER: root
       MYSQL_PASSWORD: secret
       MYSQL_DB: todos

MySQL 서비스 정의

MySQL 컨테이너 실행 명령어는 다음과 같다.

docker run -d \
  --network todo-app --network-alias mysql \
  -v todo-mysql-data:/var/lib/mysql \
  -e MYSQL_ROOT_PASSWORD=secret \
  -e MYSQL_DATABASE=todos \
  mysql:5.7

이제 node.js 애플리케이션을 정의한 것 처럼 MySQL을 서비스로 정의한다면 다음과 같다.

...

  mysql:
    image: mysql:5.7
    volumes:
      - todo-mysql-data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: secret
      MYSQL_DATABASE: todos

volumes:
   todo-mysql-data:

볼륨 마운트의 경우, 명령어에서 볼륨 이름을 정의한다면 볼륨이 자동으로 생성되지만, Docker Compose에서는 자동으로 생성되지 않는다. volumes 속성을 따로 정의한 후 해당 볼륨명을 사용하면 기본 옵션이 적용된다.

애플리케이션 스택 실행

node.js와 MySQL을 컨테이너로 분리 시키기 위해 Docker Compose로 정의하였다.

아래 명령어를 이용하여 애플리케이션 스택을 실행한다. -d 옵션으로 백그라운드에서 실행된`다.

$ docker-compose up -d

Creating network "app_default" with the default driver
 Creating volume "app_todo-mysql-data" with default driver
 Creating app_app_1   ... done
 Creating app_mysql_1 ... done

명령어를 실행하면 우선적으로 볼륨과 네트워크가 생성된다. 기본적으로 Docker Compose에서는 애플리케이션 스택을 위한 네트워크를 자동으로 생성한다. 때문에 docker-compose.yml 파일에 네트워크를 정의하지 않는다.

docker-compose logs -f 명령어를 통해 로그를 살펴볼 수 있다.

실행된 애플리케이션 스택을 내리기위해서 docker-compose down 명령어를 사용하면 된다. 해당 명령어를 통해 컨테이너가 중지되고 네트워크가 제거된다.