Docker

도커 컨테이너를 알아보자

youngmki 2022. 1. 4. 14:25

어떤 지식도 마찬가지겠지만 실제로 돌려보면서 이해하는 것만큼 강력한 공부 방법은 없는 것 같다. 도커도 마찬가지다.

도커라는 개념을 처음 마주했을 때의 그 막막함을 아직도 어렴풋이 기억한다. 컨테이너, 가상머신, 쿠버네티스 이게 다 뭐란 말인가?

천천히 하나씩 곱씹어가며 명령어를 통해 하나하나씩 이해한다면 컨테이너 환경은 충분히 이해할 수 있다고 생각한다. 그럼 시작해보자.

> 도커 허브와 같은 기본적인 환경 설정은 모두 마쳤다는 가정 하에 진행합니다.

Hello-world 실행


언어를 배울때 hello world를 처음 출력해보는 것처럼 도커에서도 hello world가 당신을 기다리고 있다.

$ docker run hello-world

$ docker run hello-world
...
Hello from Docker!
...

다음과 같은 결과를 얻을 수 있다.

동작 원리는 다음과 같다.

  1. 터미널 혹은 커맨드 프롬프트에서 docker run hello-world를 실행하면 도커 데몬(도커 엔진)에 접속한다.

  2. 커맨드 중 hello-world는 리포지터리의 이름이다.
    도커 엔진은 도커 허브의 리포지터리로부터 https://hub.docker.com/_/hello-world/ 라는 컨테이너를 위한 이미지를 로컬에 다운로드한다.

  3. 도커엔진이 위에서 다운받은 이미지로부터 컨테이너를 생성한다. 그러면 컨테이너상의 프로세스가 메시지를 표준 출력에 쓰기 시작한다.

  4. 도커 엔진은 컨테이너의 표준 출력을 도커 커맨드에 보내고 터미널에 표시된다.(도커 데몬이 출력을 도커 클라이언트에게 보내면 이후 터미널에 전달)

용어에 대해서 잠깐 짚고 넘어가자면

  • 도커의 이미지 는 운영체제와 소프트웨어를 담고 있는 컨테이너 실행 이전의 상태이다. 각 이미지는 repository:tag 로 식별된다.

  • 도커의 리포지터리는 이미지 보관소를 말한다. 리포지터리의 이름에 버전 등을 의미하는 태그를 붙여서 각각의 이미지를 구별하여 보관할 수 있다.
    태그를 생략하면 최신을 의미하는 latest가 사용된다. 클라우드 서비스의 문서 등에서는 리포지터리 대신에 레지스트리란 표현이 쓰이기도 한다.

  • 레지스트리는 Window의 설정 정보 데이터베이스를 말하기도 하지만, 도커에서는 리포지터리의 집합체로서 리포지터리를 제공하는 제공하는 서버를 말한다.

도커의 주요한 열 가지 명령어

1. 이미지 다운로드 (docker pull)


명령어 docker pull 리포지터리명[:태그] 를 실행하면 원격의 리포지터리로부터 이미지를 로드한다.

### 이미지 다운로드
$ docker pull centos:7
7: Pulling from library/centos
a1d0c7532777: Pull complete 
Digest: sha256:a27fd8080b517143cbbbab9dfb7c8571c40d67d534bbdee55bd6c473f432b177
Status: Downloaded newer image for centos:7
docker.io/library/centos:7

### 이미지 다운로드 여부 확인
$ docker images
REPOSITORY     TAG       IMAGE ID       CREATED             SIZE
centos         7         eeb6ee3f44bd   3 months ago        204MB

 

 

2. 컨테이너 실행(docker run)

###  컨테이너 실행 명령어
$ docker run [Option] 리포지터리명:태그 [Command] [Args]

### 실행 예제 -> 컨테이너를 실행한 후 컨테이너의 tty와 연결
$ docker run -it --name test1 centos:7 bash

자주 사용하는 docker run 옵션

