Docker로 Spring Boot 와 MySQL 실행 환경 표준화하기

최정윤·2026년 8월 6일

Spring

목록 보기
31/38

실행 환경을 컨테이너로 묶어야 하는 이유

기존에는 서버에 SSH로 접속한 뒤 Java를 설치하고, JAR 파일을 복사한 다음 java -jar 명령으로 어플리케이션을 실행했다.

이 방식은 서버마다 다음 조건을 직접 맞춰야 한다.

  • 올바른 Java 버전이 설치되어 있는가??
  • 필요한 파일이 정확한 위치에 있는가?
  • 실행 명령과 환경설정이 올바른가?
  • 개발 환경과 서버 환경이 동일한가?

Docker는 배포 작업 자체를 없애는 기술이 아니다. 대신 어플리케이션 실행에 필요한 환경화 실행 방법을 이미지로 표준화한다.

기존 방식
서버 준비 → Java 설치 → JAR 복사 → 설정 입력 → 애플리케이션 실행

Docker 방식
Docker 설치 → 동일한 이미지 실행 → 환경별 설정만 외부에서 주입

즉, Docker를 사용할 수 있는 환경이라면 같은 이미지로 동일한 어플리케이션을 실행할 수 있다.


Dockerfile, 이미지, 컨테이너의 관계

처음에는 Docker 이미지가 일반 사진 파일과 같은 것인지 혼동했다. 하지만 Docker 이미지의 image는 사진이 아니라 실행 환경을 포장한 결과물을 뜻한다.

구분역할비유
Dockerfile이미지를 만드는 방법을 기록한 파일레시피
Docker Image실행 환경과 애플리케이션을 포장한 결과물밀키트
Container이미지로 실제 실행한 프로세스완성된 요리

관계는 다음과 같다.

Dockerfile
    ↓ docker build
Docker Image
    ↓ docker run
Container

이미지는 같은 내용으로 여러 컨테이너를 만들 수 있는 설계도이고, 컨테이너그 이미지가 실제로 실행된 상태이다.


실행 환경과 JAR 파일 준비

Docker 이미지를 Linux 기반으로 만들기 위해 WSL2 Ubuntu에서 작업했다. Java 17 설치 후 다음 명령으로 버전을 확인했다.

java -version

WSL2 Ubuntu의 Java 17 실행 환경
WSL2 내부에 OpenJDK 17을 설치하고 java -version으로 정상 설치를 확인한 결과

Spring Boot 코드는 Docker 이미지에 바로 넣는 것이 아니라 먼저 실행 가능한 JAR 파일로 만들어야 한다.

chmod +x gradlew
./gradlew clean bootJar
ls -lh build/libs

빌드된 JAR 파일을 Dockerfile에서 다루기 쉽도록 프로젝트 최상위 경로에 복사했다.

cp build/libs/rds-0.0.1-SNAPSHOT.jar app.jar

Spring Boot 실행 JAR 빌드 성공
Gradle의 BUILD SUCCESSFUL 메세지와 생성된 파일을 함께 확인한 결과


Dockerfile로 실행 방법 정의하기

작성한 Dockerfile은 다음과 같다.

FROM amazoncorretto:17-al2023-headless
WORKDIR /app
COPY app.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

각 명령의 역할은 다음과 같다.

명령의미필요한 이유
FROMJava 17이 포함된 기본 이미지 선택컨테이너 안에서 JAR를 실행하기 위해
WORKDIR작업 위치를 /app으로 설정파일과 실행 위치를 일정하게 유지하기 위해
COPYapp.jar를 이미지 내부에 복사Spring Boot 실행 파일을 포함하기 위해
EXPOSE애플리케이션이 8080 포트를 사용함을 표시컨테이너의 사용 포트를 명시하기 위해
ENTRYPOINT컨테이너 시작 시 실행할 명령 지정java -jar app.jar를 자동 실행하기 위해

다음 명렁으로 이미지를 생성했다.

docker build -t camp-health-app:v1.0 .
  • -t: 이미지의 이름과 태그 지정
  • camp-health-app: 이미지 이름
  • v1.0: 이미지 버전
  • 마지막 .: 현재 경로의 Dockerfile과 파일을 빌드 대상으로 사용

Spring Boot Docker 이미지 생성
Dockerfile의 각 단계를 실행하고 camp-health-app:v1.0 이미지 생성을 완료한 결과


Spring Boot와 MySQL을 각각 실행하기

MySQL과 Spring Boot를 하나의 컨테이너에 모두 넣지 않고 각각 분리했다.

사용자 요청
    ↓ 8080
Spring Boot 컨테이너
    ↓ host.docker.internal:3306
MySQL 컨테이너

MySQL 컨테이너는 다음 구조로 실행했다.

docker run -d \
  -p 3306:3306 \
  --name mysql-db \
  -e MYSQL_ROOT_PASSWORD=<MYSQL_PASSWORD> \
  -e MYSQL_DATABASE=test \
  mysql:8

-p 3306:3306은 다음 순서로 읽는다.

-p 호스트_포트:컨테이너_포트

Spring Boot 컨테이너는 다음 구조로 실행했다.

docker run -d \
  -p 8080:8080 \
  --name my-app \
  -e SPRING_PROFILES_ACTIVE=local \
  -e SPRING_DATASOURCE_URL=jdbc:mysql://host.docker.internal:3306/test \
  -e SPRING_DATASOURCE_USERNAME=root \
  -e SPRING_DATASOURCE_PASSWORD=<MYSQL_PASSWORD> \
  -e SPRING_JPA_HIBERNATE_DDL_AUTO=update \
  camp-health-app:v1.0

