본문 바로가기

서버운영 (TA, ADMIN)/인프라

[Container] 도커 알아보기(1) - 이미지와 컨테이너

도커란 무엇인가?

도커(Docker)는 컨테이너형 가상화기술을 구현하기 위한 상주 애플리케이션(dockerd라는 데몬이 상주 실행된다)과 이 애플리케이션을 조작하기 위한 명령행 도구로 구성되는 프로덕트다. 애플리케이션 배포에 특화돼 있기 때문에 애플리케이션 개발 및 운영을 컨테이너 중심으로 할 수 있다.

 

도커의 이해를 돕기 위해 가장 쉽게 생각할 수 있는 유스 케이스는 애플리케이션 테스트에 사용할 경량 가상 환경이다. 웹 애플리케이션을 개발하기 위해 로컬에 아파치나 엔진엑스 같은 웹서버를 구축하는 경우를 생각해본다. 가상 환경에 운영환경과 같은 운영체제를 설치하고 문서를 참고해 패키지 관리자로 필요한 요소를 설치하는 환경 구축 작업을 많이 경험해 봤을 것이다. 이러한 기존 방법과 달리, 도커를 사용하면 좀더 편리하게 환경을 구축할 수 있다. 로컬 환경에 도커만 설치하면 몇 줄짜리 구성 파일과 명령어 한출로 애플리케이션이나 미들웨어가 이미 갖춰진 테스트용 가상환경(도커 컨테이너)을 빠르게 구축할 수 있다. 가상화 소프트ㅜ에어와 비교해도 오버헤드가 적어진다는 장점이 있다.

 

이렇듯 조작이 간편하고 경량 컨테이너라는 장점 때문에 도커는 로컬 머신의 개발 환경 구축에 널리 사용된다. 도커는 개발환경 구축뿐만 아니라 개발 후 운영 환경에 대한 배포나 애플리케이션 플랫폼으로 가능할 수 있다는 점에서 기존 가상 머신보다 더 뛰어나다.

 

도커는 기존 가상화 소프트웨어보다 더 가볍게 동작한다. 그러므로 테스트 환경뿐만 아니라 운영 환경에서도 컨테이너를 사용할 수 있다. 또한 도커는 이식성이 뛰어나다. 로컬 머신의 도커 환경에서 실행하던 컨테이너를 다른 서버에 있는 도커 환경에 배포하거나 반대로 다른 서버의 도커 환경에서 동작하던 컨테이너를 로컬로 가져올 수 있다. 즉, 개발환경과 운영환경을 거의 동등하게 재현할 수 있다.

 

이 덕분에 도커 컨테이너는 운영환경에서도 널리 사용된다. 이 외에도 컨테이너 간의 연동이나 클라우드 플랫폼 지원 등 여러면에서 장점이 있다. 도커는 시스템 개발 및 운영 환경에서 사용하는 것이 일반적이지만, 그 외에도 다양한 방법으로 활용할 수 있다.

  - 설치가 번거로운 명령형 도구를 도커 컨테이너로 가져다 사용함으로써 호스트를 깔끔하게 유지하면서도 바로 실행할 수 있다.

  - 다양한 의존 라이브러리나 도구를 도커 컨테이너에 포함시켜 배포함으로써 실행 환경과 상관없이 스크립트의 동작 재현성을 높임

  - 도커 컨테이너를 HTTP 부하 테스트의 워커(worker)로 사용해 HTTP 요청 수를 증가시킴.

 

도커는 컨테이너가 갖는 성능적 이점을 잘 살리면서도 애플리케이션 배포에 초점을 맞췄다. 도커와 LXC(Linux Containers)의 차이점은 다음과 같다.

  - 호스트 운영 체제의 영향을 받지 않는 실행 환경(Docker Engine을 이용한 실행 환경 표준화)

  - DSL(Dockerfile)을 이용한 컨테이너 구성 및 애플리케이션 배포 정의

  - 이미지 버전 관리

  - 레이어 구조를 갖는 이미지 포맷(차분 빌드가 가능함)

  - 도커 레지스트리(이미지 저장 서버 역할을 함)

  - 프로그램 가능한 다양한 기능의 API

 

도커는 컨테이너 정보를 Dockerfile 코드로 관리할 수 있다. 이 코드를 기반으로 복제 및 배포가 이루어지기 때문에 재현성이 높은 것이 특징이다. 이 외에도 만들어 둔 기존 컨테이너를 다른 환경에서 동작시키기 위한 메커니즘이 잘 갖춰져 있다.

 

도커 이전에는 애플리케이션을 호스트 운영 체제 또는 게스트 운영 체제에 배포하는 스타일이 주류였다. 이런 방식에서는 애플리케이션이 운영 체제의 영향을 강하게 받는다. 반면 도커는 컨테이너에 애플리케이션 실행 환경이 함께 배포되는 방식이다. 아예 실행 환경째로 배포하는 방식으로 골치 아픈 의존성 문제를 근본적으로 해결하는 것이다. 도커만 설치돼 있다면 CentOS에 최적화된 애플리케이션을 우분투 서버에 설치할 수도 있다. 애플리케이션 배포 환경으로 도커가 널리 쓰이는 원인은 이렇듯 환경의 영향을 덜 받고 배포가 간편하기 때문이다.

도커 스타일 체험하기

실제로 도커를 사용해 애플리케이션을 배포하는 코드를 살펴보자. 애플리케이션이 포함된 도커 이미지를 어떻게 만들고, 컨테이너를 어떻게 실행하는지 살펴본다. 여기서는 어떤 방식으로 배포가 이루어지는지 간단히 살펴보는 것이 목적이다.

 

helloworld라는 이름으로 셸 스크립트 파일을 만든다. 단순한 스크립트지만 여기서는 이것이 애플리케이션 역할을 한다.

#!/bin/sh

echo "Hello, World!"

이제 이 스크립트를 도커 컨테이너에 담아 본다. Dockerfile이나 애플리케이션 실행 파일을 사용해서 도커 컨테이너의 원형이 될 이미지를 만드는 과정을 '도커 이미지 빌드'라고 한다.

 

셸 스크립트와 같은 폴더에 Dockerfile을 작성한다. 도커가 어떻게 이미지를 만들고 실행할지가 이코드에 정의된다.

FROM ubuntu:16.04

COPY helloworld /usr/local/bin
RUN chmod +x /usr/local/bin/helloworld

CMD ["helloworld"]

Dockerfile의 FROM 절은 커넽이너의 원형(틀) 역할을 할 도커 이미지(운영 체제)를 정의한다. 여기서는 우분투 도커 이미지를 지정한다.

 

COPY 절은 조금 전에 작성한 셸 스크립트 파일(helloworld)을 도커 컨테이너 안의 /usr/local/bin에 복사하라고 정의한 것이다.

 

RUN 절은 도커 컨테이너 안에서 어떤 명령을 수행하기 위한 것이다. 여기서는 helloworld 스크립트에 실행 권한을 부여하기 위해 사용했다. 여기까지가 도커 빌드 과정에서 실행되며 그 결과 새로운 이미지가 만들어진다.

 

CMD 절은 완성된 이미지를 도커 컨테이너로 실행하기 전에 먼저 실행할 명령을 정의한다. 여기서는 사실상 애플리케이션을 실행하는 명령을 지정했다.

 

이 Dockerfile을 사용해 이미지를 빌드하고 실행해본다. Dockerfile이 있는 폴더에서 docker image build 명령을 실행한다.

$ docker image build -t helloworld:lastes .
Sending build context to Docker daemon 97.5MB

빌드가 끝난 다음 docker container run 명령으로 도커 컨테이너를 실행하는 것이 기본 사용법이다.

$ docker container run helloworld:latest
Hello, World!

이런 방식으로 도커 이미지에 애플리케이션에 필요한 파일을 운영체제와 함께 담아서 컨테이너 형태로 실행하는 것이 기본적인 스타일이다. 위 예제는 셸 스크립트를 우분투 운영체제와 함께 컨테이너로 실행한 것이다.

 

"익숙한 방식을 버림녀서까지 도커를 도입해야 할 이유는 뭘까?" 도커를 사용하는 의의를 다음과 같이 꼽는다.

  - 변화하지 않는 실행 환경으로 멱등성(Idempotency) 확보

  - 코드를 통한 실행 환경 구축 및 애플리케이션 구성

  - 실행 환경과 애플리케이션 일체화로 이식성 향상

  - 시스템을 구성하는 애플리케이션 및 미들웨어의 관리 용이성

 

웹 애프릴케이션 개발을 예로 들어본다. 도커를 사용하면 로컬 개발 환경에서 필요한 애플리케이션을 신속하게 갖출 수 있고 그대로 플랫폼과 상관없이 배포할 수 있다. 도커 컨테이너로 변화하지 않는 실행 환경을 구축해 실행 환경이 원인이 되는 말썽을 최소한 줄일 수 있다. 더욱이 웹 애플리케이션의 프로트엔드에 아파치나 엔진엑스 같은 웹서버를 두는 것도 복잡한 절차없이 컨테이너로 설정할 수 있게 된다. 미들웨어를 포함하는 시스템 구성 역시 설정 파일로 정의할 수 있다. 도커를 도입하면 개발 및 운영 업무가 쉬워진다.

 

애플리케이션은 항상 뭔가에 의존성을 갖는다. 운영체제는 물론이고, CPU나 메모리 같은 컴퓨터 리소스, 언어 런타임, 라이브러리, 애플리케이션 내부적으로 별도 프로세스로 실행하는 다른 애플리케이션 등 다양한 요소에 의존성을 가질 수 있다. 각 서버에 배포된 애플리케이션이 동일하다면 애플리케이션이 의존하는 환경의 차이를 가능한 한 배제하는 것이 이 문제를 해결하는 지름길이다.

 

환경 차이 문제를 피하려면 언제든 몇번을 실행해 같은 결과가 보장되는 멱등성을 확보해야 한다. 애플리케이션이 의존하는 런타임이나 라이브러리 모두가 확실하게 특정 버전으로 설치되도록 코드를 작성해야 한다.

 

그러나 코드 기반으로 인프라 구축을 관리한다고 해도 멱등성을 보장하기 위해 항구적인 코드를 계속 작성하는 것은 운영 업무에 부담을 주기 쉽다. 서버의 대수가 늘어날수록 모든 서버에 구성을 적용하는 시간도 늘어난다. 이 문제에 대한 대책이 불변 이늪라 개념이다. 불변 인프라는 어떤 시점의 서버 상태를 저장해 복제할 수 있게 하자는 개념이다. 제대로 설정된 상태의 서버를 항상 사용할 수 있다는 점이 가장 큰 장점이다.

 

서버에 변경을 가하고 싶은 경우에는 기존 인프라를 수정하는 대신 새로운 서버를 구축하고 그 상태를 이미지로 저장한 다음 그 이미지를 복제한다. 한번 설정된 서버는 수정없이 파기되므로 멱등성을 신경쓸 필요조차 없다. 도커를 사용하면 코드로 관리하는 인프라와 불변 인프라의 두 개념을 간단하고 낮은 비용으로 실현할 수 있다.

 

