도커 이미지 생성해보기 2편(feat. Node.js, Dockerfile)

Bakumando·2022년 5월 13일
1

Docker

목록 보기
6/7

0. 블로깅 목적

목표로 하는 모든 과정은 Node.js 앱 실행을 기반으로 한다.

  • 도커 환경에서 앱을 실행하기 위해 필요한 것이 무엇인지 이해한다.
  • Dockerfile 작성 시에 활용될 수 있는 새로운 키워드들과 동작 흐름에 대해 이해한다.
  • 앱 소스코드 변경 시 재빌드를 효율적으로 하는 방법에 대해 이해한다.
  • 도커 환경에서 앱을 띄워보고 로컬에서 접속하는 방법을 이해한다.
  • Docker Volume을 통해 로컬 디렉토리의 파일을 복사할 필요 없이, 컨테이너에서 직접 참조하는 방법을 이해한다.

1. 도커 환경에서 Node.js 앱을 띄우기 위해 필요한 것이 무엇인지 알아보자.

  • 우선 반드시 필요한 것들은 아래와 같다.
    • index.js (Node app 실행 파일)
    • package.json (app에서 사용하는 패키지들을 명세해둔 파일)
    • Dockerfile (도커 이미지 생성 및 컨테이너 실행을 위한 파일)

  • 준비한 index.js와 package.json의 소스는 아래와 같다.
  • index.js
  • package.json

  • Node.js앱을 도커 환경에서 실행하는 것이 목적이다.
  • Dockerfile을 토대로 이미지를 생성하고 컨테이너를 실행한다고 했을때 반드시 필요한 것은 아래와 같다.
    • node를 베이스 이미지로 불러오는 커맨드
    • package.json을 토대로 필요한 패키지를 설치할 커맨드
    • index.js를 실행할 커맨드
  • Dockerfile에 대한 내용은 다음 챕터에서 상세히 알아보자.

2. Dockerfile 작성 시에 활용될 수 있는 새로운 키워드들과 동작 흐름에 대해 알아보자.

$ docker built -t bakumando/docker-test1 ./
  • Dockerfile은 도커 이미지 생성을 위한 파일이다.
  • 도커 이미지는 컨테이너(인스턴스)를 생성하고 실행하기 위해 쓰인다.
  • 즉, Dockerfile은 결국 컨테이너에서 쓰일 파일들과 환경 설정값, 커맨드들을 모아둔 내역이라 할 수 있다.
  • 위의 전제를 인지한 상태에서, Dockerfile을 확인해보자.
    • 키워드부터 정리해보자.
      • FROM, RUN, CMD는 1편에서 다뤘으니 생략하겠다. 간단히 말하면, 도커 서버가 수행해야할 가장 기본적인 커맨드라 할 수 있다.
      • COPY: COPY {로컬 경로} {컨테이너 경로} 형식이며, 로컬 경로에 해당되는 모든 폴더 및 파일을 컨테이너 경로에 카피해준다.
      • WORKDIR: 형식은 WORKDIR {생성할 워킹 디렉토리 경로} 이며, 설정한 경로 그대로 컨테이너 내부에 워킹 디렉토리를 만들어 준다. 이후에 진행될 모든 COPY 키워드의 {컨테이너 경로}는, 여기에 설정한 워킹 디렉토리 경로를 루트 경로로 인식하게 된다.

실습을 진행하며 더 자세히 알아보자.

  • docker built -t bakumando/docker-test1 ./ 을 입력해보았다.
  • 흐름을 살펴보면
    1) FROM node: node를 베이스 이미지로 가져왔다.
    2) WORKDIR /usr/src/app: 컨테이너에 /usr/src/app를 만들고, 베이스 경로로 설정하였다.
    3) COPY package.json ./: 현재 로컬 경로에 있던 package.json 파일이 컨테이너의 루트 경로로 복사되었다. 여기서 루트 경로는 WORKDIR에서 새롭게 설정한 /usr/src/app이 된다.
    4) RUN npm install: package.json을 토대로 필요한 모듈들을 컨테이너에 설치한다. 3번 단계에서 컨테이너에 package.json을 복사해두었기에 가능한 부분이다. 만약 3번 과정이 없었다면 컨테이너 내부에 package.json이 없으므로 npm install 수행이 불가능하다. (컨테이너 외부에 있는 package.json을 참조할 수는 없다는 것이다.)
    5) COPY ./ ./: package.json은 외의 다른 파일들을 컨테이너의 워킹 디렉토리 루트에 복사한다. 6번의 수행을 위해 필요한 부분이다.
    6) CMD ["node", "index.js"]: 컨테이너에 컨테이너 시작 명령어를 등록하는 것이다. 이는 컨테이너 실행 시에 1회 수행될 명령어를 뜻한다. (5번 과정이 없으면 컨테이너 내부에 index.js가 없어 수행이 불가하다.)

