이번 포스트에서는 간단한 앱을 예제로 Docker Image를 만드는 법을 알아본다.

사용 할 앱은 React의 샘플 앱인 counter-app (github, live demo) 이다.

git clone https://github.com/arnab-datta/counter-app.git

소스 코드를 다운 받으면 준비 완료.


#Dockerfile 작성

우선 소스코드가 있는 디렉토리로 이동한 다음 Dockerfile을 작성해 준다.

sudo nano Dockerfile

내용은 아래와 같이 작성한다. 각 줄이 무엇을 의미하는지는 코멘트를 참조.

# Base 이미지를 설정한다. 앱이 node 11에서 돌아가기 때문에 node 11을 선택하였다.
FROM node:11-alpine

# 컨테이너 안에 디렉토리를 설정해 준다. 이름은 아무거나 원하는 것으로 하면 된다.
# 이걸로 counter-app 디렉토리가 컨테이너 안에 생성되고 위치가 이동된다.
WORKDIR /counter-app

# 로컬 디렉토리에 있는 모든 파일을 컨테이너의 현재 위치 (counter-app)에 복사한다.
# 첫번째 .은 로컬 디렉토리의 모든 파일이라는 뜻이고
# 두번째 .은 컨테이너 안에 현재 위치를 뜻한다.
COPY . .

# 앱을 구동하는데 필요한 node 패키지들을 설치하고 
# 컨테이너 이미지의 크기를 최대한 작게하기 위해서 불필요한 캐시 파일은 삭제 한다.
RUN npm install && npm cache clear --force

# 마지막으로 앱을 실행 할 커맨드를 지정
CMD npm start

#Docker Image 작성

이렇게 Dockerfile을 작성했다면 이제 이미지를 작성 할 차례. 이미지를 작성하는 커맨드는 아래와 같다.

docker build -t <이미지 이름> <Dockerfile의 위치>

이번 예제의 경우 아래와 같은 커맨드로 작성한다.

docker build -t counter-app .

마지막에 .은 현재 디렉토리를 말한다. Dockerfile이 다른 디렉토리에 있다면 해당 디렉토리의 path를 제대로 지정해줘야 한다. build 커맨드를 실행하면 "Step 1/5 ~ Step 5/5" 까지의 메세지가 뜬다. 마지막에 "Successfully tagged counter-app:latest" 라고 뜨면 성공적으로 이미지가 작성 된 것이다.


#Docker Image 실행

예제 앱은 3000번 포트에서 실행이 된다. 그렇기 때문에 컨테이너 쪽 포트는 3000으로 설정해준다.

docker run -p 3000:3000 -d counter-app

백그라운드에서 돌아가는 커맨드이기 때문에 앱의 로그가 나오지 않는다. 로그 파일을 보고 싶을 땐 우선 실행 중인 CONTAINER ID 값을 알아낸 뒤, 해당 컨테이너의 로그값을 표시하는 커맨드를 실행한다.

우선 실행중인 컨테이너의 리스트를 다음 커맨드로 나열 할 수 있다.

docker ps

아래와 비슷한 값이 출력 될 것이다.

CONTAINER ID   IMAGE          COMMAND                  CREATED         STATUS         PORTS                                       NAMES
064d8af23a0b   counter-app    "docker-entrypoint.s…"   2 minutes ago   Up 2 minutes   0.0.0.0:3000->3000/tcp, :::3000->3000/tcp   confident_borg

리스트에서 CONTAINER ID를 구했다면 다음 커맨드로 로그파일을 출력한다.

docker logs <CONTAINER ID>

예제 앱으로 만든 이미지를 구동하고 있는 컨테이너 앱의 로그에 아래와 같은 문구가 나와있다면 앱을 구동 할 수 있는 상태이다.

You can now view counter-app in the browser.

  Local:            http://localhost:3000/
  On Your Network:  http://172.17.0.3:3000/

자신의 환경에서 (일반적으로는 localhost:3000) live demo와 같은 화면이 나오면 성공적으로 앱이 컨테이너 안에서 구동이 된 것이다.


#node_modules

NodeJS를 엔진으로 사용하는 앱의 경우, 여러가지 dependency 패키지들을 사용하는 것이 일반적이다. 사용 할 패키지들이 초반에 어느정도 정해지면 그 후의 개발에서는 패키지를 새로 추가 할 일은 없고 소스코드만 추가/수정 하는 것이 일반적이다.

위 예제에서 만든 Dockerfile대로라면 소스코드를 추가/수정하고 새로운 컨테이너 이미지를 작성 할 때 마다 npm install로 매번 패키지들을 다운 받아야한다. 이미지를 작성 할 때 가장 시간이 걸리는 부분이 npm install이 실행 될 때이기 때문에, 새로운 패키지 추가가 없다면 이 부분을 스킵함으로써 이미지 작성 시간을 단축 시킬 수 있다.

그렇게 하기 위해서는 Dockerfile을 아래와 같이 바꿔준다.

FROM node:11-alpine

WORKDIR /counter-app

# 우선적으로 package.json과 package-lock.json파일을 이동한다.
COPY package*.json .

RUN npm install && npm cache clear --force

COPY . .

CMD npm start

package.json을 먼저 복사해주면, 만약 package.json에 변화가 없다면 전에 이미지를 build할 때 남겨둔 cache를 그대로 사용하게 되고, npm install 과정도 cache를 그대로 이용하게 되기 때문에 시간이 많이 단축된다.

실제로 위의 Dockerfile로 바꾼 후, build를 처음하면 npm install이 실행되면서 패키지를 다운로드 하지만, 그 이후에 package.json 이외의 소스코드만 수정 했을 땐 build 과정에서 캐시를 사용하는 것을 확인 할 수 있다.

Sending build context to Docker daemon  1.741MB
Step 1/6 : FROM node:11-alpine
 ---> f18da2f58c3d
Step 2/6 : WORKDIR /counter-app
 ---> Using cache
 ---> e2f8f4a0a8cb
Step 3/6 : COPY package*.json ./
 ---> Using cache
 ---> 4240fa32e454
Step 4/6 : RUN npm install && npm cache clear --force
 ---> Using cache
 ---> ead6b93051e0
Step 5/6 : COPY . .
 ---> 5474e6a6ebab
Step 6/6 : CMD npm start
 ---> Running in 0ec85e3b0e64
Removing intermediate container 0ec85e3b0e64
 ---> 0476988edc91
Successfully built 0476988edc91
Successfully tagged counter-app:latest

Step 3과 4에서 "Using cache"라는 문구가 뜨고 이미지의 빌드가 처음 했을 때와는 비교과 안되게 빠른 걸 체감 할 수 있을 것이다.


간단한 예제 앱을 통해 Docker Image를 만드는 법에 대해 알아보았다. 왠만한 웹 앱 관련해서는 조금만 응용하면 적용 가능 할 듯 싶다.