인프라 구성이 Dockerfile로 관리되므로 코드로 관리하는 인프라는 도커의 대원칙이다. 도커는 컨테이형 가상화 기술을 사용한다. 호스트형 가상화에엇는 가상 머신의 OS를 재현하는 것과 달리, 컨테이너형 가상화는 운영체제 대부분을 호스트 운영 체제와 공유한다. 그만큼 실행에 걸리는 시간이 수초 가까이 짧아진다. 실행에 걸리는 시간이 적은 만큼 구성을 수정하지 않고 인프라를 완전히 새로 만드는 불변 인프라와 궁합이 잘 맞는다.

 

도커는 도커 이미지(Dockerfile)로 (서버) 구성을 코드로 관리할 수 있다. 그러므로 기존 컨테이너를 빠르게 폐기하고 새로이 구축할 수 있다. 코드로 관리하는 인프라와 불변 인프라라는 두 개념 모두를 쉽게 실현할 수 있는 도구라고 할 수 있다. 웹 애플리케이션이나 API 서버처럼 스테이트리스(stateless)한 성격을 갖는 부분은 그리 큰 어려움엇이 도커화할 수 있을 것이다.

 

도커를 통해 인프라와 애플리케이션이 모두 컨테이너 형태로 제공되면서 인프라와 애플리케이션의 설정을 모두 코드 수준에서 쉽게 수정할 수 있게 됐다. 기준에는 명확했던 인프라 엔지니어와 서버 사이드 에닞니어의 영역 구분이 점점 희미해지고 있다.

 

서버 사이드 애플리케이션 개발 분야에서는 최근 마이크로서비스 아키텍처가 등장하면서 도커를 이용한 개발에도 유리한 점이 있어 애플리케이션을 잘게 분할래 만드는 스타일이 꽤 지지를 받게 되었다. 개발에 외부 API를 사용하는 경우에도 도커가 제공하는 목업 서버 개발환경을 사용하는 경우가 늘고 있다. 도커는 인프라엔지니어와 서버사이드 엔지니어의 전유물이 아니다. 현대적인 개발을 수행한다면 프론트 엔드 엔지니어와 모바일 애플리케이션 엔지니어에게도 기초 기술이 될 것이다.


01. 컨테이너로 애플리케이션 실행하기

도커 이미지 - 도커 컨테이너를 구성하는 파일 시스템과 실행할 애플리케이션 설정을 하나로 합친 것으로, 컨테이너를 생성하는 템플릿 역할을 한다.

도커 컨테이너 - 도커 이미지를 기반으로 생성되며, 파일 시스템과 애플리케이션이 구체화돼 실행되는 상태.

도커 이미지 하나로 여러 개의 컨테이너를 생성할 수 있다. 위 그림에서 도커 이미지는 우분투 파일 시스템을 실행하는 애플리케이션 파일을 담고 있다. 컨테이너가 생성될 때 이미지로부터 이를 구체화하고 컨테이너 안의 우분투 파일 시스템상에서 애플리케이션이 실행된다. 컨테이너로 애플리케이션을 실행하려면 컨테이너 형태로 구체화될 템플릿 역할을 하는 이미지를 먼저 만들어야 한다.

 

예제로 도커 이미지를 만들기 위한 Dockerfile을 작성하고, 이렇게 만든 이미지를 사용해서 도커 컨테이너를 실행해본다. 그다음 도커의 포트 포워딩 기능도 사용해 본다. 컨테이너에서 실행되는 애플리케이션에 HTTP 요청을 보내고 애플리케이션에서 돌려주는 응답을 받는 과정까지 설명한다.

 

간단한 애플리케이션과 도커 이미지 만들기

예제로 Go 언어로 만든 간단한 웹서버를 도커 컨테이너에서 실행해본다.

package main

import (
    "fmt"
    "log"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        log.Println("received request")
        fmt.Fprintf(w, "Hello Docker!!")
    })
    
    log.Println("start server")
    server:= &http.Server {
        Addr: ":8080",
    }
    if err:= server.ListenAndServe(); err != nil {
        log.Println(err)
    }
}

그다음 이 Go 언어 코드를 도커 컨테이너에 배치한다. main.go 파일을 포함하는 새로운 도커 이미지를 만드는 것이다. 이를 위해 main.go 파일과 같은 디렉토리에 Dockerfile을 다음과 같이 작성한다.

FROM golang:1.9

RUN mkdir /echo
COPY main.go /echo

CMD ["go", "run", "/echo/main.go"]

Dockerfile에 전용 도메인 언어로 이미지의 구성을 정의한다. 여기 사용된 FROM이나 RUN 같은 키워드를 인스트럭션(명령)이라고 한다. 인스트럭션의 종류가 여러가지 있으나, 우선 기본적인 것부터 사용해본다.

 

FROM 인스트럭션

FROM 인스트럭션은 도커 이미지의 바탕이 될 베이스 이미지를 지정한다. Dockerfile로 이미지를 빌드할때 먼저 FROM 인스트럭션에 지정된 이미지를 내려받는다.

 

FROM에서 받아오는 도커 이미지는 도커 허브(Docker Hub)라는 레지스트리에 공개된 것이다. 도커는 FROM에서 지정된 이미지를 기본적으로 도커 허브 레지스트리에서 참조한다.

 

main.go를 실행하려면 Go 언어의 런타임이 설치된 이미지가 있어야 한다. 이 런타임이 설치된 golang 이미지를 사용한다. 도커이미지 명에서 1.9라고 된 부분은 태그라고 한다. 각 이미지의 버전 등을 구별하는 식별자다. 각 도커 이미지는 고유의 해시값을 갖는데, 이 해시만으로는 필요한 이미지가 무엇인지 특정하기가 어렵다. 특정 버전에 태그를 붙여두면 사람이 그 내용을 쉽게 파악할 수 있다.

 

RUN 인스트럭션

RUN 인스트럭션은 도커 이미지를 실행할때 컨테이너 안에서 실행할 명령을 정의하는 인스트럭션이다. 인자로 도커 컨테이너 안에서 실행할 명령을 그대로 기술한다. 여기서는 main.go 애플리케이션을 배치하기 위한 /echo 디렉토리를 mkdir 명령으로 만들었다.

 

여러 명령을 실행할 수 있도록 RUN 인스트럭션은 여러번 사용할 수 있다. 반면 FROM이나 CMD 인스트럭션은 도커 이미지 하나에 한번밖에 사용하지 못한다. 그러나 한 파일에서 유효한 인스트럭션을 여러개 작성하는 것은 가능하다.

 

COPY 인스트럭션

COPY 인스트럭션은 도커가 동작 중인 호스트 머신의 파일이나 디렉터리를 도커 컨테이너 안으로 복사하는 인스트럭션이다. 이 예제에서는 호스트에서 작성한 main.go 파일을 도커 컨테이너 안에서 실행할 수 있도록 컨테이너 안으로 복사하는데 이 인스트럭션을 사용했다. 복사 위치는 앞의 RUN 인스트럭션에서 만든 echo 디렉터리이다.

 

COPY와 기능이 비슷한 ADD 인스트럭션도 있는데, COPY와는 용도가 좀 다르다.

 

CMD 인스트럭션

CMD 인스트럭션은 도커 컨테이너를 실행할때 컨테이너 안에서 실행할 프로세스를 지정한다. RUN 인스트럭션 이미지를 빌드할때 실행되고 CMD 인스트럭션은 컨테이너를 시작할때 한번 실행된다.  RUN은 애플리케이션 업데이트 및 배치에, CMD는 애플리케이션 자체를 실행하는 명령이라고 생각하면 된다.

 

즉, 셸 스크립트로 치면 다음과 같은 실행 명령 역할을 한다.

$ go run /echo/main.go

이 명령을 CMD 인스트럭션에 기술하면 다음과 같이 명령을 공백으로 나눈 배열로 나타낸다. CMD go run/echo/main.go처럼 인자 배열 대신 명령어를 그대로 작성할 수도 있다. 되도록이면 인자 배열 방식을 사용하는 것이 좋다.

 

CMD에 지정한 명령을 docker container run에서 지정한 인자로 오버라이드할 수도 있다. 다음과 같은 Dockerfile로 이미지를 만든 후에 컨테이너를 실행하면 uname 대신 echo yay 명령이 실행된것을 확인할 수 있다. 

인자 표기 방식 동작
CMD ["실행 파일", "인자1", "인자2"] 실행 파일에 인자를 전달한다. 사용을 권장하는 방식
CMD 명령 인자1 인자2 명령과 인자를 지정한다. 셸에서 실행되므로 셸에 정의된 변수를 참조할 수 있다.
CMD ["인자1", "인자2"] ENTRYPOINT에 지정된 명령에 사용할 인자를 전달한다.

 

LABEL 인스트럭션

이미지를 만든 사람의 이름 등을 적을 수 있다. 전에는 MAINTAINER라는 인스트럭션도 있었으나 더이상 사용하지 않는다(deprecated).

 

ENV 인스트럭션

도커 컨테이너 안에서 사용할 수 있는 환경변수를 지정한다.

 

ARG 인스트럭션

이미지를 빌드할 때 정보를 함께 넣기 위해 사용한다. 이미지를 빌드할 때만 사용할 수 있는 일시적인 환경변수다.

FROM alpine:3.7
LABEL maintainer="dockertaro@example.com"

ARG builddate
ENV BUILDDATE=${builddate}

ENV BUILDFROM="from Alpine"

ENTRYPOINT ["/bin/ash", "-c"]
CMD ["env"]
$ docker image build --build-arg builddate=today -t example/others .
Sending build context to Docker daemon  2.048kB
Step 1/7 : FROM alpine:3.7
3.7: Pulling from library/alpine
5d20c808ce19: Pull complete
Digest: sha256:8421d9a84432575381bfabd248f1eb56f3aa21d9d7cd2511583c68c9b7511d10
Status: Downloaded newer image for alpine:3.7
 ---> 6d1ef012b567
Step 2/7 : LABEL maintainer="dockertaro@example.com"
 ---> Running in 4b019ae5e3f7
Removing intermediate container 4b019ae5e3f7
 ---> 3034fbd10b00
Step 3/7 : ARG builddate
 ---> Running in b408b958b3d2
Removing intermediate container b408b958b3d2
 ---> 898cef649519
Step 4/7 : ENV BUILDDATE=${builddate}
 ---> Running in 2a783e3f0b57
Removing intermediate container 2a783e3f0b57
 ---> 6790eb672c77
Step 5/7 : ENV BUILDFROM="from Alpine"
 ---> Running in 8d309e86fabd
Removing intermediate container 8d309e86fabd
 ---> 4312034022e7
Step 6/7 : ENTRYPOINT ["/bin/ash", "-c"]
 ---> Running in 4811760d1a28
Removing intermediate container 4811760d1a28
 ---> cc482289a19d
Step 7/7 : CMD ["env"]
 ---> Running in 9e970f294846
Removing intermediate container 9e970f294846
 ---> 66454286abf6
Successfully built 66454286abf6
Successfully tagged example/others:latest
$ docker container run example/others
HOSTNAME=c3dfe734594c
SHLVL=1
HOME=/root
BUILDFROM=from Alpine
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PWD=/
BUILDDATE=today

도커 이미지 빌드하기

main.go 파일과 Dockerfile 작성이 끝났으면 docker image build 명령으로 도커 이미지를 빌드한다.

 

docker image build 명령은 도커 이미지를 빌드하기 위한 명령이다. docker image build 명령의 기본 문법은 다음과 같다.