DB 주소와 비밀번호를 이미지에 고정하지 않고 -e 옵션으로 전달했다. Spring Boot에서는 환경변수가 properties 파일의 같은 설정을 덮어쓸 수 있다.

이미지에 포함된 공통 설정
           ↓
실행할 때 전달한 환경변수가 덮어씀
           ↓
환경에 맞는 DB로 연결

이 방식을 사용하면 같은 이미지를 로컬, 개발 서버, 운영 서버에서 재사용하고 연결 정보만 다르게 전달할 수 있다.


컨테이너의 localhost를 주의해야 하는 이유

컨테이너는 각각 독립된 실행 공간이다. 따라서 Spring 컨테이너 안에서 localhost는 Windows도 아니고 MySQL 컨테이너도 아니다.

Spring 컨테이너의 localhost = Spring 컨테이너 자신
MySQL 컨테이너의 localhost = MySQL 컨테이너 자신

이번 구성에서는 Spring 컨테이너가 호스트 컴퓨터의 3306 포트에 접근하도록 다음 주소를 사용했다.

host.docker.internal:3306

host.docker.internal은 주로 Docker Desktop 환경에서 컨테이너가 호스트에 접근할 때 사용하는 주소다.

여러 컨테이너를 Docker Compose의 동일 네트워크에서 실행한다면 mysql-db와 같은 서비스 이름으로 연결할 수 있다.


트러블 슈팅

1. Gradle JDK 설정 오류

WSL2의 프로젝트를 IntelliJ에서 열었지만 Gradle JVM이 지정되지 않아 다음 오류가 발생했다.

잘못된 Gradle JDK 구성을 발견했습니다.

WSL2에 설치된 Java 17을 Gradle JVM으로 지정한 뒤 다시 동기화해 해결했다.

문제 원인

Window의 IntelliJ가 WSL 프로젝트에서 사용할 JDK를 찾지 못함

문제 해결

WSL의 Java 17 지정 → 적용 → Gradle 재동기화

알게된 점

설정을 변경해도 이전 실패 기록이 화면에 남아 있었다.
따라서 오류 문구만 보는 것이 아니라 발생 시간과 새 동기화 결과를 함께 확인해야 한다는 점도 알게 되었다.


2. MySQL의 3306 포트 충돌

MySQL 컨테이너 실행 중 다음 오류가 발생했다.

ports are not available
address already in use

Windows에 설치된 MySQL84 서비스가 이미 3306 포트를 사용하고 있었다.

Get-Service *mysql*

일반 PowerShell에서 서비스를 중지하려 하자 권한 오류가 발생했다. 관리자 PowerShell을 실행한 뒤 서비스를 중지했다.

Stop-Service -Name MySQL84

첫 실행에 실패하면서 mysql-db라는 이름의 컨테이너가 남았기 때문에 해당 컨테이너를 제거한 뒤 다시 실행했다.

docker rm mysql-db

작업 종료 후 기존 MySQL이 필요하면 관리자 PowerShell에서 다시 시작할 수 있다.

Start-Service -Name MySQL84

이 과정을 통해 하나의 호스트 포트는 동시에 하나의 프로그램만 사용할 수 있다는 원리를 확인했다.


정상 동작을 어떻게 검증했는가?

컨테이너가 생성됐다는 사실만으로 어플리케이션이 정상이라고 판단할 수 없다. 로그, 컨테이너 상태, HTTP 응답을 순서대로 확인했다.

1. Spring Boot 로그 확인

docker logs --tail 50 my-app

로그에서 다음 내용을 확인했다.

  • local 프로필 활성화
  • jdbc:mysql://host.docker.internal:3306/test 사용
  • HikariPool-1 - Start completed
  • Tomcat 8080 포트 실행
  • Started RdsApplication

Spring 컨테이너와 MySQL 연결 성공
Spring Boot가 환경변수로 전달된 JDBC 주소를 사용해 MySQL에 연결하고 Tomcat을 시작한 로그

2. HTTP 상태 확인

GET http://localhost:8080/actuator/health

결과:

{
  "groups": [
    "liveness",
    "readiness"
  ],
  "status": "UP"
}

200 OK와 UP을 통해 외부 요청을 정상적으로 처리하고 있음을 확인했다.

3. 전체 컨테이너 상태 확인

docker ps -a

Spring Boot의 my-app과 MySQL의 mysql-db가 모두 Up 상태이고, 각각 8080과 33067 포트에 연결된 것을 확인했다.

Spring Boot와 MySQL 컨테이너 실행 상태
두 컨테이너가 독립적으로 실행되면서 지정된 포트를 통해 연결된 최종 상태


마무리

Spring Boot 어플리케이션을 JAR로 빌드하고, Dockerfile을 이용해 이미지로 만든 뒤 MySQL과 각각의 컨테이너로 실행했다. 환경별 DB 설정은 이미지에 포함하지 않고 실행 시 환경변수로 전달했으며, 로그와 Actuator 응답을 통해 실제 연결 상태를 검증했다.

가장 중요한 변화는 어플리케이션을 단순히 실행하는 게 아니라, 실행 환경까지 이미지로 표준화하고 다른 환경에서도 동일한 방식으로 실행할 수 있게 된 것이다.

profile
콩떡

0개의 댓글