도커 컴포즈를 이용한 Express-Typescript-MySQL 개발환경 구축

Ywoosang·2021년 10월 16일
14
post-thumbnail

들어가며

이번에 학회사이트 외주일을 하면서 Docker 를 이용해 IaC 개발환경을 구축했고 이를 정리하고자 한다. 그림과 같이 docker-compose 를 활용해 로컬 vscode 에서 코드를 변경하면 컨테이너 안에서 프로그램이 재시작되도록 만들었다. nodemon(node monitor) 는 노드가 실행하는 파일이 속한 디렉터리를 감시하고 있다가 파일이 수정되면 자동으로 노드 애플리케이션을 재시작한다. ts-node 는 메모리 상에서 타입스크립트를 transpile 해 바로 실행할 수 있게 해준다.

파일 구성

Dockerfile.dev 는 백엔드 API 서버 빌드파일이며 start.sh 에서 패키지를 설치하고 nodemon 권한을 부여해준다. setup.sql 은 데이터베이스 테이블에 생성 등에 관련된 정보를 담고 있으며 MySQL 컨테이너 실행시 적용되도록 한다.
마지막으로 docker-compose.yaml 파일은 두 docker-compose up 명령어로 두 이미지를 빌드후 컨테이너화하면서 데이터베이스 백업 볼륨과 네트워크를 생성한다.

컴포즈로 올릴 전체적 디렉토리 구조는 아래와 같다. 뒤 이미지 빌드 부분에서 참고 하길 바란다. 모든 사람이 도커에 익숙한 것은 아닐것이므로 이해가 되지 않는 부분이 있을 수 있기 때문에 글의 마지막 부분에서 직접 실행해 볼 수 있는 실습 파트를 만들어 두었다.

/server
├── db_volume/
├── src/
├── .env
├── custom.config
└── docker-compose.yaml
├── Dockerfile.dev
├── package.json
├── package-lock.json
├── start.sh
├── setup.sql
├── tsconfig.json
... 

API 서버 이미지

소스코드 구성

학회 API 서버는 Express 와 Typescript 로 이루어져 있다. 소스폴더 src 아래에서 api 소스코드가 변경되면 nodemon 이 서버를 재시작하도록 해주어야 한다.

개발서버 시작과 관련된 소스코드는 아래처럼 server.ts 에 존재한다. App 클래스의 listen 메소드를 호출하면 서버가 시작되도록 구성했다.

// src/server.ts
... 
const app = new App(
  [
     // 컨트롤러 
     ...
  ]
);
app.listen();

// src/app.ts
... 
class App {
    public app: express.Application;
	... 
    constructor(controllers) {
       ... 
    }
    ... 

    public listen() {
        this.app.listen(this.port, () => {
            ... 
        });
    }
}

npm script 구성

이미지를 빌드하기 전 기본적으로 typescript,ts-node,nodemon 이 설치되어 있어야 한다.

npm i -D typescript ts-node nodemon

package.json 의 scriptsdev 를 추가하자. npm run dev 를 입력하면 NODE_ENV=development nodemon src/server.ts 가 실행된다.

...
"scripts": {
    ...
    "dev": "NODE_ENV=development nodemon src/server.ts",
  	...
},

Dockerfile.server.dev 구성

도커파일은 다음과 같이 구성했다.

FROM node:14.5.0
COPY start.sh /usr/server/start.sh
RUN chmod 700 /usr/server/start.sh
COPY package.json /usr/server/package.json
COPY package-lock.json /usr/server/package-lock.json
COPY tsconfig.json /usr/server/tsconfig.json
CMD /usr/server/start.sh

베이스 이미지는 node:14.5.0 이미지로 진행한다. 만약 alpine 등 다른 버전 리눅스 이미지 사용을 원한다면 공식문서 링크를 참고하자

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