docker image build -t 이미지명[:태그명] Dockerfile의_경로

그럼 이미지를 빌드해본다. 현재 작업 디렉토리에 Dockerfile이 있다면 마지막 인자를 .(현재 작업 디렉터리)로 한다.

$ docker image build -t example/echo:latest .

 

빌드를 실행하면 베이스 이미지를 내려받고 RUN이나 COPY 인스트럭션에 지정된 명령이 단계적으로 실행되는것을 알 수 있다.

$ docker image build -t 12bme/echo:latest .
Sending build context to Docker daemon  3.072kB
Step 1/4 : FROM golang:1.9
1.9: Pulling from library/golang
55cbf04beb70: Pull complete
1607093a898c: Pull complete
9a8ea045c926: Pull complete
d4eee24d4dac: Pull complete
9c35c9787a2f: Pull complete
8b376bbb244f: Pull complete
0d4eafcc732a: Pull complete
186b06a99029: Pull complete
Digest: sha256:8b5968585131604a92af02f5690713efadf029cc8dad53f79280b87a80eb1354
Status: Downloaded newer image for golang:1.9
 ---> ef89ef5c42a9
Step 2/4 : RUN mkdir /echo
 ---> Running in b5caffbd3799
Removing intermediate container b5caffbd3799
 ---> 047fee1f9648
Step 3/4 : COPY main.go /echo
 ---> 92005ef0c17c
Step 4/4 : CMD ["go", "run", "/echo/main.go"]
 ---> Running in 33fc94620d66
Removing intermediate container 33fc94620d66
 ---> fb74c092d33b
Successfully built fb74c092d33b
Successfully tagged 12bme/echo:latest

docker image ls 명령으로 생성된 이미지의 REPOSITORY, TAG, IMAGE ID, CREATED, SIZE 값 등을 확인할 수 있다. 여기까지 잘 됐다면 이미지를 성공적으로 빌드한 것이다.

 

ENTRYPOINT 인스트럭션으로 명령을 좀 더 세밀하게 실행하기

ENTRYPOINT 인스트럭션을 사용하면 컨테이너의 명령 실행 방식을 조정할 수 있다. ENTRYPOINT는 CMD와 마찬가지로 컨테이너 안에서 실행할 프로세스를 지정하는 인스트럭션이다. ENTRYPOINT를 지정하면 CMD의 인자가 ENTRYPOINT에서 실행하는 파일에 인자로 주어진다. 즉, ENTRYPOINT에 지정된 값이 기본 프로세스를 지정하는 것이다. 

 

예를 들면, golang:1.10 이미지에는 ENTRYPOINT가 지정돼 있지 않으며 CMD에 bash가 지정돼 있다. 이를 그대로 실행하면 bash가 실행된다.

$ docker container run -it golang:1.10
root@83dc0ac5cb33:/go# 

 

이 컨테이너에서 go version 명령을 실행하려면 다음과 같이 인자로 명령을 넘긴다. 그러면 기존 명령 bash가 새로운 명령으로 오버라이드돼 go version이 실행한다.

$ docker container run -it golang:1.10 go version
go version go1.10.8 linux/amd64

 

이 컨테이너에서 go 명령을 좀더 편하게 사용하기 위해 Dockerfile을 다음과 같이 작성하고 ENTRYPOINT에 go를 지정한다. CMD는 빈 문자열로 오버라이드한다.

FROM golang:1.10

ENTRYPOINT ["go"]
CMD [""]

이 Dockerfile을 ch02/golang:latest라는 이름으로 빌드한다.

$ docker image build -t ch02/golang:latest .
Sending build context to Docker daemon  2.048kB
Step 1/3 : FROM golang:1.10
 ---> 6fd1f7edb6ab
Step 2/3 : ENTRYPOINT ["go"]
 ---> Running in 34524f93abfb
Removing intermediate container 34524f93abfb
 ---> b33ca235c171
Step 3/3 : CMD [""]
 ---> Running in 29f96775547f
Removing intermediate container 29f96775547f
 ---> c7ec185c8016
Successfully built c7ec185c8016
Successfully tagged ch02/golang:latest

이 이미지로 컨테이너를 실행해보면 go 명령을 인자로 전달하지 않아도 go를 실행할 수 있다. go 이후의 명령만 인자로 넘기면 된다.

$ docker container run ch02/golang:latest version
go version go1.10.8 linux/amd64

ENTRYPOINT는 이미지를 생성하는 사람이 컨테이너의 용도를 어느 정도 제한하려는 경우에도 유용하다. 단 docker container run --entrypoint 옵션으로 실행할때 오버라이드될 수도 있다.

도커 컨테이너 실행

이미지를 생성했으니 docker container run 명령으로 컨테이너를 실행한다. 정상적으로 실행이 되면 'start server'라는 로그가 출력될 것이다.

$ docker container run 12bme/echo:latest
2020/09/29 15:29:49 start server

docker container run 명령으로 echo 컨테이너를 실행했다. 그러나 이 컨테이너는 계속 foreground에서 동작한다. 컨테이너를 종료하고 싶다면 터미널에 Ctrl+C(SIGINT 전송)를 입력한다. 이 이미지에서 Go 언어로 만든 프로그램은 서버 애플리케이션이다. docker container run 명령에 -d 옵션을 붙여 백그라운드로 컨테이너를 실행시킬 수 있다.

$ docker container run -d 12bme/echo:latest
d7ce09a158d24925542c36288911a1963feb4567c32e6f781fb7f0983791b053

-d 옵션을 붙여 컨테이너를 실행하면 표준 출력에 해시값처럼 보이는 문자열이 출력된다. 이 문자열은 도커 컨테이너의 ID다. 컨테이너 ID는 컨테이너를 실행할때 부여되는 유일 식별자로, docker 명령으로 컨테이너를 조작할때 컨테이너를 특정하기 위한 값으로 사용된다.

 

포트 포워딩

여기서 Go 언어로 작성한 코드를 보면 이 애플리케이션은 포트 8080을 리스닝한다. 로컬 환경에서 curl을 사용해 포트 8080으로 GET 요청을 보낸다.

$ curl http://127.0.0.1:8080/
curl: (7) Failed to connect to 127.0.0.1 port 8080: Connection refused

도커 컨테이너는 가상 환경이지만, 외부에서 봤을때 독립된 하나의 머신처럼 다룰수 있다는 특징이 있다. echo 애플리케이션은 8080 포트를 리스닝하고 있지만, 이 포트는 컨테이너 포트라고 해서 컨테이너 안에 한정된 포트다. curl을 컨테이너 안에서 실행하면 올바른 응답을 받을 수 있겠지만, 컨테이너 밖에서는 컨테이너 포트를 바로 사용할 수 없기 때문에 Connection refused라는 메시지가 출력되는 것이다.

 

이처럼 HTTP 요청을 받는 애플리케이션을 사용하려면 컨테이너 밖에서 온 요청을 컨테이너 안에 있는 애플리케이션에 전달해줘야 한다. 그 역할을 담당하는 것이 바로 도커의 포트 포워딩이다. 포트 포워딩이란 호스트 머신의 포트를 컨테이너 포트와 연결해 컨테이너 밖에서 온 통신을 컨테이너 포트로 전달한다. 이 기능 덕분에 컨테이너 포트를 컨테이너 외부에서도 이용할 수 있다.

$ docker container stop (docker container ls --filter "ancestor=12bme/echo" -q)

docker container ls --filter 옵션으로 실행 중인 컨테이너 중 조건을 만족하는 것(12bme/echo)만을 출력하도록 했다. -q는 컨테이너 ID만 출력하게 하는 옵션이다.

 

docker container run 명령에서 -p 옵션을 붙이면 포트 포워딩을 지정할 수 있다. -p 옵션값은 호스트_포트:컨테이너_포트 형식으로 기술하면 된다.

 

호스트 포트도 8080으로 지정하면 명령을 이해하기가 어려우므로 호스트 포트 18080을 컨테이너 포트 8080에 연결하도록 포트 포워딩을 적용해본다.

$ docker container run -d -p 18080:8080 12bme/echo:latest
0c11cf2b1cb91307e297d649027e9cc54db53e59cdff3d306bf0957d352b0ceb

컨테이너가 실행되면 호스트 포트, localhost 포트 18080에 curl로 GET 요청을 보보내본다.

$ curl http://127.0.0.1:18080/
Hello Docker!!⏎

 

호스트 포트를 다음과 같이 생략할 수도 있다. 이런 경우에는 빈포트가 ephemeral 포트로 자동 할당된다. 어떤 포트가 할당됐는지는 docker container ls 출력 결과의 PORTS 컬럼에서 확인하면 된다.

$ docker container run -d -p 8080 12bme/echo:latest

02. 도커 이미지 다루기

도커 사용법은 크게 이미지를 대상으로 하는 것과 컨테이너를 대상으로 하는 것으로 나뉜다. 도커 이미지를 다루는 법을 설명하기 전에 도커 이미지가 구체적으로 무엇인지 먼저 알아본다. 한마디로 말하면 도커 이미지는 도커 컨테이너를 만들기 위한 템플릿이다.

 

도커 이미지는 우분투 같은 운영체제로 구성된 파일 시스템은 물론, 컨테이너 위에서 실행하기 위한 애플리케이션이나 그 의존 라이브러리, 도구에 어떤 프로세스를 실행할지 등의 실행 환경의 설정 정보까지 포함하는 아카이브이다. Dockerfile 역시 이미지를 구성하는 순서를 기술한 코드에 지나지 않기 때문에 Dockerfile 자체가 이미지라고 할 수는 없다. 컨테이너의 템플릿 역할을 하는 이미지를 만드는 과정을 일반적으로 '도커 이미지를 빌드한다'고 한다. 그리고 컨테이너를 실행할 때 이 빌드된 이미지를 사용한다.

 

이미지를 다루는 하위 명령은 image 명령에 --help 옵션을 붙여 도움말을 확인하면 된다.

$ docker image --help

Usage:	docker image COMMAND

Manage images

Commands:
  build       Build an image from a Dockerfile
  history     Show the history of an image
  import      Import the contents from a tarball to create a filesystem image
  inspect     Display detailed information on one or more images
  load        Load an image from a tar archive or STDIN
  ls          List images
  prune       Remove unused images
  pull        Pull an image or a repository from a registry
  push        Push an image or a repository to a registry
  rm          Remove one or more images
  save        Save one or more images to a tar archive (streamed to STDOUT by default)
  tag         Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE

Run 'docker image COMMAND --help' for more information on a command.
 docker image build --help

Usage:	docker image build [OPTIONS] PATH | URL | -

Build an image from a Dockerfile

