현대 애플리케이션은 하나의 서버만으로 동작하지 않는다. API 서버(Spring Boot), 데이터베이스(MySQL), 캐싱 서버(Redis), 정적 프론트엔드(React), Reverse Proxy(Nginx)처럼 서로 독립된 서비스들이 협력하여 전체 시스템을 이룬다.
Docker는 "하나의 컨테이너는 하나의 책임만 가진다"는 단일 책임 원칙을 따른다. 이는 매우 중요한 설계 철학이다.
만약 Spring Boot 서버와 MySQL을 하나의 컨테이너에 함께 실행한다면 어떻게 될까? Spring 애플리케이션에 버그가 생겨 서버가 다운되면, 아무 문제 없는 데이터베이스까지 함께 중단된다. 또한 Spring Boot만 업데이트하고 싶어도 전체 컨테이너를 재시작해야 하므로 데이터베이스 연결이 끊긴다.
각 서비스를 독립된 컨테이너로 분리하면 다음과 같은 장점이 있다.
그렇다면 각 컨테이너를 일일이 실행하면 어떻게 될까? 다음과 같이 명령을 반복해야 한다.
# MySQL 실행
docker run -d --name mysql-db \
-e MYSQL_ROOT_PASSWORD=1234 \
-e MYSQL_DATABASE=testdb \
-p 3306:3306 \
--network app-network \
mysql:8
# Redis 실행
docker run -d --name redis-cache \
-p 6379:6379 \
--network app-network \
redis:7
# Spring Boot 빌드 및 실행
./gradlew bootJar
docker build -t spring-app .
docker run -d --name spring-app \
-p 8080:8080 \
-e SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/testdb \
--network app-network \
--depends-on mysql redis \
spring-app
# Nginx 실행
docker run -d --name nginx-proxy \
-p 80:80 \
-v ./nginx/default.conf:/etc/nginx/conf.d/default.conf \
--network app-network \
nginx:latest
네트워크를 먼저 생성하고, 각 컨테이너의 환경 변수와 포트를 일일이 설정하고, 의존성 순서를 고려해서 실행해야 한다. 이를 매번 반복하는 것은 매우 비효율적이다.
Docker Compose를 사용하면 이 모든 설정을 하나의 파일에 선언하고, 단 한 줄의 명령으로 전체 환경을 실행할 수 있다.
docker-compose up -d
이 명령 하나로 모든 서비스가 올바른 순서로 실행되고, 네트워크가 자동으로 연결되며, 환경 변수가 적용된다. 개발자는 복잡한 Docker 명령을 외울 필요 없이 설정 파일만 관리하면 된다.
Docker Compose는 여러 개의 Docker 컨테이너를 하나의 설정 파일(docker-compose.yml)로 묶어서 한 번에 실행, 중지, 관리하는 도구다. 이 글에서는 Docker Compose의 실행 원리, 서비스 간 의존성 처리 방식, 각 구성 요소별 설정 방법, 그리고 전체 아키텍처 흐름을 알아본다.
Spring Boot + MySQL + Redis + Nginx로 구성된 멀티 컨테이너 환경을 예시로 살펴본다. MySQL과 Redis는 Docker Hub에서 이미지를 받아오는 것으로 충분하지만, Spring Boot는 빌드를 따로 해주어야 하므로 Dockerfile이 필요하다.
중요한 점은 Dockerfile, docker-compose.yml, 그리고 빌드 결과물이 모두 같은 프로젝트 루트에 위치해야 한다는 것이다. 이렇게 하면 상대 경로로 파일을 참조할 수 있어 관리가 편리하다.
springboot-mysql-app/
│
├── docker-compose.yml ← 전체 컨테이너 설정
├── Dockerfile ← Spring Boot 애플리케이션용 도커파일
├── mysql/ ← MySQL 초기 설정 (옵션)
│ └── init.sql ← 초기 SQL 스크립트 (선택사항)
├── nginx/
│ └── default.conf ← Nginx 설정 파일
├── src/
│ └── main/
│ ├── java/...
│ └── resources/
│ └── application.properties
├── build/
│ └── libs/
│ └── app.jar ← Gradle 빌드 결과물
└── build.gradle or pom.xml ← Gradle 또는 Maven
Docker Compose 설정 파일은 YAML 형식으로 작성한다. 들여쓰기를 완벽하게 맞춰주지 않으면 인식에 실패하므로 주의해야 한다. 탭이 아니라 스페이스 2번으로 들여쓰기를 한다.
version: "3.8"
services:
spring:
build: .
container_name: spring-app
ports:
- "8080:8080"
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/testdb?serverTimezone=Asia/Seoul
SPRING_DATASOURCE_USERNAME: root
SPRING_DATASOURCE_PASSWORD: 1234
SPRING_REDIS_HOST: redis
depends_on:
- mysql
- redis
networks:
- app-network
mysql:
image: mysql:8
container_name: mysql-db
restart: always
environment:
MYSQL_ROOT_PASSWORD: 1234
MYSQL_DATABASE: testdb
TZ: Asia/Seoul
ports:
- "3306:3306"
volumes:
- mysql-data:/var/lib/mysql
networks:
- app-network
redis:
image: redis:7
container_name: redis-cache
ports:
- "6379:6379"
volumes:
- redis-data:/data
networks:
- app-network
nginx:
image: nginx:latest
container_name: nginx-proxy
ports:
- "80:80"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf
depends_on:
- spring
networks:
- app-network
volumes:
mysql-data:
redis-data:
networks:
app-network:
기본적으로 Docker Compose는 docker-compose.yml 파일을 찾는다. 하지만 프로젝트에 여러 환경 설정이 필요하거나, 더 명확한 이름을 사용하고 싶다면 파일명을 변경할 수 있다.
예를 들어 mysql8-compose.yml이라는 이름을 사용하려면 -f 옵션으로 파일을 지정해야 한다.
# 커스텀 파일명으로 실행
docker-compose -f mysql8-compose.yml up -d
# 커스텀 파일명으로 종료
docker-compose -f mysql8-compose.yml down
# 커스텀 파일명으로 로그 확인
docker-compose -f mysql8-compose.yml logs -f
여러 환경을 관리할 때 유용하다.
docker-compose -f docker-compose.dev.yml up # 개발 환경
docker-compose -f docker-compose.prod.yml up # 운영 환경
docker-compose -f mysql8-compose.yml up # MySQL 8 환경
| 항목 | 설명 |
|---|---|
| version | Compose 파일 버전 |
| services | 실행할 컨테이너 집합 |
| image | 사용할 Docker 이미지 |
| build | Dockerfile 위치 (현재 디렉토리는 .) |
| ports | 포트 매핑 |
| volumes | 데이터 저장소 |
| environment | 환경 변수 설정 |
| depends_on | 의존이 있을 때 실행 순서 지정 |
| networks | 컨테이너 간 네트워크 연결 |
컨테이너는 기본적으로 일시적(ephemeral)이다. 즉, 컨테이너를 삭제하면 그 안의 모든 데이터도 함께 사라진다. 데이터베이스처럼 중요한 데이터를 저장하는 서비스라면 이는 치명적인 문제다.
Docker Volume은 이 문제를 해결한다. 볼륨을 사용하면 컨테이너 외부의 안전한 저장소에 데이터를 보관하여, 컨테이너를 삭제하거나 재시작해도 데이터가 유지된다.
위 설정에서 MySQL과 Redis는 각각 Named Volume을 사용한다.
mysql:
volumes:
- mysql-data:/var/lib/mysql # Named Volume 사용
redis:
volumes:
- redis-data:/data # Named Volume 사용
nginx:
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf # Bind Mount 사용
Named Volume 방식
mysql-data:/var/lib/mysql: mysql-data라는 이름의 볼륨을 컨테이너 내부의 /var/lib/mysql 경로에 마운트한다/var/lib/mysql에 저장하므로, 이 경로를 볼륨으로 지정하면 데이터가 영속적으로 보관된다/data 경로에 데이터를 저장하므로 해당 경로를 볼륨으로 지정한다Bind Mount 방식
./nginx/default.conf:/etc/nginx/conf.d/default.conf: 호스트의 실제 파일을 컨테이너 내부로 직접 마운트한다파일 하단에 사용할 볼륨을 선언한다.
volumes:
mysql-data:
redis-data:
이렇게 선언된 볼륨은 Docker가 자동으로 관리한다. 실제 저장 위치는 Docker가 결정하며, Windows의 경우 WSL2 내부에, Linux의 경우 /var/lib/docker/volumes/에 생성된다.
예를 들어 다음과 같은 상황을 생각해보자.
# 컨테이너 실행 후 데이터 생성
docker-compose up -d
# MySQL에 테이블 생성 및 데이터 입력
# 컨테이너 삭제
docker-compose down
# 다시 실행
docker-compose up -d
# 이전에 입력한 데이터가 그대로 남아있음!
볼륨이 없었다면 docker-compose down 후 모든 데이터가 사라졌을 것이다.
depends_on에 의해 의존성이 설정된 서비스는 먼저 실행된다. 위 예시에서 Spring 서비스는 MySQL과 Redis에 의존하므로, 이 두 서비스가 먼저 시작된다.
서비스 이름이 자동으로 호스트(컨테이너 이름) 역할을 한다. 컨테이너 간에 같은 네트워크로 연결되어 있으므로 Spring Boot가 jdbc:mysql://mysql:3306/testdb 형태로 직접 접속할 수 있다. 여기서 mysql은 docker-compose.yml에 정의된 서비스 이름이다.
depends_on에 mysql과 redis가 있으므로 로컬 컨테이너에 없으면 Docker Hub에서 자동으로 다운로드한다. networks: - app-network 설정으로 모든 컨테이너가 같은 네트워크에 묶인다.
Docker Compose를 사용하면 다음과 같은 기능이 자동으로 처리된다.
✔ Spring Boot → MySQL로 DB 연결
✔ Spring Boot → Redis 캐싱
✔ Nginx → Spring Boot로 Reverse Proxy
✔ MySQL, Redis는 볼륨으로 데이터 영속성 보장
✔ 모든 서비스는 app-network로 자동 연결
✔ 한 명령으로 전체 환경 실행 및 종료
모든 설정이 완료되면 docker-compose up -d --build 한 문장의 명령어로 전체 환경을 실행할 수 있다.
docker-compose up
docker-compose up -d
docker-compose down
docker-compose logs -f
docker-compose up spring
docker-compose up --build
Spring Boot 애플리케이션을 위한 Dockerfile을 프로젝트 루트에 작성한다. Gradle 빌드 후 build/libs/ 안에 .jar 파일이 반드시 존재해야 한다.
FROM openjdk:17-jdk-slim
WORKDIR /app
COPY build/libs/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
Nginx를 Reverse Proxy로 사용하기 위한 설정 파일을 작성한다. 여기서 spring은 Compose 서비스 이름이며, nginx가 spring:8080으로 요청을 전달하는 Reverse Proxy 역할을 수행한다.
server {
listen 80;
location / {
proxy_pass http://spring:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
Compose에서 지정한 환경 변수를 그대로 사용하도록 설정한다.
spring:
datasource:
url: ${SPRING_DATASOURCE_URL}
username: ${SPRING_DATASOURCE_USERNAME}
password: ${SPRING_DATASOURCE_PASSWORD}
redis:
host: ${SPRING_REDIS_HOST}
port: 6379
프로젝트 루트 디렉토리에서 다음 명령을 실행한다. Dockerfile과 docker-compose.yml이 같은 위치에 있으므로 빌드와 실행이 한 번에 이루어진다.
./gradlew bootJar
docker-compose up -d --build
실행 후 다음 주소로 각 서비스에 접근할 수 있다.
http://localhost → Nginxhttp://localhost:8080 → Spring Bootlocalhost:3306 → MySQLlocalhost:6379 → Redis먼저 Spring Boot 애플리케이션을 컨테이너화하기 위해 Dockerfile을 작성하고 이미지를 빌드했다.


docker build -t spring-docker-hello .
발생한 오류:
ERROR: failed to build: failed to solve: lstat /build/libs: no such file or directory
원인: Gradle 빌드를 먼저 실행하지 않아 build/libs/*.jar 파일이 존재하지 않았다.
해결: Gradle 빌드를 먼저 실행한다.
./gradlew bootJar
docker build -t spring-docker-hello .
빌드가 성공적으로 완료되었고, 약 21MB의 빌드 컨텍스트가 전송되었다.
이제 docker-compose.yml 파일을 작성하고 멀티 컨테이너 환경을 구성했다.
docker compose up -d --build
컨테이너는 시작되었지만 로그를 확인하니 문제가 발생했다.
docker logs hello-server
발생한 오류:
java.lang.UnsupportedClassVersionError: kr/or/kosa/SpringDockerHelloApplication
has been compiled by a more recent version of the Java Runtime
(class file version 65.0), this version only recognizes class file versions up to 61.0
원인:
해결: Dockerfile의 베이스 이미지를 Java 21로 변경한다.
FROM eclipse-temurin:21-jdk
WORKDIR /app
COPY build/libs/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
약 158MB의 Java 21 이미지를 다운로드하고 다시 빌드했다.
docker compose down
docker compose up -d --build
이번에는 컨테이너가 정상적으로 실행되었다.
docker compose ps
NAME IMAGE COMMAND SERVICE STATUS PORTS
hello-server spring_docker_hello-hello-server "java -jar app.jar" hello-server Up 3 seconds 0.0.0.0:8090->8080/tcp
이제 docker-compose.yml에 MySQL과 Redis를 추가했다.
docker compose up -d --build
발생한 오류:
Error response from daemon: Bind for 0.0.0.0:3306 failed: port is already allocated
Error response from daemon: Bind for 0.0.0.0:6379 failed: port is already allocated
원인: 이전에 실행한 MySQL과 Redis 컨테이너가 이미 해당 포트를 사용 중이었다.
docker ps
CONTAINER ID IMAGE COMMAND PORTS
fa88c65b6edb mysql ... 0.0.0.0:3306->3306/tcp
51b5722291fc redis ... 0.0.0.0:6379->6379/tcp
eef0e47800f1 nginx ... 0.0.0.0:80->80/tcp
해결: 기존 컨테이너들을 모두 제거한다.
docker rm -f compose-my-cache-server-1
docker rm -f mysql-my-db-1
docker rm -f webserver
다시 실행하니 경고 메시지가 나타났다.
docker compose up -d --build
경고 메시지:
level=warning msg="docker-compose.yml: the attribute `version` is obsolete,
it will be ignored, please remove it to avoid potential confusion"
원인: 최신 Docker Compose에서는 version 속성이 더 이상 필요하지 않다.
해결: docker-compose.yml에서 version: "3.8" 줄을 제거한다.
모든 문제를 해결하고 다시 실행했다.
docker compose down
docker compose up -d --build
성공적인 실행 결과:
docker compose up -d --build

docker compose ps

모든 서비스가 정상적으로 실행 중임을 확인했다.


데이터 영속성을 위해 생성된 볼륨을 확인했다. MySQL 데이터를 저장하기 위한 Named Volume이 자동으로 생성되었다. 이제 컨테이너를 삭제하고 다시 실행해도 데이터베이스 데이터는 유지된다.
docker volume ls

docker logs로 실제 실행 상태를 확인해야 한다Docker Compose를 사용하면 복잡한 멀티 컨테이너 환경을 하나의 설정 파일로 관리할 수 있다. 각 서비스를 독립된 컨테이너로 분리하여 장애 격리와 독립적인 확장이 가능하며, 서비스 간 의존성, 네트워크 연결, 환경 변수 설정 등을 선언적으로 정의하고, 단일 명령으로 전체 환경을 실행하거나 종료할 수 있어 개발 및 배포 과정이 크게 간소화된다.
수십 줄의 Docker 명령을 외우고 반복 입력하는 대신, 한 번의 설정 파일 작성과 한 줄의 명령으로 동일한 환경을 누구나 재현할 수 있다는 것이 Docker Compose의 가장 큰 장점이다.