[Docker Mastery] - 5

데이터 관리 및 볼륨

시작

컨테이너의 데이터

이미지는 읽기 전용입니다. 변경사항이 있다면 다시 빌드해야 할겁니다. 실행 중인 애플리케이션은 실행 중엔 변경하지 않죠. 그렇다면 애플리케이션을 수행하면서 로그 데이터등은 어떻게 확인할까요? 이때 Volume설정이 필요합니다.

볼륨 설정을 하지 않는다면 컨테이너 종료와 동시에 데이터도 삭제됩니다. 볼륨 설정은 호스트의 공간과 컨테이너로 실행되는 애플리케이션 내부 공간을 동기화 합니다. 그 안에서 데이터를 읽고 쓰기가 가능하죠. 즉, 이미지는 읽기 전용이지만 컨테이너의 경우 읽고 쓰기가 가능합니다.

컨테이너의 변경 사항을 추적 파생 시키고 변경 사항을 결합시킵니다.

컨테이너에서 파일을 추가할 수도 변경할 수도 있게됩니다. 컨테이너가 중지되더라도 공간은 동기화되어 데이터는 공유 공간에 남아있게 됩니다.

우선 간단하게 제목과 설명을 입력받아 특정 폴더에 파일로 저장해주는 API가 있다고 가정하겠습니다. 도커라이징을 시작해봅시다.

yaml
FROM node:14

WORKDIR /app

# COPY package.json /app
COPY package.json .

# 종속성 설치
RUN npm install

# 나머지 코드들도 복사
COPY . .

# 호스트에 80으로 노출
EXPOSE 80

# 컨테이너로 실행시 작동
CMD [ "node", "server.js" ]

빌드해봅시다.

bash
docker build -f Dockerfile -t feedback-node . --no-cache

컨테이너를 실행합니다.

bash
# 컨테이너 내부 포트 80을 호스트의 3000 포트로 매핑
docker run -p 3000:80 -d --name feedback-app feedback-node 

이제 API를 호출해 텍스트를 불러와 봅시다. 하지만 이 파일을 확인할 방법은 컨테이너 내부로 접속해 볼 수 밖에 없습니다. 잠시 중단해봅시다.

bash
docker stop feedback-app

만일 docker run당시 --rm옵션을 사용했다면 중단과 동시에 컨테이너도 제거됐을겁니다. 현재는 남이있으므로 다시 시작하면 이전 데이터는 그대로 컨테이너에 남아있게 됩니다.

하지만 컨테이너를 제거하면 데이터는 삭제됩니다.

볼륨

볼륨은 호스트 머신의 폴더입니다. 도커 컨테이너 내부의 폴더와 매핑하여 사용하게 됩니다. 두 폴더의 변경사항은 동기화되며 컨테이너의 변경사항을 외부에서도 확인할 수 있죠.

볼륨을 추가하는 가장 쉬운 방법은 DockerfileVOLUME레이어를 추가하는겁니다.

yaml
FROM node:14

WORKDIR /app

COPY package.json .

RUN npm install

COPY . .

EXPOSE 80

# 컨테이너 내부 /app/feedback
VOLUME [ "/app/feedback" ]

CMD [ "node", "server.js" ]

빌드합니다.

bash
docker build -t feedback-node:volumes .

Anonymous Volumes방식

이번에는 --rm옵션을 이용해 컨테이너 중지시 볼륨 공간이 남아있는지 확인해봅시다.

bash
docker run -d -p 3000:80 --rm --name feedback-app feedback-node:volume

아직은 정상적으로 작동하지 않습니다. 이번에는 중지 이후 다시 시작해도 파일이 존재하지 않을 겁니다. 왜냐하면 이 방식은 Anonymous Volumes방식입니다. 이미지에 익명의 볼륨을 추가하고 컨테이너에 이 볼륨에 데이터를 저장하기 때문이죠.

이와 다른 Named Volumes 있습니다. 두 경우 모두 도커는 일부 폴더와 경로를 호스트 머신에 설정합니다. 단, Anonymous Volumes의 경우 경로를 알지 못합니다.

볼륨 공간은 다음 명령을 통해 조회할 수 있습니다.

