예... 처음엔 누구나 그럴싸한 계획을 갖고 있죠... 금방 끝날 줄 알았던 도커는 저를 참교육 하기 시작했습니다.
범인은 바로 요녀석입니다. Node에서 MySQL과 연결이 무슨 짓을 해도 안되던 겁니다.
옆친데 덮친격으로 아침 모닝콜로 시원하게 회사 면접 탈락 통보도 받았습니다.😢😢
멘탈은 탈탈 털렸지만 어쩌겠습니다... 예,,, 해야죠 ,,, 다시 삽질을 지구 멘틀 끝 까지 하다 결국 성공했습니다.
그러면서 기존 작성한 코드를 하나하나 분석하게 되어서 한번 살펴 보겠습니다. 작성자의 멘탈이 아주 안좋아 글이 다소 중구난방일 수 있습니다.
우선 파악한 원인은 바로 MySQL을 시작하기 전에 Node Server가 구동되는 것 이었습니다.
대략적인 파일 구조입니다. 디렉토리 별로 맨 위부터 살펴 보자구요~
1. mysql
config, data Directory는 mysql volume mount를 위한 폴더들입니다. docker-compose.yml 파트에서 다시 살펴볼게요!
init.sql
MySQL 이미지를 만들 때 초기 설정을 해주는 파일 입니다.
MySQL 컨테이너가 처음 실행될 때 스크립트들이 MySQL 서버에 의해 자동으로 실행됩니다.
Database와 User가 존재하지 않으면 새로 생성하고 사용할 준비를 해줍니다.
-- init.sql
-- MySQL 데이터베이스 생성
CREATE DATABASE IF NOT EXISTS cloudbridge_database SET utf8mb4 COLLATE utf8mb4_unicode_ci;;
-- 'chanho' 사용자 생성 및 권한 설정
CREATE USER IF NOT EXISTS 'chanho'@'%' IDENTIFIED BY '0908';
GRANT ALL PRIVILEGES ON cloudbridge_database.* TO 'chanho'@'%';
FLUSH PRIVILEGES;
-- 사용할 데이터베이스 선택
USE cloudbridge_database;
2. ngix
nginx 이미지의 root 폴더로 사용될 폴더입니다.
Dockerfile
nginx 이미지를 다운받고 nginx 이미지의 해당 폴더에 nginx.conf 파일의 내용을 복사합니다.
nginx.conf
upstream nodeserver {
server node:3000;
}
server {
listen 80;
location /node {
proxy_pass http://nodeserver/;
rewrite /node/(.*) /$1 break;
proxy_redirect default;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
}
}
Upstream Block
Nginx에서의 Proxy Server에 대한 설정으로 server node:3000는 해당 upstream에 속한 서버를 정의합니다.
upstream이란 여러 서버 또는 서버 그룹을 그룹화 한것입니다.
Docker compose에서 사용할 node의 이름이라서 node라고 기억하기 편하게 지정했고 3000은 Node의 Port 번호입니다.
Server Block
listen 80 : 웹 브라우저가 HTTP 프로토콜로 기본적으로 사용하는 80번 포트에서 들어오는 요청을 수신하도록 설정합니다. 예를 들어 사용자가 브라우저에서 "http://your-domain.com"과 같은 주소로 접속하면 Nginx는 80번 포트에서 요청을 수신해 설정된 엔드 포인트로 해당 요청을 Proxy 해줍니다.
location Block
특정 Url 경로로 들어온 요청을 처리합니다. 현재 코드에선 /node로 들어온 요청을 처리합니다.
proxy_pass http://nodeserver/ : 요청을 nodeserver 업스트림으로 프록시합니다.
rewrite /node/(.*) /$1 break : URI에서 /node/ 부분을 제거하여 리다이렉션 합니다.
proxy_redirect default : 프록시 응답의 Location 헤더를 수정합니다.
proxy_set_header : 지시문들은 프록시된 요청에 대한 헤더를 설정합니다.
최종적으로 다음과 같은 url로 접근하게 됩니다.
3. dockerignore
node module은 용량으로 인해 이미지 빌드 시 제외하고 호스트 시스템에 있는 node_modules 디렉토리를 복사하여 사용
## .dockerignore for node.js
.gitignore
/node_modules
4. Node DockerFile & docker-entrypoint.sh
오늘의 주인공 되시겠습니다. 자~~ 드가자 ~~~
서두에 언급했듯이 문제의 원인은 MySQL이 시작되기 전에 web server에서 연결을 시도했기 때문이었습니다. 제가 이 생각을 못한 이유는 depends_on으로 컨테이너간의 의존성을 지정해주면 의존성에 따라 상호 순서에 맞춰 알맞게 잘 작동할거라고 생각했었기 때문입니다. depends_on 으로 컨테이너들을 연결해도 왜 이런 문제가 발생할까요 ?
Docker 공식 문서에 이렇게 적혀있습니다.
Note: depends_on will not wait for db and redis to be “ready” before starting web - only until they have been started. If you need to wait for a service to be ready, see Controlling startup order for more on this problem and strategies for solving it.
"depends_on 은 웹을 시작하기 전에 db와 redis가 "준비"될 때까지 기다리지 않습니다."
그렇다. depends_on은 단순히 컨테이너들을 의존 관계에 의한 순서로 시작만 시키고 누가 먼저 실행되던 관여하지 않았습니다. 그러서 docker-compose를 사용할 때 순차 실행을 위해 dockerize를 사용하게 되었습니다.
두 번째 문단과 마지막 줄이 핵심인데 각각 dockerize를 설치하고 dockerize로 구현하고 싶은 내용을
docker-endtrypoint.sh 파일에 작성하여 컨테이너가 시작될 때 실행되는 기본 명령어로 설정해
Docker 컨테이너가 시작되면 docker-entrypoint.sh 스크립트가 실행 됩니다.
# 사용할 기본 이미지를 지정
# Alpine Linux의 Node.js 버전 14 Alpine Linux는 경량 배포판
FROM node:14-alpine
# DOCKERIZE_VERSION 모듈 설치
# docker-compose에서 docker 컨테이너 간의 실행 순서를 동기화
ENV DOCKERIZE_VERSION v0.2.0
RUN wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
&& tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz
# nodemon 모듈 설치
RUN npm install -g nodemon
RUN mkdir -p /usr/src/app
# WORKDIR은 명령을 실행하기 위한 디렉토리를 설정합니다.
WORKDIR /usr/src/app
# root 폴더에 있는 package.json를 /usr/src/app 복제합니다
COPY package.json /usr/src/app
# root 폴더에 있는 package-lock.json를 /usr/src/app 복제합니다
COPY package-lock.json /usr/src/app
# package.json에 있는 module을 install합니다
RUN npm install
# root(.) directory 전체를 복제합니다.
COPY . /usr/src/app
RUN chmod +x docker-entrypoint.sh
# script를 실행합니다.
CMD ["./docker-entrypoint.sh"]
docker-entrypoint.sh
- dockerize -wait tcp://mysql:3306 -timeout 20s : MySQL 서버가 3306 포트에서 연결을 받을 때 까지 최대 20초 대기
- nodemon server.js : MySQL 서버가 준비되면 Node.js 서버를 시작합니다.
## docker-entrypoint.sh for node.js
## 컨테이너 내부에서 cmd작업을 할 수 있도록 해주는 파일
## MySQL 서버가 준비되기 전까지 다음 단계로 진행되지 않음
## mysql : Docker Compose에서 지정한 이름
echo "wait db server"
dockerize -wait tcp://mysql:3306 -timeout 20s
echo "start node server"
nodemon server.js
5. docker-compose.yml
version: "3"
services:
nginx:
build:
context: ./nginx
dockerfile: Dockerfile
ports:
- "80:80"
depends_on:
- node
- mysql
networks:
- db-net
node:
build:
context: ./node_server
dockerfile: Dockerfile
ports:
- "3000:3000"
depends_on:
- mysql
networks:
- db-net
mysql:
image: mysql:latest
restart: always
environment:
MYSQL_HOST: mysql
MYSQL_ROOT_PASSWORD: root
MYSQL_USER: chanho
MYSQL_PASSWORD: 1234
volumes:
- ./mysql/data:/var/lib/mysql
- ./mysql/config:/etc/mysql/conf.d
- ./mysql/init:/docker-entrypoint-initdb.d
ports:
- "3307:3306"
networks:
- db-net
networks:
db-net:
6. server.js
Node.JS Docker 컨테이너에서 실행될 node.js 파일입니다. host는 docker-compose.yml에서 mysql 컨테이너의 이름,
password 값은 위의 docker-compose.yml에서 지정한 MYSQL_ROOT_PASSWORD 값입니다.
const mysql = require("mysql2/promise");
const dbConnect = async () => {
try {
const connection = await mysql.createConnection({
host: "mysql",
user: "root",
password: "root",
database: "cloudbridge_database",
});
console.log("mysql connection success");
} catch (error) {
console.log(error);
}
};
dbConnect();
이제 docker-compose up --build로 실행하면 아래와 같이 MySQL 연결을 대기합니다.
node server가 연결 후 mysql에 연결하는 것 까지 확인할 수 있습니다.
드디어 성공했습니다 ㅠㅠ 사실 도커는 깊게 할 생각이 없어서 간단하게 코드만 끌어와서 구성하려 했는데 크게 혼났네요,,, 그래도 이 참에 코드도 싹다 뜯어보고 도커에 대해 좀 더 공부할 수 있었습니다. 항상 느끼는 거지만 에러 고칠 때가 실력이 가장 느는거 같아요 그만큼 고통도 두 배지만 ^^...
'BACK END' 카테고리의 다른 글
나도 할 수 있다 REST API (2) | 2023.12.29 |
---|---|
우당탕탕 Node JS Server 다시보기 (1) | 2023.12.18 |
Docker + Node.js + Nginx 4 (0) | 2023.12.11 |
Docker + Node.js + Nginx 3 (0) | 2023.12.11 |
Docker + Node.js + Nginx 2 (0) | 2023.12.11 |