Options:
      --add-host list           Add a custom host-to-IP mapping (host:ip)
      --build-arg list          Set build-time variables
      --cache-from strings      Images to consider as cache sources
      --cgroup-parent string    Optional parent cgroup for the container
      --compress                Compress the build context using gzip
      --cpu-period int          Limit the CPU CFS (Completely Fair Scheduler) period
      --cpu-quota int           Limit the CPU CFS (Completely Fair Scheduler) quota
  -c, --cpu-shares int          CPU shares (relative weight)
      --cpuset-cpus string      CPUs in which to allow execution (0-3, 0,1)
      --cpuset-mems string      MEMs in which to allow execution (0-3, 0,1)
      --disable-content-trust   Skip image verification (default true)
  -f, --file string             Name of the Dockerfile (Default is 'PATH/Dockerfile')
      --force-rm                Always remove intermediate containers
      --iidfile string          Write the image ID to the file
      --isolation string        Container isolation technology
      --label list              Set metadata for an image
  -m, --memory bytes            Memory limit
      --memory-swap bytes       Swap limit equal to memory plus swap: '-1' to enable unlimited swap
      --network string          Set the networking mode for the RUN instructions during build (default "default")
      --no-cache                Do not use cache when building the image
  -o, --output stringArray      Output destination (format: type=local,dest=path)
      --platform string         Set platform if server is multi-platform capable
      --progress string         Set type of progress output (auto, plain, tty). Use plain to show container output (default "auto")
      --pull                    Always attempt to pull a newer version of the image
  -q, --quiet                   Suppress the build output and print image ID on success
      --rm                      Remove intermediate containers after a successful build (default true)
      --secret stringArray      Secret file to expose to the build (only if BuildKit enabled): id=mysecret,src=/local/secret
      --security-opt strings    Security options
      --shm-size bytes          Size of /dev/shm
      --squash                  Squash newly built layers into a single new layer
      --ssh stringArray         SSH agent socket or keys to expose to the build (only if BuildKit enabled) (format: default|<id>[=<socket>|<key>[,<key>]])
      --stream                  Stream attaches to server to negotiate build context
  -t, --tag list                Name and optionally a tag in the 'name:tag' format
      --target string           Set the target build stage to build.
      --ulimit ulimit           Ulimit options (default [])

docker image build --help와 같이 하위 명령의 도움말을 확인할 수 있다. docker build 같은 예전의 축약 명령은 docker image build 명령의 앨리어스로 취급된다.

docker image build - 이미지 빌드

docker image build는 Dockerfile에 기술된 구성을 따라 도커 이미지를 생성하는 명령이다.

docker image build -t 이미지명[:태그명] Dockerfile의_경로

docker image build 명령에는 옵션이 몇가지 있는데, -t 옵션은 이미지명과 태그명을 붙이는 것으로, 실제 사용에서 거의 필수적으로 쓰인다. Dockerfile 경로는 말그대로 Dockerfile이 위치한 디렉토리 경로를 기재하면 된다. build image build 명령에는 반드시 Dockerfile이 필요하므로 그 경로에 Dockerfile이 없다면 명령을 실행할 수 없다. Dockerfile이 현재 작업 디렉토리에 있다면 다음과 같이 실행한다.

$ docker image build -t example/echo:latest .

 

-f 옵션

docker image build 명령은 기본으로 Dockerfile이라는 이름으로 된 Dockerfile을 찾는다. 그 외 파일명으로 된 Dockerfile을 사용하려면 -f 옵션을 사용해야 한다. 예를 들어 Dockerfile-test라는 이름으로된 Dockerfile을 사용하려면 다음과 같이 한다.

$ docker image build -f Dockerfile-test -t example/echo:latest .

 

--pull 옵션

docker image build 명령으로 이미지를 빌드하려면 Dockerfile의 FROM 인스트럭션에 지정한 이미지를 레지스트리에서 내려받은 후, 이를 베이스 이미지로해서 새로운 이미지를 빌드한다. 이렇게 레지스트리에서 받아온 도커 이미지는 일부러 삭제하지 않는 한 호스트 운영 체제에 저장된다. 그러므로 이미지를 빌드할 때 매번 베이스 이미지를 받아오지는 않는다. 그러나 --pull 옵션을 사용하면 매번 베이스 이미지를 강제로 새로 받아온다.

$ docker image build --pull=true -t example/echo:latest .

예를 들어, 베이스 이미지가 latest인 Dockerfile이 있다고 하자. gihydocker/basetest:latest 이미지는 도커 허브에 있다.

FROM gihydocker/basetest:latest

RUN cat /tmp/version

이 Dockerfile을 이용해 docker image build 명령으로 이미지를 빌드하면 Step 2에서 베이스 이미지에 포함된 파일의 내용을 표준출력(stdout)으로 출력한다. 이때 Dockerfile에 다음과 같이 똑같은 RUN 인스트럭션을 하나 추가한다.

FROM gihydocker/basetest:latest

RUN cat /tmp/version
RUN cat /tmp/version

수정한 Dockerfile을 빌드하기 전에, 도커 허브에 있는 gihydocker/basetest:latest가 가리키는 이미지의 /tmp/version 파일 내용이 version=2로 수정되면서 업데이트됐다고 가정한다. 이 상태에서 이미지를 다시 빌드하면 Step 3에서 /tmp/version 파일의 내용이 version = 2로 출력된다. 또 Step 2를 보면 Using cache라고 나오는 것을 봐서 로컬에 미리 받아놓은 이미지를 사용하고 있음을 알 수 있다.

 

이렇듯 로컬에 베이스 이미지 캐시가 있으면 도커는 (Dockerfile에) 변경된 부분만을 반영해 빌드를 시도한다. 이미지를 빌드할때 확실하게 최신 베이스 이미지를 사용하고 싶다면 --pull=true 옵션을 붙여서 빌드하면 된다. --pull=true 옵션은 이런 유용함이 있지만, 도커 허브 등 레지스트리에서 최신 버전이 있는지를 확인하고 빌드를 하기 때문에 빌드 속도 면에서는 조금 불리하다. 실무에서는 latest로 지정하는 것을 피하고 태그로 지정한 베이스 이미지를 사용한다.

docker search - 이미지 검색

도커 허브는 도커 이미지 레지스트리로, 마치 깃허브처럼 사용자나 조직 이름으로 리포지토리를 만들수 있다. 그리고 이 리포지토리를 사용해 도커 이미지를 관리한다.

 

도커 허브에는 모든 이미지의 기반이 되는 운영 체제(CentOS, 우분투 등) 리포지토리, 언어 런타임이나 유명 미들웨어 이미지 등이 관리되는 수많은 리포지토리가 있다. 덕분에 모든 도커 이미지를 직접 만드는 대신 다른 사람이나 조직에서 만들어 둔 이미지를 사용할 수 있다.

 

도커 허브를 활용할 때 빼놓을 수 없는 것이 docker search 명령이다. docker search 명령을 사용하면 도커 허브에 등록된 리포지토리를 검색할 수 있다.

docker search [options] 검색_키워드

예를 들어 mysql을 검색어로 검색해보면 다음과 같은 결과를 볼 수 있다. 

$ docker search --limit 5 mysql
NAME                  DESCRIPTION                                     STARS               OFFICIAL            AUTOMATED
mysql                 MySQL is a widely used, open-source relation…   10003               [OK]
mysql/mysql-server    Optimized MySQL Server Docker images. Create…   731                                     [OK]
mysql/mysql-cluster   Experimental MySQL Cluster Docker images. Cr…   76
bitnami/mysql         Bitnami MySQL Docker Image                      45                                      [OK]
circleci/mysql        MySQL is a widely used, open-source relation…   19

검색 결과 첫번째 나오는 mysql 리포지토리는 리포지토리 이름에 네임스페이스가 생략돼 있는데, 이 리포지토리가 mysql 공식 리포지토리이기 때문이다. 공식 리포지토리의 네임스페이스는 일률적으로 library다. 따라서 이 리포지토리의 정확한 이름도 library/mysql이 된다. 공식 리포지토리의 네임스페이스는 생략할 수 있다.

 

검색 결과는 STARS 순으로 출력된다. 도커 허브에 등록된 리포지토리에도 깃허브처럼 스타 수가 매겨진다. 스타 수는 도커 이미지를 평가하는 주요 지표 중 하나다.

 

docker search 명령으로 지포지토리는 검색할 수 있지만, 이 리포지토리가 관리하는 도커 이미지의 태그가지는 검색할 수 없다. 리포지토리에 공개된 이미지의 태그를 알고싶다면 도커 허브의 해당 리포지토리 페이지에서 Tags를 보는 방법이나 API를 사용하는 방법 중 하나를 사용하면 된다.

docker image pull - 이미지 내려받기

도커 레지스트리에서 도커 이미지를 내려받으면 docker image pull 명령을 사용한다.

docker image pull [options] 리포지토리명[:태그명]

인자로 지정한 리포지토리명과 태그는 도커 허브에 이미 존재하는 것이어야 한다. 예를 들어 jenkins 이미지를 내려받으려면 다음과 같이 한다. 태그명을 생략하면 기본값으로 지정된 태그(대개 latest)가 적용된다.

$ docker image pull jenkins:latest
latest: Pulling from library/jenkins
...
Digest: sha256:eeb4850eb65f2d92500e421b430ed1ec58a7ac909e91f518926e02473904f668
Status: Downloaded newer image for jenkins:latest
docker.io/library/jenkins:latest

docker image pull 명령으로 내려받은 이미지는 그대로 도커 컨테이너를 생성하는데 사용할 수 있다. (docker container run jenkins:latest)

docker image ls - 보유한 도커 이미지 목록 보기

docker image ls 명령은 현재 호스트 운영 체제에 저장된 도커 이미지 목록을 보여준다. 여기서 말하는 호스트 운영체제란 도커 데몬이 동작하는 호스트 환경을 말한다. docker image pull 명령으로 원격 도커 레지스트리에서 내려받은 이미지는 물론이고 docker image build 명령을 실행하며 내려받은 이미지도 호스트 운영 체제에 저장된다.

docker image ls [options] [리포지토리[:태그]]
$ docker image ls
REPOSITORY                           TAG                                              IMAGE ID            CREATED             SIZE
12bme/echo                           latest                                           63a9ca8f99e0        35 hours ago        750MB
...

IMAGE ID는 이미지에 대한 식별자다. 컨테이너를 구분하기 위한 CONTAINER ID와는 별개의 것이니 혼동하지 않아야 한다. 도커 명령은 이미지에 대한 명령과 컨테이너에 대한 명령, 크게 2가지로 나뉜다. 즉, 이미지와 컨테이너는 별도로 관리한다는 뜻이다. 컨테이너 실행 중에는 해당 컨테이너를 만든 이미지를 삭제할 수 없는 등 서로 의존하는 부분이 있지만, 조작 대상으로서는 별개의 것이다. IMAGE ID는 이미지를 관리할때 사용하며, CONTAINER ID는 컨테이너를 관리할때 사용한다.

docker image tag - 이미지에 태그 붙이기

docker image tag는 도커 이미지의 특정 버전에 태그를 붙일때 사용한다.

 

도커 이미지의 버전

도커 이미지에 붙은 태그는 이미지의 특정 버전을 구별하기 위한 것이다. 도커 이미지의 버전은 도커에서 중요한 개념이다. 버전이란 구체적으로 어떤것을 가리킬까?

 

예를 들어 어떤 애플리케이션 example/image를 조금 수정해 이미지를 빌드하는 과정을 여러번 반복하다보면 docker image ls의 결과는 이미지가 생성된 시기에 따라 최신순으로 나열된다. IMAGE ID에 나온 해시값은 이미지마다 다르게 할당된 식별자로, 이미지를 구별하기 위해 사용한다.

 

이 IMAGE ID는 도커 이미지의 버전 넘버 역할을 한다. 애플리케이션을 수정하고 이미지를 빌드하면 매번 다른 이미지가 된다. 다시 말해 원래 같은 이미지였지만, 수정 후에는 다른 IMAGE ID 값이 할당되는 것이다. Dockerfile을 편집했을 때뿐만 아니라 COPY 대상이 되는 파일의 내용이 바뀌어도 IMAGE ID 값이 바뀐다.

 

도커 이미지의 버전이라는 표현이 널리 사용되고 있지만, 엄밀하게 말하면 이미지 ID이다. REPOSITORY와 TAG 컬럼을 보면 최신 이미지가 example/image:latest이고 그 이전의 이미지는 <none>이라고 돼 있다. <none>은 이전에 example/images였던 도커 이미지의 잔재다. 도커에서 태그 하나에 연결될 수 있는 이미지는 하나뿐이다. latest 태그는 최신 이미지에만 붙을수 있다. 그보다 오래된 이미지는 태그가 해제됐기 때문에 <none>이 된다.

 

이미지 ID에 태그 부여하기

도커 이미지 버전의 정체는 이미지 ID이다. 다시 말하면 docker image tag는 이미지 ID에 태그명을 별명으로 붙이는 명령인 것이다. 태그는 특정한 이미지를 쉽게 참조할 수 있도록 붙인 별명에 지나지 않는다. 도커 이미지는 빌드할때마다 다시 생성되는데, 그 내용의 해시값을 이미지 ID로 삼기 때문에 내용이 바뀌면 이미지 ID도 새 값이 부여된다. git의 커밋을 나타내는 해시와 같다고 생각하면 된다.

 

도커 이미지의 태그는 '어떤 특정 이미지 ID를 갖는 도커 이미지를 쉽게 식별하는 것'을 목적으로 한다. 예를 들어 어떤 애플리케이션의 특정 버전을 지원하는 이미지임을 나타내는 릴리즈 번호를 붙여서 이미지를 쉽게 관리하기 위해 사용할 수 있다.

 

도커 이미지에 태그를 부여하려면 docker image tag 명령을 사용한다. 태그를 지정하지 않고 빌드한 이미지는 기본적으로 latest 태그가 부여된다. 내용을 수정하고 차분 빌드를 적용해 다시 이미지를 빌드하면 해시값이 이전 이미지와 달라지고 새 이미지가 latest 태그를 차지한다.

 

latest 태그는 git의 master 브랜치와 같은 의미로, 항상 최신 이미지를 가리키는 태그다. 실제로 도커를 사용할때는 latest의 특정 시점에 버전 넘버 등을 태그로 붙여두고, 이 특정 버전 이미지를 사용하도록 하는 것이 좋다.

docker image tag 기반이미지명[:태그] 새이미지명[:태그]

 

예를 들어 12bme/echo의 latest 이미지에 0.1.0 태그를 부여하려면 다음과 같이 하면 된다.

$ docker image tag 12bme/echo:latest 12bme/echo:0.1.0
$ docker image ls
REPOSITORY                           TAG                                              IMAGE ID            CREATED             SIZE
12bme/echo                           0.1.0                                            63a9ca8f99e0        36 hours ago        750MB
12bme/echo                           latest                                           63a9ca8f99e0        36 hours ago        750MB

docker image push - 이미지를 외부에 공개하기

docker image push 명령은 현재 저장된 도커 이미지를 도커 허브 등의 레지스트리에 등록하기 위해 사용한다.

docker image push [options] 리포지토리명[:태그]

docker iamge tag 명령을 사용해서 example/echo 이미지의 네임스페이스를 먼저 바꿔야 한다. 도커 허브는 자신 혹은 소속 기관이 소유한 리포지토리에만 이미지를 등록할 수 있다. example 부분을 도커 허브 ID와 같이 변경해야 한다.

$ docker image tag example/echo:latest 12bme/echo:latest

그다음 docker image push 명령에 인자로 등록할 이미지를 지정한다. 푸시 도중에는 터미널에 프로그레스 바가 나타나며, 무사히 등록이 완료되면 sha256 해시값이 출력된다.

$ docker image push 12bme/echo:latest

도커 허브를 보면 등록한 이미지의 리포지토리가 생성되게 된다. 이 리포지토리는 공개 리포지토리이므로 누구나 docker image pull 명령으로 이미지를 내려받을 수 있다. 그러므로 공개 리포지토리에 등록할 이미지나 Dockerfile에는 패스워드나 API 키 값 같은 민감한 정보가 포함되지 않도록 주의한다.


03. 도커 컨테이너 다루기

도커 이미지에 이어 도커 컨테이너를 다루는 방법을 알아본다. 도커 컨테이너는 이미지를 바탕으로 만든다. 그러므로 우선 도커 이미지를 다루는 방법에 익숙해져야 한다. 한 걸음 더 나아가 도커 컨테이너를 다루는 방법을 알아본다. 겉에서 본 도커 컨테이너는 가상환경이다. 파일 시스템과 애플리케이션이 함께 담겨있는 박스라고 보면 된다. 도커 명령을 실제로 실행해 보면서 컨테이너가 구체적으로 무엇인지 기본적인 구조를 파악해본다.

도커 컨테이너의 생애 주기

도커 컨테이너가 어떻게 동작하는지를 알려면 먼저 도커 컨테이너의 생애주기를 이해해야 한다. 도커 컨테이너는 실행 중, 정지, 파기의 3가지 상태를 갖는다. 이것을 도커 컨테이너의 생애주기라고 한다. docker container run 명령으로 컨테이너를 최초 실행한 시점의 상태는 실행중이다.

 

각 컨테이너는 같은 이미지로 생성했다고 하더라도 별개의 상태를 갖는다. 이 점이 상태를 갖지 않는 도커 이미지와 컨테이너의 큰 차이의 하나다.

 

실행 중 상태

docker container run 명령의 인자로 지정된 도커 이미지를 기반으로 컨테이너가 생성되면 이 이미지를 생성했던 Dockerfile에 포함된 CMD 및 ENTRYPOINT 인스트럭션에 정의된 애플리케이션이 실행된다. 이 애플리케이션이 실행 중인 상태가 컨테이너의 실행 중 상태가 된다.

 

HTTP 요청을 받는 서버 애플리케이션이면 오류로 인해 종료되지 않는한 실행 중 상태가 지속되므로 실행 기간이 길다. 이에 비해 명령이 바로 실행되고 끝나는 명령행 도구 등의 컨테이너는 실행 중 상태가 길게 유지되지 않는다.

 

실행이 끝나면 정지 상태가 된다.

 

정지 상태

실행 중 상태에 있는 컨테이너를 사용자가 명시적으로 정지(docker container stop 명령을 사용한 경우 등)하거나 컨테이너에서 실행된 애플리케이션이 정상/오류 여부를 막론하고 종료된 경우에는 컨테이너가 자동으로 정지 상태가 된다.

 

컨테이너를 정지시키면 가상 환경으로서는 더이상 동작하지 않지만, 디스크에 컨테이너가 종료되던 시점의 상태가 저장돼 남는다(docker container ls -a 명령으로 정지시킨 것을 포함해 모든 컨테이너 확인 가능). 그러므로 정지시킨 컨테이너를 다시 실행할 수 있다.

 

파기 상태

정지 상태의 컨테이너는 명시적으로 파기하지 않는 이상 디스크에 그대로 남아있다(호스트 운영체제를 종료해도 남아 있다). 컨테이너를 자주 생성하고 정지해야 하는 상황에서는 디스크를 차지하는 용량이 점점 늘어나므로 불필요한 컨테이너를 완전히 삭제하는 것이 바람직하다.

 

한 번 파기한 컨테이너는 다시 실행할 수 없다는 점에 유의해야 한다. 같은 이미지로 새로운 컨테이너를 생성했다고 해도 각 컨테이너가 실행된 시각 등이 서로 다르고 애플리케이션의 처리 결과도 이에 따라 달라질 수 있기 때문에 완전히 같은 컨테이너를 새로 생성할 수는 없다.

docker container run - 컨테이너 생성 및 실행

docker container run 명령은 도커 이미지로부터 컨테이너를 생성하고 실행하는 명령이다. 도커 컨테이너를 실행 중 상태로 만들기 위해 사용한다.

docker container run [options] 이미지명[:태그] [명령] [명령인자...]
docker container run [options] 이미지ID [명령] [명령인자...]

12bme/echo:latest 이미지를 기반으로 컨테이너를 백그라운드에서 실행하려면 다음과 같이 하면 된다.

$ docker container run -d -p 18080:8080 12bme/echo:latest

-p 옵션을 사용해 호스트쪽 포트 18080을 컨테이너 쪽 포트 8080으로 포트 포워딩했으므로 다음과 같이 HTTP 요청을 컨테이너에 전달할 수 있다.

$ curl -XGET http://127.0.0.1:18080/
Hello Docker!!

서버 애플리케이션을 위해 도커를 사용하는 경우가 많으므로 -d나 -p 옵션은 사용할 일이 많을 것이다. 명령해 도구를 사용할 실행 환경으로 도커를 사용한다면 -i나 -t, -rm 옵션을 자주 사용한다.

 

docker container run 명령의 인자

docker container run 명령에 명령 인자를 전달하면 Dockerfile에서 정의했던 CMD 인스트럭션을 오버라이드할 수 있다. 예를 들어 library/alpine:3.7의 CMD 인스트럭션은 /bin/sh로, 셸을 실행한다. 그러나 다음과 같이 이 인스트럭션을 다른 명령으로 오버라이드할 수 있다.

$ docker image pull alpine:3.7
# docker container run -it alpine:3.7 # 셸에 들어감
$ docker container run -it alpine:3.7 uname -a

 

컨테이너에 이름 붙이기

docker container run 명령으로 컨테이너를 실행하고 나서 docker container ls 명령으로 컨테이너 목록을 보면 NAMES 칼럼에 무작위 단어로 지어진 이름을 볼 수 있다.

$ docker container ls
CONTAINER ID        IMAGE                              COMMAND                  CREATED             STATUS              PORTS                     NAMES
0c11cf2b1cb9        12bme/echo:latest                  "go run /echo/main.go"   23 hours ago        Up 23 hours         0.0.0.0:18080->8080/tcp   musing_villani

컨테이너 정지 등 컨테이너를 다루는 명령을 실행할 때는 컨테이너 ID 등으로 컨테이너를 특정해줘야 한다. 그러나 컨테이너 ID와 자동 부여된 컨테이너 이름 모두 컨테이너가 실행되고 나야 알 수 있다. 그러나 개발 업무 중에는 같은 docker 명령을 시행착오로 반복 실행하게 되는 경우가 잦으므로 매번 docker container ls 명령으로 컨테이너 ID 및 컨테이너 이름을 확인하기가 번거롭다.

 

이 문제를 해결할 수 있는 방법이 컨테이너에 이름을 붙이는 기능이다. docker container run 명령에 --name 옵션을 사용하면 컨테이너에 원하는 이름을 붙일 수 있다. 그러면 도커 명령을 사용할 때도 알기 쉬운 이름으로 컨테이너를 특정할 수 있기 때문에 편해진다.

docker container run --name [컨테이너명] [이미지명]:[태그]
$ docker container run -t -d --name gihyo-echo example/echo:latest

