backend : nestJS
frontend: react
Backend
-dockerfile.dev
-dockerfile
frontend
-dockerfile.dev
-dockerfile
-ngnix.conf
docker-compose.yml
.dockerignore
도커 파일은 총 2개씩 4개의 파일을 만들었는데 실제로 운영서버만 배포를 진행하였고 개발서버 도커파일은 로컬에서만 확인을 했다.
도커파일을 작성하는 요령은 이 게시글을 참고했다.
위 링크에서는 DB(postgre)를 포함한 상태지만 mongo-atlas
를 사용하는 경우에는 별도로 db 정보를 입력할 필요 없이 url을 환경변수로만 넣어주면 되었다.
backend
//Dockerfile.dev
FROM node:12-alpine // node를 도커허브에서 받아온다
RUN mkdir -p /svr/app //파일이 저장될 저장소 만들기
WORKDIR /svr/app // 만든 저장소를 workdir로 지정
RUN npm i -g @nestjs/cli --silent //nest cli 설치
COPY package.json .
// 필요한 dependency의 목록을 복사한다(node module을 복사하는 것이 아닌 목록을 가져옮)
COPY package-lock.json .
RUN npm install --silent
// dependency 설치 (--silent: 로그 없이 설치)
COPY . .
// 현재 디렉토리 (dockerfile이 있는 곳)에 있는 파일을 만든 저장소에 복사
CMD ["npm", "run", "start:debug"]
// nest를 디버깅 모드로 서버 시작 ( nest start --watch)
Error: Error loading shared library /app/node_modules/bcrypt/lib/binding/napi-v3/bcrypt_lib.node: Exec format error
이 에러같은 경우 bcrypt의 특성
에 따라 발생하는 에러이다. bcrypt같은 경우 python
기반으로 작동한다. dockerfile에서의 Node:12-alpine
같은 경우 필요한 최소의 기능만 담아 용량을 줄였기 때문에 python이 없고 그에 따라서 bcrypt를 인식하지 못하는 문제가 발생한다. 이 경우 가장 빠르고 편한 방법은 bcrypt 대신에 bcryptjs
를 사용하는 것이다. 이 라이브러리 같은 경우 bcrypt가 js 환경에서 더 잘 작동하도록 만들어졌다.
다른 방법은 python을 별도의 명령어로 docker file에 추가해주는 방식(참고)이 있다.
ReferenceError: TextEncoder is not defined Node.js with mongoose
이 에러 같은 경우 docker로 받아온 node의 버전이 너무 오래되어서 호환이 안되기 때문에 발생한 것이다
mongoose v6
이상은 nodee 12
이상을 요구하기 때문에 From node:10 -alpine
으로 별도의 예제를 따라하다 문제가 발생했다.
node
버전을 올려서 받아주면 문제가 해결된다.
frontend
//Dockerfile.dev
FROM node:12-slim
// alpine보다는 python이 추가된 가벼운 형태의 node (https://jadehan.tistory.com/58)
WORKDIR /usr/src/app
COPY . .
RUN npm install --silent
운영 서버에 사용되는 dockerfile의 경우 build
의 과정을 거쳐야 한다. nest, react모두 js기반이기 때문에 web브라우저가 이해하고 동작하기 위해서는 static한 파일이 되는 build를 거쳐야 하는 것이다.
추가로 frontend같은 경우 webserver를 통해서 html, css, js 파일들이 웹에 그려지기 때문에 ngnix, apache같은 웹서버 설정도 필요하다.
backend
FROM node:12-alpine
WORKDIR /app
COPY . .
RUN npm install --silent
RUN npm run build // 빌드 진행
EXPOSE 5000 //포트 설정
CMD ["node", "dist/main"] // "start:prod": "node dist/main",
frontend
FROM node:12-slim as build
//build라는 이름의 temporary image (multistage pattern)
WORKDIR /app
COPY package.json .
RUN npm install --silent
COPY . .
RUN npm run build
FROM nginx:alpine // nginx를 도커허브로 부터 받아온다.
COPY nginx.conf /etc/nginx/conf.d/default.conf
// 같은 디렉토리에 있는 nginx.conf 파일 복사
COPY --from=build /app/build /usr/share/nginx/html
//임시로 만든 build 이미지를 가져와서 그 파일을 nginx/html에 복붙!
EXPOSE 80 //port 개방
CMD ["nginx", "-g", "daemon off;"]
//nginx.conf
server {
listen 80;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri /index.html;
}
}
사용한 docker compose 코드는 아래와 같다.
version: "3" //version 지정 - multi-container같은 경우 3 추천
services:
frontend:
container_name: app_frontend // container 이름 지정
# image: ECR uri 지정
// 만약 aws ECR을 이용해서 build파일을 가져올 경우- cloud
build:
context: ./frontend // 어디서 파일을 읽어올지 (디렉토리 지정)
dockerfile: Dockerfile // 어떤 dockerfile을 읽을지
restart: always // 문제 발생시 어떻게 할지 - 항상 다시 시작
networks:
- backend // 지정한 network로 container간 통신
ports:
- "3000:80"
// port지정 - nginx가 80을 base로 사용하지만 3000으로 사용
volumes:
- "./frontend/src:/usr/src/app/src"
//file 변경에 따라서 추적
backend:
container_name: app_backend
# image: ECR uri 지정
build:
context: ./backend
dockerfile: Dockerfile
restart: always
networks:
- backend
ports:
- "5000:5000"
volumes:
- ./backend:/svr/app
- /svr/app/node_modules
environment:
MONGO_URI: 환경변수로 mongo_atlas uri 넘겨줌
SECRETKEY: jwtsecret
SWAGGER_USER: -
SWAGGER_PASSWORD: -
AUTHOR_EMAIL: -
networks: // container간 통신 방식
backend:
driver: bridge
작성한 코드 파일을 dockerfile의 디렉토리에서 복사한 내용으로 빌드를 할 수도 있고 aws 서비스 중에 하나인 ECR(elastic container resistry)
를 이용해서 cloud
로 이용할 수 있다.
ECR docker image
이 프로젝트 같은 경우 두가지 방식으로 진행을 해보았는데 아무래도 git clone을 받아서 진행을 할 것이고 매번 docker hub에 푸시를 하고 다시 ECR을 최신화하는 과정이 자동화가 되지 않으면 여간 성가신 일이 아닐 것 같아서 그냥 build
로 진행을 했다.
가장 기본적인 형태인 EC2로 배포를 진행하기로 했다.
이 튜토리얼-Docker Compose MEAN stack on AWS EC2을 기반으로 진행을 하였다.
인스턴스를 생성하여 진행을 할 때 주의해야 할 부분은 보안그룹
을 설정이다. 이 곳에서 port
와 ip
를 개방해주어야지 docker yml
에서 지정한 port로 접속이 가능하다.
편집
을 눌러서 추가해주면 된다.
프로젝트의 경우에는 frontend 포트 3000
, backend 포트 5000
, nginx 포트 80
, 기본 ssh 트래픽 80
을 지정해주었다.
그리고 접속을 어느 ip에서든 할 수 있도록 0.0.0.0
으로 지정해주었다.
EC2의 접속을 하기 위해서는 access key를 발급받아야 한다.
공식문서에 과정이 잘 나와있어 따라만 하면 되서 큰 문제는 없다. ( aws cli 설치 필요)
다운 받은 .pem
파일의 디렉토리 위치를 기억해두어야 한다.
인스턴스를 생성하고 나면 해당 인스턴스의 퍼블릭 DNS주소
를 확인할 수 있다. 이 주소를 기반으로 내가 만든 인스턴스의 shell로 접속이 가능하다.
내 accesss-key(pem 파일)
이 위치한 디렉토리에서
ssh -i '파일이름.pem' '내가지정한OS'@'내인스턴스의 퍼블릭DNS주소'
을 입력하면 ubuntu shell의 접속할 수 있다.
그 다음 sudo apt update
를 입력하여 OS를 최신으로 업그레이드
이후 sudo apt install docker.io
,sudo apt install docker-compose
를 입력하여 도커를 OS에 설치
sudo usermod -a G Docker ubuntu
로 도커에 sudo 권한 부여
이제 세팅은 끝이 났고 만든 코드를 깃헙에서 clone 받아 docker만 돌려주면 끝이난다.
git clone 레포지토리 링크
cd 해당 폴더이름
docker-compose --build up
도커 파일이 있는 곳에서 build
를 진행하고 docker-compose up
을 하면 서버가 돌아가게 된다. 코드 변화가 없는 상태에서 서버를 다시 킬때는 --build
명령어를 제외하면 된다.
No hang up
아무리 ec2에 서버를 배포했더라도 지금 내 컴퓨터가 아닌 아마존 컴터를 빌려쓰는 개념의 차이만 있다. 그래서 aws 터미널을 끄면 로컬환경에서와 같이 서버가 종료되게 된다. 터미널을 종료하더라도 백그라운드에서 계속하여 돌아가도록 리눅스의 nohup
과 같이 명령어를 입력한다.
나의 프로젝트 경우에는 nohup docker-compose up
으로 도커파일로 서버를 띄우는 명령어를 백그라운드로 실행했다.
서버를 종료하려면 해당하는 process
를 ps
로 id
를 찾은다음 kill
을 해주면 된다.
나의 경우에는 docker ps
를 통해서 확인이 가능하다.
새로 터미널을 키고 바로 docker ps
를 입력했는데 잘 돌아가고 있는 상태
똑같은 accesskey로 진행을 했는데 갑자기 진행이 안되길래 console을 확인해보니 위와 같이(검사 1/2 - 인스턴스 연결성 검사 실패
) 나왔다. 이 경우 검색을 해보니 프리티어
에 할당된 cpu, 메모리를 초과해서 사용
한 것이 원인이라고 하였다.
그래서 인스턴스 타입을 변경
을 하고 다시 서버를 돌려보니 잘 돌아가게 되었다. 단점은 타입을 변경하게 되면 public DNS
주소도 달라지고 권한
같은 경우(github)도 초기화 되는 것 같았다. 권한 문제가 발생하여 폴더를 지우고 다시 클론을 받아 진행했다.
로컬에서 문제가 없었는데 배포를 하고 나니 axios가 url을 인식하는 과정에서 오류가 발생했다. http//
가 추가로 url에 들어갔는데 이를 해결하기 위해 axios의 baseUrl
을 별도로 지정을 해주어 요청 엔드포인트
를 정확히 지정해주었다.
axios.defaults.headers.common["Access-Control-Allow-Origin"] = `${awsDNS}:3000`;
axios.defaults.baseURL = `${awsDNS}:5000/api`;
ReactDOM.render(
...
document.getElementById("root")
);
배포를 하고 나니 로컬 환경에서 일어나지 않았던 CORS이슈가 발생했다. CORS의 정의대로 same origin
이 아닌 cross-origin
에서 온 요청을 보안을 위해서 막게된 것이다. 로컬에서야 localhost로 같은 origin이지만 배포를 하면 다르게 인식이 되었다.
특히 이 부분은 nest쪽에서 handle을 했어야 했는데 대부분의 인터넷 해결방안은 모든 origin에 대해서 허용을 하는 방식
으로 진행이 되었다. 이게 근본적인 해결 방안은 아니라고 생각을 해서 직접 지정해주기로 했다.
방법이 두가지가 있었는데 하나는 앱 전체적으로 허용 origin을 지정
,각 api요청에 Allow-cross-origin 헤더 추가
방식이있었다.
앱 전체적으로 허용 origin을 지정
const app = await NestFactory.create<NestExpressApplication>(AppModule);
...
SwaggerModule.setup('docs', app, document);
app.enableCors({
origin: ['특정 url'],
credentials: true,
});
하지만 이렇게 지정을 했을때도 cors문제는 해결이 되지 않았다. (정확한 원인을 찾지는 못함)
앱 전체적으로 허용 origin을 지정
const app = await NestFactory.create<NestExpressApplication>(AppModule);
...
SwaggerModule.setup('docs', app, document);
app.enableCors({
origin: ['특정 url'],
credentials: true,
});
Allow-origin-control header추가
axios.defaults.headers.common["Access-Control-Allow-Origin"] = `${awsDNS}:3000`;
ReactDOM.render(
<React.StrictMode>
<AuthContextProvider>
<GoalContextProvider>
<App />
</GoalContextProvider>
</AuthContextProvider>
</React.StrictMode>,
document.getElementById("root")
);
axios.default.headers.commmon
을 통해서 axios
의 모든 요청에 대해서 header
를 한번에 추가할 수 있다.
api 요청에 별도의 명시를 하지 않았지만 header에 추가되어서 전달되는 것을 확인가능하다.
처음 도커 기반으로 배포를 진행해본 것이다 보닌 심플한 방법으로 접근을 했는데 추가적으로 githupaction
이나 zenkins
로 테스트코드까지 챙기고 CI/CD도 구축해봐야겠다.