도커(Docker) 입문편
컨테이너 기초부터 서버 배포까지

들어가며

도커Docker는 2013년에 등장한 컨테이너 기반 가상화 도구입니다. 파이콘 US 2013Pycon US 2013에서 솔로몬 하이크Solomon Hykes리눅스 컨테이너의 미래The Future of Linux Container라는 라이트닝 토크에서 도커를 처음 소개했습니다. 현재 도커는 고 언어Go로 개발되고 있으며, 2014년 도커콘 2014DockerCon 2014에서 1.0 버전을 발표한 이후, 2020년 현재 최신 버전은 19.03.8입니다.

도커는 리눅스 상에서 컨테이너 방식으로 프로세스를 격리해서 실행하고 관리할 수 있도록 도와주며, 계층화된 파일 시스템에 기반해 효율적으로 이미지(프로세스 실행 환경)을 구축할 수 있도록 해줍니다. 도커를 사용하면 이 이미지를 기반으로 컨테이너를 실행할 수 있으며, 다시 특정 컨테이너의 상태를 변경해 이미지로 만들 수 있습니다. 이렇게 만들어진 이미지는 파일로 보관하거나 원격 저장소를 사용해 쉽게 공유할 수 있으며, 도커만 설치되어 있다면 필요할 때 언제 어디서나 컨테이너로 실행하는 것이 가능합니다.

이 글은 도커 입문자를 위한 튜토리얼로서, 도커의 핵심적인 개념들과 기본적인 사용법을 소개합니다. 도커를 왜 사용해야하는지 궁금하신 분들은 먼저 아래 글을 읽어주세요.

44BITS 소식과 클라우드 뉴스를 전해드립니다. 지금 5,000명 이상의 구독자와 함께 하고 있습니다 📮

도커(Docker) 시작하기: 우분투에서 센트OS로 프로세스 실행하기

사용자용 가상머신Virtual Machine은 이제 대중적인 도구가 되었습니다. 가상머신을 실행하는 호스트 머신에 가상화된 하드웨어와 OS를 구축함으로써, 호스트와는 다른 환경을 구축하고 개발과 테스트를 비롯한 다양한 목적으로 사용할 수 있습니다. 하드웨어를 소프트웨어로 에뮬레이션하는 가상머신은 시스템 분리를 통한 프로세스 격리 관점에서는 아주 후한 점수를 줄 수 있지만, 단순히 프로세스를 실행하기 위한 환경으로서는 성능을 비롯한 여러 단점을 가지고 있습니다.

컨테이너는 하드웨어를 소프트웨어로 재구현하는 가상화(= 가상머신)와는 달리 프로세스의 실행 환경을 격리합니다. 컨테이너가 실행되고 있는 호스트 입장에서 컨테이너는 단순히 프로세스에 불과합니다만, 사용자나 컨테이너 입장에서는 호스트와는 무관하게 동작하는 가상머신처럼 보입니다. 그래서 컨테이너형 가상화라고 부르기도 합니다. 도커는 이러한 컨테이너 형 가상화를 지원하는 도구 중 하나입니다. 도커는 가상머신의 역할을 넘어서 어느 플랫폼에서나 특정한 상태를 그대로 재현가능한 애플리케이션 컨테이너를 관리하는 도구를 목표로 합니다. LXC(리눅스 컨테이너)로부터 파생된 도커 컨테이너는 가상머신과는 근본적으로 다른 접근이라는 점을 짚어둘 필요가 있습니다.

도커는 가상머신과 같이 하드웨어를 가상화하는 것이 아니라, 리눅스 운영체제에서 지원하는 다양한 기능을 사용해 컨테이너(하나의 프로세스)를 실행하기 위한 별도의 환경(파일 시스템)을 준비하고, 리눅스 네임스페이스와 다양한 커널 기능을 조합해 프로세스를 특별하게 실행시켜줍니다. 이는 가상머신과 같이 하드웨어를 가상화하는 것이 아니라, 운영체제 상에서 지원하는 방법을 통해서 하나의 프로세스(컨테이너)를 실행하기 위한 별도의 환경을 구축하는 일을 지원하고, 도커는 바로 프로세스를 격리시켜 실행해주는 도구라고 할 수 있습니다.

가상머신과의 차이를 보기 위해서, 간단한 예를 살펴보겠습니다. 예를 들어 우분투 18.04에서 Ubuntu에서 센트OSCentOS 환경을 구축하는데 시간이 얼마나 걸릴까요? 먼저 우분투 상에서 작업을 하고 있다고 가정해보겠습니다.

$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
DISTRIB_DESCRIPTION="Ubuntu 18.04.3 LTS"
...

이미 도커가 설치되어있는 환경이라면 명령어 하나로 센트OS 머신을 실행할 수 있습니다.*

* 아래 명령어를 실행하는 환경이 꼭 우분투여야하는 것은 아닙니다. 다른 리눅스 배포판이나 맥OSmacOS에서도 도커가 설치되어있다면 아래 내용을 똑같이 실행해볼 수 있습니다.

$ docker run -it --rm centos:latest bash
Unable to find image 'centos:latest' locally
latest: Pulling from library/centos
8d30e94188e7: Pull complete
Digest: sha256:2ae0d2c881c7123870114fb9cc7afabd1e31f9888dac8286884f6cf59373ed9b
Status: Downloaded newer image for centos:latest

[root@881189373f8b /]# 

docker run -it centos:latest bash 명령어는 centos:latest 이미지로부터 bash를 실행하라는 의미입니다. 아직 이미지가 없다면, 도커의 공식 저장소에서 이 이미지를 풀 받아옵니다.* 그리고 이 이미지를 기반으로 bash 프로세스를 실행합니다. 안정적인 네트워크 환경에서는 센트OS 이미지를 받는 데 채 1분이 걸리지 않습니다. 명령어를 실행하는 시간은 우분투 환경에서 그냥 명령어를 실행하는 것과 차이가 없다고 봐도 무방합니다. 마지막에는 셸 프롬프트가 달라진 것을 볼 수 있습니다.

그럼 지금 실행중인 셸의 환경이 정말 센트OS인지 확인해보도록 하겠습니다.

* 계속해서 보시겠지만 ‘풀’이라는 표현은 이미지를 원격 저장소에서 로컬로 다운로드 받는다는 의미입니다.

[root@881189373f8b /]# cat /etc/redhat-release
CentOS Linux release 8.1.1911 (Core)

무슨 일이 벌어진 걸까요? 😮

이 튜토리얼에서는 컨테이너의 원리보다는 도구로서 도커를 사용하는 법에 집중적으로 소개합니다. 원리적인 부분도 중요하지만 이 도구를 실제로 활용하려면 컨테이너와 이미지의 차이를 이해해야 하고, Dockerfile을 사용해 이미지 만드는 법을 익힐 필요가 있습니다. 이 글에서는 바로 이 관점에서 개발 환경이자 배포 도구로써 도커를 이해하기 위한 개념들을 소개하고 애플리케이션을 도커로 이미지화하는 부분까지 소개하고자 합니다.*

* 이 글의 마지막 부분에 도커의 원리를 해설하는 글들도 링크해놓았으니 참고해주세요.

도커(Docker) 설치하고 기본적인 설정하기

도커는 다양한 환경에서 사용할 수 있습니다. 개발 환경에서는 주로 도커와 관련 도구들로 구성된 패키지인 도커 포 맥Docker for Mac도커 포 윈도우Docker for Windows*를 사용합니다. 단, 도커는 리눅스 컨테이너를 직접 지원하지 않는 맥OSmacOS나 윈도우즈에서는 경량 가상머신이 중간에서 사용됩니다.

* 윈도우에서 도커를 사용하려면 도커 포 윈도우Docker for Windows를 사용하거나, 가상머신을 사용해야합니다. 지금까지 도커 포 윈도우는 윈도우 10 프로페셔널만을 지원해왔습니다. 현재 2020년 3월 현재 도커에서는 WSL 2를 기반으로 윈도우 10 홈 지원을 준비하고 있습니다. Docker Desktop for Windows Home is here! - Docker Blog.

리눅스 환경에서는 각 배포판에서 제공하는 패키지 관리자를 사용해서 도커를 설치할 수 있습니다. 단, 일반적으로 패키지 관리자는 보수적으로 관리되기 때문에 최신 버전을 설치하고자 한다면 도커에서 제공하는 스크립트를 사용해서 설치하는 것이 좋습니다. 여기서는 이 스크립트를 사용해 정식 릴리즈된 도커 최신 버전을 설치해보겠습니다.*

* curl이 없다면 sudo apt-get curl로 설치합니다.