bash
docker volume ls
DRIVER    VOLUME NAME
local     9e5ae86e585d164d352e1f19ed93054709edc9fdas952ce969f6256ad7e2aec8195efc

볼륨 네임이 무작위로 정해집니다. 이후 컨테이너를 중지하고 다시 명령을 수행하면 볼륨이 조회되지 않을겁니다. 컨테이너는 --rm옵션을 사용했기 때문에 삭제됩니다. 그런데 볼륨도 삭제됩니다. 성납되지 않는 조건이죠. 이는 Anonymous Volumes가 호스트가 아닌 도커에서 관리되기 때문입니다.

컨테이너에 정의된 경로는 생성된 어떤 볼륨에 매핑되고 호스트 머신 상에 생성된 경로로 연결됩니다. 컨테이너의 /app/feedback 공간은 로컬 컴퓨터의 특정 공간과 매핑됩니다. 하지만 그 공간은 도커에서 관리하므로 정확한 경로는 알 수 없죠. 또한 찾더라도 접근이 불가능합니다.

Named Volumes방식

명명된 볼륨 방식을 사용하면 위 문제를 해결할 수 있습니다. 로컬의 특정 경로를 명명하여 영구적으로 데이터를 저장할 수 있게됩니다. 관리 주체 또한 도커가 아닌 주인에게 주어집니다.

다시 도커라이징합니다. 익명 볼륨을 제거합니다.

yaml
FROM node:14

WORKDIR /app

COPY package.json .

RUN npm install

COPY . .

EXPOSE 80

CMD [ "node", "server.js" ]

이제 다시 빌드하고 실행해줍니다. 기존에 있던 컨테이너는 중지 및 제거하고 이미지도 제거해줍니다.

이제 run에 옵션을 추가해 명명된 볼륨 방식을 적용해봅시다.

bash
# 로컬의 feedback과 컨테이너의 /app/feedback 경로를 동기화한다.
docker run -d -p 3000:80 --rm --name feedback-app -v feedback:/app/feedback feedback-node:volumes

이제 API를 호출해 파일을 생서하고 다시 컨테이너를 중지해봅시다. 이후 볼륨을 조회해봅시다.

bash
docker volume ls
DRIVER    VOLUME NAME
local     feedback

이번에는 컨테이너를 종료했고 제거했음에도 볼륨이 남아있습니다. 이후 컨테이너를 실행해도 볼륨내 데이터는 남아있게 됩니다.

참고로 익명 볼륨의 경우 run당시 --rm옵션을 부여하지 않았다면 이후 docker rm명령으로 컨테이너를 제거해도 볼륨을 남아있게 됩니다. 이후 컨테이너를 시작하면 익명의 볼륨이 추가로 더 생성되기 때문에 공간을 차지하게 됩니다. docker volume rm 볼륨네임 혹은 docker volume prune명령으로 익명 볼륨을 제거해줘야 합니다.

바인드 마운트

이제까지 이미지를 빌드하게 되면 당시 스냅샷으로만 빌드되기 때문에 코드의 변경은 이루어질 수 없다고 전달했습니다. 하지만 개발 과정에서 이 과정은 수행하기 매우 힘들겁니다. 코드 변경사항이 일어날 때마다 이미지를 다시 빌드하고 컨테이너로 올려야하는 번거로운 작업이 있기 때문이죠.
이때 도움을 주는 기능이 바인드 마운트입니다. 볼륨과 비슷한 점이 있습니다. 볼륨의 경우 호스트에 데이터를 저장하긴 하지만 그 경로는 알지 못합니다. 하지만 바인드 마운트의 경우 해당 경로를 알 수 있습니다. 즉 해당 경로에 가서 컨테이너에 저장된 데이터를 읽을 수 있는거죠.
또한 쓰기도 가능하기 때문에 컨테이너에서 이를 읽어 빌드 과정없이도 반영할 수 있게됩니다. 다만 이는 개발에 특화된것이지 운영에 있어서는 그다지 좋은 방법은 아닙니다.

요약해서 바인드 마운트는 영구적이고 편집이 가능하죠 이미지가 아닌 컨테이너에만 영향이 갑니다.