start.sh 를 COPY 명령어를 이용해 /usr/server/start.sh 경로에 복사한다. 컨테이너에 파일을 복사하는 명령어는 ADDCOPY 가 있는데, ADDCOPY보다 먼저 개발된 명령어다. ADD [source] [destination] 는 단순히 파일을 호스트에서 컨테이너로 복사하는 기능뿐만 아니라 [source]url 을 입력하면 다운로드해서 컨테이너에 추가하는 기능이 있다. 만약 특정 포맷의 압축파일이 [source] 일 경우 압축을 해제하기 때문에 압축파일을 넘기고 싶을 때 문제가 되어 COPY 가 개발되었다. 현 프로젝트의 경우 ADD 를 사용해도 무방하지만 보다 명확한 의미전달을 위해 COPY 를 사용했다.

RUN 명령어로 chmod 700 /usr/server/start.sh 명령어를 실행해 start.sh 에 권한을 부여한다. 리눅스에서 파일의 권한을 구분할때 크게 읽기(r:4), 쓰기(w:2), 실행 (x:1) 3가지로 분류할 수 있다. 옆에 붙은 숫자는 각각 실행 코드로 8진수로 나타낸다. 예를 들어 74+2+1 이므로 읽고 쓰고 실행이 모두 가능하다. 자릿수는 누구에게 권한을 부여하는지에 대해 나타내는 것인데 첫자리는 소유자 권한, 두번째 자리는 그룹 사용자 권한, 세번째 자리는 기타 사용자 권한이다. 여기서 700 권한이므로 소유자에게만 읽기, 쓰기, 실행을 허용하는 권한으로 해석할 수 있겠다. 그 외 사용자는 권한을 부여하지 않았다.

COPY 명령어로 package.json, package-lock.json, tsconfig.json 을 /usr/server/ 디렉토리에 추가한다.

CMD 명령어로 /usr/server/start.sh 경로에 있는 쉘 스크립트를 실행한다.

위처럼 구성해보니 /usr/server/ 디렉토리 내부에서 움직이는 것이 보였고 아래 공식문서 링크를 참고해 WORKDIR 명령어로 도커파일을 다음과 같이 개선했다.

FROM node:14.5.0
WORKDIR /usr/server
COPY start.sh .
RUN chmod 700 ./start.sh
COPY package.json .
COPY package-lock.json .
COPY tsconfig.json .
CMD ./start.sh

https://docs.docker.com/develop/develop-images/dockerfile_best-practices/

WORKDIR 명령어는 RUN, CMD, ENTRYPOINT, COPY, ADD 명령어의 실행 디렉토리를 잡아주는 명령어로 리눅스 cd 명령어와 비슷하다고 생각할 수 있다. 만약 해당 디렉토리가 존재하지 않는다면 생성한다. 몇몇 도커파일 예제에서 RUN mkdir ~ 처럼 도커파일에 WORKDIR 과 같이 디렉토리 생성 명령어를 쓰는 경우를 간혹 볼 수 있는데 그럴 필요가 없다.

shell script 구성

-도커파일에서 생성한 /usr/server 디렉토리로 들어간다.
npm i 명령어로 패키지를 설치한다.
패키지가 설치된 후 chmod +x 로 실행 권한을 부여한다.
npm run devnpm script 를 실행한다.

#!/bin/bash
cd /usr/server
npm i
chmod +x /usr/server/node_modules/.bin/nodemon 
npm run dev

docker-compose 구성

마지막으로 docker-compose를 구성했다. docker-compose는 다중 컨테이너 애플리케이션을 정의 및 공유할 수 있도록 개발된 도구로 명령을 사용하여 모두 실행 또는 종료가 가능하다. 여기서는 데이터베이스와 백엔드 API 애플리케이션 두 개의 서비스를 docker-compose up 명령을 통해 모두 실행할 수 있다.