$ curl -s https://get.docker.com | sudo sh

스크립트를 실행하면 현재 환경을 파악하고 적절한 과정을 거쳐 도커를 설치해줍니다. 이 설치 명령어는 아주 편리하니 기억해두시면 좋습니다. 설치에는 몇 분 정도의 시간이 소요됩니다. 스크립트는 조금 복잡해보이지만*, 우분투 환경에서는, 패키지 관리자에 도커에서 관리하는 저장소를 추가하고 docker-engine 패키지를 설치하도록 되어있습니다. 스크립트가 끝나면 도커가 정상적으로 설치되었는지 확인합니다.

* https://get.docker.com를 직접 열어보면 스크립트 파일 내용을 직접 확인할 수 있습니다. 이 스크립트는 도커에서 공식적으로 제공하는 도커 설치 스크립트입니다.

$ docker -v
Docker version 19.03.8, build afacb8b7f0

2020년 3월 현재 최신 버전인 19.03.8 버전이 설치되었습니다.

$ cat /etc/apt/sources.list.d/docker.list
deb [arch=amd64] https://download.docker.com/linux/ubuntu bionic stable

$ dpkg --get-selections | grep docker
docker-ce                                       install
docker-ce-cli                                   install

스크립트로 도커 설치를 하고 나면 위와 같이 /etc/apt/sources.list.d/docker.list가 추가된 것을 볼 수 있습니다. 그리고 dpkg 명령어로 설치된 패키지 중에서 docker 이름이 들어간 패키지를 찾아봅니다. docker-cedocker-ce-cli가 설치된 것을 확인할 수 있습니다.

노트
도커의 패키지 구성

도커 패키지는 원래 docker-engine 하나였습니다만 현재는 docker-cedocker-ce-cli 두 가지로 나눠져 있습니다. 여기서 ce는 커뮤니티 에디선Community Edition의 줄임말입니다. 패키지가 나눠져있는 이유를 이해하려면 도커의 아키텍처에 대해서 알아야 합니다. 도커는 크게 도커 엔진과 클라이언트로 나뉩니다. 도커 엔진은 서버로 동작하며, 시스템 상에 서비스로 등록 됩니다. 도커 클라이언트는 사용자가 입력하는 docker 명령어입니다. 이 명령어를 실행하면 클라이언트는 도커 서버에 명령을 전달하고, 명령은 전적으로 서버에서 처리됩니다. 도커 클라이언트로 외부의 도커 서버에 명령을 내리는 것도 가능합니다. 이러한 아키텍처를 반영해 도커 엔진과 도커 클라이언트 패키지가 나눠졌습니다. 도커 설치에 대한 더 자세한 내용은 Get Docker Engine 공식 문서를 참고해주세요.

도커를 설치하면 도커 엔진의 시스템 서비스도 같이 실행됩니다. 클라이언트는 이 서비스 프로세스에 명령어를 보내는 방식으로 작동합니다. 그렇다면 이제 도커 명령어를 하나 실행해보겠습니다.

$ docker ps 
Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get http://%2Fvar%2Frun%2Fdocker.sock/v1.40/containers/json: dial unix /var/run/docker.sock: connect: permission denied

docker ps는 앞으로 자주 사용할 명령어 중 하나로, 현재 실행중인 모든 컨테이너 목록을 출력하라는 명령어입니다. 하지만 도커 데몬에 연결할 수 없다는 에러 메시지가 나옵니다. 앞서 얘기했듯이 도커를 설치하는 순간 도커 서비스도 같이 실행됩니다. 일반적으로 이러한 에러가 발생하는 이유는 사용자에게 도커 소켓에 접근할 권한이 없기 때문입니다. 관리자 권한이 있는 경우 명령어 앞에 sudo를 붙이면 정상적으로 실행될 것입니다.

$ sudo docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

사용자 계정에서도 도커를 직접 사용할 수 있도록 docker 그룹에 사용자를 추가해줍니다. 이 때 관리자 권한이 필요합니다.

$ sudo usermod -aG docker $USER
$ sudo su - $USER

이제 sudo 명령어 없이도 도커 명령어를 바로 사용할 수 있습니다.

$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAME

아직 컨테이너를 실행하지 않아서 목록에 출력되는 것은 없지만 정상적으로 실행되는 것을 확인할 수 있습니다. 이제 본격적으로 도커를 함께 탐구해보죠!

도커 이미지(Docker Image) 기초

이미지와 컨테이너는 도커를 이해하는 데 있어 가장 중요한 개념입니다. 여기서는 먼저 이미지 개념에 대해서 같이 살펴보겠습니다. 이미지는 가상머신에서 사용하는 이미지와 비슷한 역할을 합니다. 한 마디로 정의해보자면 이미지는 어떤 애플리케이션을 실행하기 위한 환경이라고 할 수 있습니다. 그리고 이 환경은 파일들의 집합입니다. 도커에서는 애플리케이션을 실행하기 위한 파일들을 모아놓고, 애플리케이션과 함께 이미지로 만들 수 있습니다. 그리고 이 이미지를 기반으로 애플리케이션을 바로 배포할 수 있습니다.

처음에 간단히 살펴 본 예제에서는 centos 컨테이너에서 bash 셸을 실행했습니다. 이 과정을 좀 더 풀어써보면 다음과 같습니다.

  1. 도커 레지스트리에서 centos 이미지를 풀(pull) 받는다.
  2. 이 이미지를 통해서 컨테이너를 실행한다.

이번에는 이 과정을 나눠서 진행해보겠습니다. 먼저 1의 이미지를 풀 받는 부분만을 진행합니다. 이제 막 도커를 설치했으니 이미지가 없다는 것을 확인해봅니다.

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE

아무것도 출력되지 않습니다. 그렇다면 centos 이미지를 도커 레지스트리 상에서 풀 받아보겠습니다. 도커에서는 docker pull <IMAGE_NAME> 명령어로 이미지를 풀 받을 수 있습니다.

$ docker pull centos
Using default tag: latest
latest: Pulling from library/centos
8a29a15cefae: Pull complete
Digest: sha256:fe8d824220415eed5477b63addf40fb06c3b049404242b31982106ac204f6700
Status: Downloaded newer image for centos:latest
docker.io/library/centos:latest

이미지 이름은 :을 구분자로 이미지 이름과 태그로 구분됩니다. 태그를 지정하지 않으면 기본값으로 latest가 사용됩니다. 따라서 centoscentos:latest와 같습니다. 도커는 먼저 이 이미지를 로컬에서 찾아보고, 찾을 수 없으면 도커 공식 저장소에서 찾아봅니다. 저장소에 해당하는 이미지가 존재하면 이미지를 풀(pull) 받아옵니다. 마지막 줄의 메시지로 부터 centos:latest 이미지가 다운로드된 것을 알 수 있습니다.

여기서도 알 수 있는 도커의 가장 큰 매력 중 하나는 별다른 설정 없이 곧바로 공식 저장소를 통해서 이미지를 받아올 수 있다는 점입니다. 리눅스의 apt, yum이나 프로그래밍 언어의 gem, cpan, pip 같은 패키지 매니저에 익숙하시다면 이 장점을 어렵지 않게 이해하실 수 있을 것입니다. 이런 도구들에 익숙하지 않다면, 어딘가에 공개된 도커 이미지를 저장해주는 서버가 있어서 필요할 때 마음껏 다운로드 받을 수 있다고 이해하셔도 무방합니다.

한 가지 재미있는 점은 pull이라는 명령어 이름입니다. 도커에서는 이미지를 다운 받을 때 install이나 download와 같은 명령 대신 pull을 사용합니다. 앞으로 살펴보겠지만 이는 단순히 이미지를 다운로드 받는 데서만 그런 것은 아닙니다. 이미지를 업로드 할 때는 push라는 명령어를 쓰고, 새로운 이미지를 생성할 때는 commit, 이미지의 차이를 확인할 때는 diff라는 명령어를 사용합니다. 이러한 명령어 이름은 깃Git이나 서브버전Subversion에서 사용되는 명령어들로 개발자들에게는 친숙한 이름들입니다. 기능적으로는 이미지를 다운로드 받아온다고 이해해주시기 바랍니다.

앞서 사용해본 images 명령어로 다운 받은 이미지를 확인해보겠습니다.

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
centos              latest              470671670cac        8 weeks ago         237MB

