저번 포스트에 이어서 도커를 활용해 가상 서버에 배포하는 과정을 알아보겠습니다.
이미지를 통해 컨테이너를 생성하면 해당 컨테이너 내의 변경점은 이미지에 반영되지 않는다. 모든 변경점은 사라지게 된다. 배포 환경에서는 코드가 변할 일이 없고 업데이트가 있다면 새로 이미지를 만들어 받아 실행하면 그만이기 때문에 문제가 없다. 하지만 실시간으로 코드가 변하는 개발 환경에서 코드가 변경될 때마다 일일이 이미지를 만들고 컨테이너를 실행하고 테스트하는 것은 비효율적이다. 따라서 도커가 제공하는 볼륨을 통해 컨테이너와 로컬 파일 시스템을 연결하여 사용하는 개발 환경을 구축할 수 있다.
컨테이너와 로컬 파일 시스템을 연결하는 방식에는 두가지 방식이 있다. 도커 볼륨을 생성하고 컨테이너와 마운트하는 방식과 로컬 디렉토리를 직접 컨테이너에 마운트하는 방식이 있다.
$ docker volume create --name [volume-name]
docker volume create
명령어를 통해 볼륨을 생성할 수 있다. 이름을 지정하지 않으면 이름 없는 볼륨이 생성된다. 이렇게 생성한 볼륨은 docker volume ls
명령어를 통해 조회할 수 있다.
$ docker volume inspect [volume-name]
docker volume inspect
명령어를 통해 볼륨의 상세 정보를 조회할 수 있고 볼륨이 사용하는 로컬 디렉토리 위치를 알 수 있다.
프로젝트 루트에 Dockerfile을 생성해 작성한다.
FROM node:16-alpine
# Korean Fonts
RUN apk --update add fontconfig
RUN mkdir -p /usr/share/fonts/nanumfont
RUN wget http://cdn.naver.com/naver/NanumFont/fontfiles/NanumFont_TTF_ALL.zip
RUN unzip NanumFont_TTF_ALL.zip -d /usr/share/fonts/nanumfont
RUN fc-cache -f && rm -rf /var/cache/*
# update pkg manager
RUN apk update
# bash install
RUN apk add bash
# Language
ENV LANG=ko_KR.UTF-8 \
LANGUAGE=ko_KR.UTF-8
# Set the timezone in docker
RUN apk --no-cache add tzdata && \
cp /usr/share/zoneinfo/Asia/Seoul /etc/localtime && \
echo "Asia/Seoul" > /etc/timezone
# Create Directory for the Container
WORKDIR /user/src/app
# add chromium
RUN apk add chromium
# Only copy the package.json file to work directory
COPY package.json .
RUN npm install
# Docker Demon Port Mapping
EXPOSE 80
# RUN SERVER
CMD ["npm", "run", "dev"]
베이스 이미지는 lts 버전인
node:16-alpine
을 사용했다. 이 이미지는 경량 리눅스인알파인 리눅스
에 노드 환경이 구성된 베이스 이미지로 기본 패키지 매니저로apk
를 사용한다
\ 을 사용하면 여러줄 입력을 사용할 수 있다. && 와 \ 을 사용하여 여러 명령어를 가독성있게 작성할 수 있다
크로미움 설치는 필자의 애플리케이션 실행에 필요하기 때문에 필요한 것이다. 크로미움 브라우저가 필요한 것이 아니라면 생략한다
컨테이너의
/usr/src/app
디렉토리를 WORKDIR로 지정하고 로컬의package.json
을 복사해 노드 관련 의존성을 설치한다
CMD npm run dev 명령어는 개발시 사용하는 nodemon 으로 앱을 실행하라는 스크립트이다. 스크립트 내용은 아래와 같다.
"dev": "export NODE_OPTIONS='--trace-deprecation --abort-on-uncaught-exception' && export NODE_ENV=development && nodemon --exec ts-node ./bin/www.ts",
도커파일 작성을 완료했다면 도커파일이 존재하는 위치에서 이미지를 빌드한다.
$ docker build -t [image-tag-name] .
정상적으로 이미지를 빌드했다면 컨테이너를 실행한다.
$ docker run -it --name sport-result \
-p 80:8080 \
[image-tag-name]
이렇게 도커를 실행하면 에러가 발생할 것이다. 왜냐하면 컨테이너 내에서는 노드 의존성만 설치되어 있지 소스 코드가 존재하지 않기 때문이다. 따라서 로컬 디렉토리를 컨테이너에 마운트해서 컨테이너가 소스 코드에 접근할 수 있도록 해야한다.
수정된 명령어는 아래와 같다.
$ docker run -it --name sport-result \
-p 80:8080 \
-v $(pwd):/usr/src/app/
[image-tag-name]
로컬 디렉토리가 컨테이너와 연결되어 잘 실행되는 모습을 볼 수 있다. 코드가 수정되면 실시간으로 재시작되는 모습도 확인할 수 있다.
하지만 데이터베이스 설정이 로컬호스트로 되어있어 에러가 발생한다. 컨테이너 DB에 연결해보자.
docker run 시 유의점
-d 옵션을 사용하지 않을시 기본적으로 포그라운드로 도커 컨테이너를 실행한다.
만약 도커 컨테이너 내에서 포그라운드로 프로세스를 실행하지 않으면 도커 컨테이너가 종료되며 다시 재시작 시켜주어야 한다.-d 옵션을 사용하여 백그라운드로 실행할 경우 마찬가지로 컨테이너 내에서 포그라운드로 프로세스를 실행해야지만 컨테이너가 종료되지 않고 유지된다.
웹 애플리케이션이라면 필연적으로 데이터베이스를 사용하게 된다. 도커 허브에서 Mysql 이미지를 받아 Mysql 컨테이너를 실행해보자
$ docker pull mysql # mysql 이미지 받아오기
$ docker run -d --name mysql-container \ # mysql 컨테이너 실행
-e MYSQL_ROOT_PASSWORD=[root-password] \
-e LC_ALL=C.UTF-8 \
-p 3306:3306 \
-v mysql-vol:/var/lib/mysql \
mysql
환경변수로 LC_ALL=C.UTF-8 을 지정해주었다. 설정하지 않으면 mysql 컨테이너 내에서 한글 사용이 제대로 되지 않는다. 필자는 데이터를 덤프하는 과정에서 한글이 제대로 입력되지 않아 설정했다.
mysql 공식 이미지는 oracle-linux를 사용하고 해당 이미지는
microdnf
패키지 매니저가 기본으로 사용된다. 또 텍스트 편집기인 vi와 vim도 깔려있지 않다. 만약 필요하다면microdnf install yum
후 yum을 사용해 vim을 설치해야한다
볼륨을 연결하여 mysql 컨테이너를 삭제하더라도 데이터를 보존할 수 있도록 하였다. 컨테이너의 /var/lib/mysql 를 로컬 또는 도커 볼륨에 마운트하여 데이터를 저장할 수 있다.
컨테이너에 접속하여 쉘 스크립트 작업을 하고 싶다면
docker exec -it [container-name] | [container-id] cmd
명령어로 접속하자. cmd에는bash
같은 쉘을 사용한다
docker ps -a
명령어로 mysql-container
이름의 컨테이너가 잘 실행된것이 보일것이다. 이제 아까 실행한 노드 컨테이너를 삭제하고 mysql 컨테이너과와 연동해보자.
// datasoure.ts
import { DataSource } from "typeorm";
import { KboMatch } from "./domain/kbo/kboMatch";
import { KboRank } from './domain/kbo/kboRank';
import { LckMatch } from './domain/lck/lckMatch';
const host = process.env.DB_HOST
const username = process.env.DB_USER
const password = process.env.DB_PASSWORD
let synchronize, logging
if(process.env.NODE_ENV === 'production') {
synchronize = false,
logging = false
} else if (process.env.NODE_ENV === 'development') {
synchronize = true
logging = true
}
export const MysqlDateSource = new DataSource({
type: 'mysql',
host,
port: 3306,
username,
password,
database: 'sport_result',
entities: [KboMatch, KboRank, LckMatch],
synchronize,
logging
})
DB 커넥션을 위한 Datasource 파일을 보면 로컬 호스트가 아닌 환경변수 DB_HOST를 사용하게 변경한다. 그리고 docker run
명령어를 실행한다.
$ docker run -it --name sport-result \
-e DB_HOST=mysql-container \
--link mysql-container \
-v $(pwd):/usr/src/app/ \
-p 80:8080 \
[image-tag-name]
환경변수로 mysql 컨테이너의 이름을 전달하고 --link 옵션을 통해 컨테이너를 연결하면 DB 연결이 완료된다.
DB 연결까지 완료하여 개발환경 구축이 완료되었다.
실시간으로 변하는 코드에 대응하기 위해 로컬 디렉토리를 컨테이너에 마운트하여 개발환경을 구축하였다. 하지만 실제 배포 환경에서는 코드가 변할 일이 없는 정적인 환경이므로 굳이 로컬 디렉토리를 마운트해서 사용할 필요가 없다.
따라서 이미지를 만들 때 프로젝트 소스파일(혹은 빌드된 파일)을 복사하여 이미지를 생성한다. 노드는 빌드된 단일 파일이 아니기 때문에 소스파일을 통째로 복사한다.
FROM node:16-alpine
# Korean Fonts
RUN apk --update add fontconfig
RUN mkdir -p /usr/share/fonts/nanumfont
RUN wget http://cdn.naver.com/naver/NanumFont/fontfiles/NanumFont_TTF_ALL.zip
RUN unzip NanumFont_TTF_ALL.zip -d /usr/share/fonts/nanumfont
RUN fc-cache -f && rm -rf /var/cache/*
# update pkg manager
RUN apk update
# bash install
RUN apk add bash
# Language
ENV LANG=ko_KR.UTF-8 \
LANGUAGE=ko_KR.UTF-8
# Set the timezone in docker
RUN apk --no-cache add tzdata && \
cp /usr/share/zoneinfo/Asia/Seoul /etc/localtime && \
echo "Asia/Seoul" > /etc/timezone
# Create Directory for the Container
WORKDIR /user/src/app
# add chromium
RUN apk add chromium
# Copy source code
COPY . .
RUN npm install
RUN npm install -g pm2
# Docker Demon Port Mapping
EXPOSE 80
# RUN SERVER
CMD ["npm", "run", "docker"]
개발 환경에서는 package.json만 복사하였지만 프로젝트 루트 폴더의 모든 파일을 복사한다. 만약 제외하고 싶은 폴더나 파일이 존재한다면 .dockerignore 파일에 지정하여 복사를 막을 수 있다. 폴더나 파일 지정 방식은
Go
언어 방식으로 지정한다.# .dockerignore .git *Dockerfile* *docker-compose* node_modules```
CMD 명령어를 배포에 사용하는 명령어로 변경하였다. 스크립트 내용은 아래와 같다.
"docker": "export NODE_ENV=production && pm2-runtime start ts-node -- -r tsconfig-paths/register ./bin/www.ts",
배포 환경에는 포그라운드 방식이 아니라 백그라운드 방식으로 도커를 실행해야한다. -d 옵션을 활용해 컨테이너를 백그라운드로 실행할 경우 도커 컨테이너 내부에서 포그라운드로 실행되는 프로세스가 존재해야지만 컨테이너가 종료되지 않는다.
따라서 일반적으로 사용하는 pm2가 아니라 pm2-runtime을 이용해 앱을 실행했다
이제 docker build
로 이미지를 빌드한다. 성공적으로 빌드가 됐다면 배포환경에 전달하기 위해 도커 허브에 이미지를 업로드한다.
빌드시 유의사항
일반적으로 배포 서버는 amd64 아키텍처를 사용한다. 하지만 최근 맥북은 arm 기반이므로 빌드시 linux/arm 으로 빌드된다.
만약 arm 기반의 m1, m2 맥북을 사용한다면 빌드 옵션으로 --platform linux/amd64 를 명시해야만 한다.
도커 데스크탑을 사용하면 빌드한 이미지를 push to hub
버튼을 통해 간단하게 푸시할 수 있다. 유의할 점은 이미지 이름을 <도커 id>/<이미지 이름> 형식으로 지정해야지만 푸시가 된다.
# 이미지 태그 작업
$ docker tag [image-name or Tag] [docker-hub-id | private-registry-ip:port]/[push-image-name]
# 이미지 푸시
$ docker push [push-image-name]
cli를 사용할 경우 이미지를 만들고 위와 같은 커맨드를 통해 푸시할 수 있다. 로그인이 되어 있지 않다면 docker login
명령어로 로그인을 수행한다.
이제 도커 허브에 이미지 업로드까지 완료하였다. 다음 포스트에는 가상 서버 호스팅 서비스인 Vultr
에 서비스를 배포하는 과정을 알아보도록 하겠습니다.