본문 바로가기
docker-kub

docker - 멀티 컨테이너 어플리케이션

by 오우지 2022. 10. 30.

지난번 포스팅에서 두 가지의 컨테이너를 띄우고 둘 사이의 네트워킹 방법에 대해 알아봤는다.

보통 도커는 각각의 어플리케이션마다 컨테이너를 띄우는 다중 컨테이너로 구성한다. 그게 도커에서 권장하는 방법이다.

 

개발 환경에서 해당 요구사항을 반영해서 어플리케이션을 각각 컨테이너화 해보자

주의해야 할 점

1. 몽고DB의 데이터는 항상 볼륨에 저장되어야 하며

2. 접근은 ID와 비밀번호를 통해 접속할 수 있어야 한다.

 

백엔드 어플리케이션은

1. 데이터가 항상 유지되어야 하며 

2. 개발 환경을 가정했으므로 코드 변동이 자동 적용되어야 한다.

 

프론트 어플리케이션은

1. 코드 변동이 자동으로 적용돼야 한다.

 

 

1. 몽고DB

docker run --name mongodb --rm -d -p 27017:27017 mongo

이전에 연습해봤다면 컨테이너 이름이 중복돼서 실행이 될 건데 이전 기본 명령어에서 했듯

docker rm mongodb
docker container prune

두 가지를 이용해 종료된 컨테이너를 제거해주고 시작하면 된다.

 

 

2. 백엔드

몽고DB는 도커허브의 몽고DB를 가져와서 컨테이너화 해줬지만 노드 백엔드는 커스텀 어플리케이션이기 때문에 폴더 안의 프로젝트를 이미지화해줘야 한다. 따라서, Dockerfile 파일을 만들어서 빌드 플로우를 적어준다.

 

FROM node

WORKDIR /app

COPY package.json .

RUN npm install

COPY . .

EXPOSE 80

CMD ["node", "app.js"]

그런다음 빌드를 해주고

docker build -t goals-node .

실행시키면

docker run --name goals-backend --rm goals-node

뱉어버렸다.

 

몽고DB에 연결하지 못했단다. 현재 실행한 이미지를 실행하면 로컬 머신에는 총 두개의 도커 컨테이너가 떠 있게 된다. 만약 DB만 컨테이너화하고 백엔드를 실행시킨다면 위에서 몽고DB는 포트를 열어줬기 때문에 DB 연결이 가능하지만 현재 서버는 로컬 머신의 포트와 연결할 방법이 없다.

 

mongoose.connect(
  'mongodb://host.docker.internal:27017/course-goals',
  {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  },
  (err) => {
    if (err) {
      console.error('FAILED TO CONNECT TO MONGODB');
      console.error(err);
    } else {
      console.log('CONNECTED TO MONGODB');
      app.listen(80);
    }
  }
);

 

이 때 백엔드 서버에 예약어 host.docker.internal을 통해서 호스트 머신의 포트와 연결해줄 수 있다.

 

여기까진 문제가 없다. 하지만, 가장 위에서 이야기했던 요건을 생각해보면 백엔드 어플리케이션과 프론트엔드 어플리케이션이 통신할 방법이 없고 실제로 로컬에서 run 시킨 react 어플리케이션이 서버를 찾지 못하고 있다.

 

docker run --name goals-backend --rm -d -p 80:80 goals-node

이렇게 포트를 연결해주면 된다.

 

3. 프론트엔드

react 어플리케이션 또한 커스텀 어플리케이션이기 때문에 빌드를 위해선 Dockerfile을 생성해줘야 한다.

FROM node

WORKDIR /app

COPY package.json .

RUN npm install

COPY . .

EXPOSE 3000

CMD [ "npm", "start"]

이렇게 만들어주고

 docker build -t goals-react .

빌드하고

 

docker run --name goals-frontend --rm -d -p 3000:3000 goals-react

실행해주자.

 

하지만 위의 리액트 어플리케이션은 종료된다.

 

attach 모드로 다시 실행해보면

docker run --name goals-frontend --rm -p 3000:3000 goals-react

react는 상호통신이 안되면 자동으로 종료되는 옵션이 있다.

docker run --name goals-frontend --rm -p 3000:3000 -it goals-react

-it 옵션을 통해 인터렉티브 모드로 실행하면 개발 서버가 실행된 채로 유지된다.

그 전에 react에서 문제가 있어서 살펴보지 latest를 사용해서 생기는 문제라 해 Dockerfile을 수정해줬다.

 

FROM node:16

WORKDIR /app

COPY package.json .

RUN npm install

COPY . .

EXPOSE 3000

CMD [ "npm", "start"]

뭔가 익숙한놈이 튀어나왔다.

 

이렇게 멀티 컨테이너 환경을 구축해서 세 개의 독립된 컨테이너를 호스트 머신의 포트를 이용해 통신하게 만들어줬다.

하지만 이건 우리가 원하던 환경이 아니다. 요건들에 대해 생각해보기 전에 우선 하나의 네트워크 안에서 구성해보자.

 

4. 네트워킹

우선 네트워크를 하나 만들어보자

docker network create goals-net

만든 네트워크 안에서 몽고 컨테이너를 띄워보자.

docker run --name mongodb --rm -d --network goals-net mongo

백엔드도 해주는데 아까 호스트 머신과 통신하기 위한 host.docker.internal을 mongodb로 바꿔주고 또 goals-net 안에 넣어주면 된다. 이제 귀찮으니까 빌드는 알아서 했고 컨테이너를 띄워보자.

 

docker run --name goals-backend --rm -d --network goals-net goals-node

 

프론트도 같은 개념으로 생각하면 

 

useEffect(function () {
    async function fetchData() {
      setIsLoading(true);

      try {
        const response = await fetch('http://localhost/goals');

        const resData = await response.json();

        if (!response.ok) {
          throw new Error(resData.message || 'Fetching the goals failed.');
        }

        setLoadedGoals(resData.goals);
      } catch (err) {
        setError(
          err.message ||
            'Fetching goals failed - the server responsed with an error.'
        );
      }
      setIsLoading(false);
    }

    fetchData();
  }, []);

위의 url에 goals-backend를 입력해주면 될 것이다.

 

입력해주고 빌드, 컨테이너를 실행하면?

docker run --name goals-frontend --network goals-net --rm -p 3000:3000 -it goals-react

개발자 도구에서 보면 맛이 간다. 지금까지 실행했던 backend 어플리케이션은 모두 로컬머신에서 실행했지만 react 어플리케이션은 브라우저에서 실행된다. 따라서 docker에 의해 url이 매핑되지 못하는 문제가 있다. 이럴땐 우선 goals-backend를 다시 localhost로 바꿔주고 로컬머신의 80 포트를 열어줘야 한다.

 

의미없어진 --network 옵션을 없애고 react를 다시 실행하고

docker run --name goals-frontend --rm -p 3000:3000 -it goals-react

 

백엔드에 기존에 있던 network 이외에도 80포트와 통신할 수 있게 포트를 열어주면 된다.

 

docker run --name goals-backend --rm -d -p 80:80 --network goals-net goals-node

 

5. 몽고DB 데이터 지속성과 보안 강화하기

1. 데이터 지속성

 

첫번째 단계에서 만들어줬던 몽고 DB는 컨테이너를 종료했다 다시 실행하면 데이터가 다 사라지는 마법이 있다.

이를 해결하기 위해 이전에 배웠던 볼륨을 이용해 볼 것이다.

 

개발용으로 당장 로컬 머신에 저장할 필요가 없으므로 바인트 마운트가 아닌 명명된 볼륨으로 만들어보자

run --name mongodb -v data:/data/db --rm -d --network goals-net mongo

이러면 컨테이너를 껐다 켜도 어플리케이션의 데이터가 사라지지 않는다.

 

2. 보안 강화

https://hub.docker.com/_/mongo

 

mongo - Official Image | Docker Hub

Quick reference Supported tags and respective Dockerfile links Note: the description for this image is longer than the Hub length limit of 25000, so the "Supported tags" list has been trimmed to compensate. See docker/hub-beta-feedback#238 for more informa

hub.docker.com

여타 DB처럼 몽고 DB도 ID와 PASSWORD를 이용한 로그인을 지원한다.

 

docker run --name mongodb -v data:/data/db --rm -d --network goals-net -e MONGO_INITDB_ROOT_USERNAME=hojin -e MONGO_INITDB_ROOT_PASSWORD=secret mongo

요로코롬 적어주면 되는데 이전의 백엔드 코드에서 ID와 PASSWORD를 입력 안해줬기 때문에 데이터 접근에 문제가 생긴다.

 

mongoose.connect(
  'mongodb://hojin:secret@mongodb:27017/course-goals?authSource=admin',
  {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  },
  (err) => {
    if (err) {
      console.error('FAILED TO CONNECT TO MONGODB');
      console.error(err);
    } else {
      console.log('CONNECTED TO MONGODB');
      app.listen(80);
    }
  }
);

몽고DB 가이드대로 ID, PASSWORD, authSource를 적어주고 다시 실행하면 정상작동하게 된다.

 

여러번 실행하면 명명된 볼륨을 다른 죽어버린 컨테이너가 선점하게 되면서 DB가 안 뜨는 경우가 있는데

docker volume rm {volume}

으로 삭제해주고 다시 실행하면 정상 실행된다.

 

6. 노드 백엔드 로그파일 유지, 개발환경에서의 라이브 코드 업데이트

docker run --name goals-backend -v C:\docker\backend:/app -v logs:/app/logs 
-v /app/node_modules -d --rm -p 80:80 --network goals-net goals-node

로그 파일 유지와 로컬의 node_modules 컨테이너 복사 방지, 코드 업데이트를 위한 볼륨 세개를 각각 선언해주고

이전에 백엔드 모듈의 소스 코드 업데이트 후 자동 재시작 기능을 제공해주던 nodemon을 dependences에 추가해준다.

 

{
  "name": "backend",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "nodemon app.js"
  },
  "author": "hojin jang",
  "license": "ISC",
  "dependencies": {
    "body-parser": "^1.19.0",
    "express": "^4.17.1",
    "mongoose": "^5.10.3",
    "morgan": "^1.10.0"
  },
  "devDependencies": {
    "nodemon": "^2.0.4"
  }
}

 

몽고 DB의 username, password는 환경변수로 치환해서 명령어에서 넣어준다.

`mongodb://${process.env.MONGODB_USERNAME}:${process.env.MONGODB_PASSWORD}@mongodb:27017/course-goals?authSource=admin`,

 

7. 리엑트 어플리케이션 라이브 소스 코드 업데이트

똑같이 개발 환경을 가정하고 리엑트 소스가 변경될 때 마다 재시작되게 해보자.

docker run -v C:\frontend\src:/app/src --name goals-frontend --rm -p 3000:3000 -it goals-react

끝.