1/26/2024

Docker Compose와 MongoDB

발단

4iz Dockerfile 파일과 Docker Compose 파일을 수정하고 헷갈리고 내용을 정리하면서 하는 김에 Wevre Dockerfile 파일과 Docker Compose 파일을 작성하기로 했다. 그런데 Wevre는 MySQL과 같은 관계형 데이터베이스가 아니라 NoSQL인 MongoDB라서 Dockerfile 파일과 Docker Compose 파일 작성하면서 여러 문제도 발생하고 해결하면서 배운 점이 많아서 이에 대해 작성한다~


Dockerfile

일단 Dockerfile의 경우 애플리케이션 자체 Dockerfile 작성에는 별 문제가 없었다. 문제는 Docker Compose를 사용할 경우 4iz처럼 작품, 카테고리, 재료와 같은 컬렉션의 도큐먼트가 미리 존재해야 한다는 것이다. Docker Hub에서 MongoDB 이미지의 세부사항을 읽었는데 JavaScript 파일(.js) 혹은 셸 스크립트 파일(.sh)을 사용하라고 적혀있다. 하지만 MySQL의 mysqldump 명령처럼 MongoDB의 mongodump 명령을 통해 추출된 데이터베이스는 BSON 파일이다. 검색을 해서 도출한 결과는 Docker Compose에서 MongoDB 서비스가 추출된 BSON 파일을 들여오는 서비스가 필요하다는 것이다. 다시 말해, 이 서비스에 해당하는 Dockerfile이 필요하다!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Dockerfile
FROM mongo:6.0.7
 
WORKDIR /opt/bson
# 대상은 원본이 대상 컨테이너 내부의 어느 위치로 복사될지를 나타내는 절대 경로 또는 WORKDIR을 기준으로 한 상대 경로이다.
# 절대 경로: /절대 경로/
# 상대 경로: <WORKDIR>/상대 경로/
# 원본이 디렉터리인 경우, 해당 디렉토리 전체 내용이 파일 시스템 메타데이터와 함께 복사되지만 디렉터리 자체는 복사되지 않는다.
COPY bson/ ./
 
WORKDIR /opt/script
# 원본 경로는 반드시 빌드 컨텍스트 내에 있어야 하는데 빌더는 컨텍스트에서만 파일에 접근할 수 있으며 ../은 빌드 컨텍스트 루트의 상위 파일 또는 디렉터리를 지정하기 때문이다.
COPY seed.sh .
 
RUN chmod +x seed.sh
 
CMD ["sh", "-c", "/opt/script/seed.sh"]
 
// seed.sh
#! /bin/bash
 
MONGO_HOST="DATABASE"
MONGO_DATABASE="wevre"
MONGO_USERNAME="csup"
MONGO_PASSWORD="csup"
 
mongorestore --host $MONGO_HOST -u $MONGO_USERNAME -p $MONGO_PASSWORD -d $MONGO_DATABASE -c users /opt/bson/users.bson
mongorestore --host $MONGO_HOST -u $MONGO_USERNAME -p $MONGO_PASSWORD -d $MONGO_DATABASE -c materials /opt/bson/materials.bson
mongorestore --host $MONGO_HOST -u $MONGO_USERNAME -p $MONGO_PASSWORD -d $MONGO_DATABASE -c categories /opt/bson/categories.bson
mongorestore --host $MONGO_HOST -u $MONGO_USERNAME -p $MONGO_PASSWORD -d $MONGO_DATABASE -c items /opt/bson/items.bson
mongorestore --host $MONGO_HOST -u $MONGO_USERNAME -p $MONGO_PASSWORD -d $MONGO_DATABASE -c rooms /opt/bson/rooms.bson
mongorestore --host $MONGO_HOST -u $MONGO_USERNAME -p $MONGO_PASSWORD -d $MONGO_DATABASE -c bids /opt/bson/bids.bson
cs
작성된 Dockerfile을 보면 빌드 컨텍스트와 원본 경로에 대한 내용이 있는데 이는 Docker Compose에서 다루기로 한다! 원본의 bson 디렉터리와 seed.sh 셸 스크립트는 seed 디렉터리 밑에 위치한다. 이 도커파일은 /opt/bson 디렉터리에 BSON 파일을 복사하고 opt/script에 seed.sh 셸 스크립트를 복사한다. 그리고 셸 스크립트에 실행 권한을 주고 셸 스크립트를 실행하며 mongorestore 명령을 사용하여 BSON 파일의 데이터를 읽어온다. 이제 Docker Compose 파일을 작성하자!