이름 붙인 컨테이너는 개발용으로는 비교적 자주 사용되지만, 운영 환경에서는 거의 사용되지 않는다. 같은 이름의 컨테이너를 새로 실행하려면 같은 이름을 갖는 기존의 컨테이너를 먼저 삭제(파기)해야하기 때문이다. 이 때문에 많은 수의 컨테이너를 계속 생성 및 실행하고, 정지시켰다가 파기시키는 과정을 반복하는 운영환경에는 적합하지 않다.

 

도커 명령에서 자주 사용되는 옵션

리눅스 대화식 실행 환경, 즉 명령행 도구로도 도커를 많이 사용한다. 이런 환경에서 docker container run 명령을 사용할때 자주 쓰는 옵션으로 -i, -t, --rm, -v의 4가지를 꼽을 수 있다.

 

-i 옵션은 컨테이너를 실행할 때 컨테이너 쪽 표준 입력과의 연결을 그대로 유지한다. 그러므로 컨테이너 쪽 셸에 들어가는 명령을 실행할 수 있다. 실제 사용에서는 -t 옵션과 함께 사용하는 경우가 많다. -t 옵션은 유사 터미널 기능을 활성화하는 옵션인데, -i 옵션을 사용하지 않으면 유사 터미널을 실행해도 여기에 입력할 수가 없으므로 -i와 -t 옵션을 같이 사용하거나 이들 옵션을 합쳐 축약한 -it 옵션을 사용한다.

 

--rm 옵션은 컨테이너를 종료할 때 컨테이너를 파기하도록 하는 옵션이다. 1번 실행한 후에 더이상 유지할 필요가 없는 명령행 도구 컨테이너를 실행할때 유용하다. -v 옵션은 호스트와 컨테이너 간에 디렉토리나 파일을 공유하기 위해 사용하는 옵션이다.

$ docker container ls -q
16434c5b8993
65fd8e8037ef
e6b73b13da51

docker container ls - 도커 컨테이너 목록 보기

docker container ls 명령은 실행 중이거나 종료된 컨테이너의 목록을 보여주는 명령이다.

docker container ls [options]

docker container ls에서 확인이 가능하도록 다음과 같이 2개의 컨테이너를 실행한다.

$ docker container run -t -d -p 8080 --name echo1 12bme/echo:latest
2d369a792eb9d149bb7beb014dadeedc287efabc5cf2000a5d59f30314a99a2b
$ docker container run -t -d -p 8080 --name echo2 12bme/echo:latest
d46e16cd34820a9834aa4ede9e26baf77c3e0e4751c0915cb30bbb3c7cd75d97

아무 옵션 없이 docker container ls 명령을 실행하면 현재 실행중인 컨테이너의 목록이 출력된다.

$ docker container ls
CONTAINER ID        IMAGE                              COMMAND                  CREATED             STATUS              PORTS                     NAMES
d46e16cd3482        12bme/echo:latest                  "go run /echo/main.go"   3 minutes ago       Up 3 minutes        0.0.0.0:32769->8080/tcp   echo2
2d369a792eb9        12bme/echo:latest                  "go run /echo/main.go"   3 minutes ago       Up 3 minutes        0.0.0.0:32768->8080/tcp   echo1

목록에 표시되는 칼럼 항목의 의미는 다음 표와 같다. docker container ls 명령은 도커 컨테이너를 다룰때 매우 자주 사용하는 명령이다.

항목 내용
CONTAINER ID 컨테이너를 식별하기 위한 유일 식별자
IMAGE 컨테이너를 만드는데 사용된 도커 이미지
COMMAND 컨테이너에서 실행되는 애플리케이션 프로세스
CREATED 컨테이너 생성 후 경과된 시간
STATUS Up(실행 중), Exited(종료) 등 컨테이너의 실행 상태
PORTS 호스트 포트와 컨테이너 포트의 연결 관계(포트 포워딩)
NAMES 컨테이너의 이름

 

컨테이너 ID만 추출하기

docker container ls 명령으로 컨테이너 ID와 생성시 사용한 이미지 등의 정보를 확인할 수 있다. 이때 -q 옵션을 사용하면 컨테이너 ID(축약형)만 추출할 수 있다. 컨테이너를 다루려면 이 컨테이너 ID가 필요하므로 이 명령 역시 자주 사용하게 될것이다.

$ docker container ls -q
d46e16cd3482
2d369a792eb9

 

컨테이너 목록 필터링하기

docker container ls로 특정 조건을 만족하는 컨테이너의 목록을 보려면 --filter 옵션을 사용하면 된다.

docker container ls --filter "필터명=값"

예를 들어, 원하는 이름과 컨테이너명이 일치하는 컨테이너의 목록을 보려면 name 필터를 사용한다.

$ docker container ls --filter "name=echo1"
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                     NAMES
2d369a792eb9        12bme/echo:latest   "go run /echo/main.go"   11 minutes ago      Up 11 minutes       0.0.0.0:32768->8080/tcp   echo1

컨테이너를 생성한 이미지를 기준으로 하려면 ancestor 필터를 사용한다.

$ docker container ls --filter "ancestor=12bme/echo"
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                     NAMES
d46e16cd3482        12bme/echo:latest   "go run /echo/main.go"   15 minutes ago      Up 15 minutes       0.0.0.0:32769->8080/tcp   echo2
2d369a792eb9        12bme/echo:latest   "go run /echo/main.go"   16 minutes ago      Up 16 minutes       0.0.0.0:32768->8080/tcp   echo1

 

종료된 컨테이너 목록 보기

-a 옵션을 사용하면 이미 종료된 컨테이너를 포함한 컨테이너 목록을 볼 수 있다. 종료된 컨테이너가 실행되던 시점의 표준 출력 내용을 확인하거나 컨테이너를 재시작하려는 경우에 사용한다.

$ docker container ls -a

docker container stop - 컨테이너 정지하기

실행 중인 컨테이너를 종료하려면 docker container stop 명령을 사용한다.

docker container stop 컨테이너ID_또는_컨테이너명
$ docker container stop 0c11cf2b1cb9
0c11cf2b1cb9

이름을 붙인 컨테이너라면 다음과 같이 하면 된다.

$ docker container stop echo1
echo1
$ docker container stop echo2
echo2

docker container restart - 컨테이너 재시작하기

파기하지 않은 정지 상태 컨테이너는 docker container restart 명령으로 재시작할 수 있다.

docker container restart 컨테이너ID_컨테이너명

정지해 둔 echo 컨테이너를 다음과 같이 재시작할 수 있다.

$ docker container ls --filter "ancestor=12bme/echo" -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                      PORTS               NAMES
d46e16cd3482        12bme/echo:latest   "go run /echo/main.go"   31 minutes ago      Exited (2) 9 minutes ago                        echo2
2d369a792eb9        12bme/echo:latest   "go run /echo/main.go"   31 minutes ago      Exited (2) 9 minutes ago                        echo1
0c11cf2b1cb9        12bme/echo:latest   "go run /echo/main.go"   24 hours ago        Exited (2) 12 minutes ago                       musing_villani
d7ce09a158d2        12bme/echo:latest   "go run /echo/main.go"   28 hours ago        Exited (2) 26 hours ago                         quizzical_saha
72f2d67d51fc        12bme/echo:latest   "go run /echo/main.go"   39 hours ago        Exited (2) 28 hours ago                         blissful_gagarin
$ docker container restart echo1
echo1

docker container rm - 컨테이너 파기하기

컨테이너를 정지하는 명령은 docker container stop이고, 정지시킨 컨테이너를 완전히 파기하려면 docker container rm 명령을 사용한다.

docker container rm 컨테이너ID_또는_컨테이너명

예를 들어 개발 업무 중 컨테이너 실행 및 정지를 반복하다 보면 다음과 같이 정지된 컨테이너가 여럿 생긴다.

$ docker container ls --filter "status=exited"
CONTAINER ID        IMAGE                COMMAND                  CREATED             STATUS                      PORTS               NAMES
d46e16cd3482        12bme/echo:latest    "go run /echo/main.go"   33 minutes ago      Exited (2) 11 minutes ago                       echo2
0c11cf2b1cb9        12bme/echo:latest    "go run /echo/main.go"   24 hours ago        Exited (2) 14 minutes ago                       musing_villani
d7ce09a158d2        12bme/echo:latest    "go run /echo/main.go"   28 hours ago        Exited (2) 26 hours ago                         quizzical_saha
72f2d67d51fc        12bme/echo:latest    "go run /echo/main.go"   39 hours ago        Exited (2) 28 hours ago                         blissful_gagarin
a50ab894b7ff        0a6a2e32018c         "go run /echo/main.go"   39 hours ago        Exited (2) 39 hours ago                         admiring_franklin
39ec87d4f9e1        f05c5b75dfb6         "go run /echo/main.go"   39 hours ago        Exited (2) 39 hours ago                         distracted_buck
8dc0d0091bc4        11818f01e9fe         "go run /echo/main.go"   39 hours ago        Exited (2) 39 hours ago                         flamboyant_chebyshev
8fa27c20d59d        1768cffb5daf         "go run /echo/main.go"   39 hours ago        Exited (2) 39 hours ago                         ecstatic_solomon
acce59270c92        fb74c092d33b         "go run /echo/main.go"   39 hours ago        Exited (2) 39 hours ago                         sweet_elgamal
c3dfe734594c        example/others       "/bin/ash -c env"        40 hours ago        Exited (0) 40 hours ago                         xenodochial_hellman
32807464f5bd        ch02/golang:latest   "go version"             40 hours ago        Exited (0) 40 hours ago                         dreamy_buck
f9b681934d50        golang:1.10          "go version"             41 hours ago        Exited (0) 41 hours ago                         inspiring_mayer
83dc0ac5cb33        golang:1.10          "bash"                   41 hours ago        Exited (0) 41 hours ago                         thirsty_elbakyan

도커 컨테이너는 정지된 상태에서도 정지 시점의 상태를 유지한 채 디스크에 남아 있다. 그러므로 컨테이너 실행 및 정지를 반복하다 보면 디스크 용량을 점점 많이 차지하게 된다.

 

또 새로 이름 붙인 컨테이너를 생성할 때 같은 이름을 가진 기존 컨테이너가 존재하는 경우 새로운 컨테이너를 생성할 수 없다. 이때는 같은 이름을 가진 기존 컨테이너를 먼저 삭제해야 한다. 다음과 같이 정지 상태인 컨테이너를 디스크에서 파기할 수 있다.

$ docker container rm d46e16cd3482
d46e16cd3482

그러나 현재 실행 중인 컨테이너는 일반적인 docker container rm 명령으로는 삭제할 수 없다. 현재 실행 중인 컨테이너를 삭제하려면 -f 옵션을 사용한다.

$ docker container rm -f 0c11cf2b1cb9

 

docker container run --rm을 사용해 컨테이너를 정지할때 함께 삭제하기

정지된 컨테이너를 디스크에 유지할 필요가 없는 경우가 있다. docker container rm 명령으로 정지된 컨테이너를 나중에 삭제할 수도 있지만, 매번 일일이 컨테이너를 삭제하는 것도 번거로운 일이다.

 

이런 경우에는 docker container run 명령에 --rm 옵션을 붙여준다. 정지된 컨테이너는 보통 디스크에 그대로 남아 있지만, --rm 옵션을 붙여 생성한 컨테이너는 실행이 끝나면 자동으로 파기된다.

 