명령을 수행하기전 호스트의 지정 폴더 경로에 도커가 액세스할 수 있는지 확인해야 합니다. 도커 기본 설정-리소스-파일 공유에 호스트의 지정 폴더 혹은 상위 폴더가 설정되어 있는지 확인이 필요합니다.

이제 다시 컨테이너를 실행합니다.

bash
# /Users/wooglim/dev/feedback-node:/app 호스트 경로는 절대경로여야 함.
docker run -d -p 3000:80 --rm --name feedback-app -v feedback:/app/feedback -v "/Users/wooglim/dev/feedback-node:/app" feedback-node:volumes

단 위 명령은 실행 직후 에러가 발생할 겁니다. 바인드 마운트의 경우 로컬 경로의 폴더를 컨테이너 지정 경로에 덮어씌우게 됩니다. 즉, node_module등의 폴더가 없다면 그대로 덮어씌어 없어지겠죠. 이는 곧 이미지 빌드시 도커라이징 과정이 무의미 해졌다는것을 의미합니다.

이미지 기반으로 컨테이너를 생성했지만 이후 로컬의 경로로 모두 덮어씌어지게 되는 문제가 발생한거죠.

볼륨 이해하기

-v 옵션을 이용해 볼륨, 바인드 마운트가 가능합니다. 컨테이너 내부 어떤 폴더가 호스트의 특정 폴더와 마운트 혹은 연결됩니다. 컨테이너 내부에 이미 파일이 있는 경우, 외부 볼륨에 데이터가 있는 경우(볼륨) 컨테이너 내부에 데이터가 존재하지 않는 경우 볼륨에서 데이터를 찾습니다.

아직 내부에 파일이 없어 파일을 로드하게되는데, 이를 위해 바인드 마운트를 사용합니다.

도커는 호스트의 로컬 파일은 덮어쓰지 않습니다. 대신 호스트 지정 폴더의 데이터를 컨테이너에 덮어씌웁니다.

이전으로 문제로 돌아가보면 로컬의 빈 폴더로 덮어씌어지기 때문에 node 애플리케이션을 실행하는데 필요한 node_module도 사라지게 됩니다. 이제 run옵션에 익명 볼륨을 할당해 봅시다. 이전과 같은 문제가 발생해도 익명 볼륨에 해당 데이터가 존재하므로 똑같은 에러는 발생하지 않을겁니다.

bash
docker run -d -p 3000:80 --rm --name feedback-app -v feedback:/app/feedback -v "/Users/wooglim/dev/dockerPractice/1/feedback-node:/app" -v /app/node_modules feedback-node:volumes

익명 볼륨의 데이터는 컨테이너로 실행되기 전 지정 폴더의 데이터를 익명 볼륨으로 로컬에 저장하게 됩니다.

컨테이너를 중지 및 제거 하고 다시 실행하면 앱은 정상적으로 실행될것입니다.

이제 추가된 바인드 마운트로 인해 코드 변경 사항이 즉시 컨테이너에도 반영됩니다. 만일 서버 파일을 실시간으로 감지하는 라이브러리(nodemon)등을 이용하지 않는다면 컨테이너를 중지하고 재실행 해줘야 합니다.

윈도우의 경우 파일시스템 환경이 다르기 때문에 잘 적용이 되지 않을수도 있습니다.

추가로 바인드 마운트로 지정된 로컬 폴더는 도커로 제어할 수 없습니다. 삭제할 경우 직접 삭제해야만 합니다.

익명 볼륨의 경우 외부 경로보다 컨테이너 내부 경로의 우선 순위를 두어 데이터를 저장하고자 할때 유용하게 사용할 수 있습니다.

읽기 전용 볼륨

바인드 마운트의 경우 변경사항이 컨테이너 내부에 반영됩니다. 호스트에서 변경한 파일은 컨테이너에서도 확인할 수 있는거죠. 반대는 어떨까요? 컨테이너에서 파일을 수정하는 경우 호스트의 데이터도 변경됩니다. 이 경우 읽기 전용으로만 하여 컨테이너를 수정을 시도해도 호스트의 데이터는 변경되지 않도록 하는 옵션이 있습니다. read only를 의미하는 ro를 바인드 마운트 끝에 붙여주면 됩니다.

bash
docker run ... -v "/Users/wooglim/dev/dockerPractice/1/feedback-node:/app:ro" -v /app/node_modules feedback-node:volumes

이때 주의할 것은 해당 -v옵션에 설정한 read-only옵션이 다른 바인드 마운트 설정에도 영향이 있는지 확인하는 겁니다. 컨테이너에서 /app내부 파일에 쓰기를 시도하면 이를 방지합니다. 다른 바인드 마운트 옵션에서는 일부 쓰기를 진행하고자 하는데, /app하위 폴더라면 쓰기 권한이 방어가 되는 문제가 발생합니다.

볼륨 관리하기

bash
docker volume --help

Usage:  docker volume COMMAND

Manage volumes

Commands:
  create      Create a volume
  inspect     Display detailed information on one or more volumes
  ls          List volumes
  prune       Remove all unused local volumes
  rm          Remove one or more volumes

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

ls 명령을 사용해봅시다. 익명 볼륨, 네이밍 볼륨을 확인할 수 있습니다. 바인드 마운트는 도커에서 관리하지 않으므로 조회되지 않습니다.

bash
docker volume ls
local     ac5a364073299d7cf559758f30b92cfa3defc46d2a32a593a96fc52f096cad52
local     e18515efd770cc2ee0ec196f1df2d888b4facd69bc62dae162e769e0c5cfb97b
local     feedback

create 옵션은 네이밍 볼륨을 생성할 수 있는 명령입니다.

bash
docker volume create --help

Usage:  docker volume create [OPTIONS] [VOLUME]

Create a volume

Options:
  -d, --driver string   Specify volume driver name (default "local")
      --label list      Set metadata for a volume
  -o, --opt map         Set driver specific options (default map[])

inspect는 볼륨에 대한 정보를 확인할 수 있는 명령입니다. 생성일, 드라이버, 라벨, 마운트 포인트, 이름, 옵션, 스코프 등을 확인할 수 있죠.

bash
docker inspect --help

Usage:  docker inspect [OPTIONS] NAME|ID [NAME|ID...]

Return low-level information on Docker objects

Options:
  -f, --format string   Format the output using the given Go template
  -s, --size            Display total file sizes if the type is container
      --type string     Return JSON for specified type

rm 옵션을 볼륨을 삭제하는 명령입니다. 제거 당시 컨테이너와 볼륨이 연결되어 있다면 컨테이너는 중지 및 제거해야 합니다.

prune 옵션은 사용하지 않는 볼륨을 모두 제거하는 명령입니다. (컨테이너에서 사용하지 않는)

COPY와 dockerignore

.dockerignore에 특정 파일을 추가하면 COPY 명령으로 복사해서는 안 되는 요소를 제외할 수 있습니다

예로, 호스트에 설치된 node_modules를 제외하고 싶다면 아래와 같이 추가하면 됩니다.

yaml
node_modules

환경변수 설정

Dockerfile은 매개변수 즉 인자($환경변수)를 받을 수 있습니다. 또한 Dockerfile파일 내부에 ENV명령 레이어를 이용해 환경변수를 설정할 수 있습니다. 이로 인해 보다 유연한 이미지를 생성할 수 있게됩니다.

yaml
# Dockerfile
# ENV 환경변수명 값 -> 이 변수를 전역 환경변수로 적용됨
ENV PORT 80

EXPOSE $PORT

또는 run명령에 환경변수 옵션을 추가해 적용할 수 있습니다.

bash
# docker run -env PORT=80 ... 동일하게 작동
docker run --env PORT=80 ...

또 다른 방법으로 환경변수 파일을 별도로 생성한 뒤 run시 적용해주는 옵션도 있습니다. --env-file옵션입니다.

bash
# --env-file 환경변수 경로
docker run --env-file ./.env

매개변수로 유용하게 사용하기

yaml
# Dockerfile
ARG DEFAULT_PORT = 80

ENV PORT ${DEFAULT_PORT}

EXPOSE $PORT

이미지 빌드시 ARG로 설정한 인자의 값을 변경할 수 있습니다. 변경하지 않는다면 기본 초기화값으로 적용됩니다. --build-arg옵션을 이용합니다.

bash
docker build -t feedback-node --build-arg DEFAULT_PORT=8000 .