Docker Compose

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
services:
  app:
    container_name: wevre-app
    image: wevre
    build:
      dockerfile: Dockerfile
      context: .
      target: dev
    command: sh -c '/wait && npm run start:dev'
    ports:
      - '3000:3000'
    volumes:
      - .:/opt/node_app/app
      - ./package.json:/opt/node_app/package.json
      - ./package-lock.json:/opt/node_app/package-lock.json
    environment:
      - NODE_ENV=development
      - HOST=localhost
      - PORT=3000
      - DB_HOST=db
      - DB_PORT=27017
      - DB_DATABASE=wevre
      # mongo-init.js 파일에서 wevre 데이터베이스에 해당하는 사용자와 비밀번호를 생성한다.
      - DB_USERNAME=csup
      - DB_PASSWORD=csup
      - DB_LOGGING=true
      - JWT_ACCESS_TOKEN_SECRET=wevre-jwt-access-token
      - JWT_ACCESS_TOKEN_EXPIRATION=1800000
      - JWT_REFRESH_TOKEN_SECRET=wevre-jwt-refresh-token
      - JWT_REFRESH_TOKEN_EXPIRATION=86400000
      - CACHE_TTL=120
      - CACHE_MAX=1000
      - REDIS_HOST=cache
      - REDIS_PORT=6379
      - WAIT_HOSTS=db:27017
      - WAIT_TIMEOUT=600
      - WAIT_SLEEP_INTERVAL=10
      - WAIT_HOST_CONNECT_TIMEOUT=10
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      disable: true
 
  db:
    container_name: wevre-db
    image: mongo:6.0.7
    ports:
      - '27017:27017'
    environment:
      - MONGO_INITDB_ROOT_USERNAME=root
      - MONGO_INITDB_ROOT_PASSWORD=root
      - MONGO_INITDB_DATABASE=wevre
    volumes:
      - ./seed/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js
      - db:/data/db
    healthcheck:
      test: '[ `echo ''db.runCommand("ping").ok'' | mongo localhost/wevre --quiet` ] && echo 0 || echo 1'
      interval: 5s
      timeout: 4s
      retries: 5
      start_period: 5m
 
  db-express:
    container_name: wevre-express
    image: mongo-express:1.0.2-20
    ports:
      - '8082:8081'
    environment:
      - ME_CONFIG_MONGODB_ENABLE_ADMIN=true
      - ME_CONFIG_MONGODB_SERVER=db
      - ME_CONFIG_MONGODB_PORT=27017
      - ME_CONFIG_BASICAUTH_USERNAME=user
      - ME_CONFIG_BASICAUTH_PASSWORD=user
      - ME_CONFIG_MONGODB_ADMINUSERNAME=root
      - ME_CONFIG_MONGODB_ADMINPASSWORD=root
 
  seed:
    container_name: wevre-seed
    image: seed
    build:
      dockerfile: Dockerfile
      # context는 Dockerfile이 포함된 디렉터리의 경로를 정의한다.
      # 상대 경로인 경우 Compose 파일의 위치를 기준으로 해석된다.
      # 명시적으로 설정되지 않으면 context는 프로젝트 디렉터리(.)로 기본 설정된다.
      context: ./seed
 
  cache:
    container_name: wevre-cache
    image: redis:7.2.4-alpine
    ports:
      - '6379:6379'
 
  cache-commander:
    container_name: wevre-cache-commander
    image: rediscommander/redis-commander:latest
    environment:
      - REDIS_HOSTS=local:cache:6379
    ports:
      - '8081:8081'
    depends_on:
      - cache
 
volumes:
  db:
cs

4iz와 달리 Wevre는 Docker Compose 파일을 작성하는데 애를 좀 먹었다. 특히 MongoDB 데이터베이스인 db 서비스에서 볼륨 부분이다! 환경 변수를 보면 MONGO_INITDB_ROOT_USERNAME와 MONGO_INITDB_ROOT_PASSWORD는 새로운 사용자를 생성하고 비밀번호를 설정한다. 이 사용자는 admin 데이터베이스에 생성되며 루트(root) 역할(즉 슈퍼사용자)을 가진다. 그리고 MONGO_INITDB_DATABASE는 docker-entrypoint-initdb.d/*.js의 생성 스크립트에 사용할 데이터베이스의 이름을 명시한다. 이 부분이 아까 언급한 셸 스크립트와 이상하다고 볼 수 있다. 왜냐하면 데이터베이스의 이름은 같지만 셸 스크립트의 MONGO_USERNAME는 csup이고 MONGO_PASSWORD도 csup이기 때문이다.

Docker Hub의 MongoDB의 데이터베이스 초기화를 보면 앞서 말한 것처럼 JavaScript 파일 혹은 셸 스크립트 파일이 필요하다고 설명한다. 다시 말해, 컨테이가 시작하면 docker-entrypoint-initdb.d/ 위치하는 파일들을 실행한다. 즉, 여기에서 MONGO_INITDB_DATABASE에 적힌 데이터베이스를 생성해야 한다는 의미이다! 상당히 오랜시간 검색을 한 결과 데이터베이스 초기화 JavaScript 파일을 작성하였다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const ADMIN_USERNAME = 'root';
const ADMIN_PASSWORD = 'root';
const ADMIN_DATABASE = 'admin';
const USERNAME = 'csup';
const PASSWORD = 'csup';
const DATABASE = 'wevre';
const ROLE = 'readWrite';
 
// 항상 생성되는 admin 데이터베이스로 이동한다.
db = db.getSiblingDB(ADMIN_DATABASE);
 
// Docker Compose의 MONGO_INITDB_ROOT_USERNAME와 MONGO_INITDB_ROOT_PASSWORD을 사용해서 루트 사용자로 로그인한다.
db.auth(ADMIN_USERNAME, ADMIN_PASSWORD);
 
// MONGO_INITDB_DATABASE에 적힌 이름으로 데이터베이스를 생성한다. 
db = db.getSiblingDB(DATABASE);
 
// 새로운 사용자를 생성한다.
db.createUser({
  user: USERNAME,
  pwd: PASSWORD,
  roles: [
    {
      role: ROLE,
      db: DATABASE,
    },
  ],
});
cs

코드를 보면 왜 app 서비스의 환경 변수에서 DB_USERNAME과 DB_PASSWORD가 csup인지 알 수 있다!


실행

docker compose up --build 명령으로 애플리케이션을 실행시키고 http://localhost:8082에 들어가면 다음과 같이 데이터베이스 wevre가 존재하고 필요한 컬렉션과 도큐먼트가 채워져있다!
성공!

update: 2024.01.26

댓글 1개: