들어가며

이번 게시글에서 어떻게 Express 서버 어플리케이션을 Docker를 이용해서 컨테이너화 후 가동하고, 어떻게 로컬에서 작업한 코드 변경사항이 현재 가동되고 있는 Express 서버 컨테이너에 즉시 반영될 수 있는지에 대해서 알아보도록 하겠습니다.

Express 서버 어플리케이션 작성

원하는 위치에 디렉토리 생성 후 그 안에 package.json 파일 생성

$ mkdir demo && cd demo
$ npm init -y

npm 을 통해 Express 패키지 설치

$ npm install express

index.js 파일 생성 후 간단한 Express 서버 코드 작성

index.js

const express = require("express");
const app = express();
const port = 3000;

app.get("/", (req, res) => res.send("Hello Express!"));

app.listen(port, () =>
  console.log(`Example app listening at http://localhost:${port}`)
);

node 로 서버를 실행하기 위한 npm start 스크립트 package.json 파일에 추가

package.json

{
  // ...
  "scripts": {
    "start": "node ."
  }
  // ...
}

이제부터 npm start 명령을 실행해 Express 서버 어플리케이션을 실행할 수 있습니다.

 

Docker 를 이용한 개발 환경을 구축하기 위해서는 Express 서버 어플리케이션에 대한 이미지를 생성하기 위한 Dockerfile 을 작성하는 것입니다.

Dockerfile 작성

Dockerfile

# Base 이미지를 nodeJS alpine 버전으로 사용
FROM node:alpine

# 작업 디렉토리 전환
WORKDIR /app

# local 컴터에있는  package.json 파일을 현재 워킹 디렉토리에 복사
COPY package*.json ./

# local machine 에서 npm install 실행 
RUN npm install --silent

# 코드 복사
COPY . .

# 어플리케이션 구동
CMD ["npm", "start"]
EXPOSE 3000

Docker는 Dockerfile에 작성된 명령어를 순차적으로 실행하면서 image 를 빌드(build)합니다.

image에는 어플리케이션 코드뿐만 아니라 어플리케이션 실행에 필요한 환경 구성, 작업 내용이 포함되어 있습니다.

 

Docker image의 파일 시스템에 있는 node_modules 디렉토리가 현재 로컬 작업 디렉토리의 node_modules 디렉토리로 덮어씌워지는 걸 방지하기 위해 .dockerignore 파일도 추가해줍니다.

.dockerignore

node_modules

.dockerignore 파일은 docker image를 빌드할 때, 어떤 파일을 제외시킬 것인지를 명시하는 파일입니다.

Docker image 빌드 및 Container 실행

다음의 명령어를 이용하여 위에서 작성한 Dockerfile 을 이용하여 Container Image 를 빌드합니다.

$ docker build --tag app:0.1 .

주의할 점은 점(.) 앞에 반드시 빈칸을 넣는 것입니다. 마지막 . 의 의미는 도커 파일의 경로를 지정하기 위한 것입니다.

 

다음의 명령어를 이용하여 빌드한 이미지를 확인할 수 있습니다.

$ docker images

빌드한 이미지를 확인했다면 이제 빌드된 이미지를 Docker container로 실행할 차례입니다.

host의 포트 8080으로 들어오는 트래픽을 container의 포트 3000으로 포워딩시키겠습니다.

$ docker run -d -p 8080:3000 app

 

현재 구동 중인 Docker container 는 다음 명령을 통해 알 수 있습니다.

$ docker ps

 

이제 브라우저 또는 새 터미널 탭에서 http://localhost:8080 에 접속하면 "Hello Express!" 를 확인하실 수 있습니다.

$ curl http://localhost:8080
Hello Express!

여기서 주의할 점은 http://localhost:3000 이 아닌 http://localhost:8080에 접속해야 한다는 것입니다.

3000은 container의 내부 네트워크에서 사용되는 포트이고, 현재 host 에서 접속하는 상황이므로 포워딩된 포트 8080으로 접속해야 합니다.

 

지금까지 Express 서버 어플리케이션을 Docker를 이용해서 어떻게 컨테이너화하고 가동시키는지 알아보았습니다.

 

이어서 어떻게 로컬에서 작업한 코드 변경사항이 현재 가동되고 있는 Express 서버 컨테이너에 즉시 반영될 수 있는지에 대해 알아보겠습니다.

코드 변경 실시간 감지를 위한 Nodemon 패키지 적용

npm 으로 nodemon 패키지 설치

$ npm install -D nodemon

node 대신 nodemon으로 서버를 실행하기 위해 npm start 스크립트를 수정합니다.

package.json

{
  // ...
  "scripts": {
    "start": "nodemon ."
  }
  // ...
}

다시 서버를 시작하고, 코드를 수정하면 바로 서버가 재시작되어 변경사항이 반영되는 것을 확인하실 수 있을 것입니다.

Image 재빌드

Nodemon 패키지를 추가로 설치했고, package.json 스크립트가 변경되었기 때문에 container image를 재빌드해야 합니다.

$ docker build --tag app:0.2 .

이제 재빌드된 image를 Docker container로 다시 실행해보면 마찬가지로 Nodemon이 어플리케이션을 실행해주는 것을 확인할 수 있습니다.

$ docker run -d -p 8080:3000 app

 

현재 구동 중인 Docker container 는 다음 명령을 통해 알 수 있습니다.

$ docker ps



로컬에서 코드를 변경한 후 변경사항이 잘 반영되는지 확인해보았습니다. Nodemon 으로 코드 변경사항을 실시간으로 감지함에도 불구하고 코드 변경사항은 반영되지 않았습니다.

그 이유는 바로 container는 host로부터 격리된 파일 시스템을 가지기 때문에 container는 image 빌드 당시의 코드 복사본을 계속 참조하고 있어 image 빌드 시점 이후의 코드 변경사항에 대해서는 감지하지 못합니다.

이 문제를 해결하기 위해 container 내부에서 구동 중인 어플리케이션도 로컬 작업 디렉토리를 참조할 수 있도록 마운트(mount)해줘야 합니다.

$ docker run -d -p 8080:3000 -v $PWD:/app app:0.2

여기서 $PWD 는 현재 로컬 작업 디렉토리 위치를 의미하며, /app 은 컨테이너에서 작업 디렉토리 위치를 의미합니다.

로컬 작업 디렉토리와 컨테이너 상의 작업 디렉토리 간 sync 를 맞춰주었습니다.
이를 바인드 마운트라고도 합니다.

바인드 마운트를 이용해서 로컬 작업 디렉터리를 컨테이너의 특정 경로(작업 디렉토리 위치)에 마운트해주면 코드를 변경할 때 마다 변경 사항을 실시간으로 컨테이너를 통해 확인할 수 있습니다.

 

이제, 로컬에서 코드를 수정하면 코드 수정 사항이 컨테이너 상의 작업 디렉토리 상에도 동일하게 반영되며 container 에서 돌아가는 Nodemon이 변경 사항을 감지하고 서버를 재시작해줄 것입니다.

 

 

복사했습니다!