앞서 간단하게 flask와 Redis로 Docker Compose를 이용하여 서비스를 실행해보았다. 이번에는 본격적으로 스프링부트와 MySql 데이터베이스를 서비스해본다.
먼저 간단한 스프링부트 프로젝트를 준비한다. 해당 프로젝트의 내용(강의에서는 제공해줌)을 현재 경로(ex08)에 docker-test-server 폴더를 생성해 넣었다. 프로젝트에는 간단한게 MySQL 데이터베이스의 내용을 요청하는 api가 정의되어 있다.
MySQL용 폴더 docker-test-db 폴더를 생성한다. 각 폴더에 Dockerfile을 작성하게 된다.

version: '3'
services:
db:
build:
context: ./docker-test-db
dockerfile: Dockerfile
ports:
- 3306:3306
volumes:
- ./docker-test-db/store:/var/lib/mysql
networks:
- network
server:
build:
context: ./docker-test-server
dockerfile: Dockerfile
restart: always
ports:
- 8080:8080
depends_on:
- db
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/metadb?useSSL=false&serverTimezone=UTC&useLegacyDatetimeCode=false&allowPublicKeyRetrieval=true
SPRING_DATASOURCE_DRIVER: com.mysql.cj.jdbc.Driver
SPRING_DATASOURCE_USERNAME: root
SPRING_DATASOURCE_PASSWORD: root1234
networks:
- network
networks:
network:
MySQL
docker-compose up 명령을 수행할 때 이때 지정된 컨텍스트 경로를 바탕으로 실행된다../docker-test-db/store 폴더로 지정했으며 해당 폴더는 아직 생성하지 않았으므로 빌드될 때 자동으로 생성된다.Spring
build : 위와 같이 context, dockerfile 옵션으로 컨텍스트와 빌드에 사용된 Dockerfile을 설정한다.
restart : 컨테이너가 종료되면 다시 시작되도록 설정한다.
depends_on : 지정한 서비스가 실행에 성공해야만 Spring 서비스를 실행하도록 한다. 만약 지정한 서비스가 실행에 실패하면 계속 restart한다.
environment : 환경변수를 설정한다. 스프링부트 프로젝트의 yml 파일(application-prod.yml)에 넣을 값들을 정의한다.
networks : MySql과 연결하기 위해 같은 도커 네트워크(network)를 지정한다.
networks: network: : 도커 네트워크의 이름을 설정한다. 해당 이름을 사용해서 서비스들을 묶어줄 수 있다.
FROM mysql:8.0
COPY init.sql /docker-entrypoint-initdb.d
ENV MYSQL_ROOT_PASSWORD=root1234
ENV MYSQL_DATABASE=metadb
CMD ["--character-set-server=utf8mb4", "--collation-server=utf8mb4_unicode_ci"]
FROM : mysql:8.0 이미지를 사용해서 생성한다.COPY : 이 Dockerfile이 존재하는 경로의 init.sql 파일을 컨테이너의 /docker-entrypoint-initdb.d 폴더에 복사하면 컨테이너가 실행되면서 해당 sql 파일을 자동으로 수행시킨다. (따라서 init.sql 에 생성시킬 테이블과 데이터를 넣을 DDL, DML 쿼리를 넣어 수행시킬 수 있다.)ENV : ENV 명령어로 MySQL 데이터베이스의 root 비밀번호와 시작할 때 자동으로 생성할 데이터베이스 이름을 지정한다.CMD : MySQL을 실행할 때 부여할 옵션을 설정한다.CREATE TABLE user_tb (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO user_tb (name) VALUES ('John');
INSERT INTO user_tb (name) VALUES ('Jane');
MySQL 컨테이너를 실행할 때 자동으로 생성할 테이블 구조와 데이터들을 init.sql 파일에 정의한다. 이 파일은 Dockerfile에서 정의했듯이 /docker-entrypoint-initdb.d 폴더로 복사돼 수행된다.
FROM openjdk:11-jdk-slim
WORKDIR /app
# COPY만 docker-compose 파일의 위치를 기반으로 작동함
COPY . .
# RUN은 현재 파일을 위치를 기반으로 작동함
RUN chmod +x ./gradlew
RUN ./gradlew clean build
ENV JAR_PATH=/app/build/libs
RUN mv ${JAR_PATH}/docker-test-server-1.0.jar /app/app.jar
ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=prod", "app.jar"]
FROM openjdk:11-jdk-slim : openjdk:11-jdk-slim 이미지를 기반으로 수행한다.WORKDIR /app : 컨테이너의 ‘/app’ 폴더를 작업 디렉토리로 설정한다.COPY . . : 현재 폴더(docker-compose.yml에서 build.context에 지정한 경로를 기준)의 모든 내용을 컨테이너의 작업 디렉토리 /app 로 복사한다. 즉 스프링 프로젝트의 내용이 컨테이너의 /app 폴더로 복사된다.RUN chmod +x ./gradlew : 빌드를 수행하기 위해 gradlew에 실행권한을 부여한다.RUN ./gradlew clean build : 실행권한을 얻은 gradlew를 이용해서 빌드된 결과물을 모두 삭제하고 다시 빌드를 수행한다.ENV JAR_PATH=/app/build/libs : 환경변수 JAR_PATH에 빌드된 결과물이 존재하는 폴더경로를 저장한다.RUN mv ${JAR_PATH}/docker-test-server-1.0.jar /app/app.jar : /app/build/libs/docker-test-server-1.0.jar 파일(빌드 결과물)을 /app폴더에 app.jar 이름으로 이동시킨다.프로젝트의 api를 호출해 mysql 데이터베이스의 내용을 정상적으로 가져오고 있는지 확인한다.
이제 최종 목표인 리버스 프록시 서버를 통해 CORS 정책에 걸리지 않게 백엔드 서버(스프링)와 프론트엔드 서버(리액트)를 연결한다.
프론트엔드 서버에서 백엔드 서버로 요청할 때 NGinx 프록시서버를 거쳐 가기 때문에 CORS 정책에 위반되지 않아 CORS 관련 어노테이션 설정은 제거한다.
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" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
upstream backend {
server backend:8080;
}
server {
listen 80;
# server_name metacoding.site www.metacoding.site;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
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-NginX-Proxy true;
}
location /api/ {
proxy_pass http://backend;
rewrite ^/api(/.*)$ $1 break; # /api/ 제거가 된다.
proxy_http_version 1.1; # 프록시패스할때 사용할 HTTP 버전지정.
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
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;
}
}
}
요약하자면 /api/로 요청이 오면 /api를 제거해 url을 수정하고 백엔드 서버로 요청을 연결한다.
중요한 점 :
proxy_pass http://backend; : /api/로 들어오는 요청을 백엔드 서버로 전달한다.rewrite ^/api(/.*)$ $1 break; : 요청된 주소에서 ‘/api’ 부분을 제거한다.upstream backend { server backend:8080; } : 여기서 backend는 docker-compose의 backend 서비스를 의미한다. backend:8080의 backend는 백엔드서버에 할당된 IP 주소로 치환된다.FROM node:alpine as build
WORKDIR /app
COPY package.json /app
RUN npm install --silent
COPY . /app
RUN npm run build
FROM nginx
COPY --from=build /app/build /usr/share/nginx/html
# (추가) 내가 설정한 nginx.conf 파일 덮어씌우기
COPY ./nginx/nginx.conf /etc/nginx/nginx.conf
ENTRYPOINT ["nginx", "-g", "daemon off;"]
추가 :
nginx.conf 파일을 /etc/nginx/nginx.conf 경로로 복사해 넣는다.스프링부트의 Dockerfile 내용은 전에 작성했던 내용과 동일하다.
openjdk 이미지를 사용하고 작업 디렉토리는 /app 으로 지정한다. 현재 경로의 내용들(프로젝트 구성 내용)을 작업디렉토리(/app)으로 복사한다.
gradlew에 실행권한을 부여하고 전에 빌드한 내용을 삭제함과 동시에 새로 빌드한다.
빌드한 결과물이 담겨있는 경로(/app/build/libs)를 환경변수 JAR_PATH에 저장하고 해당 환경변수를 이용해서 빌드 결과물을 컨테이너의 /app/app.jar 경로로 이동시킨다.
FROM openjdk:11-jdk-slim
WORKDIR /app
COPY . .
RUN chmod +x ./gradlew
RUN ./gradlew clean build
ENV JAR_PATH=/app/build/libs
RUN mv ${JAR_PATH}/*.jar /app/app.jar
ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=prod", "app.jar"]
version: '3'
services:
db:
build:
context: ./db
dockerfile: Dockerfile
ports:
- 3306:3306
volumes:
- ./db/store:/var/lib/mysql
networks:
- network
backend:
build:
context: ./product
dockerfile: Dockerfile
restart: always
ports:
- 8080:8080
depends_on:
- db
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/metadb?useSSL=false&serverTimezone=UTC&useLegacyDatetimeCode=false&allowPublicKeyRetrieval=true
SPRING_DATASOURCE_DRIVER: com.mysql.cj.jdbc.Driver
SPRING_DATASOURCE_USERNAME: root
SPRING_DATASOURCE_PASSWORD: root1234
networks:
- network
frontend:
build:
context: ./my-app
dockerfile: Dockerfile
restart: always
ports:
- 80:80
depends_on:
- backend
networks:
- network
networks:
network:
frontend 서비스를 추가한다. 컨텍스트는 리엑트 프로젝트가 들어있는 폴더인 my-app 폴더를 지정하고 해당 폴더의 Dockerfile을 실행하도록 지정했다.
backend 서비스가 정상적으로 동작하면 실행하도록 지정하고 3개의 서비스를 모두 ‘network’ 네트워크로 묶었다.
import { useState } from 'react';
import './App.css';
import Card from './components/Card';
import { useEffect } from 'react';
function App() {
const [products, setProducts] = useState([]);
async function onLoad(){
let response = await fetch("/api/products");
let responseBody = await response.json();
console.log("onLoad", responseBody);
setProducts(responseBody);
}
// 해당 페이지가 열릴 때 한번 실행이됨. (초기화 메서드)
// 어떤 값이 변경될때? 다시 그림을 그릴꺼야? 빈배열(어떤 값이든)
useEffect(()=>{
onLoad();
}, []);
return (
<div>
<h1>상품목록페이지</h1>
<hr/>
<div>
{products.map((product)=> <Card product={product}/>)}
</div>
</div>
);
}
export default App;
해당 페이지가 실행되면 http://localhost/api/products url로 요청(fetch)해 백엔드 서버로부터 데이터를 받아오게끔 코드를 작성한다. 그리고 받아온 데이터를 화면에 표시한다.
nginx.conf 파일에서 설정한 내용들로 인해 localhost/api/products 는 Nginx 리버스 프록시 서버를 거쳐 localhost/products url로 변경되어 백엔드 서버로 전달되기때문에 (프론트엔드 서버에서 직접 백엔드 서버로 요청이 가지 않기때문에) 따로 CORS 설정을 하지 않더라도 요청이 잘 수행된다.
현재 스프링에서는 CORS 관련 어노테이션 설정을 제거한 상태이다.
docker-compose up -d 를 수행하고 localhost(localhost:80과 동일)로 url을 요청해보면 리액트로 만들어낸 화면이 보이고 백엔드 서버에서 mysql 데이터베이스의 내용을 불러온 내용이 보이게 된다.
메타코딩 유튜브의 Docker 강의를 듣고 요약한 내용입니다. (강의 적극 추천)