안녕하세요 펭귄 교수입니다.
이번에는 Docker를 이용하여 React, Flask, MySQL을 이용해서 웹 서비스 개발에 전체적인 플로우를 소개하고자 합니다.
제가 개인적으로 만들어본 프로젝트에서 다음과 같이 환경 설정하고 진행한 것에 대한 포스팅입니다.
파이썬 코드 설명이나 리액트 설명, SQL문에 대한 설명은 없습니다.
정답은 아니니 편하신 다른 방법이 있을 경우 해당 방법을 사용하시면 되겠습니다.
1. 개발 환경
OS : Linux Ubuntu 22.04.4 ( Oracle Virtual Box )
Docker Version : Docker 27.2.1
Node Version : v20.17.0
NPM Version : 10.8.3
Python Version : 3.10
1-1. 디렉토리 구조
루트 디렉토리 (docker) 내 총 네 개의 폴더가 있습니다.
db, myflask, myweb, nginx
각각 프로젝트는 해당 디렉토리 하위에 존재합니다.
2. 프론트 엔드 개발 (React)
React를 사용하여 프론트 엔드 개발을 진행하였습니다.
백 엔드와의 통신은 axios 모듈을 사용하였습니다.
API URL은 '/api/menu' 와 같은 방식으로 사용했습니다.
< axios 통신 코드 일부 >
axios.get(`/api/menu?date=${date}`)
.then((response) => {
if (response.data === 'ERROR') {
setIsData(false);
console.error(response);
console.error('ERROR');
return;
}
if (response.data !== 'NONE') {
setIsData(true)
dispatch(changeMenu(response.data[date]))
return;
} else {
setIsData(false);
return;
}
})
.catch((error) => {
console.log(error)
})
그렇게 개발이 완료된 후 리액트 프로젝트를 빌드해줍니다.
해당 프로젝트에서는 리액트 서버를 바로 쓰는 것이 아닌, build 한 정적 파일들로 진행합니다.
디버그 등에서는 빌드하지 않는 것이 유리하지만, 사이트 속도 개선에 있어서는 정적 파일이 빠르며, Docker 구축할 때 nginx 서버를 사용하기 위함입니다.
# npm 을 사용해 build 할 수 있습니다.
npm run build
# 완료되면 build 폴더 내에 index.html 파일 및 여러 정적 파일이 생성됩니다.
cp -r build ../docker
# 만들어진 build 폴더를 앞으로 진행할 docker 파일들이 존재하는 docker 폴더 내부로 복사합니다.
3. 백 엔드 개발 (Flask)
먼저 Docker에서 WSGI 통신과 백그라운드 실행을 위해 gunicorn으로 해당 프로그램을 실행합니다.
그렇기 위해서 server 파일과 wsgi 파일을 분리합니다.
# server.py
from flask import Flask
server = Flask()
# ... api 작성
# wsgi.py
from app import server
if __name__ == '__main__':
server.run(host='0.0.0.0', port=80)
추후 Docker 간 통신을 위해 포트는 80번으로 엽니다.
그리고 MySQL과의 통신은 다음과 같습니다.
먼저 파이썬에서 mysql을 사용하기 위한 라이브러리, pymysql을 설치합니다.
Docker 파일에서는 "pip freeze > requirements.txt" 로 만들어진 파일인 requirements.txt 파일을
"pip install -r requirements.txt" 를 RUN 함으로써 설치합니다.
그 전에는 "pip install pymysql" 을 통해 라이브러리를 설치합니다.
그리고 파이썬 코드에서 DB를 접근하기 위한 방법은 다음과 같습니다.
db = pymysql.connect(host="mysqldb", user=USER, password=PASSWORD, db=DB, charset="utf8")
# connect 내에서 host는 이후 docker-compose 파일 작성할 때 사용된 mysql 컨테이너의 이름을 사용합니다.
cursor = db.cursor()
cursor.execute(f'SELECT * FROM menu where date=\'{date}\'')
data = cursor.fetchone()
여기서 중요한 점은 pymysql.connect() 함수 내에서 host 옵션입니다.
"localhost"나 "127.0.0.1" 이 아닌, 이후 docker-compose.yml 에서 mysql 컨테이너의 이름으로 선언해주어야 합니다.
4. MySQL 세팅
MySQL 컨테이너를 설정하기 위해 db 폴더 내에 init.sql 이라는 파일을 만들어서 설정해 줍니다.
이 파일에서는 미리 DB 안에 테이블을 만들고, 테이블 내 설정, DB 설정들을 세팅할 수 있습니다.
아래는 제가 설정한 DB 입니다.
< db/init.sql >
CREATE DATABASE IF NOT EXISTS menuDB;
USE menuDB;
CREATE TABLE IF NOT EXISTS menu
(
date DATE NOT NULL,
lunch_1_menu VARCHAR(255),
lunch_1_kcal INT ,
lunch_2_menu VARCHAR(255),
lunch_2_kcal INT ,
lunch_sub_menu VARCHAR(255),
lunch_sub_kcal INT ,
dinner_menu VARCHAR(255),
dinner_kcal INT
);
ALTER DATABASE menuDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
ALTER TABLE menu CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
아래 두 줄은 추후 SQL 사용 시 한글 사용을 위한 코드입니다.
자세한 내용은 아래 글을 보시면 됩니다.
5. nginx 설정
클라이언트에게 정적 HTML 파일을 전송할 서버입니다.
nginx 에서는 URL을 분석해서 각각 URL에 해당하는 서비스로 switching 하는 기능을 갖고 있습니다.
nginx 기능에 관한 포스팅이 아니므로, 해당 부분에 대한 자세한 설명은 넘어가겠습니다.
<nginx/nginx.conf>
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" "$request_uri" "$uri"'
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
upstream nginx-web {
server nginx:80;
}
upstream flask-web {
server flask:80;
}
server {
location /api/ {
proxy_pass http://flask-web;
proxy_redirect off;
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;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
proxy_pass http://nginx-web;
proxy_redirect off;
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;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}
위 conf 파일을 간단하게 설명하면
http://sample_url/
로 접속하면 리액트로 만든 nginx 서버로 PASS 시키고,
http://sample_url/api/
로 접속하면 플라스크로 만든 백엔드 서버로 PASS 시킵니다.
해당 기능은 proxynginx라는 컨테이너로 분리할 예정입니다.
위 그림과 같이 이해하면 되겠습니다.
그렇게 먼저 Flask를 설치할 Dockerfile 부터 만들겠습니다.
myflask 폴더 내에 Dockerfile을 만듭니다.
그리고 기존 플라스크 프로젝트는 flask_app 이라는 폴더를 myflask 하위에 생성 후 옮겨줍니다.
리눅스 명령어로 보자면 아래와 같습니다.
pwd
# ~/docker/myflask
mkdir flask_app
mv * flask_app
# 기존 프로젝트 파일들을 flask_app 폴더로 옮깁니다.
touch Dockerfile
# Dockerfile 생성
Dockerfile 안에 아래 코드를 작성합니다.
FROM python:3.10-alpine
WORKDIR usr/src/flask_app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY ./flask_app .
그러면 마지막으로 React, Flask, MySQL 들을 연결해줄 docker-compose.yml 파일을 작성합니다.
해당 파일은 루트 디렉토리에 생성 후 작성합니다.
< docker-compose.yml >
version: "3"
services:
nginxproxy:
depends_on:
- nginx
- flask
- db
image: nginx:alpine
container_name: proxyserver
restart: always
ports:
- "80:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
- ./menuweb:/usr/share/nginx/html
nginx:
image: nginx:latest
depends_on:
- flask
container_name: myweb
restart: always
volumes:
- ./myweb:/usr/share/nginx/html
flask:
build: ./myflask
restart: always
container_name: myflask
depends_on:
- db
environment:
MYSQL_ROOT_PASSWORD: manager
MYSQL_DATABASE: DBNAME
MYSQL_USER: user
MYSQL_PASSWORD: user123
command: gunicorn -w 1 -b 0.0.0.0:80 --access-logfile access.log --error-logfile error.log wsgi:server
db:
image: mysql:8.0.36
container_name: mysqldb
volumes:
- ./mysqldb/init.sql:/docker-entrypoint-initdb.d/init.sql
- mydb:/var/lib/mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: manager
MYSQL_DATABASE: DBNAME
MYSQL_USER: user
MYSQL_PASSWORD: user123
volumes:
mydb:
여기서 눈 여겨 보아야 할 것은 nginx/nginx.conf 파일과 docker-compose.yml 파일에서 어떻게 이름들이 매칭되고 있는 지에 대한 파악이 중요합니다!
마찬가지로 db 영역에서 container_name은 Flask에서 mysql 연결할 때 사용한 host 이름과 동일한 것을 볼 수 있습니다.
이렇게 어디서 연결되고, 다른 프로젝트를 진행할 때 어떻게 연결할 지를 보시면 됩니다.
해당 구문들에 대한 의미는 추후 도커 강의에서 이야기할 예정입니다.
마무리
이렇게 하면 기본적으로 docker 내부에서 연결하고 이를 결국 외부로 공유할 수 있게 됩니다.
외부망에서의 접근은 또 다른 문제니 포트포워딩에 관해 알아보시면 좋을 거 같습니다.
그와 관련해 도움되는 글이 있으니 아래 공유드리겠습니다.
Docker 관해서는 계속해서 강의 식으로 글을 작성하고 있으니 많은 관심 부탁드립니다.