M1 ‐ 애플리케이션 Docker Image 구성 - 100-hours-a-week/KTB4-4th-wiki GitHub Wiki
- 목표
- 1. 개발 환경 및 프로젝트 사전 점검
- 2. Backend Dockerfile 작성
- 3. AI Server Dockerfile 작성
- 4. Frontend Dockerfile 작성
- 5. Runtime 환경변수 외부화
- 6. Backend Local Docker Build 검증
- 7. Backend Local Container 실행 검증
- 8. Backend Image 구성 확인
- 9. Backend Git Commit SHA Tag 규칙
- 10. Backend Image Contract
Need U Frontend Next.js Application, Backend Spring Boot Application, AI Server FastAPI Application을 동일한 방식으로 Build하고 실행할 수 있는 Docker Image로 구성한다.
M1의 최종 목표는 단순히 Dockerfile을 작성하는 것이 아니라, 이후 Docker Compose와 CI/CD에서 그대로 사용할 수 있는 배포 가능한 Frontend / Backend / AI Server Image와 실행 규격을 확정하는 것이다.
Spring Boot Source
↓
Docker Build
↓
needu-backend:<tag>
↓
Container 실행
↓
Spring Boot :8080
↓
/actuator/health 정상 응답
AI Server는 별도의 Image로 관리한다.
FastAPI Source
↓
Docker Build
↓
needu-ai:<tag>
↓
Container 실행
↓
FastAPI :<AI_PORT>
↓
/health 정상 응답
Frontend도 별도의 Image로 관리한다.
Next.js Source
↓
next build
↓
standalone Runtime 생성
↓
Docker Build
↓
needu-frontend:<tag>
↓
Container 실행
↓
Next.js Server :3000
↓
Nginx Reverse Proxy
Backend 개발이 아직 본격적으로 시작되지 않았으므로, Docker Image와 이후 Compose / CI/CD 구성을 진행하기 위해 필요한 실행 규격은 Cloud에서 V1 공통 기준값을 먼저 정의한다.
Backend는 개발 과정에서 아래 기준에 맞춰 구성하고, 실제 구현상 변경이 필요한 경우에만 Cloud 설정과 함께 조정한다.
| 항목 | V1 기준 |
|---|---|
| Spring Boot | 4.1.x |
| Java | 25 LTS |
| JDK | Eclipse Temurin |
| Backend Port | 8080 |
| 운영 Profile | prod |
| Health Check | /actuator/health |
| API Base Path | /api |
| Database | MySQL |
| MySQL Port | 3306 |
| AI Server | FastAPI 별도 Application |
| AI Server 접근 | http://localhost:<AI_PORT> |
이 값들은 M1 Dockerfile뿐 아니라 M2 Docker Compose, Nginx, M3 CI/CD에서 동일하게 사용한다.
V1에서는 Backend와 AI Server를 별도의 Application으로 운영한다.
Frontend
↓
Backend (Spring Boot)
↓
AI Server (FastAPI)
Backend는 다음 역할을 담당한다.
- 카카오 로그인
- 사용자 / 친구 관리
- 일반 서비스 Business Logic
- MySQL 접근
- AI Server API 호출
AI Server는 다음 역할을 담당한다.
- AI 대화 및 취향 분석
- 외부 LLM API 호출
- Embedding API 호출
- Redis 기반 AI State / Cache
- PostgreSQL + pgvector 기반 Vector Search
- 추천 / Ranking Logic
따라서 Backend Docker Image에는 LLM_API_KEY, EMBEDDING_API_KEY, PostgreSQL / pgvector 연결 설정을 포함하지 않는다.
Backend는 AI 기능이 필요한 경우 AI_SERVER_URL을 통해 FastAPI AI Server를 호출한다.
Frontend는 Next.js App Router 기반이다.
| 항목 | V1 기준 |
|---|---|
| Framework | Next.js 16.3.5
|
| Router | App Router |
| React | 19.2.8 |
| TypeScript | 5.x |
| Node.js |
24.10.0 이상 |
| Package Manager | npm |
| Lock File | package-lock.json |
| Production Build | next build |
| Rendering | CSR + Server Component + 향후 SSR / 요청 기반 Server 기능 |
| Static Export | 사용하지 않음 |
| Docker Runtime | output: "standalone" |
| Application Port | 3000 |
| Image Name | needu-frontend |
| Production Tag | Git Commit SHA |
| Runtime User | non-root nextjs
|
Frontend는 Production에서 Next.js Server가 실행되어야 한다.
User
↓
Nginx :443
↓
Next.js Server :3000
Nginx는 / 요청을 localhost:3000으로 Reverse Proxy한다.
V1에서는 Frontend도 Backend / AI Server와 동일하게 Docker Image로 관리한다.
Docker Compose
├─ frontend
├─ backend
├─ ai
└─ redis
Frontend Docker Image는 Next.js의 output: "standalone"을 사용하여 Production 실행에 필요한 파일만 Runtime Stage로 전달한다.
Backend 기술선정 기준은 다음과 같다.
Spring Boot 4.1.x
Java 25 LTS
JDK: Eclipse Temurin
Docker Build Stage와 Runtime Stage에서도 Java 25 계열의 Eclipse Temurin Image를 사용한다.
Backend Repository가 생성되면 다음 파일을 기준으로 Java / Gradle 설정을 맞춘다.
build.gradle- Gradle Toolchain 설정
gradlewgradle/
Docker Build에서는 Project의 Gradle Wrapper를 사용한다.
chmod +x gradlew
./gradlew bootJarGradle Wrapper Version은 Backend Project 생성 시 Java 25와 Spring Boot 4.1을 모두 지원하는 Version으로 설정한다.
- Java =
25 LTS - JDK =
Eclipse Temurin - Spring Boot =
4.1.x - Build = Gradle Wrapper
- JAR 생성 =
./gradlew bootJar
Backend 실행 Port는 8080으로 통일한다.
8080
다음 구성요소가 모두 같은 값을 사용한다.
Spring Boot
↓
Docker Container
↓
Docker Compose
↓
Nginx
↓
CD Health Check
Nginx에서는 Backend 요청을 다음 주소로 전달한다.
/api
→ localhost:8080
- Backend Port =
8080 - Nginx Backend Target =
localhost:8080
Docker Compose와 CD에서 Backend 정상 상태를 판단하기 위한 Health Check Endpoint는 다음과 같이 사용한다.
GET /actuator/health
목표 응답:
HTTP 200
{
"status": "UP"
}Backend에는 Spring Boot Actuator를 포함한다.
implementation 'org.springframework.boot:spring-boot-starter-actuator'
Spring Security를 사용하는 경우에도 Container와 CD가 Health Check를 수행할 수 있도록 /actuator/health는 인증 없이 호출 가능하도록 구성한다.
V1에서는 Backend Application과 Backend가 직접 사용하는 필수 Dependency가 정상인지 확인하는 방향으로 Health Check를 구성한다.
현재 V1 Backend의 직접 Database Dependency는 MySQL이므로 MySQL 연결 상태를 Health Check에 포함한다.
- Actuator 사용
- Health Endpoint =
/actuator/health - 정상 기준 = HTTP
200+UP - Health Endpoint 인증 없이 호출 가능
- Backend Application 상태 확인
- MySQL 연결 상태 포함
Production 실행 환경은 prod Profile로 통일한다.
SPRING_PROFILES_ACTIVE=prod
Backend에서는 필요에 따라 다음과 같이 운영 설정을 분리한다.
application.yml
application-prod.yml
Cloud에서는 Docker Compose 실행 시 SPRING_PROFILES_ACTIVE=prod를 주입한다.
- Production Profile =
prod - Container 실행 시
SPRING_PROFILES_ACTIVE=prod
Production 설정값과 Secret은 Docker Image 내부에 포함하지 않는다.
EC2의 .env 파일을 Docker Compose가 읽어 Backend Container에 전달한다.
EC2 .env
↓
Docker Compose
↓
Backend Container
↓
Spring Boot
V1에서는 다음 Naming을 공통 기준으로 사용한다.
SPRING_PROFILES_ACTIVE=prod
DB_NAME=
DB_USERNAME=
DB_PASSWORD=
KAKAO_CLIENT_ID=
KAKAO_CLIENT_SECRET=
AI_SERVER_URL=http://localhost:<AI_PORT>
JWT_SECRET=
Spring Boot 설정에서는 외부 환경변수를 참조하도록 구성한다.
spring:
datasource:
url: jdbc:mysql://localhost:3306/${DB_NAME}
username: ${DB_USERNAME}
password: ${DB_PASSWORD}현재 V1에서는 Backend Container가 Host Network를 사용하므로 MySQL과 AI Server에 EC2 내부 주소로 접근한다.
MySQL
localhost:3306
AI Server
<http://localhost>:<AI_PORT>
Backend는 PostgreSQL + pgvector를 직접 사용하지 않는다.
AI 대화·취향 분석·Embedding·Vector Search 관련 요청은 AI Server API를 통해 처리한다.
환경별 Secret은 Source Code, Dockerfile, Docker Image에 포함하지 않는다.
추가 Production 환경변수가 필요한 경우 Backend 개발 과정에서 이 Naming 규칙에 맞춰 추가한다.
Cloud에서 미리 정할 수 있는 실행 환경 값은 V1 공통 규격으로 먼저 확정한다.
Java / JDK
Port
Profile
Health Check
API Base Path
DB / AI Server 연결 주소
Environment Variable Naming
반면 다음과 같이 기능 구현 과정에서 결정되는 값은 Backend 개발 후 Cloud 설정에 반영한다.
세부 API Endpoint
Kakao OAuth Callback Path
AI SSE Endpoint
SSE Timeout
기능별 추가 Environment Variable
따라서 Backend 개발이 시작되기 전에는 Cloud 작업에 필요한 공통 실행 규격을 먼저 확정하고, 애플리케이션 세부 구현값은 이후 필요한 시점에 연결한다.
- Dockerfile의 각 명령어는 순차적으로 실행되며, 각 단계가 독립적인 Layer로 쌓인다.
- Docker는 이전 Layer가 변경되지 않았다면 해당 Layer의 Cache를 재사용할 수 있다.
- 특정 Layer가 변경되면 그 이후 Layer는 다시 Build된다.
- 따라서 변경 빈도가 낮은 파일은 Dockerfile 앞쪽에, 자주 변경되는 Source Code는 뒤쪽에 배치하여 Cache 재사용률을 높인다.
이미지 Layer 관점에서는 Base Image 위에 Layer가 계속 쌓이며, 자주 변경되는 내용일수록 최종 Image의 위쪽 Layer에 위치하게 된다.
Need U Backend Dockerfile은 다음 두 가지를 중심으로 구성한다.
- Docker Layer Cache를 최대한 재사용한다.
- Build 환경과 Runtime 환경을 분리하는 Multi-stage Build를 적용한다.
추가적으로 운영 환경의 보안을 위해 Runtime Container는 non-root 사용자로 실행한다.
Dockerfile은 명령어가 순서대로 실행되며, 각 단계가 Layer로 쌓인다.
Docker는 앞선 Layer가 변경되지 않았다면 기존 Cache를 재사용할 수 있지만, 특정 Layer가 변경되면 그 이후 Layer는 다시 Build된다.
따라서 다음과 같은 순서로 구성한다.
변경 빈도 낮음
↓
Base Image
↓
Gradle Wrapper
↓
Build 설정
↓
Source Code
↓
Application Build
↓
변경 빈도 높음
Need U Backend에서는 다음 순서를 사용한다.
COPY --chmod=0755 gradlew gradlew
COPY gradle/ gradle/
COPY settings.gradle build.gradle ./
COPY src/ src/
gradlew, gradle/, build.gradle, settings.gradle은 Source Code보다 상대적으로 변경 빈도가 낮다.
반면 src/는 기능 개발 과정에서 지속적으로 변경되므로 뒤쪽에 배치한다.
예를 들어 UserService.java만 변경된 경우
Java Base Image → Cache 재사용
Gradle Wrapper → Cache 재사용
Build 설정 → Cache 재사용
src/ → 변경
bootJar → 다시 실행
반대로 다음처럼 처음부터 전체 Project를 복사하면
COPY . .
RUN ./gradlew bootJar
Source Code뿐 아니라 README와 같은 파일 하나만 변경되어도 COPY Layer가 변경되면서 이후 Build 과정이 다시 실행될 수 있다.
따라서 Need U에서는 Build에 필요한 파일을 구분해서 복사하고, 자주 변경되는 Source Code를 뒤쪽에 배치한다.
-
Multi-stage Build
- 컨테이너 이미지를 만들면서 빌드 등에서 필요하지만, 최종 컨테이너 이미지에는 필요 없는 환경을 제거할 수 있도록 단계를 나누어 기반 이미지를 만드는 방법
- 위의 그림처럼 컨테이너 실행 시에는 빌드에 사용한 파일 및 디렉토리와 같은 의존 파일들이 모두 삭제된 상태로 컨테이너가 실행되게 된다.
- 결론적으로, 좀 더 가벼운 크기의 컨테이너를 사용할 수 있게 된다.
https://docs.docker.com/build/building/multi-stage/
Spring Boot Application을 Build할 때와 실제 운영 환경에서 실행할 때 필요한 구성요소가 다르기 때문이다.
Build 단계에는 다음 항목이 필요하다.
JDK
Gradle Wrapper
Source Code
Build Cache
하지만 Build가 완료된 실제 운영 Container에서는 다음 구성만 필요하다.
Java Runtime
Spring Boot 실행 JAR
Build와 Runtime을 하나의 Image에서 처리하면 Gradle, Source Code, Build Cache 등 실제 운영에는 필요하지 않은 항목까지 최종 Image에 포함될 수 있다.
Multi-stage Build를 사용하면 Build Stage에서 생성된 JAR만 Runtime Stage로 전달할 수 있다.
Build Stage
├─ JDK
├─ Gradle
├─ Source Code
├─ Build Cache
└─ app.jar
│
│ app.jar만 복사
▼
Runtime Stage
├─ Java Runtime
└─ app.jar
이를 통해 다음과 같은 장점을 얻는다.
| 항목 | 효과 |
|---|---|
| Build / Runtime 분리 | 각 Stage의 역할을 명확하게 분리 |
| 불필요한 파일 제거 | Source Code와 Build Tool을 Runtime Image에서 제외 |
| Image 단순화 | 실행에 필요한 구성만 최종 Image에 포함 |
| 보안 | 최종 Container에 포함되는 구성요소 감소 |
| 유지보수 | Build 환경과 실행 환경을 독립적으로 관리 가능 |
V1 Backend 실행 기준은 다음과 같다.
| 항목 | 기준 |
|---|---|
| Spring Boot | 4.1.x |
| Java | 25 LTS |
| JDK | Eclipse Temurin |
| Application Port | 8080 |
| Build Tool | Gradle |
| Build 방식 | Gradle Wrapper (./gradlew) |
| Build Artifact | Spring Boot JAR |
| Runtime User | non-root app
|
Backend의 Spring Boot 실행 JAR 이름은 app.jar로 통일한다.
Backend Project 생성 후 build.gradle에는 다음 설정을 적용한다.
bootJar {
archiveFileName = 'app.jar'
}
JAR 이름을 고정하는 이유는 build/libs/*.jar와 같이 여러 JAR를 포괄적으로 지정했을 때 의도하지 않은 JAR가 선택되는 문제를 방지하기 위해서다.
Dockerfile은 다음과 같이 작성한다.
# syntax=docker/dockerfile:1
# =========================================================
# 1. Build Stage
# =========================================================
FROM eclipse-temurin:25-jdk AS builder
WORKDIR /app
# Gradle Wrapper 복사 및 실행 권한 설정
COPY --chmod=0755 gradlew gradlew
COPY gradle/ gradle/
# 상대적으로 변경 빈도가 낮은 Build 설정 먼저 복사
COPY settings.gradle build.gradle ./
# 자주 변경되는 Source Code는 뒤쪽에 복사
COPY src/ src/
# Gradle Cache를 재사용하며 Spring Boot JAR 생성
RUN --mount=type=cache,target=/root/.gradle \
./gradlew bootJar --no-daemon
# =========================================================
# 2. Runtime Stage
# =========================================================
FROM eclipse-temurin:25 AS runtime
WORKDIR /app
# Application 전용 non-root 사용자 생성
RUN groupadd --system app \
&& useradd --system --gid app app
# Build Stage에서 생성된 JAR만 복사
COPY --from=builder --chown=app:app \
/app/build/libs/app.jar app.jar
# Application을 non-root 사용자로 실행
USER app
# Spring Boot Application Port
EXPOSE 8080
# Application 실행
ENTRYPOINT ["java", "-jar", "app.jar"]
Build Stage에서는 Java 25 JDK Image를 사용한다.
FROM eclipse-temurin:25-jdk AS builder
Spring Boot Application을 Build하기 위해서는 Java Compiler가 필요하므로 JDK Image를 사용한다.
작업 Directory는 /app으로 통일한다.
WORKDIR /app
Gradle Wrapper를 먼저 복사한다.
COPY --chmod=0755 gradlew gradlew
COPY gradle/ gradle/
COPY --chmod=0755를 사용하면 파일을 복사하면서 동시에 실행 권한을 설정할 수 있다.
그 다음 상대적으로 변경 빈도가 낮은 Build 설정 파일을 복사한다.
COPY settings.gradle build.gradle ./
이후 자주 변경되는 Source Code를 복사한다.
COPY src/ src/
마지막으로 Spring Boot 실행 JAR를 생성한다.
RUN --mount=type=cache,target=/root/.gradle \
./gradlew bootJar --no-daemon
-
mount=type=cache를 사용하여/root/.gradle의 Gradle Cache를 Docker Build 사이에서 재사용한다.
이를 통해 Docker Image를 반복 Build할 때 Gradle Dependency를 매번 처음부터 다운로드하는 비용을 줄일 수 있다.
Build 결과는 다음 위치에 생성된다.
/app/build/libs/app.jar
Runtime Stage에서는 Build Stage에서 만들어진 Application JAR만 사용한다.
FROM eclipse-temurin:25 AS runtime
작업 Directory는 동일하게 /app을 사용한다.
WORKDIR /app
기본 Container Process를 root 사용자로 실행하지 않도록 Application 전용 사용자를 생성한다.
RUN groupadd --system app \
&& useradd --system --gid app app
Runtime Stage에는 Build Stage에서 생성된 app.jar만 복사한다.
COPY --from=builder --chown=app:app \
/app/build/libs/app.jar app.jar
-
chown=app:app을 사용하여 JAR 파일의 소유자를 Application 사용자로 설정한다.
이후:
USER app
을 적용하여 Spring Boot Process가 root가 아닌 app 사용자 권한으로 실행되도록 한다.
이를 통해 Application에 문제가 발생했을 때 Container 내부에서 가질 수 있는 권한의 범위를 줄이고 최소 권한 원칙을 적용한다.
Runtime Stage의 최종 구성은 다음과 같다.
Java Runtime
+
app.jar
+
non-root app User
Backend Application은 8080 Port를 사용한다.
EXPOSE 8080
EXPOSE는 실제 Host Port를 개방하는 설정이 아니다.
해당 Container에서 Application이 어떤 Port를 사용하는지 명시하는 역할을 한다.
실제 Need U V1에서는 Docker Compose에서 Host Network를 사용할 예정이므로 Spring Boot는 EC2의 8080 Port에서 실행된다.
Nginx
↓
localhost:8080
↓
Spring Boot
Container 실행 시에는 다음 명령을 실행한다.
ENTRYPOINT ["java", "-jar", "app.jar"]
실제로는 다음 명령과 동일하다.
java -jar app.jar
Backend의 정상 상태는 다음 Endpoint를 기준으로 확인한다.
GET /actuator/health
다만 Dockerfile에는 HEALTHCHECK를 직접 작성하지 않는다.
Need U에서는 Health Check 주기, Timeout, Retry 횟수와 같은 값이 Image 자체의 속성보다는 V1 운영 환경의 정책에 가깝다고 판단한다.
따라서 Health Check 정책은 M2 Docker Compose와 CD에서 관리한다.
Dockerfile
→ Application 실행 규격
Docker Compose / CD
→ Health Endpoint 호출 및 성공 여부 판단
Backend Runtime Image에는 Health Check만을 위한 curl 또는 wget을 추가하지 않는다.
따라서 실제 Probe 실행 방식은 M2 / CD에서 정하고, Dockerfile에는 HEALTHCHECK를 정의하지 않는다.
이를 통해 Health Check 설정이 Dockerfile과 운영 설정에 중복되는 것을 방지한다.
JVM Heap 크기와 Container CPU / Memory Limit 역시 Dockerfile에서 고정하지 않는다.
예를 들어 다음과 같은 값:
-Xms
-Xmx
Memory Limit
CPU Limit
은 EC2 크기와 실제 Backend 부하에 따라 달라질 수 있다.
따라서 다음과 같이 역할을 구분한다.
Dockerfile
→ Application을 어떻게 실행할지 정의
Docker Compose
→ CPU / Memory / JVM Resource 설정
필요한 경우 Compose에서 JAVA_TOOL_OPTIONS를 통해 JVM 옵션을 전달한다.
Dockerfile Layer Cache와 함께 Docker Build Context도 관리한다.
Project Root에 .dockerignore를 생성한다.
.git
.github
.gradle
build
.idea
.vscode
*.log
.env
.env.*
README.md
특히 다음 파일은 Build Context에 포함하지 않는다.
.env
.env.*
Production Secret이 Docker Build Context나 Image Layer에 포함되는 것을 방지하기 위해서다.
.dockerignore를 통해 다음 문제를 줄일 수 있다.
- 불필요한 파일의 Docker Build Context 포함
- Build Context 크기 증가
- Local Build 결과 포함
- IDE 설정 포함
- Secret 포함 위험
변경 빈도 낮음
↓
Java 25 Base Image
↓
WORKDIR
↓
Gradle Wrapper
↓
Build 설정
↓
src/
↓
Gradle Cache 활용
↓
bootJar
↓
변경 빈도 높음
Build Stage
↓
app.jar
↓
JAR만 Runtime으로 복사
↓
Runtime Stage
↓
Java Runtime + app.jar
↓
non-root app User
↓
:8080
Need U Backend Dockerfile은 변경 빈도가 낮은 Build 관련 설정을 먼저 배치하고, 자주 변경되는 Source Code를 뒤쪽에 배치하여 Docker Layer Cache를 최대한 재사용한다.
또한 Multi-stage Build를 적용하여 Build 환경과 Runtime 환경을 분리하고, 최종 Image에는 실제 Application 실행에 필요한 Java Runtime과 app.jar만 포함한다.
추가적으로 Runtime Container는 non-root 사용자로 실행하여 최소 권한 원칙을 적용하고, Gradle Build Cache를 재사용하여 반복적인 Docker Image Build 시간을 줄인다.
Health Check와 JVM Resource Limit은 Docker Image 자체가 아닌 운영 환경에 따라 변경될 수 있는 값이므로 M2 Docker Compose에서 관리한다.
AI Server는 외부 LLM 모델 자체를 EC2에 배포하는 Server가 아니라, Need U의 AI Logic을 담당하는 FastAPI Application이다.
따라서 외부 LLM / Embedding Model에는 별도 Dockerfile이 필요하지 않지만, Need U에서 직접 개발하는 FastAPI AI Server는 Backend와 마찬가지로 독립적인 Docker Image로 Build한다.
V1 AI Server 실행 기준은 다음과 같다.
| 항목 | V1 기준 |
|---|---|
| Application | FastAPI |
| Python | 3.12.11 |
| Package Manager | uv |
| Dependency 기준 |
pyproject.toml + uv.lock
|
| Runtime Server | Uvicorn |
| Image Name | needu-ai |
| Production Tag | Git Commit SHA |
| Runtime User | non-root app
|
| Health Endpoint | /health |
| Redis | localhost:6379 |
| PostgreSQL + pgvector | localhost:5432 |
| LLM / Embedding | 외부 API |
| Runtime Secret | Image 외부에서 주입 |
AI Repository는 최소 다음 구조를 가진다.
AI Repository
├─ Dockerfile
├─ .dockerignore
├─ pyproject.toml
├─ uv.lock
└─ Application Source
V1에서 Backend와 AI Server의 책임을 분리한다.
Frontend
↓
Backend (Spring Boot)
↓
AI Server (FastAPI)
├─ External LLM API
├─ Embedding API
├─ Redis
└─ PostgreSQL + pgvector
AI Server는 다음 기능을 담당한다.
- AI 대화 처리
- 사용자 취향 분석
- Prompt 구성
- 외부 LLM API 호출
- 외부 Embedding API 호출
- 응답 Parsing
- Redis 기반 AI State / Cache
- PostgreSQL + pgvector 기반 Vector Search
- 상품 Ranking / 추천 Logic
Backend는 AI 내부 Logic이나 Vector Search를 직접 수행하지 않고 AI_SERVER_URL을 통해 AI Server API를 호출한다.
Backend Dockerfile과 동일하게 AI Dockerfile도 변경 빈도가 낮은 파일을 앞쪽에, 자주 변경되는 Source Code를 뒤쪽에 배치한다.
Docker는 앞선 Layer가 변경되지 않았다면 Cache를 재사용할 수 있지만, 특정 Layer가 변경되면 이후 Layer가 다시 Build된다.
따라서 AI Server는 다음 순서를 사용한다.
변경 빈도 낮음
↓
Python Base Image
↓
uv Binary
↓
pyproject.toml
↓
uv.lock
↓
Dependency 설치
↓
Application Source
↓
Project 설치
↓
변경 빈도 높음
구체적으로는 다음 순서다.
COPY --from=ghcr.io/astral-sh/uv:0.8.4 /uv /uvx /bin/
COPY pyproject.toml uv.lock ./
RUN uv sync ...
COPY . .
RUN uv sync ...
pyproject.toml과 uv.lock은 Application Source보다 상대적으로 변경 빈도가 낮다. 반면 실제 FastAPI Source는 기능 개발 과정에서 자주 변경된다.
예를 들어 AI Logic 파일만 변경된 경우:
Python Base Image → Cache 재사용
uv Binary → Cache 재사용
pyproject.toml / uv.lock → Cache 재사용
Dependency 설치 → Cache 재사용
Application Source → 변경
Project 설치 → 다시 실행
따라서 Dependency 설치 Layer를 Source Code 복사보다 앞에 두어 Docker Layer Cache 활용도를 높인다.
Backend와 마찬가지로 AI Server도 Build 단계와 Runtime 단계를 분리한다.
Python Application은 Java처럼 JAR를 만드는 Build 과정은 없지만, Production Dependency와 Virtual Environment를 준비하는 단계와 실제 Application을 실행하는 단계를 분리할 수 있다.
Build Stage에는 다음 항목이 필요하다.
Python
uv
pyproject.toml
uv.lock
Dependency 설치 환경
Application Source
Runtime Stage에는 실제 실행에 필요한 구성만 남긴다.
Python Runtime
Virtual Environment
Application Source
non-root app User
구조는 다음과 같다.
Build Stage
├─ Python 3.12.11
├─ uv
├─ pyproject.toml
├─ uv.lock
├─ .venv
└─ Application Source
│
│ Runtime에 필요한 파일만 복사
▼
Runtime Stage
├─ Python 3.12.11
├─ .venv
├─ Application Source
└─ non-root app User
이를 통해 다음 장점을 얻는다.
| 항목 | 효과 |
|---|---|
| Build / Runtime 분리 | 각 Stage의 역할을 명확하게 분리 |
| 개발 Dependency 제외 | Runtime Image에 Test / Lint Tool을 포함하지 않음 |
| Dependency 고정 |
uv.lock 기준으로 동일 Version 설치 |
| Image 단순화 | 실행에 필요한 구성만 Runtime Stage에 포함 |
| 보안 | Runtime Process를 non-root User로 실행 |
| 유지보수 | Build 환경과 Runtime 환경을 독립적으로 관리 |
V1 AI Server Dockerfile은 다음과 같이 구성한다.
# syntax=docker/dockerfile:1
# =========================================================
# 1. Build Stage
# =========================================================
FROM python:3.12.11-slim AS builder
WORKDIR /app
# uv Binary 복사
COPY --from=ghcr.io/astral-sh/uv:0.8.4 /uv /uvx /bin/
# Python Bytecode Compile 및 venv Copy 호환성 설정
ENV UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy
# Dependency 정의 파일을 먼저 복사
COPY pyproject.toml uv.lock ./
# Production Dependency 설치
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev --no-install-project
# Application Source 복사
COPY . .
# Project 자체 설치
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev
# =========================================================
# 2. Runtime Stage
# =========================================================
FROM python:3.12.11-slim AS runtime
WORKDIR /app
# Application 전용 non-root 사용자 생성
RUN groupadd --system app \
&& useradd --system --gid app app
# Build Stage에서 Production 실행에 필요한 파일 복사
COPY --from=builder --chown=app:app /app /app
# Virtual Environment 실행 경로 설정
ENV PATH="/app/.venv/bin:$PATH"
# Application을 non-root 사용자로 실행
USER app
# AI Server 내부 Port
EXPOSE <AI_PORT>
# FastAPI Application 실행
CMD ["uvicorn", "<MODULE_PATH>:app", "--host", "0.0.0.0", "--port", "<AI_PORT>"]
<MODULE_PATH>와 <AI_PORT>는 실제 AI Repository의 FastAPI Entry Point와 실행 Port를 기준으로 확정한다.
예를 들어 실제 Entry Point가 app.main:app, Port가 8000으로 확정된다면:
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
문서 단계에서 임의로 값을 확정하지 않고 실제 Repository 실행 설정과 동일하게 맞춘다.
Build Stage에서는 Python 3.12.11 Slim Image를 사용한다.
FROM python:3.12.11-slim AS builder
작업 Directory는 /app으로 통일한다.
WORKDIR /app
AI Repository는 uv를 Package Manager로 사용하므로 공식 uv Image에서 실행 Binary만 복사한다.
COPY --from=ghcr.io/astral-sh/uv:0.8.4 /uv /uvx /bin/
CI에서도 uv 0.8.4를 사용하므로 Docker Build에서도 동일 Version을 사용한다.
Dependency 정의 파일을 먼저 복사한다.
COPY pyproject.toml uv.lock ./
이후 Production Dependency를 설치한다.
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev --no-install-project
각 Option의 의미는 다음과 같다.
-
-frozen:uv.lock을 변경하지 않고 Lock File의 Version을 그대로 사용한다. -
-no-dev: Test, Lint 등 개발용 Dependency를 Production 환경에 설치하지 않는다. -
-no-install-project: Source Code를 복사하기 전에 Dependency만 먼저 설치한다.
그 다음 Application Source를 복사한다.
COPY . .
마지막으로 Project 자체를 설치한다.
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev
Dependency가 변경되지 않은 경우 Source Code만 다시 복사하고 Project 설치 단계만 다시 실행할 수 있다.
Runtime Stage에서는 Build Stage에서 구성한 Production 환경만 사용한다.
FROM python:3.12.11-slim AS runtime
작업 Directory는 Build Stage와 동일하게 /app을 사용한다.
WORKDIR /app
Build Stage에서 생성된 /app Directory에는 Application Source, .venv, Production Dependency가 포함된다.
이를 Runtime Stage로 복사한다.
COPY --from=builder --chown=app:app /app /app
Backend Dockerfile과 동일하게 AI Server도 root가 아닌 Application 전용 사용자로 실행한다.
RUN groupadd --system app \
&& useradd --system --gid app app
Build Stage에서 파일을 복사할 때 소유권을 app:app으로 설정한다.
COPY --from=builder --chown=app:app /app /app
이후:
USER app
을 적용한다.
따라서 Uvicorn / FastAPI Process는 root 권한이 아니라 app 사용자 권한으로 실행된다.
V1 기준:
Backend Container → non-root app
AI Server Container → non-root app
Redis → 공식 Image 기본 실행 정책
uv sync는 기본적으로 Project Directory 아래에 .venv를 생성한다.
Runtime Stage에서 .venv의 실행 파일을 바로 사용할 수 있도록 PATH를 설정한다.
ENV PATH="/app/.venv/bin:$PATH"
따라서 다음 명령은 실제로 /app/.venv/bin/uvicorn을 실행한다.
CMD ["uvicorn", ...]
Runtime Stage에 uv Binary 자체를 추가로 포함하지 않아도 된다.
AI Server는 외부 사용자가 직접 접근하지 않는다.
사용자
↓
Nginx
↓
Backend
↓
AI Server
따라서 AI Server Port는 인터넷에 공개하지 않는다.
EXPOSE <AI_PORT>
EXPOSE는 실제 EC2 Security Group Port를 개방하는 설정이 아니라 Container가 사용하는 Port를 문서화하는 역할이다.
V1에서 Host Network를 사용하면 Backend는 다음 형태로 AI Server에 접근한다.
Backend
↓
<http://localhost>:<AI_PORT>
↓
AI Server
FastAPI Application 실행은 Uvicorn을 사용한다.
CMD ["uvicorn", "<MODULE_PATH>:app", "--host", "0.0.0.0", "--port", "<AI_PORT>"]
AI Server의 정상 상태는 다음 Endpoint를 기준으로 확인한다.
GET /health
정상 기준:
HTTP 200
Backend와 동일하게 AI Server Dockerfile에도 HEALTHCHECK를 직접 작성하지 않는다.
Dockerfile → Application 실행 규격
Docker Compose / CD → Health Endpoint 호출 및 성공 여부 판단
Runtime Image에 Health Check만을 위한 curl 또는 wget도 추가하지 않는다.
실제 Probe 실행 방식과 Timeout / Retry 정책은 Docker Compose 또는 CD에서 관리한다.
AI Server의 /health는 FastAPI Application 자체가 요청을 처리할 수 있는지 확인하는 가벼운 Endpoint로 구성한다.
외부 LLM API나 Embedding API가 일시적으로 실패했다고 해서 AI Server Process 자체까지 항상 unhealthy로 판단하도록 강하게 결합하지 않는다.
AI Server Process 정상
+
외부 LLM API 일시 장애
→ AI Server Container 자체는 실행 중
→ 외부 API 오류는 별도 Monitoring 대상
Redis와 PostgreSQL + pgvector 역시 Application Health와 Dependency Health를 어느 수준까지 묶을지는 실제 AI Server 구현에 맞춰 결정한다.
AI Server Memory / CPU Limit 역시 Dockerfile에서 고정하지 않는다.
Dockerfile → AI Application 실행 방식
Docker Compose → CPU / Memory Limit
V1 초기 기준은 다음과 같다.
AI Server Memory Limit ≤ 512MB
512MB는 공식 권장값이 아니라 Need U V1의 초기 Resource Budget이다.
실제 운영 및 부하 테스트에서 AI Server Memory, CPU, 외부 API 호출 Latency, Vector Search Latency, 동시 요청 수를 확인하고 필요하면 조정한다.
AI Server Secret은 Docker Image에 포함하지 않는다.
LLM_API_KEY=
EMBEDDING_API_KEY=
AI_DB_NAME=
AI_DB_USERNAME=
AI_DB_PASSWORD=
Redis와 PostgreSQL + pgvector는 같은 EC2 Host에서 실행하므로 Host Network 기준으로 다음 주소를 사용한다.
Redis → localhost:6379
PostgreSQL + pgvector → localhost:5432
Production Secret은 EC2의 AI Server용 env file에서 관리한다.
EC2 AI env file
↓
Docker Compose
↓
AI Server Container
↓
FastAPI
AI Repository에도 .dockerignore를 생성한다.
.git
.github
.venv
__pycache__
.pytest_cache
.ruff_cache
*.pyc
*.log
.env
.env.*
.idea
.vscode
README.md
특히 .env, .env.*는 Build Context에서 제외하여 Secret이 Image Layer에 포함되는 위험을 줄인다.
변경 빈도 낮음
↓
Python 3.12.11 Base Image
↓
uv 0.8.4
↓
pyproject.toml + uv.lock
↓
Production Dependency 설치
↓
Application Source
↓
Project 설치
↓
변경 빈도 높음
Build Stage
↓
Source + .venv 생성
↓
Runtime Stage로 필요한 파일 복사
↓
Python + Source + .venv
↓
non-root app User
↓
<AI_PORT>
↓
Uvicorn
docker build -t needu-ai:local .확인 항목:
- Python
3.12.11Build Stage 정상 -
uv 0.8.4사용 -
uv.lock기준 Dependency 설치 - Production Dependency만 설치
- Multi-stage Build 정상 수행
- Runtime Image 생성 성공
- Secret 파일이 Build Context 또는 Image에 포함되지 않음
Local 테스트에서는 Production Secret을 사용하지 않고 별도의 .env.local을 사용한다.
docker run --rm \
--network host \
--env-file .env.local \
needu-ai:local실행 흐름:
Container 실행
↓
non-root app User
↓
Uvicorn 실행
↓
FastAPI Startup
↓
<AI_PORT> Listen
↓
/health 확인
AI Server가 Redis와 PostgreSQL + pgvector를 Startup 시점에 요구한다면 Local 테스트 환경에도 해당 Dependency를 준비한다.
docker run --rm \
--entrypoint id \
needu-ai:local결과에서 root가 아닌 app 사용자가 확인되어야 한다.
docker history needu-ai:localRuntime Image의 목표 구성은 다음과 같다.
Python Runtime
+
Application Source
+
.venv
+
non-root app User
확인 대상:
-
.env파일이 포함되지 않았는지 - 개발용 Test / Lint Cache가 포함되지 않았는지
- Local
.venv가 Build Context에서 복사되지 않았는지 - Production
.venv가 정상적으로 생성되었는지 - FastAPI Source가 정상적으로 존재하는지
- Runtime User가
app인지
Production AI Image에는 latest를 Version 기준으로 사용하지 않는다.
Git Commit SHA
↓
Docker Build
↓
needu-ai:<Git Commit SHA>
↓
Amazon ECR
↓
Production Deployment
Backend와 AI Server는 Repository가 다르므로 각각 독립적인 SHA를 사용한다.
| 항목 | V1 기준 |
|---|---|
| Image Name | needu-ai |
| Python | 3.12.11 |
| Package Manager | uv 0.8.4 |
| Dependency |
pyproject.toml + uv.lock
|
| Framework | FastAPI |
| Runtime Server | Uvicorn |
| Runtime User | non-root app
|
| Application Port | 실제 AI Repository 실행 설정과 동일하게 확정 |
| Health Endpoint | /health |
| Redis 접근 | localhost:6379 |
| PostgreSQL 접근 | localhost:5432 |
| LLM / Embedding | 외부 API |
| Runtime Secret | Image 외부에서 주입 |
| Production Tag | Git Commit SHA |
| Health Probe | Docker Compose / CD에서 관리 |
| CPU / Memory | Docker Compose에서 관리 |
| 항목 | Backend | AI Server |
|---|---|---|
| Language | Java 25 | Python 3.12.11 |
| Framework | Spring Boot | FastAPI |
| Build / Dependency | Gradle Wrapper | uv 0.8.4 |
| Dependency 고정 | Gradle 설정 | uv.lock |
| Build 결과 | app.jar |
Application Source + .venv
|
| Runtime | Java Runtime | Python Runtime + Uvicorn |
| Runtime User | non-root app
|
non-root app
|
| Image | needu-backend |
needu-ai |
| Health | /actuator/health |
/health |
| MySQL | 직접 접근 | 직접 접근하지 않음 |
| PostgreSQL + pgvector | 직접 접근하지 않음 | 직접 접근 |
| LLM / Embedding API | 직접 호출하지 않음 | 직접 호출 |
최종적으로 V1에서는 Backend와 AI Server를 각각 별도의 Docker Image로 관리한다.
Backend Source → Backend Dockerfile → needu-backend:<SHA>
AI Source → AI Dockerfile → needu-ai:<SHA>
Frontend까지 포함하면 V1에서 관리하는 Application Image는 다음과 같다.
| 구분 | Frontend | Backend | AI Server |
|---|---|---|---|
| Image | needu-frontend |
needu-backend |
needu-ai |
| Runtime | Node.js + Next.js | Java Runtime | Python + Uvicorn |
| Port | 3000 |
8080 |
<AI_PORT> |
| Runtime User | nextjs |
app |
app |
| Production Tag | Git SHA | Git SHA | Git SHA |
| Registry | Amazon ECR | Amazon ECR | Amazon ECR |
세 Image 모두 다음 원칙을 공통으로 적용한다.
- 동일 Dockerfile을 Local / CI / Production에서 사용
- Production Secret을 Image에 포함하지 않음
- Runtime Process는 non-root
app사용자로 실행 - Resource Limit은 Dockerfile이 아닌 Docker Compose에서 관리
- Health Check 실행 정책은 Docker Compose / CD에서 관리
- Production Version은 Git Commit SHA로 추적
Frontend는 Next.js App Router를 사용하며 CSR, Server Component, 향후 SSR 및 요청 기반 Server 기능을 사용한다.
따라서 output: "export" 기반 정적 배포를 사용하지 않고 Next.js Server가 실행되는 Docker Container로 배포한다.
| 항목 | V1 기준 |
|---|---|
| Framework | Next.js 16.3.5
|
| Router | App Router |
| React | 19.2.8 |
| TypeScript | 5.x |
| Node.js |
24.10.0 이상 |
| Package Manager | npm |
| Lock File | package-lock.json |
| Build | next build |
| Runtime Output | output: "standalone" |
| Runtime Server | Node.js + Next.js Server |
| Application Port | 3000 |
| Image Name | needu-frontend |
| Runtime User | non-root nextjs
|
| Production Tag | Git Commit SHA |
Frontend Repository는 최소 다음 구조를 가진다.
Frontend Repository
├─ Dockerfile
├─ .dockerignore
├─ package.json
├─ package-lock.json
├─ next.config.*
├─ public/
├─ app/
└─ Application Source
Frontend를 Docker Container로 실행하므로 V1에서는 Next.js의 standalone Output을 사용한다.
next.config.ts를 사용하는 경우:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone",
};
export default nextConfig;Build 이후 다음 Runtime 결과가 생성된다.
.next/
├─ standalone/
└─ static/
standalone에는 Production Server 실행에 필요한 최소 Node.js Dependency와 Server Code가 포함된다.
Runtime Container에서는 전체 node_modules와 Source Code를 다시 설치하지 않고 standalone 결과를 사용한다.
next build
↓
.next/standalone
↓
Runtime Image
↓
node server.js
Frontend도 변경 빈도가 낮은 Dependency 정의 파일을 먼저 복사하고 Source Code를 뒤쪽에 배치한다.
변경 빈도 낮음
↓
Node.js Base Image
↓
package.json
↓
package-lock.json
↓
npm ci
↓
Application Source
↓
next build
↓
변경 빈도 높음
이를 통해 Source Code만 변경된 경우 npm Dependency 설치 Layer를 재사용할 수 있다.
Frontend Image도 Build Stage와 Runtime Stage를 분리한다.
Dependencies Stage
├─ Node.js
├─ package.json
├─ package-lock.json
└─ node_modules
Builder Stage
├─ node_modules
├─ Application Source
└─ next build
Runtime Stage
├─ Node.js Runtime
├─ .next/standalone
├─ .next/static
├─ public
└─ non-root nextjs User
Build Tool과 전체 Source Code를 Runtime Image에 불필요하게 포함하지 않는다.
https://docs.docker.com/guides/nextjs
# syntax=docker/dockerfile:1
# =========================================================
# 1. Dependencies Stage
# =========================================================
FROM node:24.10.0-alpine AS deps
WORKDIR /app
# Dependency 파일만 먼저 복사
COPY package.json package-lock.json ./
# Lock File 기준 Dependency 설치
RUN --mount=type=cache,target=/root/.npm \
npm ci
# =========================================================
# 2. Build Stage
# =========================================================
FROM node:24.10.0-alpine AS builder
WORKDIR /app
# Next.js Telemetry 비활성화
ENV NEXT_TELEMETRY_DISABLED=1
# 설치된 Dependency 복사
COPY --from=deps /app/node_modules ./node_modules
# Source Code 복사
COPY . .
# Production Build
RUN npm run build
# =========================================================
# 3. Runtime Stage
# =========================================================
FROM node:24.10.0-alpine AS runtime
WORKDIR /app
# Production Runtime 설정
ENV NODE_ENV=production \
NEXT_TELEMETRY_DISABLED=1 \
PORT=3000 \
HOSTNAME=0.0.0.0
# non-root 사용자 생성
RUN addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 nextjs
# Public Asset 복사
COPY --from=builder /app/public ./public
# .next Directory 권한 설정
RUN mkdir .next \
&& chown nextjs:nodejs .next
# Standalone Runtime 복사
COPY --from=builder --chown=nextjs:nodejs \
/app/.next/standalone ./
# Static Asset 복사
COPY --from=builder --chown=nextjs:nodejs \
/app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
# Next.js Server 실행
CMD ["node", "server.js"]
Dependency 설치는 package-lock.json을 기준으로 npm ci를 사용한다.
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
npm install 대신 npm ci를 사용하여 Lock File에 기록된 Dependency Version을 기준으로 재현 가능한 Build를 만든다.
npm Cache는 Docker BuildKit Cache Mount를 사용한다.
Dependencies Stage의 node_modules를 복사한 뒤 Application Source를 복사한다.
COPY --from=deps /app/node_modules ./node_modules
COPY . .
Production Build:
RUN npm run build
현재 package.json의 Build Script는 next build를 실행하는 것을 기준으로 한다.
Build 결과:
.next/standalone
.next/static
Runtime Stage에는 Next.js Production Server 실행에 필요한 파일만 복사한다.
public/
.next/standalone/
.next/static/
Application Source 전체와 Build용 Dependency를 Runtime Image에 다시 복사하지 않는다.
Standalone Server는 다음 명령으로 실행한다.
CMD ["node", "server.js"]
V1에서는 Docker Runtime에 맞춰 next start 대신 standalone Output의 server.js를 실행한다.
Frontend Runtime Process도 root로 실행하지 않는다.
RUN addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 nextjs
Runtime 파일은 nextjs:nodejs 소유권으로 복사하고:
USER nextjs
를 적용한다.
최종적으로:
Backend → non-root app
AI Server → non-root app
Frontend → non-root nextjs
로 운영한다.
Next.js Server Port는 3000으로 사용한다.
EXPOSE 3000
Frontend도 V1 Docker Compose의 Host Network를 사용하므로 EC2에서 다음 주소로 접근한다.
Nginx
↓
localhost:3000
↓
Next.js Container
Nginx Routing:
/
→ <http://localhost:3000>
/api
→ <http://localhost:8080>
Frontend 3000 Port는 외부 Security Group에 직접 공개하지 않는다.
외부 사용자는 Nginx의 HTTPS 443을 통해서만 접근한다.
Frontend Dockerfile에도 HEALTHCHECK를 직접 넣지 않는다.
Dockerfile
→ Next.js 실행 규격
Docker Compose / CD
→ Health / Smoke Check 정책
CD에서는 EC2 Host에서 다음 Endpoint의 HTTP 응답을 확인한다.
<http://localhost:3000>
Runtime Image에 Health Probe만을 위한 별도 Tool을 추가하지 않는다.
Frontend Runtime 환경변수와 Secret도 Docker Image에 직접 포함하지 않는다.
Runtime에서 필요한 Server-side 환경변수는 Docker Compose를 통해 주입한다.
EC2 Frontend env file
↓
Docker Compose
↓
Frontend Container
↓
Next.js Server
주의할 점은 Next.js의 NEXT_PUBLIC_* 값은 Browser Bundle에 포함될 수 있다는 것이다.
따라서 Secret은 NEXT_PUBLIC_* 변수로 관리하지 않는다.
Frontend Repository에 다음 .dockerignore를 사용한다.
.git
.github
node_modules
.next
coverage
*.log
.env
.env.*
.idea
.vscode
README.md
Local node_modules, .next, Secret 파일을 Docker Build Context에서 제외한다.
docker build -t needu-frontend:local .확인 항목:
- Node.js
24.10.0Build / Runtime Stage -
npm ci성공 -
next build성공 -
.next/standalone생성 - Multi-stage Build 성공
- Runtime Image 생성
- Secret 파일 미포함
docker run --rm \
-p 3000:3000 \
--env-file .env.local \
needu-frontend:local확인:
Container 실행
↓
node server.js
↓
Next.js Server Startup
↓
3000 Port Listen
↓
Browser / HTTP 응답 확인
docker run --rm \
--entrypoint id \
needu-frontend:localroot가 아닌 nextjs 사용자로 실행되는지 확인한다.
docker history needu-frontend:localRuntime Image 목표:
Node.js Runtime
+
Next.js standalone Server
+
Static Asset
+
public/
+
non-root nextjs User
확인 대상:
- Local
node_modules가 그대로 포함되지 않았는지 - Source Code 전체가 불필요하게 Runtime Image에 포함되지 않았는지
-
.env/.env.*가 포함되지 않았는지 -
.next/standalone이 정상적으로 포함되었는지 -
.next/static이 포함되었는지 -
public/이 포함되었는지 - Runtime User가
nextjs인지
Frontend Production Image도 latest를 Version 기준으로 사용하지 않는다.
Git Commit SHA
↓
Docker Build
↓
needu-frontend:<Git Commit SHA>
↓
Amazon ECR
↓
Production Deployment
Frontend / Backend / AI Server Repository는 각각 독립적인 Git Commit SHA를 사용한다.
| 항목 | V1 기준 |
|---|---|
| Image Name | needu-frontend |
| Framework | Next.js 16.3.5
|
| Router | App Router |
| React | 19.2.8 |
| TypeScript | 5.x |
| Node.js | 24.10.0 |
| Package Manager | npm |
| Lock File | package-lock.json |
| Build | next build |
| Runtime Output | standalone |
| Runtime Command | node server.js |
| Application Port | 3000 |
| Runtime User | non-root nextjs
|
| Nginx Target | localhost:3000 |
| Runtime Secret | Image 외부에서 주입 |
| Production Tag | Git Commit SHA |
| Health / Smoke Check | Docker Compose / CD에서 관리 |
| CPU / Memory | Docker Compose에서 관리 |
V1의 Application Image는 다음 세 개로 관리한다.
Frontend Source
→ Frontend Dockerfile
→ needu-frontend:<Frontend SHA>
Backend Source
→ Backend Dockerfile
→ needu-backend:<Backend SHA>
AI Source
→ AI Dockerfile
→ needu-ai:<AI SHA>
Docker Compose에서는:
Docker Compose
├─ frontend
├─ backend
├─ ai
└─ redis
를 실행한다.
Nginx는 Host에서 실행하며 외부 HTTPS 요청을 Frontend와 Backend에 전달한다.
User
↓
Nginx :443
├─ / → localhost:3000 → Frontend
└─ /api → localhost:8080 → Backend
Docker Image에는 환경에 따라 달라지는 값이나 Secret을 포함하지 않는다.
예를 들어 Dockerfile에 다음과 같이 직접 작성하지 않는다.
ENV DB_PASSWORD=...
ENV KAKAO_CLIENT_SECRET=...
ENV AI_SERVER_URL=http://localhost:<AI_PORT>...
ENV JWT_SECRET=...
Need U V1에서는 Production 환경변수를 EC2의 .env 파일에서 관리하고, Docker Compose가 이를 Backend Container에 전달한다.
EC2 .env
↓
Docker Compose
↓
Backend Container
↓
Spring Boot
V1 기본 환경변수는 다음과 같이 관리한다.
SPRING_PROFILES_ACTIVE=prod
DB_NAME=
DB_USERNAME=
DB_PASSWORD=
KAKAO_CLIENT_ID=
KAKAO_CLIENT_SECRET=
AI_SERVER_URL=http://localhost:<AI_PORT>
JWT_SECRET=
따라서 Docker Image에는 다음 항목만 포함한다.
Java Runtime
+
Spring Boot Application JAR
반면 다음 값은 Image 외부에서 관리한다.
Production Secret
Environment-specific Configuration
JVM Resource Option
이를 통해 동일한 Docker Image를 Local, Test, Production 환경에서 재사용할 수 있다.
Dockerfile 작성 후 Local 환경에서 Image가 정상적으로 Build되는지 확인한다.
docker build -t needu-backend:local .
Image 생성 여부를 확인한다.
docker images needu-backend
현재 Dockerfile에는 다음 요소가 포함되어 있으므로 Build 과정에서 정상 동작하는지 확인한다.
Java 25 Build Stage
↓
Gradle Wrapper 실행
↓
Gradle Cache Mount 사용
↓
bootJar 실행
↓
app.jar 생성
↓
Runtime Stage로 app.jar 복사
- Docker Build 성공
-
needu-backend:localImage 생성 -
./gradlew bootJar정상 수행 -
/app/build/libs/app.jar생성 - Multi-stage Build 정상 수행
- Secret이 Build Context 또는 Image에 포함되지 않음
Build된 Image를 직접 실행하여 Spring Boot Application이 정상적으로 시작되는지 확인한다.
예:
docker run --rm \
-p 8080:8080 \
--env-file .env.local \
needu-backend:local
Local 검증에서는 Production .env를 사용하지 않고 별도의 .env.local을 사용한다.
.env.local
→ Local Test용 환경변수
실제 Backend가 Application 시작 과정에서 MySQL 연결을 요구하므로 Local 테스트용 MySQL을 준비한다.
AI Server 연동은 Backend Docker Image 자체의 기동 검증과 분리할 수 있다. M1에서는 필요 시 AI Server를 Mock 처리하거나 별도 연결 설정을 사용한다.
실행 흐름은 다음과 같다.
Container 실행
↓
java -jar app.jar
↓
Spring Boot Startup
↓
8080 Port Listen
↓
/actuator/health 확인
Health Check:
curl <http://localhost:8080/actuator/health>
정상 응답 기준:
{
"status": "UP"
}
- Container 정상 실행
- Spring Boot Startup 성공
- Port
8080정상 -
/actuator/healthHTTP200 - 환경변수 정상 주입
- MySQL 연결 정상
- AI Server URL 환경변수 정상 주입
Runtime Container를 app 사용자로 실행하는 경우 다음도 확인한다.
docker run --rm \
--entrypoint id \
needu-backend:local
Container가 root가 아닌 app 사용자 권한으로 실행되는지 확인한다.
Multi-stage Build가 의도한 대로 적용되어 Runtime Image에 불필요한 Build 파일이 포함되지 않았는지 확인한다.
Image Layer:
docker history needu-backend:local
필요한 경우 Container 내부도 확인한다.
docker run --rm \
--entrypoint sh \
needu-backend:local
Runtime Image의 목표 구성은 다음과 같다.
Java Runtime
+
app.jar
+
Application 실행 사용자
확인 대상:
- Source Code가 Runtime Image에 포함되지 않았는지
- Gradle Wrapper가 Runtime Image에 포함되지 않았는지
- Gradle Cache가 Runtime Image에 포함되지 않았는지
- Build Tool이 Runtime Image에 포함되지 않았는지
-
.env등 Secret 파일이 포함되지 않았는지 -
app.jar만 정상적으로 전달되었는지
단순히 Image 크기를 최소화하는 것이 목표가 아니라, 실제 Application 실행에 필요한 구성만 포함한 재현 가능한 Runtime Image를 만드는 것을 기준으로 한다.
Production Backend Image에는 latest Tag를 사용하지 않는다.
Backend Image Tag는 Git Commit SHA를 사용한다.
Git Commit SHA
예:
needu-backend:a82f3c1
Local에서 Tag 규칙을 확인할 수 있다.
GIT_SHA=$(git rev-parse --short HEAD)
docker tag \
needu-backend:local \
needu-backend:$GIT_SHA
확인:
docker images needu-backend
Git Commit과 Docker Image를 동일한 SHA로 연결한다.
Git Commit
a82f3c1
↓
Docker Build
↓
needu-backend:a82f3c1
↓
ECR
↓
Production Deployment
이를 통해 현재 운영 중인 Backend가 어떤 Source Code로 Build된 Image인지 추적할 수 있다.
또한 Rollback 시 이전 Commit SHA Image를 그대로 다시 실행할 수 있다.
M1에서는 Dockerfile뿐만 아니라 이후 실행 환경에서도 동일하게 사용할 수 있도록 Backend Image 실행 규격을 정의한다.
| 항목 | V1 기준 |
|---|---|
| Image Name | needu-backend |
| Java | 25 LTS |
| JDK Distribution | Eclipse Temurin |
| Application JAR | app.jar |
| Application Port | 8080 |
| Profile | prod |
| Health Endpoint | /actuator/health |
| Health 성공 기준 | HTTP 200 / UP
|
| Database | MySQL |
| DB 접근 | localhost:3306 |
| AI Server 접근 | http://localhost:<AI_PORT> |
| Runtime Secret | Image 외부에서 주입 |
| Production Tag | Git Commit SHA |
| Health Check 정책 | Docker Compose에서 관리 |
| JVM Resource | Docker Compose에서 관리 |
| Runtime User | non-root app
|
Dockerfile에서는 다음 항목을 고정하지 않는다.
Health Check Interval
Health Check Timeout
Health Check Retry
CPU Limit
Memory Limit
JVM Heap
Production Environment Variable 실제 값
이 값은 운영 환경에 따라 변경될 수 있으므로 Docker Compose에서 관리한다.
V1 초기 Compose 기준은 다음과 같이 시작한다.
Frontend Memory Limit = M2 Resource Budget에서 확정
Backend Memory Limit = 1536m
AI Server Memory Limit = 512m
Redis Memory Limit = 256m
이 중 M1 문서는 Backend Image 규격을 다루므로 Backend Image 자체에는 Memory Limit을 포함하지 않고 M2 Compose에서 적용한다.
즉 역할은 다음과 같이 나눈다.
M1 Dockerfile
→ 어떤 Application Image를 만들 것인가
M2 Docker Compose
→ 해당 Image를 운영 환경에서 어떻게 실행할 것인가