version: '3.7'
services:
  server:
    build:
      context : .
      dockerfile: Dockerfile.dev
    container_name: kscic-app
    env_file: ./.env
    ports:
      - "3000:3000"
    depends_on:
      - database
    networks: 
      - kscic_net 
    environment:
      - COOKIE_SECRET=$COOKIE_SECRET
      - DATABASE_HOST=$DATABASE_HOST
      - DATABASE_PASSWORD=$MYSQL_ROOT_PASSWORD
      - PORT=$PORT
      - ACCESS_TOKEN_SECRET=$ACCESS_TOKEN_SECRET
      - AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID
      - AWS_SECRET_KEY_ID=$AWS_SECRET_KEY_ID
    volumes:
      - ./src:/usr/server/src
  database:
    image: mysql:8.0
    container_name: kscic-db
    environment:
      - MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_PASSWORD
    restart: always
    ports:
      - 3306:3306
    networks: 
      - kscic_net
    volumes:
      - ./my.cnf:/etc/mysql/conf.d/my.cnf
      - ./db_volume:/var/lib/mysql
      - ./setup.sql:/docker-entrypoint-initdb.d/setup.sql
    cap_add:
      - SYS_NICE

networks: 
  kscic_net:

volumes:
  kicic_volume:

구성한 컴포즈에 대한 내용을 한줄씩 설명한다.

버전, 서비스

version: '3.7'
컴포즈 버전으로 3.7 버전을 사용한다. 각 버전별 차이점은 공식문서에 나와있다.

https://docs.docker.com/compose/compose-file/compose-versioning/#version-38

services:
컴포즈 문서에 서비스들을 정의한다. 여기서는 server 와 database 라는 이름으로 두 개 서비스를 한번에 정의한다. 컴포즈에서는 컨테이너 대신 서비스라는 개념을 사용한다.

애플리케이션

build:
기존에 만들어진 이미지를 사용하지 않고 도커파일로 사용자 정의 이미지를 빌드한다.

context: .
docker build 명령을 실행할 디렉터리 경로다.

dockerfile: Dockerfile.dev
사용한 도커파일 이름이다.

container_name: kscic-app
서비스를 빌드할 때 사용할 컨테이너 이름을 지정할 수 있다.
docker run 명령어에서 --name 옵션에 해당하는 부분이라고 보면 된다. 현재 프로젝트에서는 kscic-db 와 kscic-app 으로 컨테이너 이름을 지정했으므로 도커 프로세스를 확인해보면 아래 이미지와 같이 표시된다.

env_file: ./.env
컴포즈 파일이 있는 디렉토리의 .env 파일을 이용한다. environment 에서 $NAME 형식으로 이용할 수 있다

ports:
- "3000:3000"
호스트머신의 3000 번 포트와 컨테이너 내부 3000 번 포트를 매핑한다. 쉽게 말해 컨테이너의 포트는 express 어플리케이션을 실행하는 포트고 호스트머신의 포트는 localhost로 접속할 포트라고 생각하자. 이때 호스트에서 3000번 포트로 접속하면 컨테이너의 3000번 포트에 매핑된다. 포트는 곧 실행할 프로그램이라고 생각할 수 있으므로 Express 애플리케이션이 실행된다.

depends_on:
- database
애플리케이션은 데이터베이스 컨테이너 프로세스가 준비된 상태에서 연결해야 한다. 따라서 컨테이너 실행 순서를 depends_on 을 이용해 데이터베이스가 준비되면 애플리케이션이 뜰 수 있도록 설정했다.

environment:
컨테이너 내부의 환경 변수를 여기서 설정한다.

- COOKIE_SECRET=$COOKIE_SECRET
- DATABASE_HOST=$DATABASE_HOST
- DATABASE_PASSWORD=$MYSQL_ROOT_PASSWORD
- PORT=$PORT
- ACCESS_TOKEN_SECRET=$ACCESS_TOKEN_SECRET
- AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID
- AWS_SECRET_KEY_ID=$AWS_SECRET_KEY_ID