옵션 설명
-i 로컬 머신의 키보드 입력을 컨테이너의 표준 입력에 연결하여, 키보드 입력을 컨테이너의 쉘에 보낸다.
-t 터미널을 통해 tty의 조작이 가능하게 한다.
-d 백그라운드로 컨테이너를 돌려 터미널과 연결하지 않는다.
--name 컨테이너에 이름을 설정한다. 해당 머신의 시스템 상에서 고유한 값이여야 하며, 옵션을 생략하면 이름이 랜덤으로 부여된다.
--rm 컨테이너가 종료하면 종료 상태의 컨테이너를 자동으로 삭제한다.

 

3. 컨테이너의 상태 출력 (docker ps)

실행 중이거나 정지 상태에 있는 컨테이너 목록을 출력한다. 옵션을 생략한 경우에는 실행 중인 컨테이너만을 출력하며 -a 옵션을 추가하면 정지 상태인 컨테이너도 출력된다.

-q 옵션을 추가할 경우, 컨테이너의 ID 값만 출력되어, 모든 컨테이너를 지정할 수도 있게 된다.

 

4. 로그 출력(docker logs)

정지 상태인 컨테이너는 삭제될 때까지 남아 있으며, 실행 중 발생한 표준 출력과 표준 에러 출력을 간직하고 있다.

명령어 docker logs [option] 컨테이너ID | 컨테이너명으로 확인할 수 있다.

옵션 -f 를 사용하면 컨테이너가 실행 중인 상태에서 실시간으로 발생하는 로그를 볼 수 있다.

 

5. 컨테이너 정지(docker stop, docker kill)

실행 중인 컨테이너를 정지시키는 방법은 다음과 같이 세 가지가 있다.

  1. 컨테이너의 PID=1 인 프로세스를 종료된다.
  2. docker stop 컨테이너ID | 컨테이너명
  3. docker kill 컨테이너ID | 컨테이너명
### PID=1 인 프로세스의 종료
$ docker run ubuntu:latest 
## 컨테이너를 생성하고 실행했지만 실행 목록에는 존재하지 않는다.
$ docker ps
>CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
##실행하자마자 곧바로 종료된 상태임을 확인할 수 있다.
$ docekr ps -a
>8877efe0f1ea   ubuntu:latest   "bash"    About a minute ago   Exited (0) About a minute ago             ecstatic_ellis

→ 별다른 커맨드 지정없이 컨테이너를 실행하여, 실행하자마자 PID=1인 쉘이 종료하여 컨테이너 자체가 종료되었다.

# 컨테이너에 bash 쉘을 통해 연결
$ docker run -it --name test ubuntu bash

root@f3cd4310ea21:/# ps -ax            
  PID TTY      STAT   TIME COMMAND
    1 pts/0    Ss     0:00 bash
   10 pts/0    R+     0:00 ps -ax
root@f3cd4310ea21:/# exit    # bash 쉘 종료
exit

$ docker ps -a
CONTAINER ID   IMAGE           COMMAND   CREATED              STATUS                      PORTS     NAMES
f3cd4310ea21   ubuntu          "bash"    About a minute ago   Exited (0) 22 seconds ago             test

→ docker 커맨드에서 옵션으로 bash를 지정했다.

PID 값이 1인 bash가 기동되었음을 ps 명령어를 통해 확인할 수 있었고, 이후 exit 명령을 통해 bash의 프로세스를 종료하니 컨테이너가 종료 상태에 들어갔음을 확인할 수 있었다.

stop으로 종료하는 것과 kill로 종료하는 것의 차이는 간단하다. stop으로 종료하는 경우, SIGTERM을 통해 프로세스가 정상 종료된다. kill은 이름에서 미뤄볼 수 있듯 SIGKILL이 전달되어 강제 종료가 이뤄짐을 알 수 있다.

 

6. 컨테이너 재기동 (docker start)

$ docker start [Option] [컨테이너ID | 컨테이너명]

앞의 5에서 종료했던 컨테이너를 다시 재기동하는 명령어이다.

-i 옵션을 통해 컨테이너의 STDIN에 터미널을 붙일 수도 있다. 앞선 docker run-i 와 동일하다.

주의할 점은 앞서서 docker run을 통해 도커를 실행할때 -t 옵션을 주지 않았다면, docker start를 통해 재기동을 하면서 -i 옵션을 주었다고 해도 아무런 반응도 하지 않게 된다.

$ docker ps -a
CONTAINER ID   IMAGE     COMMAND   CREATED          STATUS                      PORTS     NAMES
b03625685ee5   ubuntu    "bash"    3 minutes ago    Exited (0) 47 seconds ago             thirsty_bohr

$ docker start -i b03625685ee5
root@b03625685ee5:/#

 

 

7.  이미지를 로컬에 commit (docker commit)

### 앞서 작업하였던 b03625685ee5에 이어서 작업
root@b03625685ee5:/# apt-get update
...
root@b03625685ee5:/# apt-get install git
...
root@b03625685ee5:/# exit
exit

### 로컬 도커 저장소에 커밋
$ docker commit b03625685ee5 ubuntu:git
sha256:476cc38fb8164ceff4155cc019c161e7ded3d40f56262c3ba5257f5bb07b1f32

### 새로운 이미지가 생성되었음을 확인할 수 있다.
$ docker images
REPOSITORY         TAG       IMAGE ID       CREATED         SIZE
ubuntu             git       476cc38fb816   5 minutes ago   207MB
ubuntu             latest    ba6acccedd29   2 months ago    72.8MB

변경 사항을 이미지로 만들어 로컬의 리포지터리에 보관한다. 이때 이미지를 쓰는 동안은 컨테이너가 일시정지한다.

추가적으로 새롭게 만들어지는 이미지는 기존의 이미지와 구분하기 위해서 이미지의 태그를 버전이나 기타 의미 있는 문자열을 사용하여 다른 이미지와 구분할 수 있도록 한다.

기존의 git 명령어와 매우 유사하다. 로컬에 올렸다면 그 다음은 당연히 리모트 리포지터리에 올려야겠지.

 

8. 이미지를 원격에 push (docker push)

쿠버네티스에서 이미지를 활용하려면, 컨테이너 이미지를 가지고 있어야 할 것이다.

이때 쿠버네티스 클러스터 외부에서 작업한 이미지를 클러스터 내부에서 띄우고 싶다면, 원격 컨테이너 저장소를 거치는 것이 가장 간편하다.

가장 먼저 도커 허브를 원격 저장소로 활용하는 경우를 살펴보자.

본인의 도커 허브에 인증이 되어 있다는 전제하에 docker push [이미지명 | 이미지 ID] 를 주기만 하면 로컬 이미지를 원격으로 올릴 수 있다. git과 매우 유사하다. 다만 이미지명 | 이미지ID 필드는 반드시 채워져야 한다는 차이가 있겠다.

주의할 점으로는 원격에 올릴때는 이미지명과 이미지에 붙은 태그를 더 명확하게 작성해줄 필요가 있다는 것이다.

이를 위해서 docker tag 명령어를 활용할 수 있겠다.

### 컨테이너 이미지에 리포지터리의 이름과 태그 붙이기
$ docker tag ubuntu:git youngmki/ubuntu:git

$ docker images
REPOSITORY         TAG       IMAGE ID       CREATED         SIZE
ubuntu             git       476cc38fb816   9 minutes ago   207MB
youngmki/git       latest    476cc38fb816   9 minutes ago   207MB

기존의 이미지에 리포지터리 이름과 태그를 변경하여 마치 별도의 이미지처럼 다룰 수 있게 된다.

잘보면 이미지 ID가 동일한데, 내용물은 동일함을 이를 통해 알 수 있다. 이후 리포지터리 이름과 태그가 수정되었다면 원격 리포지터리에 올리면 되겠다.

