node.js 기반 앱을 docker 환경에서 실행해보자
요약
dockerfile 생성, .dockerignore 생성
dockerfile 캐싱에 유의하며 작성
docker build -t name 으로 도커 이미지 빌드
docker run -p 사용할 포트:컨테이너 포트 도커이미지를 실행하여 컨테이너 환경에서 앱 실행
코드 수정 후 이미지 재빌드 => 재실행의 과정은 매우 번거롭기 때문에
volume 옵션을 사용하여 로컬을 참조하게 만들어 이미지 빌드를 다시 하지 않아도 된다. 재실행만 진행해주도록하자.
다음과 같이 아주 간단한 express 앱을 docker 환경에서 실행해보자.
const express = require("express");
const app = express();
const PORT = 4000;
const HOST = "127.0.0.1";
app.get("/", (req, res) => {
res.send("home");
});
app.listen(PORT, () => console.log(`http://${HOST}:${PORT}`));
우선 Dockerfile을 작성하자.
node기반 앱이고, 내가 현재 사용하고 있는 node 버전이 12.16.2이다. 해당 버전과 일치하는 tags 가 있는지 도커허브에서 확인해본 결과 있었다! 이를 적용하기로 했다. 왜 alpine이 아니라 node를 사용하느냐. alpine에는 npm이 없으니 npm i 를 할 수 없어서다. 너무 당연함 ㅎ...
hub.docker.com/_/node?tab=tags&page=1
Dockerfile
여기서 처음 보는 WORKDIR, COPY, CMD가 등장합니다. 이에 대해서는 하단에 설명하겠습니다.
FROM node:14.16.0
# workdir 지정. 반드시 상위에 적을 것(COPY등 경로에 영향을 줘야하기 때문)
# 절대경로여야 함. 상대 경로 ㄴㄴ
WORKDIR /usr/src/app
# 로컬에 있는 package.json을 컨테이너의 ./ 경로에 COPY 종속성 설치해야 하니까
COPY package.json ./
COPY package-lock.json ./
# 종속성 설치(한 번 빌드하면 cache되기 때문에 속도 걱정은 ㄴㄴ)
RUN yarn
# 전체 소스 코드를 다 들고오자. 단, dockerignore는 없어야 함.
COPY ./ ./
CMD ["node", "app.js"]
우선, Dockerfile을 작성했으므로 이를 이용하여 이미지를 빌드합니다.
docker build -t node/test ./
이미지 빌드에 성공했다면 포트 매핑을 한 채로 실행해줘야 합니다.
-p [접속할 포트]: [컨테이너 내부 포트]
docker run -p 1234:4000 node/test:1
위 명령어를 입력하면 로컬에서는 1234 포트로 접근하면 컨테이너 속 4000번 포트로 접속하는 것이 됩니다.
소스코드는 4000번 포트로 지정을 해뒀으니 컨테이너 내부 포트는 4000번으로 지정해놓고, 외부에서 접근하는 포트는 기존에 사용하지 않는 포트면 뭐든지 괜찮습니다.
사실 4000:4000으로 줘도 되는데 차이를 보여주기 위해 고의적으로 다른 포트를 지정한 것입니다.
PORTS 1234가 4000/tcp로 매핑이 된 것을 확인할 수 있습니다.
주의점 1. COPY
주의할 점은 이미지를 빌드할 때 파일 스냅샷에 package.json, server.js 등 소스코드가 없으므로 COPY로 넣어줘야 한다는 점입니다.
COPY [현재 로컬에서 가져갈 파일] [컨테이너로 옮길 경로]
보통은 전부 다 넣고, dockerignore를 통해 제외할 부분을 지정합니다. (git이랑 똑같습니다)
COPY ./ ./
넣지 않는다면 다음과 같은 오류를 냅니다. npm i 단계부터 package.json이 없어 오류를 내는 모습입니다.
npm WARN saveError ENOENT: no such file or directory, open '/package.json'
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN enoent ENOENT: no such file or directory, open '/package.json'
npm WARN !invalid#2 No description
npm WARN !invalid#2 No repository field.
npm WARN !invalid#2 No README data
npm WARN !invalid#2 No license field.
주의점 2. WORKDIR
WORKDIR 란 어플리케이션 소스 코드를 가지고 있을 디렉토리를 생성하는 것입니다.
working 디렉토리가 필요한 이유는 지정하지 않으면 node_module 등 설치로 인해 생기는 부산물들이 정리되지 않은 채 루트 디렉토리에 전부 설치됩니다. 이는 관리상 좋지 않으므로 지정하는 것이 좋습니다.
또, 컨테이너의 쉘의 최초 접속 경로가 workdir가 됩니다. (편리성을 위해)
실제로 그런지 exec 명령어로 확인해봤습니다. 진짭니다.
docker exec -it [컨네이너 이름, 해시값] sh
주의점 3. cache와 dockerfile의 효율적 구성
효율적인 dockerfile 설계에 대한 첫 설명입니다.
package.json만 우선 COPY한 후 npm i를 해주는 것이 더 효율적입니다.
소스코드만 수정했다고 가정합시다. 그런데 ./ ./를 통해 가져오게 된다면 종속성에는 변화가 없는데도 불구하고 종속성을 다시 다운로드 받습니다. (cache를 사용하지 않는다는 말입니다.)
때문에 package.json를 먼저 COPY하여 붙여넣은 다음에 npm i를 하게 되면 종속성에 변화가 없는 이상 cache를 사용하게 되어 더욱 효율적으로 빌드할 수 있게 됩니다.
비효율적
COPY ./ ./
RUN npm install
효율적
COPY package.json ./
RUN npm install
COPY ./ ./
build 시에 cache가 되었는지 안 되었는지는 명령창을 살펴보도록합시다. 소스코드만 바꾸고 종속성을 바꾸지 않았는데 그 결과 package.json COPY와 npm i 는 cache가 되었지만 그 외 부분을 COPY하는 부분에는 cache가 되지 않았습니다.
주의점 4. Volume
COPY를 통해 특정 파일, 폴더를 복사하는 방법보다 로컬의 특정 경로를 참조하도록 매핑하는 것이 더 효율적입니다.
새로 이미지를 빌드하지 않아도 변경 사항을 반영하기 때문입니다.
darkrasid.github.io/docker/container/volume/2017/05/10/docker-volumes.html
dockerfile에 volume을 지정하는 방법도 있지만 docker run할 때 -v 옵션을 주는 방법을 더 자주 사용하는 듯합니다.
: 를 기준으로 전자에 있는 내용을 후자에 있는 컨테이너 경로에서 참조합니다.
pwd면 현재 경로니까 현재 경로의 내용(로컬)을 컨테이너의 /usr/src/app(원격) 에서 매핑하라는 말이네요
-v $(pwd):/usr/src/app
:이 없으면 매핑하지 말라고 하는 것입니다.
도커 기반에서 개발할 때는 컨테이너에서 node_modules를 생성하면 되므로 로컬 환경에서는 node_modules를 가지고 있을 이유가 없겠죠. 따라서, 매핑하지 말라고 지정할 수 있습니다.
-v /usr/src/app/node_modules
이 둘을 이용하여 다음과 같이 작성하여 Volume을 사용할 수 있습니다.
// linux
docker run -d -p 5000:4000 -v /usr/src/app/node_modules -v $(pwd):/usr/src/app [이미지 아이디]
// window
docker run -d -p 5000:4000 -v /usr/src/app/node_modules -v %cd%:/usr/src/app [이미지 아이디]
주의점 5. .dockerignore 파일 생성
node_modules
npm-debug.log