기존에는 서버에 SSH로 접속한 뒤 Java를 설치하고, JAR 파일을 복사한 다음 java -jar 명령으로 어플리케이션을 실행했다.
이 방식은 서버마다 다음 조건을 직접 맞춰야 한다.
Docker는 배포 작업 자체를 없애는 기술이 아니다. 대신 어플리케이션 실행에 필요한 환경화 실행 방법을 이미지로 표준화한다.
기존 방식
서버 준비 → Java 설치 → JAR 복사 → 설정 입력 → 애플리케이션 실행
Docker 방식
Docker 설치 → 동일한 이미지 실행 → 환경별 설정만 외부에서 주입
즉, Docker를 사용할 수 있는 환경이라면 같은 이미지로 동일한 어플리케이션을 실행할 수 있다.
처음에는 Docker 이미지가 일반 사진 파일과 같은 것인지 혼동했다. 하지만 Docker 이미지의 image는 사진이 아니라 실행 환경을 포장한 결과물을 뜻한다.
| 구분 | 역할 | 비유 |
|---|---|---|
| Dockerfile | 이미지를 만드는 방법을 기록한 파일 | 레시피 |
| Docker Image | 실행 환경과 애플리케이션을 포장한 결과물 | 밀키트 |
| Container | 이미지로 실제 실행한 프로세스 | 완성된 요리 |
관계는 다음과 같다.
Dockerfile
↓ docker build
Docker Image
↓ docker run
Container
이미지는 같은 내용으로 여러 컨테이너를 만들 수 있는 설계도이고, 컨테이너는 그 이미지가 실제로 실행된 상태이다.
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은 다음과 같다.
FROM amazoncorretto:17-al2023-headless
WORKDIR /app
COPY app.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
각 명령의 역할은 다음과 같다.
| 명령 | 의미 | 필요한 이유 |
|---|---|---|
FROM | Java 17이 포함된 기본 이미지 선택 | 컨테이너 안에서 JAR를 실행하기 위해 |
WORKDIR | 작업 위치를 /app으로 설정 | 파일과 실행 위치를 일정하게 유지하기 위해 |
COPY | app.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 이미지 생성을 완료한 결과
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로 연결
이 방식을 사용하면 같은 이미지를 로컬, 개발 서버, 운영 서버에서 재사용하고 연결 정보만 다르게 전달할 수 있다.
컨테이너는 각각 독립된 실행 공간이다. 따라서 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와 같은 서비스 이름으로 연결할 수 있다.
WSL2의 프로젝트를 IntelliJ에서 열었지만 Gradle JVM이 지정되지 않아 다음 오류가 발생했다.
잘못된 Gradle JDK 구성을 발견했습니다.
WSL2에 설치된 Java 17을 Gradle JVM으로 지정한 뒤 다시 동기화해 해결했다.
Window의 IntelliJ가 WSL 프로젝트에서 사용할 JDK를 찾지 못함
WSL의 Java 17 지정 → 적용 → Gradle 재동기화
설정을 변경해도 이전 실패 기록이 화면에 남아 있었다.
따라서 오류 문구만 보는 것이 아니라 발생 시간과 새 동기화 결과를 함께 확인해야 한다는 점도 알게 되었다.
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 응답을 순서대로 확인했다.
docker logs --tail 50 my-app
로그에서 다음 내용을 확인했다.
local 프로필 활성화
Spring 컨테이너와 MySQL 연결 성공
Spring Boot가 환경변수로 전달된 JDBC 주소를 사용해 MySQL에 연결하고 Tomcat을 시작한 로그
GET http://localhost:8080/actuator/health
결과:
{
"groups": [
"liveness",
"readiness"
],
"status": "UP"
}
200 OK와 UP을 통해 외부 요청을 정상적으로 처리하고 있음을 확인했다.

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

Spring Boot와 MySQL 컨테이너 실행 상태
두 컨테이너가 독립적으로 실행되면서 지정된 포트를 통해 연결된 최종 상태
Spring Boot 어플리케이션을 JAR로 빌드하고, Dockerfile을 이용해 이미지로 만든 뒤 MySQL과 각각의 컨테이너로 실행했다. 환경별 DB 설정은 이미지에 포함하지 않고 실행 시 환경변수로 전달했으며, 로그와 Actuator 응답을 통해 실제 연결 상태를 검증했다.
가장 중요한 변화는 어플리케이션을 단순히 실행하는 게 아니라, 실행 환경까지 이미지로 표준화하고 다른 환경에서도 동일한 방식으로 실행할 수 있게 된 것이다.