### 원격 리포지터리에 push
$ docker push youngmki/ubuntu:git
The push refers to repository [docker.io/youngmki/ubuntu]
67e0f7b9c01c: Pushed 
9f54eef41275: Mounted from library/ubuntu 
git: digest: sha256:8bd8a7e93e68af3bb90ccd3bf2720de1ec41185197066fcfc3b077ab25142f38 size: 741

 

 

9. 컨테이너 이미지 제거(docker rmi)

앞서 생성했던 도커 이미지를 삭제해보려고 한다.

### 도커 이미지 로컬에서 삭제
$ docker rmi youngmki/ubuntu:git

### 도커 이미지 확인
$ docker images
REPOSITORY     TAG       IMAGE ID       CREATED          SIZE
ubuntu         git       476cc38fb816   36 minutes ago   207MB

같은 ID를 가진 도커 이미지는 여전히 살아있음을 확인할 수 있다. 리포지터리 이름과 태그가 다르다면 별도의 이미지로 처리가 되는 듯한 모습을 볼 수 있다.

 

10. 컨테이너 삭제(docker rm)

컨테이너가 종료되었다고 해서 컨테이너가 삭제된 것은 아니다.

### 컨테이너 목록
$ docker ps -a
CONTAINER ID   IMAGE     COMMAND   CREATED          STATUS                      PORTS     NAMES
b03625685ee5   ubuntu    "bash"    49 minutes ago   Up 45 minutes                         thirsty_bohr
ba2151e732a4   ubuntu    "bash"    50 minutes ago   Exited (0) 50 minutes ago             vigorous_allen
3421792cb04e   ubuntu    "bash"    59 minutes ago   Exited (0) 59 minutes ago             serene_jang

### 현재 생성되어 있는 모든 컨테이너 삭제
$ docker rm $(docker ps -a -q)
ba2151e732a4
3421792cb04e
b03625685ee5

### 컨테이너 목록
$ docker ps -a
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

 

컨테이너 ≠ 가상머신


컨테이너와 가상머신은 분명히 다르다. 구체적으로 어떻게 다를까? 에 대한 궁금증을 해소하고자 한다.

방법은 이렇다. 도커로 띄운 컨테이너에 여러 터미널로 다중 접속을 해보고 각 터미널의 설정을 서로 비교해본다.

### 생성과 함께 tty1로 접속
$ docker run -it --name test ubuntu
### 본인의 터미널 디바이스 출력
$ tty
/dev/pts/0
$ ls /dev/pts
0  ptmx

### exec를 통해 기존의 컨테이너에 별도의 tty2로 접속
### docker exec -it [컨테이너명 | 컨테이너ID] 쉘종류
$ docker exec -it test bash
$ tty
/dev/pts/1

$ ls /dev/pts
0  1  ptmx

### exec를 통해 기존의 컨테이너에 별도의 tty2로 접속
### docker exec -it [컨테이너명 | 컨테이너ID] 쉘종류
$ docker exec -it test bash
$ tty
/dev/pts/2

$ ls /dev/pts
0  1  2  ptmx

→ 결국 컨테이너 하나하나는 가상 서버가 아님을 볼 수 있다.

컨테이너는 하나의 호스트 아래서 목표로 하는 프로세스만을 실행할 수 있도록 고안된 실행 환경 이라고 정의할 수 있겠다.

또한 컨테이너에서는 기존의 리눅스 가상머신에서는 가능할 w 명령어를 통한 로그인한 다른 유저의 정보를 볼 수도 없다.

컨테이너는 로그인을 통한 유저 인증 기능도 존재하지 않고, 유저를 애초에 관리하지 않는다.

$ w
05:19:53 up  2:32,  0 users,  load average: 0.00, 0.02, 0.00
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT

→ 아무것도 나오지 않는다.

결론적으로 컨테이너 각각은 하나의 독립적인 운영체제처럼 보이지만 실은 호스트의 커널을 공유하여 동작하는 리눅스 프로세스임을 알 수 있다.