centos:latest 이미지가 추가된 것을 볼 수 있습니다. 앞서 pull 명령어가 VCS의 명령어와 비슷한 것처럼 도커에서는 하나의 이미지를 저장소repository라고 부릅니다. TAG는 임의로 붙여진 추가적인 이름입니다. 일반적으로 이미지의 버저닝을 하기 위해서 사용됩니다. 앞서 이야기했듯이 도커 명령어에서 이미지를 지정할 때 태그를 생략하면 latest가 사용됩니다. IMAGE ID는 이미지를 가리키는 고유한 해시 값입니다. CREATED는 이미지가 생성된 시간, SIZE는 이미지의 용량을 나타냅니다. docker images는 앞으로 자주 사용하게 될 명령어이니 꼭 기억해두시기 바랍니다.

도커 허브(Docker Hub) - 공식 이미지 레지스트리

도커에서 제공하는 이미지 호스팅 서비스 도커 허브Docker Hub에 대해서 조금 더 살펴보도록 하겠습니다. docker info를 통해서 클라이언트에 지정된 기본 레지스트리의 주소를 확인할 수 있습니다.

$ docker info | grep Registry
 Registry: https://index.docker.io/v1/

index.docker.io는 도커 허브의 과거 도메인입니다. 이미지와 관련된 정보 도커 허브에서 확인할 수 있습니다. 예를 들어 앞서 풀 받은 centos 이미지 페이지에 대한 더 자세한 내용을 확인할 수 있습니다.

도커 허브의 CentOS 이미지 저장소

도커 허브에는 아주 많은 이미지들이 등록되어 있습니다. 이 이미지들은 도커 사에서 공식적으로 제공하는 이미지와 사용자들이 직접 만들어 올린 이미지들로 나눠집니다.

도커 사에서는 우분투Ubuntu, 센트OSCentOS와 같이 리눅스 운영체제 이미지와 MySQL, 레디스Redis, 워드프레스Wordpress와 같이 자주 사용되는 애플리케이션에 대한 공식 이미지를 제공하고 있습니다. 앞서 풀 받은 센트OS 이미지는 도커가 공식적으로 제공하는 이미지에 해당합니다. 일반적으로 도커가 제공하는 공식 이미지에는 네임스페이스가 없습니다. 네임스페이스는 이미지 이름에서 슬래시(/)로 구분되며, 슬래시 앞 부분이 네임스페이스가 되고, 슬래시 뒷 부분이 실제 이미지 네임이 됩니다. 도커의 공식 저장소에서는 사용자 이름을 네임스페이스로 사용합니다. 예를 들어 user1932라는 사용자가 wordpress라는 이름의 이미지를 만들면 전체 이미지 이름은 user1932/wordpress가 됩니다. centos는 사용자 이미지와 달리 네임스페이스가 없습니다. 도커 사에서 제공하는 공식 이미지는 다음 링크에서 확인할 수 있습니다.

이미지를 공유할 수 있는 도커 허브 서비스는 도커가 초반에 자리잡을 수 있도록 중요한 역할을 했습니다. 이에 대한 자세한 내용은 뒤에서 다루겠습니다. 우선 도커에 대해서 조금 더 알아보고 나서 직접 만든 이미지를 도커 허브에 올리는 법에 대해서도 소개하겠습니다.

컨테이너(Container) 이해하기 - 격리된 환경에서 실행되는 프로세스

앞서 centos 컨테이너를 실행하는 것은 다음과 같은 두 단계를 거친다고 이야기했습니다.

  1. 도커 레지스트리에서 centos 이미지를 풀 받아서 로컬로 다운로드 받는다.
  2. 이 이미지를 통해서 컨테이너를 실행한다.

이번에는 두번째 단계에 대해서 좀 더 자세히 다뤄보겠습니다. 이미지는 어떤 환경이 구성되어있는 상태를 저장해놓은 파일 집합이라고 이야기했습니다. 바로 이 이미지의 환경 위에서 특정한 프로세스를 격리시켜 실행한 것을 컨테이너라고 부릅니다. 컨테이너를 실행하려면 반드시 이미지가 있어야합니다. 다시 한 번 정리합니다. 이미지는 파일들의 집합이고, 컨테이너는 이 파일들의 집합 위에서 실행된 특별한 프로세스입니다.

컨테이너는 docker run를 통해서 실행할 수 있습니다. 여기서는 셸을 실행하기 위해서 -it를 옵션을 붙였습니다. docker run -it <이미지이름:태그> <명령어>centos 이미지 기반의 컨테이너를 하나 실행시켜보겠습니다.

$ docker run -it centos:latest bash
[root@d3fef9c0f9e9 /]#

셸 프롬프트가 [root@d3fef9c0f9e9 /]#로 바뀌었습니다. 우분투 환경에서 센트OS환경에 접속했습니다. 하지만 접속했다는 말에는 함정이 있습니다. 이 말은 마치 다른 서버에 SSH 프로토콜을 사용해 접근한 것과 같은 착각을 일으킵니다. 여기서는 SSH를 통해 서버에 접속한 것이 아니라, 호스트OS와 격리된 환경에서 bash 프로그램을 실행했다고 이해하는 것이 더 정확합니다. 컨테이너란 사실 프로세스에 불과하기 때문에 bash 대신 SSH 서버를 실행하고 SSH 클라이언트를 통해서 접속하는 것도 물론 가능합니다. 하지만 더 헷갈리기 때문에 일단 어디에 접속한 것이 아니라는 점을 기억해주세요.

이제 새로운 명령어를 하나 배워보도록 하겠습니다. 앞서 사용가능한 이미지들을 확인하는 명령어는 docker images였습니다. 이번에 사용할 명령어는 현재 실행중인 컨테이너 목록을 출력하는 명령어 docker ps입니다. 컨테이너로 실행된 배시 셸을 그대로 놔두고 별도의 셸이나 터미널을 열고 docker ps 명령어를 실행해보시기 바랍니다.

$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
d3fef9c0f9e9        centos:latest       "bash"              3 minutes ago       Up 3 minutes                            sweet_keldysh

3분 전에 만들어진 컨테이너가 실행되고 있는 것을 알 수 있습니다. ps 명령어의 실행 결과로부터 실행중인 컨테이너 정보를 확인할 수 있습니다. 이 예제에서는 centos:latest 이미지로부터 컨테이너를 생성했고, 이 격리된 환경에서 /bin/bash라는 명령어로 컨테이너를 실행했습니다. 그 외에도 맨 앞의 컨테이너 아이디는 앞으로 도커에서 컨테이너를 조작할 때 사용하는 아이디이기 때문에 필수적으로 알아둘 필요가 있습니다. 마지막 컬럼은 임의로 붙여진 컨테이너의 이름입니다. 컨테이너를 조작할 때는 컨테이너 아이디를 사용할 수도 있고, 이름을 사용할 수도 있습니다. 이름은 docker run을 할 때 --name으로 옵션을 사용해 명시적으로 지정할 수 있지만, 지정하지 않으면 임의의 이름이 자동적으로 부여됩니다.

위의 예제에서는 직접 명령어를 넘겨서 이미지를 컨테이너로 실행시켰습니다만, 보통 이미지들은 명령어 기본값이 지정되어 있습니다. 예를 들어 레디스Redis, 마리아DBMariaDB, 루비 온 레일스Ruby on Rails 어플리케이션을 담고 있는 이미지라면, 기본적으로 각각의 애플리케이션을 실행하거나 애플리케이션을 실행하는 스크립트가 기본 명령어로 지정되어 있습니다. 컨테이너는 독립된 환경에서 실행됩니다만, 컨테이너의 기본적인 역할은 이미지 위에서 미리 규정된 명령어를 실행하는 일입니다. 이 명령어가 종료되면 컨테이너도 종료 상태(Exit)에 들어갑니다. 이러한 죽은 컨테이너의 목록까지 확인하려면 docker ps -a 명령어를 사용하면 됩니다. 셸을 종료하고 컨테이너 목록을 확인해보겠습니다.

앞서 실행된 센트OS의 배시 셸에서 exit 명령어로 셸을 종료합니다.

root@65d60d3dd306:/# exit

컨테이너가 종료되었는지 docker ps를 통해서 살펴보겠습니다.

$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

컨테이너 목록이 비어있는 것을 볼 수 있습니다. 종료된 컨테이너까지 보기 위해서는 -a 옵션을 사용해야합니다. 확인해보겠습니다.

$ docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                      PORTS               NAMES
d3fef9c0f9e9        centos:latest       "bash"              13 minutes ago      Exited (0) 34 seconds ago                       compassionate_turing

13분 전에 만들어진 d3fef9c0f9e9 컨테이너가 34초 전에 종료된 것을 알 수 있습니다. 여기서 중요한 점은 컨테이너는 SSH 서버가 아니라 배시 셸 프로세스이기 때문에, 셸을 종료하면 컨테이너도 종료된다는 점입니다.* 이번엔 restart 명령어를 통해 이미지를 되살려보겠습니다.