애플리케이션에서 로컬 개발환경을 사용할 때 .env 파일에서 정의했던 것처럼 - 뒤 이름에 해당하는 환경변수를 사용할 수 있다.

export default {
    ... 
    development : {
        // 컴포즈 파일에서 정의 
        host: process.env.DATABASE_HOST,
        user: "root",
        port:3306,
        // 컴포즈 파일에서 정의 
        password: process.env.DATABASE_PASSWORD,
        database: "ClothingDB",
        connectionLimit: 5000,
        dateStrings: "date",
    },
    ... 
};

volumes:

- ./src:/usr/server/src

볼륨을 이용해 호스트 머신의 src 와 컨테이너 내부 /usr/server/src 파일의 내용을 동기화한다.

데이터베이스

image: mysql:8.0
도커 허브에 정의되어 있는 mysql:8.0 이미지를 그대로 사용할 것이다.

environment:

- MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_PASSWORD

환경변수에는 MYSQL_ROOT_PASSWORD 를 줄 것인데 Dockerfile 만 사용했을 때 MYSQL_ALLOW_EMPTY_PASSWORD 또는 MYSQL_ROOT_PASSWORD 를 설정했던것과 동일하다. 루트 계정 비밀번호를 환경변수로 정의한다.

volumes:

- ./my.cnf:/etc/mysql/conf.d/my.cnf

mysql이 시작할 때 /etc/mysql/my.cnf 파일의 설정을 사용하는데 /etc/mysql/conf.d에 설정이 있으면 이 두가지를 합친다. 만약 conf.d에 있는 설정과 /etc/my.cnf 설정이 중복되면 conf.d에 있는 것을 우선적용한다. 따라서 컨테이너 시작시에 호스트 시스템에 my.cnf 파일을 읽도록 볼륨으로 연결해야 한다.

[mysqld]
default-authentication-plugin=mysql_native_password
character-set-client-handshake=FALSE
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci

[client]
default-character-set = utf8mb4

[mysql]

default-character-set = utf8mb4

[mysqldump]
default-character-set = utf8mb4

- ./db_volume:/var/lib/mysql

컨테이너 프로세스가 내려갔다가 다시 올라와도 데이터를 유지하기 위해 볼륨으로 연결한다. 백업 볼륨 경로는 컴포즈 파일과 동일한 루트 리렉토리 안에 있는 db_volume 디렉토리로 지정했다. 다음과 같이 백업 파일 및 폴더가 생성된다.

- ./setup.sql:/docker-entrypoint-initdb.d/setup.sql

/docker-entrypoint-initdb.d/ 디렉토리 내부에 setup.sql 파일을 볼륨으로 연결한다. 컨테이너가 처음 시작했을 때 /docker-entrypoint-initdb.d/ 폴더 내부에 .sh, .sql, .sql.gz 파일이 있다면 해당 파일을 실행시킨다. 해당 내용은 도커 허브 공식문서를 통해 찾을 수 있었다.

Initializing a fresh instance 부분
https://hub.docker.com/_/mysql/

cap_add: - SYS_NICE

해당 부분은 mbind: Operation not permitted 이슈를 해결하기 위해 추가했다. mbind는 리눅스 명령어로 메모리 범위에 대한 메모리 정책 설정을 뜻한다. mbind를 사용하는 명령어에서 권한이 없는 문제가 발생했을 때 해당 이슈가 발생할 수 있는데 보통 컨테이너를 띄워두고 시간이 지났을때 발생한다. cap-add로 SYS_NICE 권한을 주면 더 이상 mbind: Operation not permitted 오류가 발생하지 않게 된다.

참고
https://data-engineer-tech.tistory.com/27

볼륨, 네트워크

기본적으로 컴포즈를 실행할때 별도 네트워크 혹은 볼륨 이름을 명시하지 않으면 default 로 네트워크 혹은 볼륨이 생성된다. default 네트워크 이름은 docker-compose.yml가 위치한 디렉토리 이름 뒤에 _default가 붙는다.