참고 1) Running in {컨테이너 ID}: Step 1~6동안 임시 컨테이너를 구축해온 것이며, 이제 가동하여 이미지 생성에 돌입한 것이다.
참고 2) Removing itermediate container {컨테이너 ID}: 새로운 이미지를 만드는 데 활용된 임시 컨테이너를 삭제하는 것이다.
참고 3) Successfully built {이미지 ID}: 이미지가 성공적으로 생성 완료되고 아이디 값이 부여된 것이다.
참고 4) Successfully tagged bakumando/docker-test1:latest: 새롭게 생성 완료된 이미지에 네이밍을 해주는 것이다.


3. 앱 소스코드 변경 시 재빌드를 효율적으로 하는 방법에 대해 살펴보자.

docker build -t bakumando/docker-test2 ./
  • 2번에서 살펴본 Dockerfile은 이미 효율적인 재빌드를 고려하여 작성된 것이다.
  • 어떤 부분이 재빌드의 효율화가 적용된 것인지 짚어보자.
  • Dockerfile에 COPY 키워드는 두 부분으로 나뉘어져 있다.
  • 실제 위에서 진행했던 빌드 내용을 보면, RUN의 전후로 1번씩 COPY가 진행되었음을 확인할 수 있다.
  • 사실 RUN 이전에 COPY ./ ./ 를 한번만 작성하더라도 이후 과정이 진행되는 데에 있어 무리가 없다.
  • 그럼에도 굳이 COPY를 두 부분으로 나누어서 작성한 이유는 뭘까?
    • 이것이 바로 재빌드 과정에서의 효율화를 위한 안배인 것이다.
    • 빌드시에 RUN 커맨드가 실행되는 요건은 다음과 같다.
      • 첫째, 최초의 빌드일 때 -> 반드시 실행된다.
      • 둘째, 재빌드 일 때 -> 복사된 소스코드 파일들 중 단 하나라도 변경이 있으면 실행된다. (즉, 변동사항이 없으면 실행되지 않음)
    • 문제는 해당 명령어가 npm install이라는 것이다. 사실 package.json이 변화한 게 아니라면 굳이 npm install을 또 수행할 필요가 없다.
    • 허나 만약 다른 파일에 변동사항이 있다면, 해당 명령어가 강제되면서 모든 모듈(node_modules)을 다시 받는 문제가 생길 수밖에 없다.
    • 이를 해결하기 위해, RUN 이전에는 package.json만 COPY를 진행하고, RUN 이후에 나머지 다른 소스코드 파일을 COPY 함으로써 모듈 전체를 다시 받는 문제를 해결한 것이라 할 수 있다.

명확한 이해를 위해 실습을 진행해보자.

  • Dockerfile에 COPY를 RUN 이전 한개로 합쳐서 빌드한 결과를 한번 확인해보자.
  • 이를 위해 아래 명령어를 두 번 수행하였다. (1번째는 최초 빌드, 2번째는 index.js를 부분 수정한 뒤에 재빌드)
  • docker build -t bakumando/docker-test2 ./ (* 2)
    • COPY를 하나로 묶어 3번 스텝에서 COPY ./ ./ 를 수행하였음을 확인할 수 있다. (RUN 이전에 모든 소스 파일이 컨테이너에 복사된 것)
    • 4번 스텝이 문제다. pacakge.json의 변화가 없음에도 다른 파일의 변화가 감지되어 모듈 전체를 다시 받는 문제가 발생하고 있다.
    • 아직 확인하고 싶은 부분이 더 있을 것이다. COPY를 두 부분으로 나누어 재빌드를 진행해보진 않았기 때문이다. 이는 다음 챕터를 진행하며 체크해보겠다.

4. 생성한 이미지로 Node.js 앱 컨테이너를 띄워보고 로컬에서 접속하는 방법을 이해해보자.

$ docker run -d -p 4000:8080 bakumando/docker-test1
  • docker run -d -p {로컬 포트}:{컨테이너 포트} {이미지 이름}
    • 도커 환경에서 띄운 서버를 로컬에서 접속하기 위해선, 위와 같은 커맨드가 필요하다.

바로 실습을 통해 이해해보자.

  • docker run -d -p 4000:8080 bakumando/docker-test1을 입력해보았다. 만들어 둔 bakumando/docker-test1 이미지를 토대로 컨테이너를 생성 및 실행하는 커맨드이다.
    • -d: detach 옵션으로서 컨테이너를 실행중인 상태로 두고 빠져나오는 것이다. 만약 붙이지 않으면 도커 내부에서 서버를 띄운 상태를 유지하게 된다.
    • -p: 포트 부여 및 연결을 위한 옵션이다.
    • {로컬 포트}:{컨테이너 포트}: 도커 컨테이너에 띄워진 서버를 로컬에서 아무런 연결점 없이 접속하는 것은 불가능하다. 로컬 포트와 컨테이너 포트의 매핑이 필요하다. {로컬 포트}:{컨테이너 포트} 규칙에 맞게 기입하면 서로 간 연결이 이루어진다. 컨테이너 포트는 index.js에서 확인할 수 있듯이 8080으로 설정해두었고, 로컬 포트는 마음껏 지정해도 좋다. 여기선 4000으로 해보았다.
      • 접속이 정상적으로 이루어짐을 확인할 수 있다.
    • 마지막엔 역시 {이미지 아이디}가 들어가게 된다. 즉, docker run{이미지 아이디} 사이에 포트 옵션을 넣으면 되는 것이다.

  • 재빌드의 효율화도 테스트 해보자.
  • 현재 Dockerfile의 COPY 부분은 RUN 이전과 이후로 각각 나뉘어 있다. (RUN 이전: COPY package.json ./, RUN 이후: COPY ./ ./)
  • 또한 index.js의 hello world를 -> hi world로 바꾸었다.
    • 즉, package.json이 아닌 다른 파일의 소스를 변경한 셈이다.

  • 그리고 아래 커맨드를 수행하였다.
  • docker build -t bakumando/docker-test1 ./
    • 4번 스텝에서 npm install이 이루어지지 않고, 캐싱만 한 뒤 끝이 났음을 확인할 수 있다. RUN 이전에 복사된 파일이 이전 빌드와 달라진 게 없다면 RUN이 수행되지 않는 것이다.

  • 마지막으로 index.js의 소스코드가 변경된 결과가 반영되었는지도 포트 접속을 통해서 확인해보자.
    • 같은 포트로 연결해보았다.
    • 소스코드 변경이 정상적으로 이루어졌음이 확인되었다.