명령행 도구가 담긴 도커 컨테이너를 사용할때 이 --rm 옵션이 특히 유용하다. 예를 들어 JSON 문자열을 다루는 도구인 jq를 다음과 같이 사용할 수 있다.

$ echo '{"version":100}' | jq '.version'

이를 도커 컨테이너 형태로 사용하려면 다음과 같이 한다. gihyodocker/jq:1.5 이미지는 jq 명령을 포함한 도커 이미지로, docker container run 명령에 인자로 jq를 추가해 명령행 도구를 사용할 수 있다.

$ echo '{"version":100}' | docker container run -i --rm gihyodocker/jq:1.5 '.version'
100

이렇듯 명령행 도구를 담은 컨테이너는 도구의 실해이 끝나면 더는 디스크에 유지할 필요가 없다. 그러므로 --rm 옵션을 사용해 바로 파기하는 것이 좋다.

 

또 --rm 옵션은 컨테이너에 이름을 붙이는 --name 옵션과 함께 사용하는 경우가 많다. 이름을 붙인 컨테이너를 정지시킨 상태에서 같은 이름으로 컨테이너를 실행하려고 하면 이름이 충돌해 오류가 발생한다. 이런 오류를 피하려면 다른 이름을 붙여 컨테이너를 실행하던지, 같은 이름의 기존 컨테이너를 먼저 삭제해야 한다. 그러므로 이름이 붙은 컨테이너를 자주 생성하고 정지해야 한다면 --rm 옵션을 사용하는 것이 편리하다.

docker container logs - 표준 출력 연결하기

docker container logs 명령을 사용하면 현재 실행 중인 특정 도커 컨테이너의 표준 출력 내용을 확인할 수 있다. 컨테이너의 출력 내용 중 표준 출력으로 출력된 내용만 확인할 수 있으므로 파일 등에 출력된 로그는 볼 수 없다.

 

일반적으로 도커 컨테이너의 로그라고 하면 컨테이너의 표준 출력으로 출력된 내용을 가리킨다.

docker container logs [options] 컨테이너ID_또는_컨테이너명

-f 옵션을 사용하면 새로 출력되는 표준 출력 내용을 계속 보여준다 (tail -f 명령과 비슷하게 동작한다)

docker container exec - 실행 중인 컨테이너에서 명령 실행하기

docker container exec 명령을 사용하면 실행 중인 컨테이너에서 원하는 명령을 실행할 수 있다. 명령을 실행하려는 컨테이너의 컨테이너 ID나 컨테이너명을 인자로 지정한 다음, 그 뒤에 다시 실행할 명령을 인자로 추가한다.

docker container exec [options] 컨테이너ID_또는_컨테이너명 컨테이너에서_실행할_명령

시험 삼아 컨테이너에서 pwd 명령을 실행해본다. 이 컨테이너의 작업 디렉토리는 /go 디렉토리이므로 표준 출력으로 /go 가 출력될 것이다.

$ docker container exec echo1 pwd
/go

docker container exec 명령을 사용하면 마치 컨테이너에 ssh로 로그인한 것처럼 컨테이너 내부를 조작할 수 있다. 컨테이너 안에서 실행할 셸(sh나 bash)을 실행하면 마찬가지 결과를 얻을 수 있기 때문이다. 표준 입력 연결을 유지하는 -i 옵션과 유사 터미널을 할당하는 -t 옵션을 조합하면 컨테이너를 셸을 통해 다룰 수 있다. 이런 용도로 컨테이너를 사용하고 싶다면 무조건 -it 옵션을 붙인다.

$ docker container exec -it echo1 sh
# pwd
/go

docker container exec 명령은 이렇게 컨테이너 내부의 상태를 확인하거나 디버깅하는 용도로 사용할 수 있다. 다만, 컨테이너 안에 든 파일을 수정하는 것은 애플리케이션에 의도하지 않은 부작용을 초래할 수 있으므로 운영환경에서는 절대 해서는 안된다.

docker container cp - 파일 복사하기

docker container cp 명령은 컨테이너끼리 혹은 컨테이너와 호스트 간에 파일을 복사하기 위한 명령이다. Dockerfile에 포함된 COPY 인스트럭션은 이미지를 빌드할때 호스트에서 복사해 올 파일을 정의하기 위한 것이고, docker container cp 명령은 실행 중인 컨테이너와 파일을 주고받기 위한 명령이다.

docker container cp [options] 컨테이너ID_또는_컨테이너명:원본파일 대상파일
docker container cp [options 호스트_원본파일 컨테이너ID_또는_컨테이너명:대상파일]

컨테이너 안에 있는 /echo/main.go 파일을 호스트의 현재 작업 디렉토리로 복사하려면 다음과 같이 하면된다.

$ docker container cp echo1:/echo/main.go .

반대로 호스트 쪽에서 컨테이너로 파일을 복사하려면 다음과 같이 한다.

$ touch dummy.txt
$ docker container cp dummy.txt echo1:/tmp
$ docker container exec echo1 ls /tmp | grep dummy
dummy.txt

docker container cp 명령은 디버깅 중 컨테이너 안에서 생성된 파일을 호스트로 옮겨 확인할 목적으로 사용하는 경우가 대부분이다. 또한 아직 파기되지 않은 정지 상태의 컨테이너에 대해서도 실행할 수 있다.


04. 운영과 관리를 위한 명령

마지막으로 도커를 운영하고 관리하기 위한 명령을 알아본다.

prune - 컨테이너 및 이미지 파기

docker container prune

도커를 오랜 기간 사용하다 보면 디스크에 저장된 컨테이너와 이미지가 점점 늘어나게 마련이다. 이런 경우에 prune 명령을 사용해 필요없는 이미지나 컨테이너를 일괄 삭제할 수 있다.

 

docker container prune 명령은 실행 중이 아닌 모든 컨테이너를 삭제하는 명령이다.

 

docker container ls -a 명령으로 정지 중인 것을 포함해 모든 컨테이너 목록을 볼 수 있다. 정지된 컨테이너도 디스크에 남아 있기 때문에 종료한 컨테이너의 로그를 확인하거나 docker container restart 명령으로 컨테이너를 다시 시작할 수 있다. 테스트 등의 업무에 이런 특징이 유용하기도 하지만, 정지시킨 대부분의 컨테이너는 그리 쓸모가 없다. 정기적으로 이들을 삭제하는 것이 좋다.

docker container prune [options]

재차 확인을 요구하므로 y를 입력해 동의하면 정지된 모든 컨테이너가 삭제된다.

 

docker image prune

이미지도 컨테이너와 마찬가지로 사용하지 않는 것이 점차 누적된다. 그러나 디스크 용량을 너무 차지하지 않도록 정기적으로 삭제한다. docker image prune 명령은 태그가 붙지 않은(dangling) 모든 이미지를 삭제한다.

docker image prune [options]
$ docker image prune
WARNING! This will remove all dangling images.
Are you sure you want to continue? [y/N] y
Deleted Images:
deleted: sha256:1768cffb5daf22ed312e0092b77f7792bb629220cff807564db91844786a26c1
deleted: sha256:0efd9557572efe879ea5205bc5e3d769610aeccb51f9667d90ae64ebce035432
deleted: sha256:e3e9abdaa38bc311e7e6e73d431ece1f41a2c18b8f5c55def0297ee1077be444
...

Total reclaimed space: 177.3MB

이미지 일괄 삭제 후 docker image ls 명령으로 확인해보면 아직 남은 이미지가 있다. 이 이미지는 실행 중인 컨테이너의 이미지 등 이유가 있어 도커가 남겨 놓은 것이다.

 

docker system prune

사용하지 않는 도커 이미지 및 컨테이너, 볼륨, 네트워크 등 모든 도커 리소스를 일괄적으로 삭제하고 싶다면 docker system prune 명령을 사용한다.

$ docker system prune
WARNING! This will remove:
  - all stopped containers
  - all networks not used by at least one container
  - all dangling images
  - all dangling build cache

Are you sure you want to continue? [y/N] y
Deleted Containers:
039f9bc4b9e554e3e2a500467d668eec9fc3f0acef156928e69e97de213d5dd3
c29273dbbf271ef04e3ab8717457e094efd06b95561e84f52218daabea1e7bff
ac523045d38be0dddc994e950a23eae7a34259bdb0dec78f3a898a6b6bb6028f

Total reclaimed space: 2.05kB

docker container stats - 사용 현황 확인하기

시스템 리소스 사용 현황을 컨테이너 단위로 확인하려면 docker container stats 명령을 사용한다. 유닉스 계열 운영 체제의 top 명령과 같은 역할을 한다고 보면 된다. (docker top 명령으로 컨테이너에서 실행 중인 프로세스를 확인할 수 있다. 이 명령과 혼동하지 않도록 주의하도록 한다)

docker container stats [options] [대상_컨테이너ID ...]

05. 도커 컴포즈로 여러 컨테이너 실행하기

도커 이미지를 만들거나 도커 허브에서 내려받은 이미지로 도커 컨테이너를 실행하고 컨테이너의 포트를 호스트 머신 포트로 포트 포워딩하는 방법 등이 존재한다. 이는 모두 중요한 기초 지식이다. 그러나 기본적인 사용법만으로는 실용적인 시스템을 구축하는 데 도커를 어떻게 사용해야 할지 감이 잘 오지 않을것이다.

 

시스템은 일반적으로 단일 애플리케이션이나 미들웨어만으로 구성되는 것이 아니다. 웹 애플리케이션은 리버스 프록시 역할을 하는 웹서버를 프론트엔드에 배치하고 그 뒤로 비즈니스 로직이 담긴 애플리케이션 서버가 위치해 데이터 스토어 등과 통신하는 구조로 완성된다. 여러 애플리케이션 간의 연동 및 통신, 그리고 이들 간의 의존관계를 통해 하나의 시스템이 구성되는 것이다.

 

도커는 애플리케이션 배포에 특화된 컨테이너다. 도커 컨테이너 = 단일 애플리케이션이라고 봐도 무방하다. 가상 서버와는 대상 단위 크기(granularity) 자체가 다르다.

 

애플리케이션 간의 연동 없이는 실용적 수준의 시스템을 구축할 수 없다. 다시 말하면, 도커 컨테이너로 시스템을 구축하면 하나 이상의 컨테이너가 서로 통신하며, 그 사이에 의존관계가 생긴다.

 

이런 방식으로 시스템을 구축하다 보면 단일 컨테이너를 다룰 때는 문제가 되지 않던 부분에도 주의가 필요하다. 컨테이너의 동작을 제어하기 위한 설정 파일이나 환경 변수를 어떻게 전달할지, 컨테이너 간의 의존관계를 고려할때 포트 포워딩을 어떻게 설정해야 하는지 등의 요소를 적절히 관리해야 한다.

 

도커를 사용해서 이런 시스템을 만들려면 기본 사용법만 알아서는 부족할 것이다.

docker-compose 명령으로 컨테이너 실행하기

이때 필요한 것이 도커 컴포즈(Docker Compose)다. Compose는 yaml 포맷으로 기술된 설정 파일로, 여러 컨테이너의 실행을 한 번에 관리할 수 있게 해준다. 윈도우용/macOS용 도커가 로컬 환경에 설치돼 있다면 docker-compose 명령을 바로 사용할 수 있다.

$ docker-compose version
docker-compose version 1.27.2, build 18f557f9
docker-py version: 4.3.1
CPython version: 3.7.7
OpenSSL version: OpenSSL 1.1.1g  21 Apr 2020

