๊ฐ๊ฐ, SpringBoot, React+nginx, MySQL ์ ๋ํ Docker ์ปจํ ์ด๋๋ฅผ ๋ง๋ค์ด์ ๋ฐฐํฌํ ๋ ค๊ณ ํฉ๋๋ค.
1) github
backend(SpringBoot) ํ๋ก์ ํธ,
frontend(React+nginx) ํ๋ก์ ํธ ๊ฐ Github ํ๋ก์ ํธ๋ก ๋ง๋ค์ด์ ธ์ผํฉ๋๋ค.
2) Dockerfile
๊ทธ๋ฆฌ๊ณ ๊ฐ ํ๋ก์ ํธ์ Dockerfile์ด ๋ฃจํธ(./)๊ฒฝ๋ก์ ์์นํฉ๋๋ค.
3) Dockerhub ์ imge push
์ค์ Dockrfile๋ก ๋ง๋ค์ด๋ธ docke image๋ค์ DockerHub์ ์๋ ๋ ํฌ์งํ ๋ฆฌ์ push๋ฅผ ํ๊ณ ,
4) EC2 ์๋ฒ
์๊ฒฉ์ ์๋ EC2์๋ฒ์ docker image๋ฅผ pullํ๊ณ docker run์ ํด์ ๋ฐฐํฌํ ๊ฒ์
๋๋ค.
๋ณด์๊ทธ๋ฃน -> 3000, 8080, 3306, 80 ํฌํธ๋ฅผ ์ด์ด์ผ ํฉ๋๋ค!
Dockerfile
FROM mysql:8.0
ENV MYSQL_ROOT_PASSWORD=1234
ENV MYSQL_DATABASE=test
ENV MYSQL_USER=dsg
ENV MYSQL_PASSWORD=dsg
ENV TZ=Asia/Seoul
EXPOSE 3306
docker ๋ช ๋ น์ด
docker build -t dockerhub๋๋ค์/repository์ด๋ฆ
docker push dockerhub๋ค์/repository์ด๋ฆ
docker run -d -p 3308:3306 docker๋๋ค์/repository์ด๋ฆ
or
docker run \
--name mysql_8.0 \
-d \
--restart unless-stopped \
-e MYSQL_ROOT_PASSWORD=1234 \
-e MYSQL_DATABASE=test \
-e MYSQL_USER=dsg \
-e MYSQL_PASSWORD=dsg \
-e TZ=Asia/Seoul \
-p 3308:3306 \
-v $(pwd)/mysql/conf.d:/etc/mysql/conf.d \
mysql:latest \
--character-set-server=utf8mb4 \
--collation-server=utf8mb4_general_ci
build.gradle
...
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
runtimeOnly 'com.mysql:mysql-connector-j'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
// jpa
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// env
implementation 'io.github.cdimascio:java-dotenv:5.1.1'
}
...
// plain jar ์์๊ธฐ๊ฒ ์ค์
jar {
enabled = false
}
applciation.yml
host.docker.internal
๋ก ๋ณ๊ฒฝํด์ค๋๋ค!server:
port: 8080
spring:
jpa:
hibernate:
ddl-auto: update # ํ
์ด๋ธ ์์ฑ ๋ฐ ์
๋ฐ์ดํธ ์ ๋ต
properties:
hibernate:
format_sql: true # SQL ํฌ๋งทํ
highlight_sql: true # ํ์ด๋ผ์ดํธ SQL ์ถ๋ ฅ
use_sql_comments: true # ์ค์ JPQL SQL ์ฃผ์ ์ฌ์ฉ
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://host.docker.internal:3308/test?serverTimezone=Asia/Seoul&characterEncoding=UTF-8
username: root
password: 1234
logging:
level:
org.hibernate.SQL: debug # Hibernate์ SQL์ ์ถ๋ ฅ
org.hibernate.orm.jdbc.bind: trace # Hibernate์ SQL ๋ฐ์ธ๋ฉ์ ์ถ๋ ฅ
org.springframework.transaction.interceptor: trace # Hibernate์ SQL ๋ฐ์ธ๋ฉ์ ์ถ๋ ฅ
Test
@Builder
@Getter
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "tbl_test")
public class Test {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
}
TestController
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/test")
public class TestController {
private final TestRepository testRepository;
@GetMapping("/list")
public List<TestDTO> test() {
return testRepository.findAll().stream()
.map(test -> TestDTO.builder()
.id(test.getId())
.title(test.getTitle())
.build())
.toList();
}
@GetMapping
public String testHello() {
return "Hello World!";
}
}
NotProd
: ๋ฏธ๋ฆฌ application ์์์, test table์ row 5๊ฐ insert ํด์ฃผ๋ init ์์
์ ํด์ค๋๋ค.
@Slf4j
@Configuration
@RequiredArgsConstructor
//@Profile("!prod")
public class NotProd {
private final TestRepository testRepository;
@Bean
CommandLineRunner initData() {
return (args) -> {
log.info("init data start...");
if(testRepository.count() > 0) {
log.info("init data already exists...");
return;
}
LongStream.rangeClosed(1, 5).forEach(i -> {
// test data
Test test = Test.builder()
.title("title_" + i)
.build();
testRepository.save(test);
});
};
}
}
Dockerfile
# ์ต์ 17-jdk-alpine ์ด๋ฏธ์ง๋ก๋ถํฐ ์์
FROM openjdk:17-jdk-alpine
# ์์
๋๋ ํ ๋ฆฌ๋ฅผ /app์ผ๋ก ์ค์
WORKDIR /app
# ํ์ฌ ๋๋ ํ ๋ฆฌ์ ๋ชจ๋ ํ์ผ์ ์ปจํ
์ด๋์ /app ๋๋ ํ ๋ฆฌ๋ก ๋ณต์ฌ
COPY . .
# gradlew์ ์คํ ๊ถํ ๋ถ์ฌ
RUN chmod +x ./gradlew
# ํ๋ก์ ํธ ๋น๋
RUN ./gradlew clean build
#ENV SPRING_PROFILES_ACTIVE=prod
# ์ปจํ
์ด๋๋ก ๋ด์์ app.jar ์์น ๋ณ๊ฒฝ
ARG JAR_FILE=build/libs/*SNAPSHOT.jar
RUN mv ${JAR_FILE} app.jar
# ์ปจํ
์ด๋๊ฐ ์คํ๋ ๋ ์คํ๋ ๋ช
๋ น์ด ์ง์
# ์ค๋ฅ๋จ! ์๋ํ๋ฉด, spring.profiles.active=prod๋ฅผ ์ฌ์ฉํ๋ ค๋ฉด, application.properties์ spring.profiles.active=prod๋ฅผ ์ถ๊ฐํด์ผํจ
ENTRYPOINT ["java", "-jar", "app.jar"]
#ENTRYPOINT ["java", "-Dspring.profiles.active=prod", "-jar","app.jar"]
docker ๋ช ๋ น์ด
docker build -t dockerhub๋ค์/repository์ด๋ฆ
docker push dockerhub๋ค์/repository์ด๋ฆ
docker run -d -p 8080:8080 dockerhub๋ค์/repository์ด๋ฆ
npm ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ค์น
npm i axios
npm install react-router-dom
index.js
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);
testApi.js
import axios from "axios";
// .env ํ์ผ์ ์๋ AWS_EC2_IP_URL ๊ฐ์ ๊ฐ์ ธ์ค๊ธฐ
// const backendIpUrl = process.env.REACT_APP_BACKEND_IP_URL;
const backendIpUrl = "http://43.203.42.37";
// ํ
์คํธ API ํธ์ถ
export const getTest = async () => {
console.log("backendIpUrl: ", backendIpUrl);
const response = await axios.get(`${backendIpUrl}/api/test/list`);
return response;
};
Home.js
import React, { useEffect, useState } from "react";
import { getTest } from "../api/testApi";
const Home = () => {
// test
const [tests, setTests] = useState([]);
useEffect(() => {
getTest().then((res) => {
console.log(res);
setTests(res.data);
});
}, []);
return (
<div>
<h1>dsg ์๋์ ์ค์ ๊ฑธ ํ์ํฉ๋๋ค!</h1>
<h2>์ค๋๋ ์ฆ๊ฑฐ์ด ํ๋ฃจ ๋์ธ์!</h2>
<br />
<ul style={{ listStyle: "none" }}>
{tests.map((item) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
</div>
);
};
export default Home;
App.js
import './App.css';
import {Route, Routes} from "react-router-dom";
import Home from "./components/Home";
function App() {
return (
<div className="App">
<Routes>
<Route path="/" element={<Home />} />
</Routes>
</div>
);
}
export default App;
Dockerfile
# ๋น๋๋ฅผ ์ํ Node.js์ ๊ฒฝ๋ ๋ฒ์ ์ธ Alpine ์ด๋ฏธ์ง๋ฅผ ์ฌ์ฉํฉ๋๋ค.
FROM node:alpine as build
# ์์
๋๋ ํ ๋ฆฌ๋ฅผ /app์ผ๋ก ์ค์ ํฉ๋๋ค.
WORKDIR /app
# package.json๊ณผ package-lock.json ํ์ผ์ ํ์ฌ ์์
๋๋ ํ ๋ฆฌ๋ก ๋ณต์ฌํฉ๋๋ค.
COPY package.json package-lock.json ./
# ์ข
์์ฑ์ ์ค์นํฉ๋๋ค. --silent ์ต์
์ ์ฌ์ฉํ์ฌ ์ถ๋ ฅ ๋ฉ์์ง๋ฅผ ์ต์ํํฉ๋๋ค.
RUN npm install --silent
# ํ์ฌ ๋๋ ํ ๋ฆฌ์ ๋ชจ๋ ํ์ผ์ ์์
๋๋ ํ ๋ฆฌ๋ก ๋ณต์ฌํฉ๋๋ค.
COPY . /app
# ์ ํ๋ฆฌ์ผ์ด์
์ ๋น๋ํฉ๋๋ค.
RUN npm run build
# Nginx์ ๊ฒฝ๋ ๋ฒ์ ์ธ Alpine ์ด๋ฏธ์ง๋ฅผ ์ฌ์ฉํฉ๋๋ค.
FROM nginx:alpine
# ๋น๋๋ ํ์ผ์ Nginx์ ์ ์ ํ์ผ ์ ๊ณต ๋๋ ํ ๋ฆฌ๋ก ๋ณต์ฌํฉ๋๋ค.
COPY --from=build /app/build /usr/share/nginx/html
# Nginx ์ค์ ํ์ผ์ ์ปจํ
์ด๋์ Nginx ์ค์ ๋๋ ํ ๋ฆฌ๋ก ๋ณต์ฌํฉ๋๋ค.
COPY ./nginx/nginx.conf /etc/nginx/conf.d/default.conf
# Nginx๋ฅผ ํฌ๊ทธ๋ผ์ด๋ ๋ชจ๋(daemon off)๋ก ์คํํฉ๋๋ค.
ENTRYPOINT ["nginx", "-g", "daemon off;"]
./nginx/nginx.conf
upstream backend {
server 172.17.0.1:8080; # backend -> ๊ฒ์ดํธ์จ์ด ์๋ฒ 172.17.0.1
}
server {
listen 80;
server_name 43.203.42.37 dsgshop.store www.dsgshop.store; # ๋๋ฉ์ธ ์ด๋ฆ ์ถ๊ฐ
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://backend;
}
}
docker ๋ช ๋ น์ด
docker build -t dockerhub๋ค์/repository์ด๋ฆ
docker push dockerhub๋ค์/repository์ด๋ฆ
docker run -d -p 80:80 dockerhub๋ค์/repository์ด๋ฆ
docker, docker-compose ์ค์น
์ฐธ๊ณ ๋ธ๋ก๊ทธ) https://velog.io/@mooh2jj/AWS-EC2-Docker-์ค์น
dockerHub์ ์๋ ๊ฐ๊ฐ(backend, frontend) ๋ ํฌ์งํ ๋ฆฌ์ push๋ docker image ๋ค์ docker pull ๋ฐ์์ EC2 ์๋ฒ์ ๊ฐ์ ธ์ต๋๋ค.
docker run ์ผ๋ก ๋ถ๋ฌ์ ํ๋ก์ ํธ ์ปจํ ์ด๋๋ฅผ ์คํํ ์ ์์ต๋๋ค.
ํฅํ, ์ด๋ฅผ ์๋ํํ๋ CICD๋๊ตฌ์ธ github-Action ์ ์ฌ์ฉํ๋ฉด ๋ ๊ฐํธํ๊ฒ EC2 ์ธ์คํด์ค์ ๋ฐฐํฌํ ์ ์์ต๋๋ค.
docker-compose.yml
version: '3.3'
services:
db:
image: mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: 1234
MYSQL_DATABASE: test
TZ: Asia/Seoul
volumes:
# - ./mysql_data:/var/lib/mysql # load failed ๊ฐ ๋จ!
- ./mysql/conf.d:/etc/mysql/conf.d
ports:
- "3306:3306"
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_general_ci
healthcheck:
test: [ "CMD", "mysqladmin", "ping" ]
interval: 5s
retries: 10
networks:
- test_network
backend:
restart: on-failure
# .env ํ์ผ์ ๋ก๋ํ์ฌ ํ๊ฒฝ๋ณ์๋ฅผ ์ค์
env_file:
- .env
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:8080"
depends_on:
- db
# condition: service_healthy
networks:
- test_network
frontend:
restart: on-failure
build:
context: ./frontend
dockerfile: Dockerfile
ports:
- "80:80"
depends_on:
- backend
networks:
- test_network
networks:
test_network:
driver: bridge
docker compose ๋ช ๋ น์ด
docker compose up -d --build
docker compose down
# ๋์ปค ์์ฌ์ฉํ๋ ๋คํธ์ํฌ, ๋ณผ๋ฅจ, ์ด๋ฏธ์ง, ์ปจํ
์ด๋ ์ ๋ถ ์ญ์
docker system prune -f
./nginx/nginx.conf
upstream backend {
server backend:8080; # backend -> ๊ฒ์ดํธ์จ์ด ์๋ฒ 172.17.0.1
}
server {
listen 80;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://backend;
}
}