가장 중점적으로 봐야할 것은 Dockerfile 을 어떤식으로 작성해야 하는지가 중요하다.
- Node.js APP 을 만들고 도커 이미지를 생성 해서 컨테이너로 실행해보자.
- 즉 도커 컨테이너 안에서 Node.js 애플리케이션이 실행될 수 있도록 하자.
Node.js APP 만들기
- package.json : 프로젝의 정보와 프로젝트에서 사용 중인 패키지의 의존성을 관리하는 곳
- server.js : Node.js 에서 진입점이 되는 파일
터미널에서 npm init 명령어로 package.json 만들기
- npm = Node Package Manager
"dependencies": {
"express" : "4.18.3"
}
- 만들어진 package.json 에서 express 라는 라이브러리를 추가
- express 는 Node.js 를 더 쉽고 유용하게 사용할 수 있도록 해준다.
const express = require('express');
const PORT = 8080;
const app = express();
app.get('/' , (req,res) => {
res.send("Hello World")
});
app.listen(PORT)
Dockerfile 작성하기
- Node.js APP 을 도커 환경에서 실행하려면 먼저 이미지를 생성하고 그 이미지를 이용해서 컨테이너를 실행한 후 그 컨테이너 안에서 Node.js APP 을 실행해야 한다.
- 그래서 그 이미지를 먼저 생성하기 위해서 Dockerfile 을 먼저 작성해야 한다.
- Dockerfile 만드는 부분은 도커와 CI 환경 - 2 참고
기본적인 명령어로 작성해보기
FROM node:10
RUN npm install express
CMD ["node" , "server.js"]
FROM
- 전에 했었던 것 처럼 FROM alpine 으로 하면 오류가 발생한다.
- 그 이유는 npm 을 위한 파일이 없기 때문에 아래에 RUN 부분을 실행할 수 없는 것이다.
- 그래서 npm 을 사용할 수 있도록 node:10 베이스 이미지를 사용해야 한다.
RUN
- npm 은 Node.js 로 만들어진 모듈을 웹에서 받아서 설치하고 관리해주는 프로그램
- npm install 은 package.json 에 적혀있는 종속성들을 웹에서 자동으로 다운 받아서 설치해주는 명령어
- 결론적으로 현재 Node.js APP 을 만들때 필요한 모듈들을 다운 받아 설치하는 역할을 한다.
- Node.js 에서 npm install 이 실행되면 NPM Registry 모듈들이 저장되어 있는 곳이 있고 우리는 Express 라는 라이브러리를 사용했기 떄문에 이 모듈을 내려 받아서 Node.js APP 에 전달해준다.
CMD
- 노드 웹 서버를 작동시키려면 node + 엔트리 파일 이름을 입력해야하기 때문에 위와 같이 넣어줬다.
위에 처럼 작성하고 docker build ./ 명령어를 실행하면 오류가 발생한다..
그 이유가 무엇일까?
Package.json 파일이 없다고 나오는 이유?
- 우선 결론부터 말하면 COPY 라는 것을 안해서 발생하는 오류이다.
Dockerfile 을 이미지로 만드는 과정
- 도커와 CI 환경 - 2 에 자세히 있다.
- 간략하게 설명하면 Dockerfile 에 있는 베이스 이미지로 임시 컨테이너를 만들고 그 임시컨테이너에서 이미지를 만들고 임시컨테이너는 삭제된다.
- 해당 예제에서는 Node 베이스 이미지로 임시 컨테이너를 생성하고, 그 임시 컨테이너로 이미지를 만들것이다.
- 하지만 임시 컨테이너에는 package.json 이 Node 이미지 파일 스냅샷에 포함되어 있지 않다.
- 임시 컨테이너 하드디스크 부분에 파일 스냅샷에는 package.json 도 없고 server.js 도 없어서 파일들을 찾지 못해 생기는 오류인 것이다.
- package.json 은 컨테이너 밖에 있는 상황인 것이다.
그래서 도커 컨테이너의 복사를 해줘서 현재 디렉토리에 있는 모든 파일을 컨테이너 안으로 집어넣어줘야 한다.
FROM node:10
COPY ./ ./
RUN npm install express
CMD ["node" , "server.js"]
- COPY 에서 ./ ./ 부분은 현재 디렉토리 즉 package.json , server.js 파일 모두를 도커 컨테이너에 복사를 해주는 것이다.
- docker build -t supportkim/nodejs
- docker run spportkim/nodejs
오류가 발생하지 않아서 8080 포트로 접속해본 결과 또 접속할 수 없다는 오류가 발생한다..
그 이유는 또 무엇일까?
생성한 이미지로 어플리케이션 실행 시 접근이 안 되는 이유
- 결론은 포트 매핑을 하지 않아서 접근이 안 되는 것이다.
- 앞으로는 컨테이너를 실행할 때 다음과 같이 사용해야 한다.
- docker run -p PORT:PORT IMAGE_NAME
-p PORT:PORT
- 우리가 이미지를 만들 때 로컬에 있던 파일(package.json , server.js) 등을 컨테이너에 복사해줘야 했다.
- 그것과 비슷하게 네트워크도 로컬 네트워크에 있던 것을 컨테이너 내부에 있는 네트워크에 연결을 시켜줘야 한다.
브라우저 -> 로컬호스트 네트워크 -> 컨테이너 네트워크
- 위와 같은 순서를 거쳐서 컨테이너에 잇는 네트워크에 도달하게 된다.
- docker run -p 5000:8080 supportkim/nodejs
- 위 명령어를 실행하고 localhost:5000 으로 접속하면 Hello World 라는 문구가 보이게 된다.
- 즉 브라우저의 5000번 포트와 컨테이너에 있는 8080 포트가 매핑이 된 것이다.
- 5000 포트가 아닌 1234 포트로 설정하게 된다면 localhost:1234 로 접속하면 된다.
Working Directory 명시해주기
- 도커 파일에 WORKDIR 이라는 부분을 추가해야한다.
- 무엇을 위해서 추가해야할까?
이미지 안에서 애플리케이션 소스 코드를 가지고 있을 디렉토리를 생성하는 것이다.
- 그리고 이 디렉토리가 애플리케이션에 working directory 가 된다.
왜 따로 working directory 를 만들어야 할까?
비교
- docker run -it node ls
- docker run -it supportkim/nodejs ls
- 2개의 명령어 차이를 봐보자.
- 첫 번째는 베이스 이미지인 Node 의 이미지인데 그 이미지 안에는 home , bin , dev 등의 파일들이 있다.
- 두 번째는 현재 우리가 만든 이미지인데 그 이미지 안에는 Dockerfile , package.json , server.js 등 더 많은 파일들이 있다.
- COPY 명령어를 컨테이너 안으로 들어온 것들이다.
workdir 를 지정하지 않고 그냥 COPY 할 때 생기는 문제점
- 혹시 이 중에서 원래 이미지에 있던 파일이 같다면, 원래 있던 폴더가 덮어 써져 버린다.
- 모든 파일이 한 디렉토리에 들어가기 떄문에 너무 정리 정돈이 되지 않는다.
그래서 모든 어플리케이션을 위한 소스들은 WORK 디렉토리를 따로 만들어서 보관해야 한다.
FROM node:10
WORKDIR /usr/src/app
COPY ./ ./
RUN npm install express
CMD ["node" , "server.js"]
- WORKDIR 이후에 Working Directory 의 경로를 적어주면 된다.
- 위와 같이 Dockerfile 을 만들고 다시 이미지 생성하고 컨테이너에서 실행한 후 docker run -it IMAGE_NAME sh 명령어를 실행해보자.
- 실행한 후 ls 를 입력하면 Dockerfile , package.json 등 우리가 COPY 한 파일들이 나온다.
- 그 이유는 WORKDIR 을 설정하게 되면 기본적으로 work 디렉토리에서 시작하게 된다.
- 그래서 cd / 로 루트로 들어가본 후 다시 /usr/src/app 을 따라 가보면 애플리케이션과 관련된 소스들이 들어있다 (워크 디렉토리)
어플리케이션 소스 변경으로 다시 빌드하는 것에 대한 문제점
- 어플리케이션을 만들다 보면 소스 코드를 계쏙 변경시켜줘야 하며 그에 따라서 변경된 부분을 확인하면서 개발을 해나가야 한다.
- 그렇다면 도커를 이용해서 어떻게 실시간으로 소스가 반영되게 하는지 알아보자.
그전에 만약 Hello World! 가 아닌 반갑습니다. 라는 문구를 화면에 노출시키는 걸로 수정했다고 가정해보자.
- 먼저 도커 파일을 작성하고 나면 이 도커 파일을 도커 이미지로 만들고(build) 도커 이미지를 가지고 컨테이너를 생성(run) 한다.
- 만약 소스 코드가 변경된다면 build -> run 부분을 다시 해줘야 하는 것이다.
이렇게 해야 하는 이유는 COPY 부분에서 server.js 와 같은 파일을 가지고 있기 때문에 다시 COPY 하기 위해서 이미지를 다시 빌드를 해야하는 것
- 즉 모든 모듈에 있는 종속성들까지 다시 다운을 받아줘야한다.
- 소스 하나 변경시켰다고 이미지를 다시 생성하고 다시 컨테이너를 실행시켜줘야 한다니.. 매우 비효율적이다.
어떻게 해결해야할까?
어플리케이션 소스 변경으로 재빌드 시 효율적으로 하는 방법
- 우선 위에 있는 문제를 해결하기 위한 방법을 알아보기 전에 재빌드 자체를 할 때 효율적으로 하는 방법부터 알아보자.
- 결론적으로는 아래와 같이 Dockerfile 을 작성하면 된다.
FROM node:10
WORKDIR /usr/src/app
COPY package.json ./
RUN npm install express
COPY ./ ./
CMD ["node" , "server.js"]
- 달라진 점은 RUN 위에 COPY 가 하나 더 주가되고 원래의 COPY 는 RUN 아래에 작성한다.
그 이유는?
- npm install 할 때 불 필요한 다운로드를 피하기 위해서이다.
- 원래 모듈을 다시 받는 것은 모듈에 변화가 생겨야만 다시 받아야 하는데 소스 코드에 조금의 변화만 생겨도 모듈 전체를 다시 받는 문제점이 있다.
- server.js 의 소스가 변해도 변한 부분만 COPY 를 하는게 아니라 npm install 을 실행하여 변하지도 않은 모듈들을 다시 다운로드 하는게 문제다.
- 새롭게 server.js 에 소스를 바꾸고 나서는 npm install 을 실행하고 그 후 바로 한 번더 build 했을때는 cache 를 사용한다.
결국은 RUN npm install 전 단계에서 COPY 할 때 조금이라도 바뀐 것이 있다면 npm install 이 다시 실행된다.
그러기에 RUN npm install 전 단계에서 COPY 할 때는 오직 모듈에 관한 것만 해준다.
그리고 RUN npm install 이후에 다시 모든 파일들을 COPY 한다.
그래서 먼저 package.json 파일을 COPY 하고 그 이후 RUN npm , install COPY ./ ./ 를 해주면서 모듈에 변화가 생길 때만 다시 다운로드하여주며, 소스 코드에 변화가 생길 때 모듈을 다시 받는 현상을 없앨 수 있다.
정리하면 package.json 부터 COPY 하고 여기에 변경사항이 없다면 모듈을 다시 받는 현상을 없애 효율적으로 재빌드 할 수 있게 됐다.
Docker Volume 이란?
- 이제는 npm install 전에 package.json 만 따로 변경을 해줘서 쓸 때 없이 모듈을 다시 받지는 않아도 된다.
- 하지만 아직도 소스를 변경할 떄 마다 변경된 소스 부분은 COPY 한 후 이미지를 다시 빌드를 해주고 컨테이너를 다시 실행줘야지 변경된 소스가 화면에 반영이 된다.
- 이러한 작업은 너무나 시간 소요가 크고 이밎도 너무나 많이 빌드된다.
이때 Docker Volume 을 사용하면 된다.
지금까지 이용한 방식
- 지금은 로컬에 있는 파일들을 도커 컨테이너로 "복사" 를 했다.
- 하지만 Docker Volume 을 사용하면 도커 컨테이너가 로컬에 있는 파일들을 "참조" 한다.
즉 Docker Volume 은 도커 컨테이너에서 호스트 디렉토리에 있는 파일을 참조할 수 있게 한다.
Volume 을 사용해서 어플리케이션을 실행하는 방법
- docker run -p 5000:8080 -v /usr/src/app/node_modules -v $(pwd):/usr/src/app IMAGE_ID
- 우선 첫번째 -v 부터 살펴보면, node_module 파일은 컨테이너에서 참조(매핑)을 하지 말라고 지정하는 것이다. (WORKDIR 경로 + /node_modules)
- npm install 로 다운 로드 받은 종속성들은 node_module 파일에 들어가는데, 해당 파일은 없기 떄문에 참조하지 말라고 지정하는 것이다.
- 두 번째 -v 는 일단 pwd 경로에 있는 디렉토리 혹은 파일을 /usr/src/app 경로에서 참조할 수 있도록 하는 것이다.
정리하면 일단 node_modules 는 컨테이너에서 참조하지 말고 /usr/src/app 경로에 있는 파일을 참조하나는 것이다.
- /usr/src/app 는 WORKDIR 로 우리가 만들었던 파일들이 존재하는 곳이기 때문에 server.js 와 같은 파일을 "참조" 할 수 있게 된다.
이렇게 Volume 을 이용해서 빌드할 때 소스를 바꾸더라도 stop 했다가 다시 run 만 바로 반영이 된다.
- COPY 로만 만들었을 때는 다시 빌드를 하고 run 을 했어야 했지만 Volume 은 바로 run 만 해도 된다.
- 컨테이너에서 로컬 파일을 "참조" 하고 있기 때문이다.
Docker Compose 란?
- docker compose 는 다중 컨테이너 도커 애플리케이션을 정의하고 실행하기 위한 도구
- docker compose 에 대해 이해하기 위해서 페이지를 새로고침했을 때 숫자가 1씩 계속 올라가는 간단한 앱을 만들어보면서 배워보자.
전체적인 구조는 컨테이너 2개를 사용한다.
애플리케이션 소스 작성
- 새로운 폴더를 만들고 npm init 으로 기본적인 노드 부분을 완성해준다.
- server.js 를 작성해보기 전에 Redis 를 간단하게 알고 작성해보자.
Redis 란?
- Rdis(Remote Dictionary Server) 는 메모리 기반의 key-value 구조
- 데이터 관리 시스템이며, 모든 데이터를 메모리에 저장한다.
Redsi 사용 이유?
- 메모리에 저장을 하기 떄문에 MySQL 같은 데이터베이스에 데이터를 저장하는 것과 데이터를 볼러올 때 훨씬 빠르게 처리할 수 있다.
- 비록 메모리에 저장하지만 영속적으로도 보관이 가능하다.
- 그래서 서버를 재부팅해도 데이터를 유지할 수 있는 장점이 있다.
Node.js 환경에서 Redis 사용 방법
- 먼저 redis-server 를 작동시킨다.
- redis 모듈을 다운받고, 레디스 클라이언트를 생성한다.
- redis server 가 작동하는 곳과 Node.js APP 이 작동하는 곳이 다르다면 host 인자와 port 인자를 명시해줘야한다.
만약 도커를 사용하지 않는 환경이라면 host: "https://redis-server.com" 와 같은 형식으로 넣어주면 되지만, Docer Compose 를 사용할 때 host 옵션을 docker-compose.yml 파일에 명시한 컨테이너 이름으로 주면 된다.
const express = require("express");
const redis = require("redis");
const client = redis.createClient({
socket: {
host: "redis-server",
port: 6379
}
});
const app = express();
app.get('/', async (req, res) => {
await client.connect();
let number = await client.get('number');
if (number === null) {
number = 0;
}
console.log('Number: ' + number);
res.send("숫자가 1씩 올라갑니다. 숫자: " + number)
await client.set("number", parseInt(number) + 1)
await client.disconnect();;;
})
app.listen(8080);
console.log('Server is running');
Dockerfile 작성하기
FROM node
WORKDIR /usr/src/app
COPY ./ ./
RUN npm install express
CMD ["node" , "server.js"]
- 같은 Node.js 를 위한 이미지를 만들기 위해서 Dockerfile 을 만드는 것이기 때문에 전에 만들었던 Dockerfile 과 유사하게 만들면 된다.
Docker Containers 간 통신 할 때 나타나는 에러
- Dockerfile 을 만들었으니 실제로 어플을 실행해보자.
- 우선 어플이 어떤식으로 실행이 되는지 살펴보자.
현재 컨테이너가 2개가 동작하고 있다.
- (Node.JS APP , Redis Client) + (Redis Server)
- 컨테이너를 하나씩 실행해보자.
- 먼저 터미널을 열고 docker run redis 로 Redis Server 를 기동하고 또 다른 터미널에서는 Node.js 를 위한 컨테이너를 실행하면 된다.
- build 를 하고 run 을 하면 된다.
이때 에러가 발생하는데 이유가 무엇일까?
- 현재 서로 다른 컨테이너에 2개가 있는데, 컨테이너끼리 통신을 할 때 아무런 설정 없이는 접근을 할 수 없다.
- 즉 Node.js APP 이 Redis Server 에 접근할 수 없다.
그러면 어떻게 컨테이너 사이에 통신을 할 수 있을까?
멀티 컨테이너 상황에서 쉽게 네트워크를 연결시켜주기 위해서는 Docker Compose 를 이용하면 됩니다.
Docker Compose 파일 작성하기
- Docker Compose 가 컨테이너 사이에 네트워크를 연결시켜준다는 것을 알았다.
- 그렇다면 본격적으로 Docker Compose 파일을 작성해보자.
docker-compose 파일 구조
- docker-compose 안에 Redis-Server + Node-APP 2개의 컨테이너를 가지도록 한다.
version: "3"
services:
redis-server:
image: "redis"
node-app:
build: .
ports:
- "5000:8080"
- docekr-compose up 명령어로 실행
Docker Compose 로 컨테이너 멈추기
- 다른 터미널에서 docker-compose down 으로 컨테이너를 멈출 수 있다.
docker compose up : 이미지가 없을 때 이미지를 빌드하고 컨테이너 실행
docker compose up --build : 이미지가 있든 없든 이미지를 빌드하고 컨테이너 실행
만약 다른 터미널을 키지 않고 하나의 터미널로 해결하고 싶다면?
- docker compose up -d
- docker compose up 을 할 때 -d 옵션을 추가해주면 된다.
- -d 는 detached 모드로, 앱을 백그라운드에서 실행시키기 때문에 앱에서 나오는 output 을 표출하지 않는다.
그래서 -d 모드로 앱을 실행한다면 하나의 터미널에서 앱을 작동시키고 중단시킬 수 있다.
참고자료