5. Docker Volume을 통해 로컬 디렉토리의 파일을 복사할 필요 없이, 컨테이너에서 직접 참조하는 방법을 이해한다.

$ docker run -p 4000:8000 -v /usr/src/app/node_modules -v $(pwd):/usr/src/app bakumando/docker-test1
  • 이전 챕터를 통해 Dockerfile의 COPY 구역을 RUN 이전과 이후로 나누어 줌으로써 재빌드 시에 불필요한 모듈 다운을 방지하는 방법에 대해 살펴보았다.
  • 다만 아직도 불편한 점이 있다면, 소스코드 변경시 마다 재빌드를 해주고 다시 컨테이너를 실행해줘야 변경된 소스가 반영된다는 점일 것이다.
  • 이러한 방식은 시간적으로나 비용적으로나 비효율적이며, 이미지를 지속해서 많이 빌드하게 되는 문제점이 있다.
  • 이를 해결하기 위해 사용되는 방법이 Docker Volume이다.

  • Docker Volume은 도커 컨테이너에서 직접 로컬 디렉토리에 있는 파일들을 매핑(참조)해서 사용한다.
  • 로컬 디렉토리의 파일을 도커 컨테이너에 복사할 필요가 없게 되는 것이다.
  • 형식은 아래와 같다.
  • docker run -p 4000:8000 -v {매핑하지 않을 컨테이너 경로} -v $(pwd):{매핑할 컨테이너 경로} {이미지 아이디}

    • 첫번째 -v옵션: -v 뒤에, 로컬에서 {매핑하지 않을 컨테이너 경로}를 넣는다. 보통 모듈 파일 경로를 여기에 적용한다. 그렇지 않으면 이어질 -v $(pwd):{매핑할 컨테이너 경로}에서 로컬 경로의 모듈 파일도 매핑에 해당될 것이다. 문제는 로컬에선 npm install을 진행하지 않았기 때문에 node_modules(모듈 파일)이 존재하지 않는다는 점이다.
      • 첫번째 -v옵션을 주지 않으면 아래와 같은 에러가 발생하게 된다.
      • 따라서 첫번째 -v옵션을 적용함으로써 에러 발생을 방지하고, 컨테이너에 있는 모듈 파일을 활용할 수 있게 된다.

    • 두 번째 -v옵션: :(콜론)을 경계로 $(pwd){매핑할 컨테이너 경로}를 넣어주면 된다.
      • pwd는 로컬의 현재 경로 값을 뜻한다.
      • :(콜론)은 포트 연결에서도 확인했듯이 전후를 매핑할 때 쓰이는 키워드이다.
      • 즉, pwd(로컬의 현재 경로)에 있는 파일들이 {매핑할 컨테이너 경로}에 있는 파일들을 대체해서 컨테이너가 실행되는 것이다.

    • volume은 다 좋지만 아무래도 매핑 관계의 디테일한 설정을 해야하기 때문에 커맨드가 굉장히 길어진다는 점은 단점이 있다.
      • 나중에 블로깅하겠지만 docker-compose와 yml 파일을 통해 이러한 단점을 해결할 수 있다.

실습을 통해 확인해보자.

  • index.js의 hi world를 hello hi world로 바꿔보았다.
  • 그리고 아래 명령어를 실행해보았다.
  • docker run -p 4000:8000 -v /usr/src/app/node_modules -v $(pwd):/usr/src/app bakumando/docker-test1
    • 서버가 정상적으로 가동되었다.
    • 여기선 -d 옵션을 안줘서 서버를 띄운 후 빠져나오진 않았다.

  • localhost:4000에 접속해보자.
    • hello hi world가 확인되었다.
    • 재빌드하지 않았음에도 변경된 소스코드를 바탕으로 컨테이너를 실행하는데 성공한 것이다.
profile
그렇게 바쿠만도는 개발에 퐁당 빠지고 말았답니다.

0개의 댓글