* 셸은 대화형으로 리눅스 머신에 명령을 실행하기 위한 커맨드라인 도구입니다. 프로세스이기 때문에 셀을 종료하면, 그걸로 끝입니다. 반면에 SSH는 외부에서 접속하기 위해 설치해두는 서버 프로세스입니다. 따라서 SSH 서버에 접속해서 셸을 사용하고 종료하더라도 SSH 서버는 그대로 살아서 다른 접속을 기다립니다. 겉보기에는 비슷하지만 도커로 셸을 직접 실행해서 사용하는 것과 외부 서버에 SSH로 접속하는 것의 차이를 명확하게 이해해야 도커 컨테이너와 가상머신이 헷갈리지 않을 수 있습니다.

$ docker restart d3fef9c0f9e9
d3fef9c0f9e9
$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
d3fef9c0f9e9        centos:latest       "bash"              14 minutes ago      Up 8 seconds                            compassionate_turing
$

컨테이너가 되살아났습니다! 하지만 셸과 입출력을 주고받을 수 있는 상태는 아닙니다. 컨테이너로 실행된 프로세스와 터미널 상에서 입출력을 주고 받으려면 attach 명령어를 사용해야합니다.

$ docker attach d3fef9c0f9e9
[root@d3fef9c0f9e9 /]#

다시 도커 컨테이너 안으로 들어왔습니다! 여기까지 도커 컨테이너의 생명 주기를 살펴보았습니다. 도커 컨테이너를 실행시키는 run부터 실행이 종료되었을 때 다시 실행하는 restart를 배웠습니다. 이 외에도 실행된 컨테이너를 강제로 종료시키는 stop 명령어가 있으며, 종료된 컨테이너를 삭제하는 rm 명령어도 있습니다. 잠시 기억을 더듬어 올라가 이 글의 초반부에서 run 명령어와 함께 사용한 --rm 플래그는 컨테이너가 종료 상태가 되면 자동으로 삭제를 해주는 옵션입니다.

이쯤에서 이미지와 컨테이너를 명확하게 짚고 넘어갈 필요가 있을 것 같습니다. 이미지와 컨테이너가 아주 헷갈리는 개념은 아닙니다. 이미지가 미리 구성된 환경을 저장해 놓은 파일들의 집합이라면, 컨테이너는 이러한 이미지를 기반으로 실행된 격리된 프로세스입니다.

이미지는 가상머신 이미지와 비슷합니다. 하지만 가상머신에서는 저장된 이미지를 기반으로 가상머신을 특정 상태를 복원합니다. 컨테이너는 가상머신처럼 보이지만 가상머신은 아닙니다. 가상머신이 컴퓨터라면, 컨테이너는 단지 격리된 프로세스에 불과합니다. 보통 도커 컨테이너를 처음 다루는 예제에서 셸을 많이 다루기 때문에 컨테이너가 마치 가상머신처럼 보이는 착각을 일으킵니다. 다시 한 번 강조합니다. 컨테이너는 가상머신이라기보다는 프로세스입니다. 이 사실을 꼭 기억해주시기 바랍니다.

도커와 버전 관리 시스템

계속 해서 강조하지만 이미지와 컨테이너는 다릅니다. 그렇다면 한가지 질문을 던져보죠. 어떤 컨테이너를 지지고 볶고 삶고 데치고 하면 이미지에는 어떤 변화가 생길까요?

컨테이너를 지지고 볶고 삶고 데치고 해도, 이미지에는 아무런 변화도 생기지 않습니다.

아주 간단한 예를 들어보자면, 윈도우 CD로 윈도우를 설치해서 사용한다고 설치한 윈도우 CD에 어떤 변화가 생기지는 않는 것과 같은 이치입니다. 이미지는 어디까지나 고정되어있습니다. 도커에서 이미지는 불변Immutable한 저장 매체입니다. 그도 그럴 것이 한 번 생성된 이미지를 실행한다고 변형이 된다고 하면 이미지를 통해 어떤 환경을 재현한다는 건 아무런 의미가 없어져버립니다.

이미지는 불변이지만, 그 대신 도커에서는 이 이미지 위에 무언가를 더해서 새로운 이미지를 만들어내는 일이 가능합니다. 이미지를 기반으로 만들어진 컨테이너는 변경 가능Mutable하기 때문입니다. 도커의 또 하나 중요한 특징은 바로 계층화된 파일 시스템을 사용한다는 점입니다. 특정한 이미지로부터 생성된 컨테이너에 어떤 변경사항을 더하고(파일들을 변경하고), 이 변경된 상태를 새로운 이미지로 만들어내는 것이 가능합니다. 도커의 모든 이미지는 기본적으로 이 원리로 만들어집니다. 바로 이러한 점 때문에 도커에서는 저장소Repository, 풀Pull, 푸시Push, 커밋Commit, 차분Diff 등 VCS에서 사용하는 용어들을 차용한 것으로 보입니다.

마치 깃Git 저장소에 새로운 커밋을 추가하듯이, 도커에서 새로운 이미지를 생성하는 과정을 따라가보겠습니다. 이번에는 우분투 이미지를 사용하겠습니다. 먼저 우분투 바이오닉 비버Ubuntu Bionic Beaver 이미지를 다운로드 받고, 앞선 예제와 같이 배시 셸을 실행해봅니다.*

* 바이오닉 비버(Bionic Beaver)는 우분투 18.04 LTS의 코드명입니다. 2028년 EOLEnd of Life 예정입니다.

$ docker pull ubuntu:bionic
...
$ docker run -it ubuntu:bionic bash
root@65d60d3dd306:/#

이 컨테이너에 깃Git을 설치해보겠습니다. 먼저 이 우분투 기본 이미지에는 깃이 설치되어있지 않다는 것을 확인해봅니다.

root@65d60d3dd306:/# git --version
bash: git: command not found

깃을 설치하기 전에 하나 더 확인하고 넘어가겠습니다. 도커는 마치 자신이 VCS라도 된 것마냥, 어떤 컨테이너와 이 컨테이너의 부모 이미지 간의 파일 변경사항을 확인할 수 있는 명령어를 제공합니다. git diff 명령어로 프로젝트의 변경사항을 확인하듯이, docker diff 명령어로 부모 이미지와 여기서 파생된 컨테이너의 파일 시스템 간의 변경사항을 확인할 수 있습니다. 우분투에 셸이 실행된 컨테이너를 그대로 두고, 다른 셸에서 docker diff 명령어를 실행해보겠습니다.

$ dockre ps
CONTAINER ID        IMAGE               COMMAND             CREATED              STATUS              PORTS               NAMES
65d60d3dd306        ubuntu:bionic       "bash"              About a minute ago   Up About a minute                       distracted_ardinghelli

$ dokcker diff 65d60d3dd306

아무것도 출력되지 않나요? 그럼 정상입니다. 왜냐하면 이 컨테이너는 아직 이미지 파일 시스템 상태 그대로이기 때문입니다. 이제 깃을 설치합니다. 다음 명령어들을 차례대로 실행합니다.*

* 공식 우분투 이미지는 사용자가 루트로 설정되어있습니다. 따라서 sudo와 같은 명령어 없이도 apt를 직접 사용해 패키지를 설치할 수 있습니다.

root@65d60d3dd306:/# apt update
...
root@65d60d3dd306:/# apt install -y git
...
root@65d60d3dd306:/# git --version
git version 2.17.1

우분투의 패키지 관리자 apt를 사용해 버전 관리 시스템 깃Git 명령어를 설치했습니다. 다시 셸을 그대로 두고 다른 셀에서 diff를 다시 실행해봅니다.

$ docker diff 65d60d3dd306 | head
C /usr
C /usr/lib
A /usr/lib/ssl
A /usr/lib/ssl/certs
A /usr/lib/ssl/misc
A /usr/lib/ssl/misc/CA.pl
A /usr/lib/ssl/misc/tsget
A /usr/lib/ssl/misc/tsget.pl
A /usr/lib/ssl/openssl.cnf
A /usr/lib/ssl/private

결과가 너무 많기 때문에 head로 앞부분만 출력하도록 했습니다. 여기서 A는 ADD, C는 Change, D는 Delete를 의미합니다. 결과를 보면 알 수 있지만 패키지 하나를 설치하는 것만으로도 아주 많은 파일들이 추가되거나 변경된 것을 알 수 있습니다. 앞서 이야기했지만 컨테이너에서 어떤 작업을 한다고 원래의 이미지가 달라지지는 않습니다. 이를 확인하기 위해서 원래의 이미지에서 새로운 컨테이너를 하나 더 실행하고 git 명령어가 있는지 확인해보겠습니다.

