๐ŸŒŸ EC2 ๋ฐฐํฌ, Docker๋กœ SpringBoot + MySQL DB + Nginx+React ์—ฐ๋™

devdoยท2024๋…„ 12์›” 23์ผ
0

Docker

๋ชฉ๋ก ๋ณด๊ธฐ
10/10
post-thumbnail

๊ฐ๊ฐ, 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

MySQL

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

backend

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

  • datasource ๋‚ด url localhost -> 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

  • gradlew build ๊นŒ์ง€ ํฌํ•จํ•œ Dockerfile
  • ์ด์ œ docker build ๋ฅผ ํ•˜๋ฉด, gradlew build ์ž‘์—…๊นŒ์ง€ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
# ์ตœ์‹  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์ด๋ฆ„

nginx+react

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์ด๋ฆ„

EC2(ubuntu) ์— Docker ์ง„ํ–‰

  • 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

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;
    }
}


์ฐธ๊ณ 

profile
๋ฐฐ์šด ๊ฒƒ์„ ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค.

0๊ฐœ์˜ ๋Œ“๊ธ€