내가 나중에 보기 편하려고 쓴 블로그 포스트이다.
Dockerfile
# ====== build args는 반드시 FROM보다 위에 선언 ======
ARG BUILDER_IMAGE=gradle:7.6.0-jdk17
ARG RUNTIME_IMAGE=amazoncorretto:17.0.7-alpine
# ============ (1) Builder ============
FROM ${BUILDER_IMAGE} AS builder
ENV GRADLE_USER_HOME=/home/gradle/.gradle
USER root
WORKDIR /app
RUN mkdir -p $GRADLE_USER_HOME && chown -R gradle:gradle /home/gradle /app
USER gradle
# enabling the gradle wrapper
COPY --chown=gradle:gradle gradlew ./
COPY --chown=gradle:gradle gradle ./gradle
COPY --chown=gradle:gradle build.gradle settings.gradle ./
RUN chmod +x ./gradlew
RUN ./gradlew --no-daemon --refresh-dependencies dependencies || true
COPY --chown=gradle:gradle src ./src
RUN ./gradlew clean build --no-daemon --no-parallel -x test
RUN ls -l /app/build/libs
# ============ (2) Runtime ============
FROM ${RUNTIME_IMAGE}
# ENV should come after FROM
ENV PROJECT_NAME=discodeit
ENV PROJECT_VERSION=1.2-M8
ENV SPRING_PROFILES_ACTIVE=prod
ENV JVM_OPS=''
WORKDIR /app
COPY --from=builder /app/build/libs/${PROJECT_NAME}-${PROJECT_VERSION}.jar app.jar
EXPOSE 80
ENTRYPOINT ["sh", "-c", "java $JVM_OPTS -jar app.jar"]
- COPY --chown=gradle:gradle gradlew ./ 와 COPY --chown=gradle:gradle gradle ./gradle
- Gradle Wrapper를 구성하는 파일들이다. 시스템에 Gradle을 설치하지 않아도 프로젝트를 빌드하게 해주는 스크립트다.
- COPY --chown=gradle:gradle build.gradle settings.gradle ./
- 프로젝트 의존성을 복사한다.
- COPY --chown=gradle:gradle src ./src
- 로컬 컴퓨터의 /src 파일들을 이미지 안으로 옮긴다.
- RUN ./gradlew --no-daemon --refresh-dependencies dependencies || true
- 의존성을 다운로드하는 명령어다.
- --no-daemon: 컨테이너 환경에 맞게 일회성 프로세스로 실행시킨다.
- --refresh-dependencies: 의존성의 최신 버전을 강제로 확인한다.
- || true: 명령이 실패해도 Docker 빌드가 중단되지 않게 하는 셸 트릭이다.
- RUN ./gradlew clean build --no-daemon --no-parallel -x test
- 이미지 내부에서 명령을 실행해 .jar 파일을 실제로 생성한다.
- Docker는 파일이 변경되면 COPY src 단계부터 빌드를 다시 시작하므로, 캐시와 변경된 파일만으로 효율적으로 재빌드한다.
- ENTRYPOINT [...]:
- 컨테이너가 시작될 때 실행될 주 명령어를 설정한다.
- ["...", "...", "..."] 형식은 "exec" 형식이라 부른다.
- ENTRYPOINT ["sh", "-c", "java $JVM_OPTS -jar app.jar"]
- "sh": 셸을 실행하라는 의미다.
- "-c": 다음에 오는 문자열 전체를 하나의 명령어로 실행하라는 의미다.
- "java $JVM_OPTS -jar app.jar": $JVM_OPTS 같은 변수를 해석한 후, 최종 java 명령을 실행한다.
docker build -t myapp:local . // 빌드
docker run -d -p 8081:80 --env-file .env myapp:local // 실행
이 명령어로 빌드 -> 실행할 수 있다.
compose.yml
services:
# looks for dockerfile to get/build image
app:
build: .
ports:
- '8081:80'
environment:
JAVA_OPTS: -Xms256m -Xmx512m
SPRING_PROFILES_ACTIVE: prod
SPRING_DATASOURCE_URL: ${SPRING_DATASOURCE_URL}
SPRING_DATASOURCE_USERNAME: ${SPRING_DATASOURCE_USERNAME}
SPRING_DATASOURCE_PASSWORD: ${SPRING_DATASOURCE_PASSWORD}
# AWS
AWS_CREDENTIALS_ACCESS_KEY: ${AWS_S3_ACCESS_KEY}
AWS_CREDENTIALS_SECRET_KEY: ${AWS_S3_SECRET_KEY}
AWS_REGION: ${AWS_S3_REGION}
AWS_S3_BUCKET: ${AWS_S3_BUCKET}
AWS_S3_PRESIGNED_URL_EXPIRATION: ${AWS_S3_PRESIGNED_URL_EXPIRATION}
env_file: [ .env ]
depends_on:
- db
volumes:
- app-storage:/app/.discodeit/storage
db:
# don't look for dockerfile, instead pull this image
image: postgres:16
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
env_file: [ .env ]
volumes:
- db-data:/var/lib/postgresql/data
- ./src/main/resources/schema.sql:/docker-entrypoint-initdb.d/schema.sql:ro
volumes:
db-data: { }
app-storage: { }
여기서 중요한 도커 볼륨 (Docker Volume)
- 도커 볼륨은 컨테이너의 데이터를 컴퓨터(호스트)에 영구적으로 저장하기 위한, Docker가 관리하는 특별한 저장 공간이다.
- 컨테이너는 삭제되면 내부의 모든 데이터가 함께 사라지는 임시 저장소와 같다. 이 문제를 해결하기 위해 데이터 영속성(data persistence)을 보장하는 것이 바로 볼륨이다.
- 가장 쉬운 비유는 컨테이너를 위한 외장 하드 드라이브다. 컨테이너라는 컴퓨터 본체는 언제든 교체될 수 있지만, 중요한 데이터가 담긴 외장 하드(볼륨)는 그대로 남아 있어 다른 본체에 연결해 계속 사용할 수 있다. 이 외장 하드를 컨테이너에 연결하는 과정을 마운팅(mounting)이라고 부른다.
compose.yml 파일 분석
- volumes: db-data: {}, app-storage: {} (맨밑)
- Docker Compose에게 "이름표가 붙은 외장 하드 두 개를 준비해줘"라고 볼륨을 선언하는 부분이다.
- db-data: {}: db-data라는 이름의 볼륨을 생성한다.
- app-storage: {}: app-storage라는 이름의 볼륨을 생성한다.
- 이 시점에서는 아직 어떤 컨테이너에도 연결되지 않은, Docker가 관리하는 빈 저장 공간이 준비된 상태다.
- Docker Compose에게 "이름표가 붙은 외장 하드 두 개를 준비해줘"라고 볼륨을 선언하는 부분이다.
- db 서비스의 - db-data:/var/lib/postgresql/data
- 이 부분은 준비된 외장 하드를 실제 컨테이너에 연결(마운트)하는 설정이다.
- db-data: 위에서 선언한 db-data 볼륨(외장 하드)을 의미한다.
- :: "연결한다"는 의미의 구분 기호다.
- /var/lib/postgresql/data: 컨테이너 내부의 특정 폴더 경로다. Postgres 이미지는 이 경로에 모든 데이터베이스 파일(테이블, 인덱스 등)을 저장하도록 설계되어 있다. (참고로 여기서 저장되는건 User/Message와 같은 entity의 데이터)
- 이렇게 하면 db 컨테이너가 생성하는 모든 데이터는 컨테이너 내부가 아닌 db-data 볼륨에 저장되어, 컨테이너가 삭제되거나 재시작되어도 데이터가 안전하게 보존된다
- 이 부분은 준비된 외장 하드를 실제 컨테이너에 연결(마운트)하는 설정이다.
- app 서비스의 - app-storage:/app/.discodeit/storage
- 이것 역시 db 서비스와 동일한 원리다.
- app-storage라는 외장 하드를 app 컨테이너의 /app/.discodeit/storage 폴더에 연결하라는 뜻이다. 이 경로는 애플리케이션이 생성하는 파일(예: 사용자가 업로드한 이미지)을 저장하는 곳이다 (application.yml + .env에 이렇게 설정해주었다). 이 데이터를 app-storage 볼륨에 저장함으로써 영속성을 확보한다.
.env
# Variables for the PostgreSQL container to set itself up
POSTGRES_DB=
POSTGRES_USER=
POSTGRES_PASSWORD=
# Variables for your Spring App to connect to the PostgreSQL container
# Notice the hostname is 'db', not 'host.docker.internal'
SPRING_DATASOURCE_URL=jdbc:
SPRING_DATASOURCE_USERNAME=
SPRING_DATASOURCE_PASSWORD=
# storage
STORAGE_LOCAL_ROOT_PATH=.discodeit/storage
STORAGE_TYPE=local
# S3 and AWS
AWS_S3_ACCESS_KEY=
AWS_S3_SECRET_KEY=
AWS_S3_REGION=ap-northeast-2
AWS_S3_BUCKET=
AWS_S3_PRESIGNED_URL_EXPIRATION=600
변수를 분리하는 이유
사실 분리를 하지 않아도 괜찮지만 가독성을 위해, 그리고 더 명시적으로 적기 위해 분리해주었다.
- POSTGRES_... 변수 (서버 설정)
- PostgreSQL 컨테이너 자체를 위한 명령어이다.
- db 컨테이너가 빈 데이터 볼륨으로 처음 시작될 때, POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD를 사용하여 일회성 설정 스크립트를 실행한다.
- 이 스크립트는 데이터베이스와 사용자 계정을 생성한다. 설치 마법사와 같다.
- SPRING_DATASOURCE_... 변수 (클라이언트 로그인)
- 이 변수들은 애플리케이션을 위한 명령어이다.
- 이미 실행되고 있는 PostgreSQL DB에 접속하기 위해서만 쓰인다
- 스프링 앱은 데이터베이스 서버에 연결해야 하는 클라이언트 역할을 한다.
- 데이터베이스 주소(db:5432), 사용할 데이터베이스, 그리고 로그인할 자격 증명을 알기 위해 SPRING_DATASOURCE_URL, USERNAME, PASSWORD를 사용한다.
- 이 변수들은 애플리케이션을 위한 명령어이다.
- AWS_S3_... (AWS S3)
- S3에 관한 환경변수이다
host.docker.internal 대신 db를 사용하는 이유
- docker-compose up을 실행하면, 비공개 가상 네트워크를 생성하고 해당 파일에 정의된 모든 서비스(app와 db)를 여기에 연결한다.
- 어떤 컨테이너든 다른 컨테이너를 찾을 때 서비스 이름을 호스트 이름으로 사용할 수 있다.
- 이 파일에서 PostgreSQL 서비스의 이름은 db이므로, app 컨테이너는 단순히 db라고 주소를 지정하여 접근할 수 있다.
- host.docker.internal
- 호스트 머신(노트북이나 데스크톱)의 IP 주소로 확인되는 특별한 DNS 이름이다.
- 컨테이너 내부의 애플리케이션이 컨테이너 외부, 즉 머신에서 직접 실행 중인 서비스에 연결해야 할 경우에만 이것을 사용한다.
docker compose up VS docker compose up --build
- docker compose up
- app: app 이미지가 로컬에 이미 존재하는지 확인한다. 있다면 그것을 사용하고, 없다면 Dockerfile로부터 하나를 빌드한다.
- db: postgres:16 이미지가 로컬에 존재하는지 확인한다. 있다면 그것을 사용하고, 없다면 도커 허브(Docker Hub)에서 풀(pull)한다.
- docker compose up --build
- app: 기존의 app 이미지를 무시하고 Dockerfile로부터 항상 새로운 이미지를 빌드한다.
- db: 위와 동일하다... 빌드 명령이 없기 때문에 --build는 영향을 미치지 않는다.
이제 여기서 명령어를 입력하고 테스트를 하면 된다:
docker compose up --build -d
# 닫을 때 (+ 데이터 삭제)
docker compose down -v
.env-> compose.yml -> application.yml 흐름
- .env 파일
- 환경 변수를 KEY=VALUE 형식으로 저장하는 간단한 텍스트 파일이다.
- 주로 데이터베이스 비밀번호, API 키 등 민감한 정보를 코드와 분리하여 보관하는 용도로 사용된다.
- docker-compose.yml
- 여러 컨테이너로 구성된 Docker 애플리케이션의 서비스, 네트워크, 볼륨 등을 정의하는 파일이다.
- .env 파일에서 이 정보들을 가져와서, 컨테이너가 시작될 때 컨테이너의 환경 변수(Environment Variables)로 설정해 준다.
- application.yml
- 컨테이너 안에서 실행되는 SpringBoot 애플리케이션의 설정 파일이다.
- 이 파일은 docker compose가 만든 컨테이너의 환경 변수를 읽어서 애플리케이션 설정을 최종적으로 완료한다.
.env → docker-compose.yml → application.yml 순서로 값이 전달되고 적용된다!
'Codeit > 스프린트 과제' 카테고리의 다른 글
| [sprint8] RDS/EC2, ECR, ECS 배포 (4) | 2025.08.27 |
|---|---|
| [sprint8] S3 관련 코드 + S3Client와 Presigned Url (3) | 2025.08.25 |
| [sprint5] 과제에서 사용한 swagger 간단하게 정리 (0) | 2025.07.14 |
| [sprint4] IOException과 @Transactional (멘토님 피드백) (0) | 2025.07.14 |
| [sprint3] 심화 2 - application.yaml로 Bean 구현하기 (File*Repository 구현체의 파일 저장 경로 설정하기 (2) | 2025.06.28 |