$ docker run -it --rm ubuntu:bionic bash
root@33f6039322df:/# git --version
bash: git: command not found
root@33f6039322df:/# exit

분명히 이전 컨테이너는 git 명령어가 추가되었지만 이번에 실행한 컨테이너에는 git 명령어가 없습니다. 이제 ubuntu:bionic 이미지에 깃이 설치된 새로운 이미지를 생성해보도록하겠습니다. 이 또한 VCS와 매우 비슷합니다. 도커에서는 이 작업을 commit이라고 합니다.

$ docker commit 65d60d3dd306 ubuntu:git
sha256:12924460218feb38da74e9a64c95acd55d16297346b2698f47f396936636c93d
$ REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu              git                 12924460218f        4 seconds ago       186MB
ubuntu              bionic              4e5021d210f6        8 hours ago         64.2MB
centos              latest              470671670cac        2 months ago        237MB

이미지 만들기 별 거 없죠? 단지 커밋을 하고 뒤에 이름을 붙여주면 바로 새로운 도커 이미지가 생성됩니다. 이미지로부터 컨테이너를 실행시키고 이 컨테이너의 수정사항을 통해서 새로운 이미지를 만들었습니다. 그렇다면 정말로 이 이미지를 통해서 컨테이너를 실행하면 git 명령어가 있을까요?

직접 확인해보죠.

$ docker run -i -t ubuntu:git bash
root@2a00b9b2b7cc:/# git --version
git version 2.17.1
root@2a00b9b2b7cc:/# exit

브라보! git이 설치되어 있는 걸 활인할 수 있습니다.

하지만 이 이미지는 별로 쓸모가 없네요. 필요없는 이미지는 정리하고 넘어가겠습니다. 하나 알아두셔야 하는 중요한 사항은, 이미지에서 파생된 (종료 상태를 포함한) 컨테이너가 하나라도 남아있다면 이미지는 삭제할 수 없습니다. 따라서 먼저 컨테이너를 종료하고, 삭제까지 해주어야합니다. docker rm은 컨테이너를 삭제하는 명령어이고, docker rmi는 이미지를 삭제하는 명령어입니다. 이 두 명령어를 혼동하지 않아야합니다.

먼저 컨테이너를 지우고, 이미지를 삭제해보겠습니다.

$ docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                        PORTS               NAMES
2a00b9b2b7cc        ubuntu:git          "bash"              2 minutes ago       Exited (130) 3 seconds ago                        cranky_franklin
$ docker rm 2a00b9b2b7cc
2a00b9b2b7cc
$ docker rmi ubuntu:git
Untagged: ubuntu:git
Deleted: sha256:487a3619305e68483059caa21eb54d1d812ced4282df9e2ba05ec46ed9a2b8f4
Deleted: sha256:9b6621e819f094c16ea9f63af90f7cb564a48133c05504fad0f28563865f957d

이번에는 도커 이미지의 생명주기를 배웠습니다. 도커 이미지를 pull로 받아오고 commit으로 새로운 이미지를 만들고 rmi 명령어로 삭제해보았습니다. 컨테이너와 이미지의 생명주기만 이해하고 나면 도커의 대부분을 이해한 거나 다름 없습니다. 도커로 할 수 있는 대부분의 작업은 이미지와 컨테이너 개념으로 커버할 수 있습니다. 이제 남은 일은 자신에게 필요한 이미지를 만들고 이 이미지를 통해서 컨테이너를 실행하는 일입니다.

그리고 이제 기본적인 개념들을 배웠으니 중앙 저장소인 도커 허브에서 이미 만들어져있는 다양한 이미지들을 활용할 수도 있습니다. 무엇을 하건 도커의 모토를 잊지 마시기 바랍니다. 거짓말 조금 보태서(?!) 완성된 이미지는 언제 어디에서라도 실행 가능합니다.

Dockerfile로 이미지 만들기

도커 이미지를 추가하는 방법은 크게 세 가지가 있습니다. 먼저 pull을 사용해 미리 만들어져있는 이미지를 가져오는 방법입니다. 그리고 컨테이너의 변경사항으로부터 이미지를 만드는 법에 대해서도 소개했습니다. 두번째 방법은 아주 흥미롭지만, 이렇게 이미지를 만드는 경우는 거의 없습니다. 세번째 방법은 Dockerfile을 빌드하는 방법입니다. Dockerfile은 도커만의 특별한 DSL로 이미지를 정의하는 파일입니다. 여기서부터는 세 번째 방법으로 도커 이미지를 만들어보겠습니다.

Dockerfile로 Git이 설치된 우분투 이미지 정의

먼저 Dockerfile을 저장해놓기 위한 디렉터리를 하나 만듭니다.

$ mkdir git-from-dockerfile
$ cd git-from-dockerfile

이제 다음 내용으로 Dockerfile을 작성합니다.

FROM ubuntu:bionic
RUN apt-get update
RUN apt-get install -y git

앞서 commit 명령어로 도커 이미지를 만들었던 것과 이 Dockerfile 내용이 그 방식이 그리 다르지 않음을 알 수 있습니다. 먼저 FROM은 어떤 이미지로부터 이미지를 생성할지 지정합니다. Dockerfile에서는 필수 항목이며, 여기서는 ubuntu:bionic을 지정했습니다. 다음으로 RUN 지시자는 명령어를 실행하라는 의미입니다. 먼저 apt-get update를 실행하고, 다음으로 apt-get install -y git을 실행합니다. 앞서 apt 명령어를 사용했지만, 여기서는 apt-get 명령어를 사용했습니다. 명령어의 차이일 뿐 git 패키지를 설치하는 것은 같습니다.

자, 그렇다면 이 Dockerfile로 이미지를 빌드해보겠습니다.

$ docker build -t ubuntu:git-from-dockerfile .
Sending build context to Docker daemon  3.072kB
Step 1/3 : FROM ubuntu:bionic
 ---> 4e5021d210f6
Step 2/3 : RUN apt-get update
...
 ---> c2110a74f55e
Step 3/3 : RUN apt-get install -y -qq git
...
 ---> cc770735315e
Successfully built cc770735315e
Successfully tagged ubuntu:git-from-dockerfile

그럼 새로 만든 이미지에 깃Git이 잘 설치되었는지 확인해보겠습니다.

$ docker run -it ubuntu:git-from-dockerfile bash
root@f98f0bd06b67:/# git --version
git version 2.17.1

🎉 깃이 실행되는 것을 확인할 수 있습니다.

모니위키(moniwiki) 도커 파일 작성하기

이번에는 웹 애플리케이션 서버를 실행하기 위한 도커 이미지를 작성해보겠습니다. 예제로 사용해볼 웹 애플리케이션은 PHP와 아파치 서버를 기반으로 동작하는 모니위키입니다.* 애플리케이션 실행을 위해 도커 이미지를 만드는 작업을 도커라이징Dockerizing이라고도 합니다. 여기서는 미리 만들어둔 Dockerfile의 내용을 보고 내용을 같이 살펴보겠습니다. 먼저 깃으로 예제 도커파일 저장소를 클론 받습니다.

$ git clone https://github.com/nacyot/docker-moniwiki.git
$ cd docker-moniwiki/moniwiki

이 디렉터리에 포함된 Dockerfile의 내용에 대해서 살펴보겠습니다.

FROM ubuntu:14.04

RUN apt-get update &&\
  apt-get -qq -y install git curl build-essential apache2 php5 libapache2-mod-php5 rcs

WORKDIR /tmp
RUN \
  curl -L -O https://github.com/wkpark/moniwiki/archive/v1.2.5p1.tar.gz &&\
  tar xf /tmp/v1.2.5p1.tar.gz &&\
  mv moniwiki-1.2.5p1 /var/www/html/moniwiki &&\
  chown -R www-data:www-data /var/www/html/moniwiki &&\
  chmod 777 /var/www/html/moniwiki/data/ /var/www/html/moniwiki/ &&\
  chmod +x /var/www/html/moniwiki/secure.sh &&\
  /var/www/html/moniwiki/secure.sh

RUN a2enmod rewrite

ENV APACHE_RUN_USER www-data
ENV APACHE_RUN_GROUP www-data
ENV APACHE_LOG_DIR /var/log/apache2

EXPOSE 80

CMD bash -c "source /etc/apache2/envvars && /usr/sbin/apache2 -D FOREGROUND"

조금 길어보이지만, 리눅스 작업에 익숙하신 분들이라면 대략적인 내용은 바로 이해되실 겁니다. 한 줄 씩 살펴보겠습니다.

FROM ubuntu:14.04

