Docker
컴퓨터 환경을 컨테이너로 만들어 사용할 수 있도록 하는 기술
도커는 컨테이너라는 가상머신을 구축, 배포, 복사하여 서로 다른 환경에서 사용할 수 있다. 컨테이너를 통해 프로세스를 분리/독립적으로 실행될 수 있도록하며, 이러한 독립성은 여러 프로세스를 개별적으로 실행하여 인프라를 더 효과적으로 활용하고, 개별 환경에서도 동일한 보안을 유지할 수 있다.
💡 Docker를 쓰는 이유
도커를 사용하지 않았을 경우
도커를 이용한 컨테이너화
Docker Container
도커 컨테이너는 Linux 컨테이너 기반이다.
컨테이너는 인프라 또는 애플리케이션을 이루는 프로세스로, 이러한 프로세스를 실행하는데 필요한 모든 파일은 고유한 이미지로 저장된다. 따라서 전통적인 개발 환경을 구축하는데 의존하는 개발 파이프라인보다 사용 시점을 앞당길 수 있다.
가상머신과 차이점
가상머신의 가상화는 단일 하드웨어 시스템에서 여러 OS가 동시에 실행될 수 있지만, 컨테이너는 동일한 OS 커널을 공유하고 애플리케이션을 격리한다.
가상머신은 하이퍼바이저를 이용하여 하드웨어를 애뮬레이션(시스템 복제)한다. 이 경우 컨테이너를 사용하는 것만큼 경량화할 수 없다.
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: getting-started
- 각각의 작업을 컨테이너로 분리
- Container: Front-End
- Container: Back-End
- Container: Database
컨테이너 네트워킹
컨테이너는 기본적으로 격리되어 있기 때문에 다른 프로세스나 컨테이너를 알 수 없다. 때문에 각각의 컨테이너는 동일한 네트워크를 사용해야 통신이 가능하다.
데이터베이스 분리
네트워크에 컨테이너를 설정하는 방법은 다음과 같다.
- 컨테이너 시작 시 네트워크 할당
- 기존 컨테이너에 네트워크 연결
아래 명령어로 네트워크를 생성할 수 있다.
$ 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레코드가 호스트 이름 mysql
과 IP=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 /app
는 working_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
명령어를 사용하면 된다. 해당 명령어를 통해 컨테이너가 중지되고 네트워크가 제거된다.