Docker로 배포 자동화하기
개발 환경
- spring boot 3.2.2
- react 18.2.0
- Node.js 18.x
- docker 27.1.1
- mysql 8.0.36
- Gradle
프로젝트 구조 및 환경
이전에는 Spring Boot와 React를 통합 환경에서 작업했습니다. 즉, 백엔드 프로젝트 내에 프론트엔드를 포함한 형태였고, 배포할 때도 하나의 jar 파일을 만들어서 EC2에 올려서 실행했습니다. 이 방식은 초기에는 간편했지만, 프로젝트가 커지고 수정할 때마다 jar 파일을 생성해야 하는 번거로움이 있었습니다. 또 빌드 시간이 길어지는 문제도 발생했습니다.
왜 프론트와 백엔드를 나누게 됐을까?
프로젝트를 하면서 프론트엔드와 백엔드를 나눠서 관리하기로 한 이유는 몇 가지가 있습니다
역할 분리: 보통 프론트엔드와 백엔드를 각각 다른 사람이 맡아서 작업하는 경우가 많습니다. 따로 나눠두면 각자 맡은 부분만 신경 쓰면 되니까 작업도 훨씬 수월해집니다. 배포할 때도 각각 관리할 수 있어서 훨씬 유리합니다.
Docker 사용의 편리함: 통합 환경에서는 Docker를 도입할 때 어려움이 있었습니다. 개발할 때는 잘 돌아가는데, 컨테이너 안에서 실행하려고 하면 환경이 충돌하거나 제대로 작동하지 않는 경우가 많았습니다. 그래서 프론트와 백엔드를 따로 나누고, Docker 컨테이너로 분리해서 각각 관리할 수 있게 만들었습니다.
프로젝트 구조
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
├─📂.gradle
├─📂.idea
├─📂backend
│ ├─📂build
│ ├─📂src
│ │ ├─📂main
│ │ └─📂test
│ ├─📜Dockerfile
│ └─📜build.gradle
├─📂frontend
│ ├─📂node_module
│ ├─📂public
│ ├─📂src
│ ├─📜Dockerfile
│ └─📜package.json
├─📂bin
├─📂gradle
├─📂wrapper
├─📜docer-compose.yml
└─📜.env
백엔드: Spring Boot로 작성한 백엔드 부분은 ./backend 폴더에 있습니다. Dockerfile을 사용해 이 애플리케이션을 컨테이너로 빌드할 수 있도록 설정해두었습니다.
프론트엔드: React로 만든 프론트엔드는 ./frontend 폴더에 있고, 마찬가지로 Docker로 빌드해서 백엔드와는 독립적으로 실행됩니다.
Docker Compose: docker-compose.yml 파일로 MySQL, 프론트엔드, 백엔드를 한꺼번에 실행할 수 있게 했습니다. 각 서비스는 컨테이너로 나눠서 동작하고, 서로 통신할 수 있게 네트워크도 설정해두었습니다.
프로젝트 빌드 및 Dockerfile 작성
백엔드 Dockerfile
1
2
3
4
5
6
7
8
9
10
11
FROM openjdk:17-jdk-alpine
WORKDIR /src
CMD ["./gradlew", "clean", "build"]
COPY build/libs/backend-0.0.1-SNAPSHOT.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","./app.jar"]
프론트엔드 Dockerfile
1
2
3
4
5
6
7
8
9
10
11
12
13
FROM node:18-alpine
WORKDIR /src
COPY package.json .
RUN npm install
COPY . .
EXPOSE 3000
CMD [ "npm", "start" ]
Docker Compose 설정
Docker Compose를 사용하여 전체 애플리케이션을 한꺼번에 실행할 수 있도록 하였습니다. 이 설정 파일을 통해 MySQL, 프론트엔드, 백엔드를 각각의 컨테이너에서 관리합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
services:
db:
image: mysql:8.0.36
volumes:
- ./db_data:/var/lib/mysql
ports:
- "3307:3306"
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
healthcheck:
test: [ "CMD", "mysqladmin" ,"ping", "-h", "contenthub-db-1" ]
interval: 3s
timeout: 20s
retries: 10
networks:
- network
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
container_name: contenthub-frontend
ports:
- "3000:3000"
networks:
- network
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: contenthub-backend
ports:
- "8080:8080"
depends_on:
- db
environment:
SPRING_DATASOURCE_URL: ${SPRING_DATASOURCE_URL}
SPRING_DATASOURCE_USERNAME: ${SPRING_DATASOURCE_USERNAME}
SPRING_DATASOURCE_PASSWORD: ${SPRING_DATASOURCE_PASSWORD}
networks:
- network
networks:
network:
driver: bridge
중요한 설정 값들은 .env 파일을 이용해서 관리하고 있습니다.
Docker Compose 실행
Docker Compose를 사용해 애플리케이션을 실행할 때는 docker-compose up --build 명령어를 사용합니다. 이 명령어는 docker-compose.yml 파일에 정의된 서비스에 맞게 이미지를 생성한 후, 컨테이너를 실행합니다. 만약 이미지를 다시 빌드하고 싶다면 이 명령어를 사용하면 됩니다.
1
docker-compose up --build
이 명령어는 변경 사항이 있을 때 유용하게 사용할 수 있습니다.
컨테이너를 중지하고 모든 자원을 정리하고 싶다면 docker-compose down 명령어를 사용하면 됩니다. 이 명령어는 실행 중인 컨테이너를 중지하고, 생성된 네트워크와 볼륨을 삭제합니다.
1
docker-compose down
MySQL 설정 및 데이터베이스 연결
MySQL 도커 연결 문제
1. Ports are not available 에러: 포트 중복 문제
Ports are not available: exposing port TCP 0.0.0.0:3306 -> 0.0.0.0:0: listen tcp 0.0 .0.0:3306: bind: Only one usage of each socket address (protocol/network address/port) is normally permitted.
이미 포트 3306이 사용 중이어서 문제가 발생했습니다. 로컬에서 MySQL Workbench를 이용하며 진행하는 중 도커와 MySQL 간의 연결 시 3306 포트 충돌이 발생한 것입니다.
해결 방법 Docker Compose에서 MySQL 도커 실행 명령어에서 다른 포트를 사용하도록 변경해주었습니다.
1 2
ports: - "3307:3306" # 호스트의 3307 포트를 컨테이너의 3306 포트로 매핑
2. access denied for user 에러 : 권한 문제
access denied for user 'root'@'localhost' (using password: yes)
MySQL에 로그인할 때 잘못된 비밀번호를 사용했거나 환경 변수 설정에서 문제가 발생합니다.
- 해결 방법
docker-compose.yml파일에서 도커 컨테이너 시작 시 비밀번호가 올바르게 설정되었는지 확인해야 합니다.1 2
environment: MYSQL_ROOT_PASSWORD: your_password
저는 MySQL Workbench의 비밀번호와 혼동하여 발생한 문제였습니다.
혹시 몰라서
docker exec -it your_mysql_container mysql -u root -p명령어로 MySQL에 접속한 후 비밀번호를 설정해주었습니다.1
ALTER USER 'root'@'localhost' IDENTIFIED BY 'new_password';
3. Communications link failure: 연결 문제
com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure </n> The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.
MySQL 서버에 연결할 수 없을 때 발생하는 에러입니다. MySQL 서버가 실행 중이지 않거나 잘못된 네트워크 설정으로 인해 접근할 수 없는 경우입니다.
해결 방법
spring.datasource.url=${SPRING_DATASOURCE_URL}에서 일반적으로 로컬 환경에서는jdbc:mysql://localhost:3306/데이터베이스명?serverTimezone=UTC&characterEncoding=UTF-8로 설정하지만, 도커를 이용할 경우엔 localhost를 MySQL 도커 컨테이너 이름으로 바꾸어야 합니다.추가로 useSSL=false&allowPublicKeyRetrieval=true 설정을 더했습니다.
4. react와 500에러
위의 1~3번 에러를 해결한 후에는 데이터베이스와 서버 간의 연결 오류는 발생하지 않았습니다. 하지만, React에서 500 에러가 발생했습니다.
- 해결 방법 package.json에서 프록시 설정을 변경해줘야합니다.
1
"proxy": "http://backend:8080",
localhost를 도커 서비스명으로 설정해야 합니다.