먼저 맨 위에 정의된 FROM은 어떤 이미지로부터 새로운 이미지를 생성할 지를 지정합니다.

RUN apt-get update &&\
  apt-get -qq -y install git curl build-essential apache2 php5 libapache2-mod-php5 rcs

RUN은 직접 명령어를 실행하는 지시자입니다. RUN 바로 뒤에 명령어가 실행됩니다. 위의 두줄은 모니위키 실행을 위한 우분투 패키지들을 설치하는 명령어입니다. 여기서 사용한 RUN 명령어는 두 개의 명령어를 두 줄로 작성했습니다. RUN 명령어를 두 개로 명령어를 하나씩 실행해도 무방합니다. 이 글에서 자세히 다루지는 않지만 Dockerfile의 한 줄 한 줄은 레이어라는 형태로 저장되기 때문에 RUN을 줄이면 레이어가 줄어들고, 캐시도 효율적으로 관리할 수 있습니다. 여기서 &&은 여러 명령어를 이어서 실행하기 위한 연산자이고, \은 명령어를 여러줄에 작성하기 위한 문자입니다. 이는 Dockerfile을 작성할 때 자주 사용되는 패턴이니, 여러 명령어를 이어 붙일 때 RUN 하나에 &&\로 나눠 여러 명령어를 실행하는 데 사용한다고 기억해두면 도움이 됩니다.

FROM, RUN에 대해서는 이미 깃이 포함된 우분투 이미지를 도커라이징하면서 살펴보았습니다. WORKDIR은 이번 Dockerfile에서 새롭게 등장한 지시자입니다.

WORKDIR /tmp

이 지시자는 이후에 실행되는 모든 작업의 실행 디렉터리를 변경합니다. 리눅스 명령어로 생각해보면 cd라고 생각할 수 있습니다. RUN 명령어를 실행할 때 앞에 매번 cd를 붙여줄 수도 있습니다만, 매번 실행 위치가 초기화되기 때문에 WORKDIR을 사용하면 작업할 때 좀 더 편리합니다.

RUN \
  curl -L -O https://github.com/wkpark/moniwiki/archive/v1.2.5p1.tar.gz &&\
  tar xf /tmp/v1.2.5p1.tar.gz &&\
  mv moniwiki-1.2.5p1 /var/www/html/moniwiki &&\
  chown -R www-data:www-data /var/www/html/moniwiki &&\
  chmod 777 /var/www/html/moniwiki/data/ /var/www/html/moniwiki/ &&\
  chmod +x /var/www/html/moniwiki/secure.sh &&\
  /var/www/html/moniwiki/secure.sh

RUN a2enmod rewrite

다음으로 모니위키를 설치합니다. 여기서는 깃허브GitHub 저장소에 릴리스되어있는 모니위키를 다운로드 받아 아파치2Apache2로 동작하도록 셋업합니다. 첫 번째 RUN은 모니위키를 셋업하는 내용입니다. 여기서도 RUN 하나에 여러 명령어들을 &&로 연결해주었습니다. 조금 길어보이지만, 기본적인 리눅스 명령어들로 구성되어있습니다. PHP 코드의 압축을 풀고, 아파치가 접근하는 디렉터리로 복사하고 접근 권한을 설정합니다. 두 번째 RUN은 아파치2의 모듈을 활성화하는 내용입니다.*

* RUN을 사용할 때 어떤 명령어를 이어 붙이고, 어떤 명령어를 분리할 지에 대한 명확한 규칙은 없습니다. 개인적으로 가독성이나 캐시 적용 등을 고려해 논리적으로 같은 작업이라면 연결해서 작성하는 편입니다.

ENV APACHE_RUN_USER www-data
ENV APACHE_RUN_GROUP www-data
ENV APACHE_LOG_DIR /var/log/apache2

ENV도 이번에 처음 나왔네요. ENV는 컨테이너 실행 환경에 적용되는 환경변수의 기본값을 지정하는 지시자입니다. 리눅스에서는 환경변수로 애플리케이션의 동작을 제어하는 경우가 자주 있습니다. 도커에서는 이러한 방식을 권장하는 편이며, 직접 애플리케이션을 작성하는 경우에도 환경변수로 설정값을 넘겨받아 처리할 수 있도록 코딩합니다. 여기서는 아파치에서 사용하는 APACHE_RUN_USER, APACHE_RUN_GROUP, APACHE_LOG_DIR 환경변수를 정의했습니다.

EXPOSE 80
CMD bash -c "source /etc/apache2/envvars && /usr/sbin/apache2 -D FOREGROUND"

이제 마지막입니다. EXPOSE는 가상머신에 오픈할 포트를 지정해줍니다. 마지막 줄의 CMD에는 컨테이너에서 실행될 명령어를 지정해줍니다. 이 글의 앞선 예에서는 docker run을 통해서 bash를 실행했습니다만, 여기서는 아파치 서버를 FOREGROUND에 실행합니다. 이 명령어는 기본값이고 컨테이너 실행시에 덮어쓸 수 있습니다.

자, 기다리고 기다리던 대망의 순간이 왔습니다. 직접 이 Dockerfile을 빌드할 차례입니다.

$ docker build -t nacyot/moniwiki:latest .
Sending build context to Docker daemon   7.68kB
Step 1/10 : FROM ubuntu:14.04
 ---> 6e4f1fe62ff1
Step 2/10 : RUN apt-get update &&  apt-get -qq -y install git curl build-essential apache2 php5 libapache2-mod-php5 rcs
...
 ---> becdcac5d788
Step 3/10 : WORKDIR /tmp
...
 ---> dbdc86a08299
Step 4/10 : RUN   curl -L -O https://github.com/wkpark/moniwiki/archive/v1.2.5p1.tar.gz &&  tar xf /tmp/v1.2.5p1.tar.gz &&  mv moniwiki-1.2.5p1 /var/www/html/moniwiki &&  chown -R www-data:www-data /var/www/html/moniwiki &&  chmod 777 /var/www/html/moniwiki/data/ /var/www/html/moniwiki/ &&  chmod +x /var/www/html/moniwiki/secure.sh &&  /var/www/html/moniwiki/secure.sh
...
 ---> 48926b3b3da0
Step 5/10 : RUN a2enmod rewrite
...
 ---> 98a0ed3df283
Step 6/10 : ENV APACHE_RUN_USER www-data
...
 ---> a1f1247b98cb
Step 7/10 : ENV APACHE_RUN_GROUP www-data
...
 ---> dee2cb60f1cc
Step 8/10 : ENV APACHE_LOG_DIR /var/log/apache2
...
 ---> df4bd9e4dd7e
Step 9/10 : EXPOSE 80
...
 ---> f68911b22856
Step 10/10 : CMD bash -c "source /etc/apache2/envvars && /usr/sbin/apache2 -D FOREGROUND"
...
 ---> 408c35f3d162
Successfully built 408c35f3d162
Successfully tagged nacyot/moniwiki:latest

도커 파일을 한 단계 씩 빌드해가며 이미지가 만들어지는 과정을 확인할 수 있습니다. 이제 모니위키를 실행해보겠습니다.

$ docker run -d -p 9999:80 nacyot/moniwiki:latest
746443ad118afdb3f254eedaeeada5abc2b125c7263bc5e67c2964b570166187

다시 docker run 입니다. 이번에는 -d-p 플래그를 사용합니다. 앞서서 자세히 설명하진 않았습니다만, -d 플래그는 -i의 반대 역할을 하는 옵션으로, 컨테이너를 백그라운드에서 실행합니다. -p는 포트포워딩을 지정하는 옵션입니다. :을 경계로 앞에는 외부 포트, 뒤에는 컨테이너 내부 포트를 지정합니다. 참고로 컨테이너 안에서 아파치가 80포트로 실행됩니다. 따라서 여기서는 9999로 들어오는 연결을 컨테이너에서 실행된 서버의 80포트로 보냅니다.

이제 마지막입니다. 로컬 머신의 9999 포트에 접근해 모니위키 서버가 잘 실행중인지 보도록 하겠습니다.

http://127.0.0.1:9999/moniwiki/monisetup.php
모니위키 설치 페이지

모니위키에 접속이 되는 것을 확인할 수 있습니다. 간단한 셋업을 거치고 정상 동작하는지 FrontPageHello, world를 추가해보았습니다.

모니위키의 FrontPage를 수정해봅니다

정상적으로 동작하는 것을 확인할 수 있습니다.

노트
도커파일 작성하기

여기서는 사실상 미리 만들어진 Dockerfile을 예제로 사용했습니다. 당연한 이야기입니다만, 처음 Dockerfile을 작성할 때는 빈 파일에서 시작해야합니다. FROM으로 시작하고, 그 다음에는 주로 RUN을 사용해 리눅스 명령어를 사용하듯이 환경을 구축해나갑니다. 가장 중요한 작업은 패키지 관리자로 애플리케이션 실행 환경을 구축하고, 실행할 애플리케이션을 다운로드 받거나 복사하는 작업입니다. 이 예제에서는 도커 빌드 과정에서 온라인 상의 모니위키 소스코드를 다운로드 받았지만, 직접 작성하는 애플리케이션의 경우 COPYADD 지시자를 사용해 로컬 파일을 복사하는 방법을 주로 사용합니다. 여기서는 다루지 않은 지시자들도 있지만 대부분의 경우 FROM, RUN, WORKDIR, ADD, CMD 정도면 원하는 이미지를 만들 수 있습니다.

Dockerfile을 처음부터 완성하기보다는 중간중간 빌드하면서 작업하는 것을 추천드립니다. 도커는 도커 이미지 빌드 과정을 캐시하기 때문에 내용이 변하지 않은 부분까지는 빠르게 빌드가 이루어집니다. 하지만 여전히 쉬운 과정은 아닙니다. 빌드만으로 어렵다면, 직접 이미지에 들어가서 작업을 해보는 것도 추천합니다. 예를 들어 베이스 이미지의 bash 셸을 실행해서 프로비저닝 작업을 수행해봅니다. 그리고 필요한 명령어나 지시자들을 Dockerfile로 작성하면 좀 더 수월한 편입니다.

노트
도커의 이미지 빌드 과정

앞서 도커 빌드를 할 때 도커 파일을 할 줄씩 빌드하는 것을 확인할 수 있었습니다. 도커 파일에서 FROM, RUN, WORKDIR 같은 지시자 하나 하나가 바로 스탭이 됩니다. 도커는 FROM에 지정된 이미지에서 부터 시작합니다. 이 이미지로 컨테이너를 만들고, 다음 지시자의 명령을 수행합니다. 그리고 이 내용을 도커 이미지로 저장합니다. 예를 들어 다음 지시자가 RUN이라면 명령어를 실행하고 이를 이미지로 만듭니다. 그리고 이렇게 만들어진 이미지를 기반으로 다음 지시자를 실행합니다. 즉, 스탭 하나를 빌드할 때마다, 1. 컨테이너 생성, 2. 지시자 실행, 3. 임시 이미지 생성 과정을 거칩니다. 이 과정을 Dockerfile의 지시자수(즉, 스탭 수)만큼 반복합니다.

이 과정에서 생성된 중간 이미지들은 docker history 명령어로 확인할 수 있습니다.

$ docker hisotry moniwiki:latest
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
408c35f3d162        7 hours ago         /bin/sh -c #(nop)  CMD ["/bin/sh" "-c" "bash...   0B
f68911b22856        7 hours ago         /bin/sh -c #(nop)  EXPOSE 80                    0B
df4bd9e4dd7e        7 hours ago         /bin/sh -c #(nop)  ENV APACHE_LOG_DIR=/var/l...   0B
dee2cb60f1cc        7 hours ago         /bin/sh -c #(nop)  ENV APACHE_RUN_GROUP=www-...   0B
a1f1247b98cb        7 hours ago         /bin/sh -c #(nop)  ENV APACHE_RUN_USER=www-d...   0B
98a0ed3df283        7 hours ago         /bin/sh -c a2enmod rewrite                      30B
48926b3b3da0        7 hours ago         /bin/sh -c curl -L -O https://github.com/wkp...   7.32MB
dbdc86a08299        7 hours ago         /bin/sh -c #(nop) WORKDIR /tmp                  0B
becdcac5d788        7 hours ago         /bin/sh -c apt-get update &&  apt-get -qq -y...   184MB
6e4f1fe62ff1        3 months ago        /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B
<missing>           3 months ago        /bin/sh -c mkdir -p /run/systemd && echo 'do...   7B
<missing>           3 months ago        /bin/sh -c set -xe   && echo '#!/bin/sh' > /...   195kB
<missing>           3 months ago        /bin/sh -c [ -z "$(apt-get indextargets)" ]     0B
<missing>           3 months ago        /bin/sh -c #(nop) ADD file:276b5d943a4d284f8...   196MB

이미지가 <missing>인 경우가 있는데, 이는 베이스 이미지인 우분투 이미지의 빌드 내용입니다. 중간 이미지는 로컬 머신에서 직접 빌드한 경우에만 생성됩니다. 이 중간 이미지에서 컨테이너를 직접 실행하는 것도 물론 가능합니다. 궁금하다면 직접 한 번 실행해보시기 바랍니다.

실전: 도커 이미지로 서버 애플리케이션 배포하기

어떤 애플리케이션을 서버에서 운영하려면 프로비저닝 과정을 거쳐야 합니다. 프로비저닝은 서버의 환경을 어떤 애플리케이션이 실행가능한 상태로 준비하는 과정을 의미합니다. 기존에 서버 운영에 있어서 프로비저닝 작업은 가장 중요하고, 그리고 가장 어려운 작업 중 하나였습니다. 모니위키를 생각해보겠습니다. 이 글을 처음 공개하던 시점은 2014년입니다. 도커가 이제 막 등장한 시점이었고, 우분투 최신 버전은 아직 14.04였고, 12.04도 현역이던 시절입니다. 모니위키는 현재 개발이 사실상 중지된 상태로 비교적 오래된 PHP5와 아파치2 환경에서 구동됩니다. 현재 시점에서 보면 최신 우분투에 PHP5 패키지를 설치하는 것도 쉽지 않습니다. 명령어 하나 하나 실행해가며 실행 환경을 구축해야합니다. 이런 과정은 지루하고, 까다롭고, 추상화하기도 어렵습니다.

물론 도커를 사용한다고 프로비저닝 과정 자체가 없어지지는 않습니다. 앞서 Dockerfile을 작성하고 이미지를 만드는 게 도커에서는 프로비저닝 작업이라고 할 수 있습니다. 프로비저닝을 어렵게 만드는 어려운 요소는 서버 환경이 동일하지 않다는 점과 서버 환경이 지속적으로 변화한다는 점입니다. 도커는 바로 이 문제를 해결해줍니다. 도커만 있으면 미리 준비한 이미지를 실행할 수 있기 때문에 리눅스 계열이라면 서버 환경에 크게 구애받지 않습니다. 또한 컨테이너로 실행되는 독자적인 환경을 가지고 있기 때문에 서버의 변화에도 거의 영향을 받지 않습니다. 과거에 만들어둔 이미지가 있다면, 대부분의 경우 잘 동작합니다.*

* 이미지는 없고 Dockerfile만 있다면 잘 되리라고 보장하기가 어렵습니다. 도커 빌드 역시 시점에 영향을 많이 받기 때문에 5년 전에 빌드가 잘 되었더라도, 지금은 빌드가 잘 되지 않을 수 있습니다. 이와 달리 5년 전에 만들어둔 이미지가 잘 저장되어있고, 초기화 작업에 외부 의존성이 없다면 큰 문제 없이 실행됩니다.

위에서 직접 만든 도커 이미지는 아래 명령어 하나로 실행할 수 있습니다. 아직은 이 도커 이미지를 만든 컴퓨터에서만 동작하지만요.

$ docker run -d -p 9999:80 nacyot/moniwiki:latest

그런데 이 이미지를 정말 다른 서버에서도 쉽게 사용할 수 있을까요? 가상 서버를 한 대 만들어서 온라인 상에 모니위키를 배포해보겠습니다.

도커 허브에 이미지 올리기

앞서 도커 공식 이미지를 도커 허브에서 받아온다는 것을 확인해보았습니다. 다시 한 번 docker info 명령어로 확인해보겠습니다.

$ docker info | grep Registry
 Registry: https://index.docker.io/v1/

이론 상 다수의 도커 레지스트리가 존재할 수 있습니다만, 도커 클라이언트는 기본적으로 도커 허브를 사용합니다. 출력 결과에 보이는 https://index.docker.io/v1/이 도커 허브의 API 서버 주소입니다. 실제로 도커 이미지의 풀네임에는 도커 레지스트리의 주소가 포함되어있습니다. 예를 들어 ubuntu:bionic 이미지의 풀네임은 docker.io/library/ubuntu:bionic입니다. docker pull 명령어로 한 번 확인해보겠습니다.

$ docker pull docker.io/library/ubuntu:bionic
bionic: Pulling from library/ubuntu
Digest: sha256:bec5a2727be7fff3d308193cfde3491f8fba1a2ba392b7546b43a051853a341d
Status: Image is up to date for ubuntu:bionic
docker.io/library/ubuntu:bionic

이미지가 정상적으로 풀 되는 것을 알 수 있습니다. 이미지의 풀네임은 크게 4부분으로 구성되어있습니다. 맨 앞의 docker.io는 도커 레지스트리의 서버 주소입니다. 슬래시로 구분된 library는 네임스페이스입니다. 도커 허브의 경우 네임스페이스가 곧 사용자 이름이기도 합니다. library는 도커 공식 이미지를 제공하는 특별한 네임스페이스입니다. 다음 슬래시 다음에 오는 문자열이 이미지 고유의 이름입니다. 그리고 : 뒤에는 추가적으로 이미지를 구분해주기 위한 태그가 따라옵니다. 하나의 이미지 저장소는 태그가 다른 다수의 이미지를 가질 수 있습니다. 도커 클라이언트에서는 편의상 docker.io/를 생략하더라도 기본 레지스트리 서버로 사용합니다.

도커 허브는 공식 이미지를 풀 받는 데 사용할 수도 있지만, 직접 만든 이미지를 올리거나 배포하는 데도 사용할 수 있습니다. 퍼블릭 도커 이미지의 경우는 무료로 제공되며, 프라이빗 이미지를 사용하는 경우 비용이 발생합니다. 도커 허브를 사용하려면 먼저 회원 가입을 진행해야합니다.

도커 허브를 사용하려면 먼저 계정을 생성해야합니다

계정을 생성하고 나면 도커 클라이언트를 사용해 로그인할 수 있습니다.

$ docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username: <DOCKER_HUB_ID>
Password: <PASSWORD>
WARNING! Your password will be stored unencrypted in /home/vagrant/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

Login Succeeded

ID와 패스워드를 입력하면 로그인이 됩니다. 로그인 명령어에도 서버를 지정할 수 있는데, 지정하지 않으면 기본값으로 도커 허브에 로그인을 합니다. 이제 이미지를 바로 도커 허브에 올려보겠습니다. 앞서 모니위키 이미지 이름을 nacyot/moniwiki:latest라고 지었습니다. 여기서 nacyot은 네임스페이스에 해당하는 부분으로 도커 허브에서는 사용자 이름을 의미합니다. 즉, 자신의 아이디로 이미지 이름을 변경해줍니다.

$ docker tag nacyot/moniwiki:latest <DOCKER_HUB_ID>/moniwiki:latest

이름이 변경되었으면 바로 이미지를 푸시해봅니다. 푸시한 이미지는 공개 저장소로 등록됩니다.

$ docker push <DOCKER_HUB_ID>/moniwiki:latest 

이름 규칙만 맞으면 도커 허브는 아직 <DOCKER_HUB_ID>/moniwiki 이미지 저장소가 없더라도, 푸시 작업 과정에서 이미지 저장소를 생성해줍니다. 도커 허브에서도 푸시한 이미지를 확인할 수 있습니다.

도커 허브에서 푸시한 이미지 저장소를 확인할 수 있습니다

이제 로컬에서 이 이미지를 삭제하더라도 도커 허브에서 다시 풀 받을 수 있습니다. 또한 다른 서버에서 이 이미지를 풀 받아 실행하는 것도 가능합니다.

디지털오션(Digital Ocean)에서 도커로 모니위키 컨테이너 실행

도커 허브에 이미지를 푸시했으니 이번에는 정말 다른 서버에서 배포를 해보겠습니다. 어떤 서버를 준비해도 무방하지만, 여기서는 디지털오션DigitalOcean의 가상 서버를 사용하고자 합니다. 디지털오션을 사용하려면 계정 생성을 해야하고, 사용 시간에 따라 비용이 발생할 수 있습니다.

디지털오션 메인 페이지

자신의 계정으로 로그인을 하고 드롭릿(가상 서버)를 하나 생성합니다.

디지털오션에 드롭릿(가상 머신)을 생성합니다

우분투 18.04, 스탠다드 플랜, $5/mo(1GB RAM, 1 CPU), 싱가폴 리전, 원타임 패스워드를 선택했습니다. 월 $5지만 시간당으로 요금이 계산되므로 몇 시간 실행한다고 가정하면 몇 십원 정도의 요금이 발생합니다. 서버를 생성합니다. 서버가 생성되고 나면 이메일로 IP, 사용자, 임시 패스워드가 전달됩니다.

Droplet Name: ubuntu-s-1vcpu-1gb-sgp1-01
IP Address: 128.199.131.36
Username: root
Password: <PASSWORD>

이 정보를 바탕으로 서버에 접속해봅니다. 서버에 처음 접속하면 패스워드를 변경해야합니다. 적절한 값으로 변경해줍니다.

$ ssh root@128.199.131.36
root@128.199.131.36's password:
root@ubuntu-s-1vcpu-1gb-sgp1-01:~#

먼저 도커를 설치합니다.

$ curl -s https://get.docker.com | sudo sh
$ docker --version
Docker version 19.03.8, build afacb8b7f0

이제 앞서 푸시했던 모니위키 이미지로 컨테이너를 실행해주기만 하면 됩니다.

$ docker run -d -p 9999:80 <DOCKER_HUB_ID>/moniwiki:latest
$ docker ps
CONTAINER ID        IMAGE                    COMMAND                  CREATED             STATUS              PORTS                  NAMES
59f10c4efe3d        <DOCKER_HUB_ID>/moniwiki:latest   "/bin/sh -c 'bash -c..."   7 seconds ago       Up 3 seconds        0.0.0.0:9999->80/tcp   relaxed_bohr

웹브라우저에서 9999 포트로 접근해보면 모니위키 서버가 실행중인 것을 확인할 수 있습니다.

디지털오션 드롭릿에서 실행중인 모니위키 서버. 이 서버는 인터넷으로 접근할 수 있습니다

도커 허브에 이미지를 푸시해놓으니, 도커 설치하고, 컨테이너 실행하고, 딱 2단계로 애플리케이션 배포가 끝났네요.

어떤가요, 애플리케이션 배포 참 쉽죠? 물론 실제로 서비스를 운영하기 위해서는 아직 할 일이 더 많이 남아있습니다. 도메인도 셋업해야하고 HTTPS 인증서도 발급 받아야합니다. 또한 컨테이너 데이터를 영구적으로 보존하기 위해 데이터 볼륨도 설정해야하고 안정적인 서버 운영을 위해 다수의 인스턴스를 생성하고 트래픽을 분산하려면 로드밸런서가 필요합니다. 컨테이너를 효율적으로 사용하려면 컨테이너 오케스트레이션도 도입해야겠네요. 이렇게 더해가면 끝이 없습니다만, 도커 이미지 하나면 어디서든 원하는 서버를 실행할 수 있다는 사실을 확인했습니다.

요금이 발생할 수 있으니 테스트 후에는 꼭 드랍릿을 삭제(Destroy)도 잊지 말아주세요.

마치며

이 글에서는 도커의 기본적인 사용법에 대해서 알아보았습니다. 컨테이너를 향한 여정은 이제 시작일 뿐입니다. 도커와 컨테이너에 대해서 더 공부하고 싶으신 분들은 다음 글들도 읽어보세요.

컨테이너의 동작원리가 궁금하시면 다음 글들을 추천합니다.

도커 컴포즈를 사용하면 다수의 컨테이너를 한꺼번에 관리하는 것도 가능합니다. 도커 컴포즈나 도커를 사용해 개발환경을 구성하는 데 관심이 있다면 다음 글을 추천합니다.

프로덕션 수준에서 컨테이너 기반의 운영이 궁금하다면 컨테이너 오케스트레이션에 대해서 알아보세요.

긴 글 읽어주셔서 감사합니다. 다른 글에서 또 만나요. 👋

44BITS 로고

컨테이너란? 리눅스의 프로세스 격리 기능

🏷️ 키워드, 2020-01-23 - 리눅스 컨테이너는 운영체제 수준의 가상화 기술로 리눅스 커널을 공유하면서 프로세스를 격리된 환경에서 실행하는 기술을 의미합니다. 하드웨어를 가상화하는 가상 머신과 달리 커널을 공유하는 방식이기 때문에 실행 속도가 빠르고, 성능 상의 손실이 거의 없다는 장점이 있습니다.
도움이 되셨나요?
RSS 리더 피들리에서 최신 글을 구독할 수 있습니다.
트위터, 페이스북으로 44BITS의 새소식을 전해드립니다.
✔ 44BITS의 다른 활동도 확인해보세요. 다양한 채널에서 만나볼 수 있습니다.
✔ 따뜻한 댓글 하나와 피드백은 큰 힘이 됩니다.