먼저 컨테이너 하나를 실행해본다. 그리고 같은 작업을 docker-compose를 사용해 다시 수행할 것이다. 임의의 디렉토리에서 docker-compose.yml라는 파일명으로 다음과 같은 내용을 작성한다. 이제 이 설정 파일로 위의 명령과 같은 내용을 수행할 수 있다. 파일 맨 앞의 version: "3" 부분은 이 docker-compose.yml 파일의 내용을 해석하는데 필요한 문법 버전(Version 3은 문법 정의 중 안정 버전이다)을 선언한 것이다. 여기서 만든 12bme/echo:latest 이미지를 도커 컴포즈에서 사용해본다.

version: "3"
services:
  echo:
    image: 12bme/echo:latest
    ports:
      - 9000:8080

이제 docker-compose.yml 파일의 내용을 살펴본다. services 요소 아래의 echo는 컨테이너 이름으로, 그 아래에 다시 어떤 이미지를 실행할지가 정의된다. image 요소는 도커 이미지, ports는 포트포워딩 설정을 지정한다.

 

이 파일을 이용해 도커 컨테이너를 실행해 보겠다. docker-compose.yml 파일이 위치한 디렉토리에서 이 정의에 따라 여러 컨테이너를 한꺼번에 시작하려면 docker-compose up 명령을 사용하면 된다

$ docker-compose up -d
Creating network "ch02_dockercompose_default" with the default driver
Creating ch02_dockercompose_echo_1 ... done

이번에는 컨테이너를 정지해 보겠다. docker-compose down 명령을 사용하면 docker-compose.yml 파일에 정의된 모든 컨테이너가 정지 혹은 삭제된다. 정지할 컨테이너의 ID를 일일이 지정해야 하는 docker container stop 명령보다 훨씬 간단하다.

$ docker-compose down
Stopping ch02_dockercompose_echo_1 ... done
Removing ch02_dockercompose_echo_1 ... done
Removing network ch02_dockercompose_default

컴포즈를 사용하면 이미 존재하는 도커 이미지뿐만 아니라 docker-compose up 명령을 실행하면서 이미지를 함께 빌드해 새로 생성한 이미지를 실행할 수도 있다. 12bme/echo 이미지를 생성했던 echo 디렉토리에서 docker-compose.yml 파일을 다음과 같이 작성한다. 작성후의 디렉토리 상태는 다음과 같을 것이다.

$ ls
Dockerfile         docker-compose.yml main.go

이번에는 docker-compose.yml 파일에서 image 속성을 지정하는 대신, build 속성에 Dockerfile이 위치한 상대 경로를 지정했다. 같은 디렉토리에 Dockerfile 파일이 위치하고 있으니 .(현재 작업 디렉토리)로 지정하면 된다.

version: "3"
services:
  echo:
    build: .
    ports:
      - 9000:8080

다시 docker-compose up 명령을 실행한다. 이미 컴포즈가 이미지를 빌드한 적이 있다면 빌드를 생략하고 컨테이너가 실행되지만, --build 옵션을 사용하면 docker-compose up 명령에서도 도커 이미지를 강제로 다시 빌드하게 할 수 있다. 개발 과정에서 이미지가 자주 수정되는 경우에는 --build 옵션을 사용하는 것이 좋다.

$ docker-compose up -d --build
Creating network "docker01_default" with the default driver
Building echo
Step 1/4 : FROM golang:1.9
 ---> ef89ef5c42a9
Step 2/4 : RUN mkdir /echo
 ---> Using cache
 ---> 047fee1f9648
Step 3/4 : COPY main.go /echo
 ---> Using cache
 ---> 622abdaf93cb
Step 4/4 : CMD ["go", "run", "/echo/main.go"]
 ---> Using cache
 ---> 8bd7b58bbbd0

Successfully built 8bd7b58bbbd0
Successfully tagged docker01_echo:latest
Creating docker01_echo_1 ... done

06. 컴포즈로 여러 컨테이너 실행하기

docker-compose.yml 파일로 작성하면 기존 docker 명령을 사용해 컨테이너를 실행할 때 매번 부여하던 옵션을 설정 파일로 관리할 수 있다. 이것만으로도 충분히 유용하지만, 컴포즈를 사용한 구성 관리 기능의 진가는 여러 컨테이너를 실행할 때 발휘된다.

 

컴포즈를 사용해 여러 컨테이너를 실행하기 위해 필요한 기본 요소를 파악하기 위해 젠킨스(Jenkins)를 예제 삼아 컴포즈로 실행해보자.

젠킨스 컨테이너 실행하기

다음과 같이 docker-compose.yml 파일을 작성한다.

version: "3"
services:
  master:
    container_name: master
    image: jenkinsci/jenkins:2.142-slim
    ports:
      - 8080:8080
    volumes:
      - ./jenkins_home:/var/jenkins_home

젠킨스 이미지는 도커 허브에 올라와 있는 것을 이용한다.

 

volumes라는 설정 항목이 처음 등장했다. volumes 항목은 호스트와 컨테이너 사이에 파일을 복사하는 것이 아니라 파일을 공유할 수 있는 메커니즘이다. Dockerfile의 COPY 인스트럭션이나 docker container cp 명령은 로스트와 컨테이너 사이에 파일을 복사하는 기능이었지만, volumes는 공유라는 점에서 차이가 있다. 젠킨스 컨테이너를 호스트 쪽에서 편하게 다룰수 있도록 docker-compose.yml 파일에 volumes를 정의해 호스트 쪽 현재 작업 디렉토리 바로 아래에 jenkins_home 디렉토리를 젠킨스 컨테이너의 /var/jenkins_home에 마운트한다.

 

이제 컴포즈를 실행한다. -d 옵션을 사용하지 않고 포어그라운드로 컨테이너를 실행하면 Jenkins initial setup is required라는 메시지가 표준 출력으로 출력된 다음, 초기 설정에서 패스워드가 생성되는데 이 패스워드를 잘 복사해 놓는다.

 

젠킨스 공식 이미지에서는 /var/jenkins_home 아래에 데이터가 저장된다. 그러므로 컴포즈로 실행한 젠킨스 컨테이너를 종료했다가 재시작해도 초기 설정이 유지된다.

마스터 젠킨스 용 SSH 키 생성

젠킨스를 운영할때 단일 서버로 운영하는 경우는 그리 흔치 않다. 관리 기능이나 작업 실행 지시 등은 마스터 인스턴스가 맡고, 작업을 실제로 진행하는 것은 슬레이브 인스턴스가 담당한다. 이러한 구성을 컴포즈로 만들수 있다.

 

먼저 준비 작업으로 마스터가 슬레이브에 접속할 수 있도록 마스터 컨테이너에 SSH 키를 생성한다. 나중에 마스터와 슬레이브와 소통할 수 있으려면 이 키가 반드시 필요하므로 꼭 만들어야 한다. 지금 실행 중인 첫번째 컨테이너가 마스터 젠킨스 역할을 할것이다.

$ docker container exec -it master ssh-keygen -t rsa -C ""

지금 만든 /var/jenkins_home/.ssh/id_rsa.pub 파일은 마스터 젠킨스가 슬레이브 젠킨스에 접속할때 사용할 키다. 이 키를 슬레이브를 추가할때 설정할 것이다.

슬레이브 젠킨스 컨테이너 생성

슬레이브 인스턴스 역할을 할 젠킨스 컨테이너를 추가한다. 마스터 컨테이너는 master, 슬레이브 컨테이너는 slave01로 각각 이름을 붙인다.

version: "3"
services:
  master:
    container_name: master
    image: jenkinsci/jenkins:2.142-slim
    ports:
      - 8080:8080
    volumes:
      - ./jenkins_home:/var/jenkins_home
    links:
      - slave01

slave01:
  container_name: slave01
  image: jenkinsci/ssh-slave
  environment:
    - JENKINS_SLAVE_SSH_PUBKEY=ssh-rsa AAAB...

 

SSH 접속 허용 설정

새로 추가하는 슬레이브 컨테이너를 만들때는 SSH로 접속하는 슬레이브 용도로 구성된 도커 이미지 jenkinsci/ssh-slave를 사용한다. jenkinsci/ssh-slave 이미지에 다시 환경변수 JENKINS_SLAVE_SSH_PUBKEY를 설정하는데, SSH로 접속하는 상대가 이 키를 보고 마스터 젠킨스임을 식별하게 된다. 이 환경 변수는 호스트 파일 시스템의 ./jenkins_home/.ssh/id_rsa.pub의 내용을 그대로 붙여 넣으면 된다. 슬레이브 컨테이너 안에서 키를 받아오거나 설정해서는 안되며, 외부 환경 변수로 받아오게 해야 한다.

 

SSH 접속 대상 설정

슬레이브 컨테이너의 기본 준비는 끝났다. 그러나 아직 마스터 컨테이너가 어떻게 슬레이브 컨테이너를 찾아 추가할 것인가 하는 문제가 남아있다. IP 주소를 찾아 설정하는 방법도 있지만, 컴포즈를 사용하면 좀 더 깔끔하게 이 문제를 해결할 수 있다. links 요소를 사용해 다른 services 그룹에 해당하는 다른 컨테이너와 통신하면 된다.

 

여기서는 master에 slave01에 대한 links를 설정했다. 이것으로 master에서 slave01이라는 이름으로 슬레이브 컨테이너를 찾아갈 수 있다.

 

컨테이너 간의 관계 정리 및 준비 완료

새로운 요소가 많이 나오므로 지금까지 진행한 과정을 다시 되짚어 본다.

  - 마스터 컨테이너를 먼저 생성한 다음, 마스터의 SSH 공개키를 생성

  - docker-compose.yml 파일에 슬레이브 컨테이너를 추가하고, 앞에서 만든 마스터의 SSH 공개키를 환경 변수 JENKINS_SLAVE_SSH_PUBKEY에 설정

  - links 요소를 사용해 마스터 컨테이너가 슬레이브 컨테이너로 통신할수 있게 설정

 

이제 마스터/슬레이브를 구성하는 젠킨스의 docker-compose.yml 파일 작성이 끝났으니 이를 실행해본다. master와 slave01이라는 이름이 붙은 컨테이너가 실행됐음을 알수 있다.

$ docker-compose up -d
$ docker-compose ps
 Name                Command               State                 Ports
-------------------------------------------------------------------------------------
master    /sbin/tini -- /usr/local/b ...   Up      50000/tcp, 0.0.0.0:38080->8080/tcp
slave01   setup-sshd                       Up      22/tcp

 

더 나은 컨테이너 개발

컴포즈를 이용해 마스터/슬레이브로 구성된 젠킨스를 구축했다. 그러나 이상적인 도커 구성 관리는 애플리케이션을 바로 이용할 수 있는 수준까지 수작업 없이 애플리케이션을 배포하는 것이 목표다. 이번에 살펴본 젠킨스 예제는 컨테이너나 컴포즈로 가능한 설정만으로는 모든 구성을 갖출 수 없었으나, 직접 개발한 애플리케이션에 대한 구성 관리를 할때는 애플리케이션 제어 및 컨테이너 간의 연동을 설정하는 것만으로 충분하도록 애플리케이션 및 도커 이미지, 컨테이너를 구성해야 한다.