기본적으로 3개의 네트워크를 확인할 수 있다. 여기서 네트워크 이름을 명시하지 않고 컴포즈를 실행해보자.

default 네트워크가 생성된 것을 볼 수 있다. 다음으로 네트워크 이름을 명시해서 컴포즈를 실행한다.

별칭으로 지정한 네트워크가 생성된 것을 볼 수 있다. 볼륨도 마찬가지로 별칭으로 지정된다. 여기서는 default 네트워크 대신 해당 서비스 이름에 맞는 별칭을 부여해 추후 다른 컴포즈를 띄울때 생길 네트워크들과 구별을 용이하게 했다. 현 개발환경에서 없애고 싶다면 없애도 되는 부분이다.

접속 테스트

컴포즈로 접속을 테스트해보았다. 아래처럼 Vue 프론트엔드에서 컴포즈로 띄운 주소로 요청을 전송해보았다.

# .env.dev
NODE_ENV=dev
VUE_APP_BASE_URL=http://localhost:3000/
// utils/request.ts
... 
const service = axios.create({
    baseURL : process.env.VUE_APP_BASE_URL,
    timeout : 5000
});
... 

네트워크 탭과 컴포즈 로그를 확인해보면 정상적으로 응답이 온 것을 확인할 수 있었다.

DB 볼륨 테스트

마지막으로 컨테이너를 내려도 데이터베이스가 정상적으로 백업이 되는지 확인해보았다.

테스트 게시글을 하나 작성한다.

컴포즈를 내린다.

다시 컴포즈를 올리고 mysql 컨테이너에 접속해본다.

게시글을 확인하면 컨테이너를 내렸다 올리기 전 작성한 게시글이 잘 살아 있다.

실습

여기까지 구성한 개발환경을 실습할 수 있도록 레파지토리를 만들었다.

실습 Github 주소
https://github.com/Ywoosang/Docker-Express-Typescript-MySQL

Readme 에 써놓은대로 작업을 수행한다. 한국어 버전도 추가했다.

먼저 위 주소로 접속해 원하는 디렉토리에서 프로젝트를 클론한다.

.env.example.env 로 파일 이름을 변경한다.

docker-compose up 명령어를 입력한다. 성공적으로 컴포즈가 올라간 것을 볼 수 있을 것이다.

타입스크립트 소스코드를 변경해보면 컨테이너 내부 소스코드 또한 변경된다. 공백을 추가하고 저장하면

서버가 재시작되는 것을 볼 수 있다.

마지막으로 REST 요청이 잘 들어가는지 확인해보자. 애플리케이션은 localhost 의 3000 번 포트로 프로그램이 실행되고 있을것이다.
request.rest 파일에서 SendRequest 로 요청을 보내 확인해볼 수 있도록 구성했다.

요청이 성공적으로 응답되었다면 성공이다.

마무리말

지금까지 Node.js, Express, Typescript 로 구성된 어플리케이션과 Mysql 데이터베이스로 이루어진 개발 환경을 도커 컴포즈를 통해 구성하는 방법을 알아보았습니다. 저는 처음 도커를 배울때 막막했었는데 이번 포스팅이 다른 분들께 유익했으면 좋겠습니다.

실습이 도움이 되었다면 레파지토리에 🌟 를 눌러주시면 감사하겠습니다.
버그 제보는 issue 로 부탁드립니다.
개선사항은 pull request 를 올려주시면 확인하겠습니다.

profile
백엔드와 인프라에 관심이 많은 개발자 입니다

4개의 댓글

comment-user-thumbnail
2021년 10월 17일

재밌네요~~~

1개의 답글
comment-user-thumbnail
2021년 10월 17일

ㄷㄷㄷ 포스팅이 되게 자세하네욤

1개의 답글