주요글: 도커 시작하기

도커 시작하기 전체 글 목록

  1. GetLight 2020.02.29 14:17 신고

    감사합니다.
    도커 공부 하는데 많은 도움을 받았습니다.

도커를 사용하면 mysql이나 nginx처럼 이미 제공하는 이미지를 사용해서 소프트웨어를 쉽게 실행할 수 있지만 단지 이것만이 도커를 사용하는 아니다. 도커를 사용하는 또 다른 이유는 직접 개발한 소프트웨어를 도커 이미지로 만들어 배포하고 실행하기 위함이다. 즉 도커를 잘 활용하려면 이미지에 대한 이해가 필요하다.

이미지 이름

도커 이미지를 이용해서 컨테이너를 생성할 때 이미지 이름을 사용한다.

docker run -it --rm alpine:3.10 sh

위 코드는 alpine:3.10을 이미지 이름으로 사용했다. alpine은 리포지토리 이름이고 3.10은 태그이다. docker images 명령어를 실행하면 로컬에 존재하는 이미지 목록을 표시한다. 이미지 목록을 보면 REPOSITORY 칼럼과 TAG 칼럼을 통해서 이미지의 리포지토리와 태그 값을 확인할 수 있다.

vagrant@ubuntu-bionic:~$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
adminer             latest              09a06e7c3196        2 days ago          87.6MB
nginx               1.17.4              f949e7d76d63        4 days ago          126MB
nginx               latest              f949e7d76d63        4 days ago          126MB
mysql               5.7                 383867b75fd2        2 weeks ago         373MB
mysql               latest              b8fd9553f1f0        2 weeks ago         445MB
centos              7                   67fa590cfc1c        5 weeks ago         202MB
alpine              3.10                961769676411        5 weeks ago         5.58MB
openjdk             8u212-jdk-alpine    a3562aa0b991        4 months ago        105MB
hello-world         latest              fce289e99eb9        9 months ago        1.84kB

위 결과에서 mysql 리포지토리는 태그가 5.7인 이미지와 태그가 latest인 이미지가 존재한다. 여기에 표시된 리포지토리 명은 완전한 리포지토리 명을 짧게 표시한 것이다. 실제 리포토리명은 다음의 세 가지 요소를 가진다.

  • 리포지토리호스트/조직(계정)이름/짧은이름

alpine이나 nginx와 같이 도커가 제공하는 공식 이미지는 짧은 이름으로 이미지를 사용할 수 있다. 실제 공식 이미지의 리포지토리 이름은 다음 형식을 갖는다.

  • docker.io/library/nginx

도커 허브에 개인 계정을 만들어 리포지토리를 만들 수도 있다. 예를 들어 필자는 도커 허브에 madvirus라는 이름으로 가입했는데 이 경우 리포지토리 이름은 madvirus/openjdk-pinpoint와 같은 형태를 갖는다.

도커 허브가 아닌 사설 도커 리포지토리를 사용할 경우 호스트명을 포함한 완전한 리포지토리 이름을 사용해야 한다.

보통 태그 값으로는 버전을 사용한다. 한 이미지에 여러 태그를 붙일 수 있는데 보통 마지막 버전에 해당하는 이미지에는 latest 태그를 함께 붙인다. 이 글을 쓰는 시점에서 nginx:latest 이미지는 nginx:1.17.4 이미지와 같은 이미지이다. docker images 결과를 보면 nginx의 latest 태그와 1.17.4 태그의 이미지ID가 같은 것을 알 수 있다.

참고로 컨테이너를 생성할 때 태그를 지정하지 않으면 latest를 기본 값으로 사용한다.

이미지 레이어

기존 이미지를 이용해서 새로운 이미지를 만들어보자.

vagrant@ubuntu-bionic:~$ docker run --name alpine_custom alpine:3.10 touch /mycustom.txt

vagrant@ubuntu-bionic:~$ docker commit alpine_custom myimage
sha256:422d23665db418c26a463a3aeb4d92b43a9c51c056aae34dea26a3a4067c1f9a

위 코드는 alpine:3.10 이미지를 이용해서 alpine_custom 컨테이너를 생성한다. 컨테이너를 실행할 때 touch 명령어를 사용해서 루트에 mycustom.txt 파일을 생성한다.

docker commit 명령어는 컨테이너를 이용해서 새로운 이미지를 생성한다. 위 코드는 alpine_custom 컨테이너를 이용해서 myimage라는 이미지를 생성한다. 태그를 지정하지 않았으므로 latest를 태그로 사용한다.

이제 생성한 alpine_custom 컨테이너를 삭제하고 새로 생성한 myimage 이미지를 이용해서 새로운 컨테이너를 사용해보자.

vagrant@ubuntu-bionic:~$ docker rm -v alpine_custom
alpine_custom
vagrant@ubuntu-bionic:~$ docker run --rm myimage ls -la /
total 64
drwxr-xr-x    1 root     root          4096 Sep 29 11:28 .
drwxr-xr-x    1 root     root          4096 Sep 29 11:28 ..
-rwxr-xr-x    1 root     root             0 Sep 29 11:28 .dockerenv
...생략
-rw-r--r--    1 root     root             0 Sep 29 11:15 mycustom.txt
...생략
drwxr-xr-x   11 root     root          4096 Aug 20 10:30 var

myimage 이미지로 생성한 컨테이너에서 ls 명령어를 실행하면 mycustom.txt 파일이 표시된 것을 알 수 있다.

myimage를 조금 더 살펴보자. docker image history 명령어를 실행하면 이미지의 내역을 볼 수 있다. 다음은 실행 결과이다.

vagrant@ubuntu-bionic:~$ docker image history myimage:latest
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
422d23665db4        7 minutes ago       touch /mycustom.txt                             0B
961769676411        5 weeks ago         /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B
<missing>           5 weeks ago         /bin/sh -c #(nop) ADD file:fe64057fbb83dccb9…   5.58MB

위 결과에서 IMAGE 칼럼에 표시된 값과 docker images 명령어의 IMAGE ID 칼럼 값을 비교해보자. 결과를 보면 docker image history에서 출력한 9617961769676411가 alpine:3.10 IMAGE ID와 같다.

vagrant@ubuntu-bionic:~$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
myimage             latest              422d23665db4        12 minutes ago      5.58MB
...생략
alpine              3.10                961769676411        5 weeks ago         5.58MB
...생략

 

이번에는 alpine:3.10 이미지의 내역을 보자. myimage 이미지의 내역에서 9617961769676411 부분부터 <missing>의 내용이 완전 동일하다.

vagrant@ubuntu-bionic:~$ docker image history alpine:3.10
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
961769676411        5 weeks ago         /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B
<missing>           5 weeks ago         /bin/sh -c #(nop) ADD file:fe64057fbb83dccb9…   5.58MB

도커 이미지 내역은 도커 이미지를 생성할 때 사용한 변경 내역을 보여준다. 도커는 이미지를 생성할 때 전체 파일을 새로 만들지 않고 변경한 파일만 이용해서 새로운 레이어를 생성한다. 예를 들어 myimage:latest 이미지는 alpine:3.10 이미지에서 변경한 내용만 새로운 레이어로 생성한다. 비슷하게 alpine:3.10 이미지는 <missing>으로 표시된 이미지에서 변경한 내용만 레이어로 생성한다. 즉 myimage:latest 이미지는 alpine 이미지로 생성한 컨테이너에서 변경한 파일인 mycustom.txt 파일만 포함한다.

이런 특징을 잘 활용하면 다운로드 받을 이미지의 크기를 줄일 수 있다. 예를 들어 10개의 자바 어플리케이션을 이미지로 만들 때 openjdk:8u212-jdk-alpine 이미지를 하위 레이어로 사용하면 openjdk:8u212-jdk-alpine 이미지와 관련된 파일은 한 번만 다운로드 하고 10개 자바 어플리케이션의 변경 부분만 다운로드하므로 어플리케이션을 구동하기 위해 다운로드해야 하는 이미지의 크기가 줄어든다.

docker commit으로 생성한 이미지 파일은 도커 허브나 별도로 구성한 도커 레지스트리를 이용해서 다른 사람과 공유할 수 있다. 그런데 docker commit을 이용해서 도커 이미지를 만드는 과정은 수작업으로 이루어지므로 불편하고 실수하기 좋다. Dockerfile을 사용하면 이미지 생성 과정을 쉽게 자동화할 수 있는데 다음 글에서 Dockerfile을 이용한 이미지 생성 방법을 살펴보자.

관련 글

컨테이너를 구동할 때 -p 옵션을 이용해서 컨테이너의 포트와 연결된 호스트 포트를 설정했다.

docker run --name mysqldb \
-e MYSQL_ROOT_PASSWORD=rootpw \
-p 33060:3306 -d mysql:5.7

실행한 MySQL에 접속하려면 호스트IP:33060으로 연결하면 된다.

도커 컨테이너 간에 연결할 경우에는 어떻게 할까? 각 컨테이너마다 환경 변수를 이용해서 연결할 호스트의 IP와 포트를 설정할 수 있을 것이다. 이 방법이 안 되는 것은 아니지만 이보다 좋은 방법은 도커 네트워크를 이용하는 것이다.

도커 네트워크

docker network ls 명령어를 사용하면 도커가 제공하는 네트워크 목록을 확인할 수 있다.

vagrant@ubuntu-bionic:~$ docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
e83fd9260276        bridge              bridge              local
f6a428e9d599        host                host                local
2e3d5809c8ac        none                null                local

도커는 기본으로 세 개의 네트워크를 제공한다. 각 네트워크는 다음과 같다.

  • bridge 네트워크 : bridge 드라이버로 제공하는 네트워크로 컨테이너간 연결을 제공하며 컨테이너를 구동하는 호스트 네트워크가 구분된다.
  • host 네트워크 : host 드라이버로 제공하는 네트워크로 컨테이너를 위한 별도 네트워크없이 호스트와 동일한 네트워크를 사용한다.
  • none 네트워크 : null 드라이버를 사용하며 네트워크 연결을 갖지 않는다.

네트워크 SCOPE는 네트워크의 범위를 의미하며 다음의 세 가지 범위가 존재한다.

  • local : 컨테이너간 연결은 네트워크가 존재하는 호스트로 제한된다.
  • global : 클러스터의 모든 노드에 네트워크가 존재하지만 호스트 간 라우팅은 지원하지 않는다.
  • swarm : 도커 스웜에 참여하는 모든 호스트로 연결을 확장한다.

이 글에서는 단일 호스트에서 네트워크를 연결하는 방법을 살펴본다. 여러 호스트에서 실행 중인 컨테이너 간의 연결은 도커 스웜을 설명할 때 살펴볼 것이다.

컨테이너 간 연결 : 기본 bridge 네트워크 이용

컨테이너 간에 연결하는 가장 쉬운 방법은 기본으로 제공하는 bridge 네트워크를 이용하는 것이다. 먼저 다음 명령을 이용해서 mysqldb 컨테이너를 생성하자. -p 옵션을 사용하지 않았으므로 호스트에서 컨테이너의 MySQL DB에 연결할 수 없다.

$ docker run --name mysqldb --rm \
-e MYSQL_ROOT_PASSWORD=rootpw \
-d mysql:5.7

이제 웹 기반 DB 관리도구 중 하나인 adminer를 다음 명령어를 이용해서 구동하자. Ctrl+C를 누르면 컨테이너가 종료되니 주의한다.

$ docker run --rm --name dbadmin --link mysqldb:db -p 8080:8080 adminer
PHP 7.3.10 Development Server started at Sun Sep 29 08:50:22 2019

여기서 --link 옵션이 중요하다. --link 옵션의 값으로 "mysqldb:db"를 주었는데 여기서 앞의 mysqldb는 컨테이너의 이름이며 뒤의 db는 컨테이너 내부에서 사용할 식별자이다. 즉 dbadmin 컨테이너 내부에서 db라는 이름으로 mysqldb 컨테이너에 접근할 수 있다는 것을 의미한다.

실제로 그런지 http://호스트IP:8080으로 연결해서 확인해보자. adminer 첫 화면에 연결하면 DB 로그인 폼이 표시되는데 여기서 서버에 "db:3306"이라고 입력한다. 사용자이름과 비밀번호는 각각 "root", "rootpw"를 입력한다(앞서 mysqldb 컨테이너를 구동할 때 MYSQL_ROOT_PASSWORD 환경 변수의 값으로 rootpw를 주었다).

로그인 버튼을 눌러보자. 다음과 같이 DB에 연결한 결과를 볼 수 있다.

dbadmin 컨테이너에서 db:3306으로 연결한 DB가 실제 mysqldb 컨테이너가 제공하는 DB인지 확인해보자. 먼저 mysqldb 컨테이너의 mysql db에 연결해서 이름이 test인 DB를 생성한다.

vagrant@ubuntu-bionic:~$ docker exec -it mysqldb mysql -u root -p
Enter password: (암호입력)
Welcome to the MySQL monitor.  Commands end with ; or \g.
...생략

mysql> create database test;
Query OK, 1 row affected (0.00 sec)

mysql> exit
Bye
vagrant@ubuntu-bionic:~$

그런 뒤 adminer 웹 화면에서 데이터베이스를 새로 고침해보자. 그러면 생성한 test DB가 목록에 표시될 것이다.

 

컨테이너 간 연결 : bridge 네트워크를 생성해서 연결

bridge 네트워크를 직접 생성할 수도 있다. 

$ docker network create \
--driver bridge \
--attachable \
--scope local \
--subnet 10.0.7.0/24 \
--ip-range 10.0.7.0/24 \
mynet

mynet 네트워크는 bridge 네트워크로 --attachable 옵션은 컨테이너가 언제든지 네트워크에 연결하거나 떨어질 수 있게 설정한다. --subnet과 --ip-range는 서브넷과 할당 가능한 IP 범위를 지정한다.

네트워크를 생성했다면 컨테이너를 네트워크에 붙일 수 있다. 컨테이너를 실행할 때 --network 옵션을 사용하면 된다.

docker run --name mysqldb \
--network mynet \
-e MYSQL_ROOT_PASSWORD=rootpw \
-d mysql:5.7

같은 네트워크에 참여하는 컨테이너는 이름을 사용해서 다른 컨테이너에 연결할 수 있다. 아래 코드를 보자.

vagrant@ubuntu-bionic:~$ docker run --name cent7 -it --network mynet centos:7 bash
[root@fc0936cc3177 /]# ping mysqldb
PING mysqldb (10.0.7.2) 56(84) bytes of data.
64 bytes from mysqldb.mynet (10.0.7.2): icmp_seq=1 ttl=64 time=0.183 ms
64 bytes from mysqldb.mynet (10.0.7.2): icmp_seq=2 ttl=64 time=0.136 ms
^C
--- mysqldb ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 0.136/0.159/0.183/0.026 ms

이 코드는 mynet 네트워크에 참여하는 cent7 컨테이너를 생성하고 bash 명령어를 실행한다. mynet 네트워크에 mysqldb 컨테이너도 참여했으므로 cent7 컨테이너는 컨테이너 이름을 사용해서 mysqldb에 연결할 수 있다. ping 명령어를 실행할 때 표시된 IP 주소는 10.0.7.2인데 이 IP 주소는 mynet 네트워크를 생성할 때 지정한 IP 범위에 속한다.

이미 생성한 컨테이너에 네트워크를 연결하고 싶거나 연결된 네트워크를 끊고 싶을 때에는 docker network connect 명령어나 docker network disconnet 명령어를 사용한다.

관련 글

도커 시작하기 3에서 컨테이너의 변경 내역을 유지하기 위해 --mount 옵션을 사용해서 호스트 파일 시스템에 컨테이너의 경로를 마운트하는 방법을 살펴봤는데 이 장에서는 스토리지에 대한 추가적인 내용을 살펴보겠다.

컨테이너와 로컬 파일 시스템

컨테이너의 파일은 호스트의 파일 시스템에 마운트되어 있다. 컨테이너는 이미지로부터 만들어지므로 크게 다음의 호스트 파일 경로에 마운트된다.

  • 이미지의 파일 내용을 저장하는 호스트 파일 경로(이미지는 실제로 레이어로 구성되므로 여러 경로 사용)
  • 컨테이너 구동 이후 변경 사항을 저장하기 위한 호스트 파일 경로

이 외에 --mount 옵션을 이용해서 호스트 파일 시스템이나 도커 볼륨에 마운트 할 수 있다.

docker inspect 명령어를 사용하면 호스트 파일 시스템에 마운트된 경로를 확인할 수 있다.

"Data": {
    "LowerDir": "/var/lib/docker/overlay2/8bec........생략",
    "MergedDir": "/var/lib/docker/overlay2/8bec.../merged",
    "UpperDir": "/var/lib/docker/overlay2/8bec.../diff",
    "WorkDir": "/var/lib/docker/overlay2/8bec.../work"
},

docker inspect 명령어의 출력 결과에서 Data 속성은 마운트된 위치를 표시한다. 여기서 UppderDir 속성은 컨테이너의 변경 내역을 저장한다. 콘테이너에 새로운 파일을 생성하고 UpperDir 속성의 디렉토리로 이동하면 컨테이너에 변경한 내역이 존재하는 것을 확인할 수 있다.

읽기 전용 컨테이너

--read-only 옵션을 사용하면 컨테이너를 읽기 전용으로 생성할 수 있다. 

vagrant@ubuntu-bionic:~$ docker run -d --name web --read-only -p 8080:80 nginx:latest
6077fb45aea3b5b71f4161211949b4e6ed4c9eb38c89d49b07daf1887608570d

읽기 전용으로 컨테이너를 구동하면 컨테이너의 파일을 새로 생성하거나 기존 파일을 변경할 수 없다. nginx의 경우 서브를 구동할 때 파일을 새로 생성해야 하는데 파일을 생성할 수 없으므로 위 명령어로 생성한 컨테이너는 정상적으로 시작하지 못하고 종료된다. docker ps 명령어로 web 컨테이너의 상태를 보면 다음과 같이 STATUS가 Exited로 표시된다.

vagrant@ubuntu-bionic:~$ docker ps -a
CONTAINER ID        IMAGE        ...  STATUS                     ... NAMES
6077fb45aea3        nginx:latest ...  Exited (1) 3 minutes ago   ... web

docker logs 명령어로 컨테이너의 로그를 보면 다음고 같이 파일 생성에 실패한 것을 확인할 수 있다.

vagrant@ubuntu-bionic:~$ docker logs web
2019/09/28 11:19:30 [emerg] 1#1: mkdir() "/var/cache/nginx/client_temp" failed (30: Read-only file system)
nginx: [emerg] mkdir() "/var/cache/nginx/client_temp" failed (30: Read-only file system)
vagrant@ubuntu-bionic:~$

호스트 파일 시스템으로 마운트하기

--mount 옵션을 사용하면 컨테이너의 파일 시스템을 로컬 파일 시스템으로 마운트할 수 있다. 다음은 사용 예이다. 호스트 파일 경로에 마운트할 때에는 type 속성을 bind로 설정하고 source에는 호스트의 절대 경로를 지정하고, target에는 호스트 파일 경로에 마운트할 컨테이너의 경로를 지정한다.

$ docker run -d --name web --rm \
   --mount type=bind,source=/home/vagrant/nginx/html,target=/usr/share/nginx/html \
   --mount type=bind,source=/home/vagrant/nginx/run,target=/run \
   --mount type=bind,source=/home/vagrant/nginx/cache,target=/var/cache/nginx \
   -p 8080:80 \
   nginx:latest

위 명령을 실행한 뒤 /home/vagrant/nginx/run 디렉토리를 보면 nginx가 서버를 구동할 때 생성하는 nxinx.pid 파일이 생성된 것을 확인할 수 있다. 비슷하게 /home/vagrant/nginx/cache 디렉토리에는 nginx가 cache 목적으로 생성한 디렉토리가 생성된 것도 확인할 수 있다.

호스트 파일 경로에 마운트할 때 읽기 전용으로 마운트할 수도 있다. --mount 옵션을 지정할 때 readonly 옵션을 추가하면 된다.

docker run -d --name web --rm \
   --mount type=bind,source=/home/vagrant/nginx/html,target=/usr/share/nginx/html,readonly \
   --mount type=bind,source=/home/vagrant/nginx/run,target=/run \
   --mount type=bind,source=/home/vagrant/nginx/cache,target=/var/cache/nginx \
   -p 8080:80 \
   nginx:latest

읽기전용으로 지정한 마운트 경로는 컨테이너 내부에서는 수정이 안 된다.

vagrant@ubuntu-bionic:~/nginx$ docker exec web touch /usr/share/nginx/html/a.txt
touch: cannot touch '/usr/share/nginx/html/a.txt': Read-only file system

--mount 옵션에서 source 속성 대신에 src 속성을, target 속성 대신에 dst 속성을 사용해도 된다.

메모리 파일 시스템에 마운트하기

메모리 파일 시스템에 마운트할 수도 있다. --mount 옵션에서 type 값을 tmpfs로 지정하면 된다.

docker run -d --name web --rm \
  --mount type=bind,source=/home/vagrant/nginx/html,target=/usr/share/nginx/html,readonly \
  --mount type=bind,source=/home/vagrant/nginx/run,target=/run \
  --mount type=tmpfs,target=/var/cache/nginx \
  -p 8080:80 \
  nginx:latest

메모리에 파일 내용을 유지하므로 컨테이너를 종료하면 저장한 파일도 함께 삭제된다.

tmpfs는 메모리를 사용하므로 호스트 파일 시스템의 경로를 지정할 필요가 없다.

도커 볼륨

도커 볼륨은 도커가 관리하는 파일 시스템이다. 컨테이너의 파일 시스템도 도커 볼륨을 이용해 관리한다.

필요하면 도커 볼륨을 직접 생성하고 컨테이너에 마운트할 수 있다. 아래 명령어는 이름이 myvol인 볼륨을 생성한다.

vagrant@ubuntu-bionic:~$ docker volume create --driver local myvol
myvol

--driver 옵션은 볼륨을 생성할 때 사용할 스토리지 드라이버를 지정한다. 위 명령어는 local 드라이버를 사용해서 볼륨을 생성했다. local 드라이버는 호스트의 파일 시스템에 생성한 볼륨이 위치한다.

볼륨을 생성하면 --mount 옵션을 사용해서 컨테이너 경로를 볼륨에 마운트할 수 있다.

docker run -d --name web --rm \
  --mount type=volume,src=myvol,dst=/usr/share/nginx/html \
  -p 8080:80 \
  nginx:latest

--mount 옵션에서 type 속성은 volume을 지정하고 src에는 볼륨 이름을 지정한다.

도커 볼륨은 여러 컨테이너에서 공유할 수 있다. 다음과 같이 새로운 컨테이너에서 같은 볼륨을 사용하는 컨테이너를 만들고 해당 위치에 파일을 생성하자.

vagrant@ubuntu-bionic:~/nginx/html$ docker run -it --name gen --rm \
> --mount type=volume,src=myvol,dst=/website \
> centos:7 bash
[root@f27afdbfac6d /]# echo "hello world" > /website/hello.txt

위 코드는 myvol 볼륨을 /website 경로에 마운트한 컨테이너를 생성하고 이 컨테이너 안에서 /website/hello.txt 파일을 생성한다. 앞서 web 컨테이너는 myvol 볼륨을 /usr/share/nginx/html 경로에 마운트했는데 두 경로는 myvol 볼륨을 사용하므로 web 컨테이너의 /usr/share/nginx/html 경로에 hello.txt 파일이 생성된다. (http://호스트:8080/hello.txt 주소로 연결하면 gen 컨테이너에서 생성한 파일 내용이 표시된다.)

docker volume rm 명령어는 볼륨을 삭제한다. 단 볼륨을 사용중인 컨테이너가 존재하면 볼륨을 삭제할 수 없다.

vagrant@ubuntu-bionic:~/nginx/html$ docker volume rm myvol
Error response from daemon: remove myvol: volume is in use - [dd963...]

 

사용중인 컨테이너가 없는 볼륨을 모두 삭제하고 싶다면 docker volume prune 명령어를 사용하면 된다. 이 명령어를 사용하면 일일이 볼륨 이름을 지정할 필요가 없어 불필요한 볼륨을 삭제할 때 편리하다.

도커가 제공하는 local 스토리지 드라이버는 로컬 파일 시스템에 대한 볼륨을 지원한다. 여러 호스트에서 공유할 수 있는 볼륨을 생성하려면 별도 플러그인을 사용해야 한다. local 스토리지 드라이버는 NFS를 지원하므로 각 호스트에서 NFS로 연결한 볼륨을 같은 이름으로 생성해도 동일한 결과를 얻을 수 있다.

REX-Ray 같은 플러그인을 사용하면 클라우드 환경에서 볼륨을 생성하고 공유할 수 있다.

관련 글

호스트 포트 연결

컨테이너와 호스트 포트를 연결하는 방법은 이미 앞서 nginx 이미지로 컨테이너를 생성할 때 사용했다. -p 옵션을 사용해서 포트를 지정한다(또는 --publish 옵션을 사용).

docker run -d -p 8080:80 --name web nginx:latest

연결 포트는 "호스트포트:컨테이너포트" 형식으로 지정한다.

환경 변수 설정

-e 옵션(--env 옵션)을 사용하면 컨테이너를 실행할 때 환경 변수를 전달할 수 있다. 예를 들어 mysql 이미지의 실행 프로그램은 MYSQL_ROOT_PASSWORD 환경 변수를 이용해서 DB의 root 암호를 설정한다. 따라서 root 암호를 원하는 문자열로 지정하고 싶다면 다음과 같이 컨테이너를 구동할 때 -e 옵션을 사용해서 환경 변수를 전달하면 된다.

docker run --name mysqldb \
           -e MYSQL_ROOT_PASSWORD=rootpw \
           -p 33060:3306 -d mysql:5.7

도커 허브(hub.docker.com)에서 이미지가 어떤 환경 변수를 사용하는지 확인할 수 있다.

로컬 스토리지 연결

아래와 같이 nginx 이미지를 이용해서 생성한 컨테이너에 bash로 연결해서 /usr/share/nginx/html 디렉토리에 echo.txt 파일을 생성해보자.

vagrant@ubuntu-bionic:~$ docker run -d -p 8080:80 --name web nginx:latest

vagrant@ubuntu-bionic:~$ docker exec -it web bash

root@fe306ef365a7:~# cd /usr/share/nginx/html/

root@fe306ef365a7:/usr/share/nginx/html# echo "echo file" > echo.txt

root@fe306ef365a7:/usr/share/nginx/html# exit

웹 브라우저에서 http://호스트:8080/echo.txt를 실행하면 방금 생성한 파일이 출력될 것이다. 컨테이너를 중지하고 다시 시작해도 컨테이너에 생성한 파일은 유지되는 것을 확인할 수 있다.

컨테이너에 파일을 생성하고 수정하고 삭제하는 것이 가능은 하지만 컨테이너의 파일 시스템을 직접 변경하는 것은 추천하지는 않는다. 컨테이너를 삭제하면 변경 내역도 함께 사라지기 때문이다. 컨테이너의 삭제 여부에 상관없이 파일을 유지해야 한다면 로컬 스토리지나 볼륨을 연결해야 한다.

로컬 스토리지와 컨테이너를 연결할 때는 --mount 옵션을 사용한다. 테스트를 위해 앞서 생성한 web 컨테이너를 삭제하고 홈 디렉토리에 html 디렉토리를 생성하고 이 폴더에 index.html 파일과 echo.txt 파일을 생성하자. 그리고 다음 명령어를 사용해서 컨테이너를 생성한다.

vagrant@ubuntu-bionic:~/html$ echo '<html><body>index</body></html>' > index.html

vagrant@ubuntu-bionic:~/html$ echo 'echo file in local' > echo.txt

vagrant@ubuntu-bionic:~/html$ docker run -d --name web --rm \
>   --mount type=bind,src=/home/vagrant/html,dst=/usr/share/nginx/html \
>   -p 8080:80 \
>   nginx:latest
d1530bacb7176c9fe36d0f1097661deaf6f471edd3ddd3d849c51eeeb43b16c0

vagrant@ubuntu-bionic:~/html$

웹 브라우저를 열고 http://호스트:8080/index.html 이나 http://호스트:8080/echo.txt에 연결해보자. 로컬에 생성한 파일이 브라우저에 표시되는 것을 알 수 있다. ~/html 디렉토리에 새로운 파일을 추가하거나 삭제한 뒤에 브라우저에 확인해보자. 바로 반영될 것이다.

--mount 옵션에서 type을 bind로 지정하면 컨테이너의 파일 시스템을 호스트의 파일 시스템으로 대체한다. src는 호스트 경로를 값으로 갖고 dst는 대체할 컨테이너 경로를 값으로 갖는다. 위 설정은 생성한 컨테이너의 /usr/share/nginx/html 경로를 로컬 호스트의 /home/vagrant/html로 연결한다고 설정한다.

컨테이너의 경로를 호스트로 연결하면 컨테이너를 삭제해도 파일 변경 내역이 유지되므로 다음 작업에 이점이 생긴다.

  • 이미지를 변경해서 컨테이너를 새로 만들어도 데이터가 유지된다.
  • 이미지에 이미 존재하는 파일을 다른 파일로 쉽게 교체할 수 있다.

로컬 스토리지 연결뿐만 아니라 메모리 파일 시스템 연결과 볼륨이 있는데 이에 대한 내용은 다음 편에 이어서 살펴본다.

관련 글

컨테이너 시작과 중지

도커를 사용한다는 것은 결국 이미지를 이용해서 컨테이너를 생성하고 실행한다는 것이다. docker run 명령어를 사용하면 컨테이너를 실행할 수 있다. 다음 명령어를 보자.

$ docker run -p 8080:80 nginx:latest

이 명령어를 실행하면 hello-world를 실행했을 때와 다르게 nginx 이미지를 실행하면 컨테이너가 구동된 채로 대기한다. 여기서 -p 옵션은 도커를 실행하는 호스트의 8080 포트를 컨테이너의 80 포트로 연결하도록 설정한다. nginx 이미지는 내부적으로 80 포트를 사용해서 웹 서버를 구동하는데 호스트의 8080 포트와 컨테이너의 80 포트를 연결한 것이다.

컨테이너가 실행 중인 상태에서 웹 브라우저를 띄워 http://호스트:8080에 연결해보자. 다음과 같이 nginx 웹 서버의 응답 화면을 볼 수 있다.

docker 명령어를 실행한 콘솔에는 아래와 같이 접근 로그가 출력될 것이다.

vagrant@ubuntu-bionic:~$ docker run -p 8080:80 nginx:latest 
192.168.1.1 - - [21/Sep/2019:14:32:33 +0000] "GET / HTTP/1.1" 304 0 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36" "-"

docker 명령어를 실행한 콘솔에서 Ctrl+C키를 누르면 컨테이너를 종료한다. nginx 이미지는 "nginx" 서버를 포그라운드로 실행하는데 Ctrl+C를 누르면 nginx 서버가 종료된다. 컨테이너를 구동할 때 실행한 프로그램이 종료되면 컨테이너도 함께 종료된다. nginx 이미지의 경우 실행하는 프로그램이 nginx 서버이므로 nginx 서버가 종료되면 컨테이너도 함께 종료된다.

이제 컨테이너를 백그라운드로 실행해보자. 아래와 같이 -d 옵션을 사용한다. -d 옵션은 컨테이너의 프로그램을 실행할 때 터미널에 연결하지 않는다.

vagrant@ubuntu-bionic:~$ docker run -d -p 8080:80 nginx:latest
7d78d9bf30ad21ad021a737886f517cb6cae98cd7b0535307dc583c32bd53e54
vagrant@ubuntu-bionic:~$

명령어 실행 결과로 7d78d9bf로 시작하는 문자열을 출력했는데 이 문자열은 컨테이너를 구분할 때 사용할 고유 ID이다.

컨테이너가 실행중이므로 웹 브라우저에서 http://호스트:8080에 연결하면 응답이 표시된다. docker container -ls 명령어를 사용해서 실제 실행중인 컨테이너 목록을 확인할 수 있다. (docker container 대신 docker ps 명령어를 사용해도 된다.)

vagrant@ubuntu-bionic:/vagrant$ docker container ls
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                  NAMES
7d78d9bf30ad        nginx:latest        "nginx -g 'daemon of…"   4 minutes ago       Up 4 minutes        0.0.0.0:8080->80/tcp   goofy_chaplygin

CONTAINER ID에 표시된 값이 앞서 출력된 컨테이너 ID의 앞 부분과 같은 것을 알 수 있다. STATUS는 Up인데 이는 컨테이너가 실행 중임을 뜻한다.

실행 중인 컨테이너를 중지하려면 docker stop 명령어를 사용하면 된다.

docker stop 7d78d9

docker stop 명령어 뒤에는 컨테이너를 구분할 수 있는 값을 입력한다. 여기서는 7d78d9를 입력했는데 고유 ID 전체가 아니라 다른 컨테이너와 구분되는 앞 글자 일부만 입력해도 된다.

컨테이너 이름

docker container ls -a 명령어를 입력해보자. ls 명령어에 -a 옵션을 붙이면 실행 중인 컨테이너뿐만 아니라 종료된 컨테이너도 모두 표시한다. 결과를 보면 nginx:latest 이미지를 이용해 여러 컨테이너가 생성된 것을 알 수 있다. 여기서 NAMES 값은 도커가 임의로 생성한 컨테이너 이름이다.(비슷하게 docker ps -a 옵션을 사용해도 모든 컨테이너 목록을 보여준다.)

vagrant@ubuntu-bionic:~$ docker container ls -a
CONTAINER ID        IMAGE               COMMAND                  ...생략 NAMES
7d78d9bf30ad        nginx:latest        "nginx -g 'daemon of…"   ...생략 goofy_chaplygin
7cf417035656        nginx:latest        "nginx -g 'daemon of…"   ...생략 condescending_albattani
346023f2afbc        nginx:latest        "nginx -g 'daemon of…"   ...생략 focused_heyrovsky
bb62c718f7a1        nginx:latest        "nginx -g 'daemon of…"   ...생략 peaceful_cerf
f2033c7f6be9        nginx:latest        "nginx -g 'daemon of…"   ...생략 unruffled_greider
dc5e650a1d70        centos:7            "/bin/bash"              ...생략 fervent_cartwright
420592ad5d1e        hello-world         "/hello"                 ...생략 youthful_lovelace
6441f60ab2e2        hello-world         "/hello"                 ...생략 youthful_lamarr

--name 옵션을 사용하면 컨테이너 이름을 직접 지정할 수 있다. 다음은 --name 옵션의 사용 예를 보여준다. --name 값이 web인 컨테이너를 실행한 뒤에 컨테이너 목록을 보면 NAMES 값이 web인 것을 알 수 있다.

vagrant@ubuntu-bionic:~$ docker run -d --name web -p 8080:80 nginx:latest
b91561be1280c8601efab3aa1edb4373b6a3beafc2507df1fc7f274fba341905
vagrant@ubuntu-bionic:~$ docker ps
CONTAINER ID        IMAGE               ...생략  NAMES
b91561be1280        nginx:latest        ...생략  web

컨테이너를 중지할 때는 컨테이너 ID뿐만 아니라 이름을 사용해도 된다.

vagrant@ubuntu-bionic:~$ docker stop web
web

이름이 web인 컨테이너를 주징했다면 다시 같은 이름의 컨테이너를 생성해보자. 다음과 같이 생성에 실패한다.

vagrant@ubuntu-bionic:~$ docker run -d --name web -p 8080:80 nginx:latest
docker: Error response from daemon: Conflict. The container name "/web" is already in use
by container "b91561be1280c8601efab3aa1edb4373b6a3beafc2507df1fc7f274fba341905". 
You have to remove (or rename) that container to be able to reuse that name.
See 'docker run --help'.

컨테이너 ID와 마찬가지로 컨테이너 이름도 고유하기 때문에 같은 이름의 컨테이너를 중복해서 생성할 수 없다.

컨테이너 삭제

매번 동일한 이름으로 컨테이너를 생성해야 한다면 컨테이너를 삭제하고 다시 생성하면 된다. docker rm 명령어를 사용하면 된다. docker rm 명령어에 컨테이너 ID(구분되는 일부)나 이름을 지정하면 해당 컨테이너를 삭제한다.

vagrant@ubuntu-bionic:~$ docker rm web
web

또는 컨테이너를 실행할 때 --rm 옵션을 줄 수도 있다. 이 옵션을 사용하면 컨테이너가 종료될 때 컨테이너를 자동으로 삭제한다.

vagrant@ubuntu-bionic:~$ docker run -d --rm --name web -p 8080:80 nginx:latest

 

컨테이너 상태

컨테이너는 크게 다음의 상태를 가진다.

docker run 명령어는 실제로 다음의 세 명령어를 한 번에 실행하는 것과 같다.

  • docker pull : 이미지를 다운로드한다. docker run 명령어는 이미지가 로컬에 없으면 이미지를 다운로드한다.
  • docker create : 이미지로부터 컨테이너를 생성한다.
  • docker start : 컨테이너를 시작한다.

인터랙티브 컨테이너

컨테이너를 구동하고 컨테이너를 위한 가상 터미널을 할당할 수도 있다. 다음 명령어를 실행해보자.

vagrant@ubuntu-bionic:~$ docker run -it centos:7 /bin/bash
[root@ebe53b326516 /]#

이 명령은 centos:7 이미지를 이용한 컨테이너를 생성하고 컨테이너의 /bin/bash 명령을 실행한다(이미지 이름 뒤에 실행할 명령어를 지정하지 않으면 이미지를 생성할 때 지정한 명령어를 기본으로 실행한다). 여기서 -it 옵션이 중요하다. -i 옵션은 --interactive와 같은 옵션으로 컨테이너의 표준입력을 연결한다. -t는 -tty와 같은 옵션으로 컨테이너를 위한 가상 터미널을 할당한다. 즉 -it 옵션을 이용하면 해당 프로그램을 실행하고 컨테이너에 터미널로 연결한다. 이제 컨테이너 내부에서 ls나 ps와 같은 명령을 사용해서 컨테이너 내부를 확인할 수 있다.

exit 명령어를 bash를 종료하므로 컨테이너가 종료된다.

docker exec는 실행 중인 컨테이너에서 특정 명령을 실행할 때 사용하는데 이를 사용하면 실행 중인 컨테이너에 터미널로 붙을 수도 있다. 다음은 실행 예이다.

vagrant@ubuntu-bionic:~$ docker exec -it web /bin/bash
root@1227ce888767:/#

관련 글

도커는 소프트웨어를 빌드하고 실행하기 위한 소프트웨어다. 도커를 사용하면 웹 서버, 명령행 프로그램 등의 소프트웨어를 설치하고, 출시하고, 실행하고, 삭제하는 과정을 단순화할 수 있다. 이를 위해 도커는 OS의 컨테이너 기술을 사용한다.

docker run hello-world 실행 과정 보기

도커 시작하기 0 : 우분투에 도커 설치하기에서 docker run hello-workd 명령어를 실행했는데 이 명령어를 처음 실행할 때 표시되는 메시지는 다음과 같다.

vagrant@ubuntu-bionic:~$ docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
1b930d010525: Pull complete
Digest: sha256:b8ba256769a0ac28dd126d584e0a2011cd2877f3f76e093a7ae560f2a5301c00
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

...생략

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

첫 번째 실행하면 "Hello form docker!" 문장이 출력되기 이전에 'hello-world:latest' 이미지를 로컬에서 찾을 수 없어 다운로드했다는 메시지를 볼 수 있다.

docker run hello-world 명령어를 다시 실행해보자.

vagrant@ubuntu-bionic:~$ docker run hello-world

Hello from Docker!
This message shows that your installation appears to be working correctly.
...생략

이번에는 이미지를 다운로드하지 않는다. hello-world는 도커가 소프트웨어를 배포할 때 사용하는 단위인 이미지의 이름으로 docker run 명령을 이용해서 이미지를 실행하면 도커는 다음 과정을 거친다.

docker run 명령어 실행 과정

도커는 실행할 이미지가 로컬에 존재하는지 확인한다. 존재하지 않으면 이미지를 먼저 다운로드한다. 이미지 파일은 도커 허브라는 곳에 위치하며 도커 허브에서 해당 이미지 파일을 다운로드한다. 다운로드가 끝나면 이미지에서 컨테이너를 생성하고 실행한다. 프로그램 파일이 있고 그 프로그램을 실행하면 프로세스가 생기는 것처럼 이미지 파일이 있고 이 이미지를 실행하면 컨테이너가 생성된다.

docker images 명령어를 실행하면 로컬에 설치된 이미지를 표시한다. 실행하면 다음과 같이 hello-world:latest 이미지가 존재하는 것을 확인할 수 있다.

vagrant@ubuntu-bionic:~$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
hello-world         latest              fce289e99eb9        8 months ago        1.84kB

이번에는 docker container ls -a 명령어로 컨테이너 목록을 확인하자. 아래와 같이 두 개의 컨테이너가 표시되는 것을 알 수 있다. docker run 명령어는 실행할 때마다 컨테이너를 생성하므로 docker run hello-world 명령어를 실행한 횟수만큼 컨테이너가 만들어졌다.

vagrant@ubuntu-bionic:~$ docker container ls -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                      PORTS               NAMES
420592ad5d1e        hello-world         "/hello"            24 minutes ago      Exited (0) 24 minutes ago                       youthful_lovelace
6441f60ab2e2        hello-world         "/hello"            39 minutes ago      Exited (0) 39 minutes ago                       youthful_lamarr

 

컨테이너

컨테이너 대한 소개는 https://www.docker.com/resources/what-container 문서를 참고한다. 이 문서는 컨테이너를 다음과 같이 설명하고 있다.

Containers are an abstraction at the app layer that packages code and dependencies together. Multiple containers can run on the same machine and share the OS kernel with other containers, each running as isolated processes in user space. Containers take up less space than VMs (container images are typically tens of MBs in size), can handle more applications and require fewer VMs and Operating systems.

 

컨테이너와 VM 차이

어플리케이션을 구동하는데 필요한 의존은 컨테이너 안에 포함된다. 한 컨테이너에 포함된 의존은 다른 컨테이너에 영향을 주지 않는다. 예를 들어 app 1은 lib 1 버전이 필요하고 app 2는 lib 2 버전이 필요하다고 하자. 한 OS에서 app 1과 app 2를 함께 실행하려면 lib 1과 lib 2를 모두 설치해야 한다. 만약 lib 2를 설치하면 lib 1이 비정상 동작한다면 한 OS에서 app 1과 app 2를 함께 구동할 수 없게 된다. 컨테이너를 사용하면 이런 문제가 발생하지 않는다. 컨테이너는 서로 격리된 환경에서 구동되므로 라이브러리 버전 충돌이 발생하지 않는다.

컨테이너는 격리된 환경에서 돌아가므로 한 컨테이너의 어플리케이션에 문제가 발생하더라도 OS나 다른 컨테이너에 주는 영향을 최소화할 수 있다.

도커, 컨테이너

도커는 이 컨테이너를 사용하는데 필요한 도구를 제공한다. 도커는 cgroup과 네임스페이스에 대한 자세한 이해가 없어도 컨테이너를 사용할 수 있게 만들어 주었다. 이런 이유는 도커는 출시 이후 빠르게 컨테이너를 위한 대세 기반 기술로 자리 잡았다.

관련 글

베이그런트(Vagrant)를 이용해서 우분투 설치하기

이 글에서는 단일 노드뿐만 아니라 다중 노드에 도커를 설치하고 실행하는 연습을 하기 위해 아래 환경을 사용한다.

  • 버추얼박스
  • 베이그런트

버추얼박스와 베이그런트를 차례대로 설치한 뒤에는 우분투 단일 노드 환경을 위한 Vagrant 파일을 작업할 폴더에 작성한다. 이 글에서는 E:\vn\vagrant\ubuntu 폴더에 Vagrant 파일을 생성했다고 가정한다.

# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/bionic64"
  config.vm.network "private_network", ip: "192.168.1.2"
  config.vm.provider "virtualbox" do |vb|
    vb.memory = 2048
    vb.cpus = 2
  end
end

"ubuntu/bionic64"는 우분투 18.04 버전에 해당하는 베이그런트 박스 이미지다. 작업 폴더에서 vagrant up 명령어를 사용해서 우분투 서버를 구동한다.

E:\vm\vagrant\ubuntu>vagrant up
...생략

E:\vm\vagrant\ubuntu>vagrant ssh
Welcome to Ubuntu 18.04.2 LTS (GNU/Linux 4.15.0-54-generic x86_64)
...생략

vagrant ssh로 우분투 서버에 접속하고 도커 설치를 위해 root 계정으로 전환한다. "sudo su -" 명령어를 사용해서 root 계정으로 전환할 수 있다.

vagrant@ubuntu-bionic:~$ sudo su -
root@ubuntu-bionic:~#

우분투에 도커 설치하기

https://docs.docker.com/install/linux/docker-ce/ubuntu/ 사이트를 참고해서 우분투에 도커 커뮤니티 버전을 설치한다. 아래는 설치 과정에서 명령어만 정리한 것이다.

도커 리포지토리 설치

apt 패키지 인덱스 업데이트

# apt-get update

apt가 HTTPS 기반 리포지토리 사용하도록 설정

# apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg-agent \
    software-properties-common

도커 공식 GPG 키 추가

# curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -  
# apt-key fingerprint 0EBFCD88   
pub   rsa4096 2017-02-22 [SCEA]
      9DC8 5822 9FC7 DD38 854A  E2D8 8D81 803C 0EBF CD88   
uid           [ unknown] Docker Release (CE deb) <docker@docker.com>   
sub   rsa4096 2017-02-22 [S]

리포지토리 추가

# add-apt-repository \
   "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"

도커 엔진 설치

apt 패키지 인덱스 업데이트

# apt-get update

도커 설치

# apt-get install docker-ce docker-ce-cli containerd.io

도커 실행 확인(root 계정으로 실행)

root@ubuntu-bionic:~# docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
1b930d010525: Pull complete
Digest: sha256:b8ba256769a0ac28dd126d584e0a2011cd2877f3f76e093a7ae560f2a5301c00
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

docker run 명령어가 어떻게 동작하는지는 뒤에서 설명한다. 일단 지금은 hello-world라는 도커 이미지를 다운로드 받아 실행한다는 정도로만 이해하고 넘어가자.

root 아닌 계정으로 도커 실행하기

docker 명령어는 root 권한을 가진 계정으로 실행해야 한다. 일반 계정은 sudo를 이용해서 docker 명령어를 실행해야 한다. 매번 sudo를 입력하는 귀찮다면 docker 그룹에 사용자를 추가하면 된다. 다음은 현재 사용자를 docker 그룹에 추가하는 명령어 실행 순서를 표시한 것이다. 자세한 내용은 https://docs.docker.com/install/linux/linux-postinstall/ 문서를 참고한다.

  1. 현재 사용자를 docker 그룹에 추가: sudo usermod -aG docker $USER
  2. 그룹 추가를 현재 콘솔에 반영: newgrp docker
  3. 실행 확인: docker run hello-world

관련 글

+ Recent posts