M2 ‐ Docker Compose 기반 서비스 실행 환경 구성 - 100-hours-a-week/KTB4-4th-wiki GitHub Wiki
- 1. 목적
- 2. V1 실행 구조
- 3. Compose 구성 결정
- 4. Docker Compose
- 5. Environment Variable
- 6. Health Check 및 Service Dependency
- 7. 운영 설정
- 8. Nginx Reverse Proxy
- 9. 독립 배포 및 Rollback
- 10. 검증
- 11. 문제 및 해결
- 12. 현재 한계 및 다음 단계
- 13. 완료 기준
- 관련 자료
M1에서 생성한 Application Image를 V1 EC2 실행 환경에 연결하고, Frontend, Backend, AI Server, Redis의 실행 설정을 Docker Compose로 통합 관리한다.
V1에서는 다음과 같이 구성한다.
- Docker Container: Frontend, Backend, AI Server, Redis
- EC2 Host: Nginx, MySQL, PostgreSQL + pgvector
- Image Registry: ECR
- Application 배포 단위: Git Commit SHA 기반 Docker Image
Frontend는 Next.js Server를 Container로 실행하고, Nginx가 Frontend와 Backend로 Reverse Proxy한다.
EC2
│
Nginx :443
/ \
/ \
▼ ▼
Frontend :3000 Backend :8080
│ │
│ ├── MySQL :3306
│ ├── Redis :6379
│ └── AI Server :8000
│ │
│ ├── Redis :6379
│ └── PostgreSQL :5432
│
Docker Compose
│
┌────────┼────────┐
│ │ │
Frontend Backend AI Server
│ │ │
└────────┼────────┘
│
Redis
MySQL ───────────── Host Process
PostgreSQL + pgvector ─ Host Process
Client
↓ HTTPS
Nginx
├─ / → Frontend :3000
└─ /api/ → Backend :8080
Backend
├─ MySQL :3306
└─ AI Server :8000
AI Server
├─ Redis :6379
└─ PostgreSQL + pgvector :5432
AI Server는 외부에 직접 노출하지 않고 Backend를 통해 호출한다.
| 항목 | 결정 |
|---|---|
| Compose | Docker Compose V2 (docker compose) |
| Compose 파일 | Compose Specification |
| Service |
frontend, backend, ai-server, redis
|
| Frontend | Next.js Server Container |
| Backend | Spring Boot Container |
| AI Server | FastAPI Container |
| Redis | redis:7-alpine |
| Network | Host Network |
| Frontend Port | 3000 |
| Backend Port | 8080 |
| AI Server Port | 8000 |
| Redis Port | 6379 |
| Database | EC2 Host Process |
| Image Tag | Git Commit SHA |
| Restart Policy | unless-stopped |
| Logging |
json-file + Log Rotation |
| Redis Dependency | AI Server → Redis service_healthy
|
| Persistent Volume | Application / Redis에는 사용하지 않음 |
V1에서는 모든 Application과 Database가 하나의 EC2에 존재한다.
따라서 별도의 Bridge Network 대신 Host Network를 사용하여 다음과 같이 단순하게 통신한다.
Backend
→ localhost:3306 MySQL
→ localhost:8000 AI Server
AI Server
→ localhost:5432 PostgreSQL
→ localhost:6379 Redis
services:
frontend:
image: ${FRONTEND_IMAGE}
network_mode: host
environment:
NODE_ENV: production
PORT: 3000
HOSTNAME: 0.0.0.0
BACKEND_API_BASE_URL: ${BACKEND_API_BASE_URL}
restart: unless-stopped
healthcheck:
test:
[
"CMD",
"node",
"-e",
"require('http').get('http://localhost:3000/', (res) => { process.exit(res.statusCode >= 200 && res.statusCode < 500 ? 0 : 1) }).on('error', () => process.exit(1)"
]
interval: 10s
timeout: 5s
retries: 3
start_period: 30s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
backend:
image: ${BACKEND_IMAGE}
network_mode: host
environment:
DB_USERNAME: ${DB_USERNAME}
DB_PASSWORD: ${DB_PASSWORD}
KAKAO_REST_API_KEY: ${KAKAO_REST_API_KEY}
KAKAO_CLIENT_SECRET: ${KAKAO_CLIENT_SECRET}
KAKAO_REDIRECT_URI: ${KAKAO_REDIRECT_URI}
KAKAO_FRIEND_REDIRECT_URI: ${KAKAO_FRIEND_REDIRECT_URI}
JWT_SECRET: ${JWT_SECRET}
AUTH_COOKIE_SECURE: ${AUTH_COOKIE_SECURE}
AI_SERVER_BASE_URL: ${AI_SERVER_BASE_URL}
AI_SERVICE_TOKEN: ${AI_SERVICE_TOKEN}
restart: unless-stopped
healthcheck:
test:
[
"CMD",
"curl",
"-f",
"http://localhost:8080/actuator/health"
]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
ai-server:
image: ${AI_IMAGE}
network_mode: host
environment:
APP_ENV: ${APP_ENV}
APP_VERSION: ${APP_VERSION}
LOG_LEVEL: ${LOG_LEVEL}
SERVICE_TOKEN: ${SERVICE_TOKEN}
REDIS_URL: ${REDIS_URL}
AI_DATABASE_URL: ${AI_DATABASE_URL}
RESPONSE_MODEL_NAME: ${RESPONSE_MODEL_NAME}
EXTRACTION_MODEL_NAME: ${EXTRACTION_MODEL_NAME}
OPENROUTER_API_KEY: ${OPENROUTER_API_KEY}
MODEL_API_KEY: ${MODEL_API_KEY}
EMBEDDING_BASE_URL: ${EMBEDDING_BASE_URL}
EMBEDDING_MODEL_NAME: ${EMBEDDING_MODEL_NAME}
EMBEDDING_DIMENSIONS: ${EMBEDDING_DIMENSIONS}
UPSTAGE_API_KEY: ${UPSTAGE_API_KEY}
RESPONSE_MODEL_TIMEOUT_SECONDS: ${RESPONSE_MODEL_TIMEOUT_SECONDS}
EXTRACTION_MODEL_TIMEOUT_SECONDS: ${EXTRACTION_MODEL_TIMEOUT_SECONDS}
SUMMARY_MODEL_TIMEOUT_SECONDS: ${SUMMARY_MODEL_TIMEOUT_SECONDS}
depends_on:
redis:
condition: service_healthy
restart: unless-stopped
healthcheck:
test:
[
"CMD",
"python",
"-c",
"import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"
]
interval: 10s
timeout: 3s
retries: 3
start_period: 30s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
redis:
image: redis:7-alpine
network_mode: host
command:
[
"redis-server",
"--bind",
"127.0.0.1",
"--protected-mode",
"yes",
"--save",
"",
"--appendonly",
"no"
]
restart: unless-stopped
healthcheck:
test:
[
"CMD",
"redis-cli",
"ping"
]
interval: 5s
timeout: 3s
retries: 5
start_period: 10s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"services: frontend: image: ${FRONTEND_IMAGE} network_mode: host
environment:
NODE_ENV: production
PORT: 3000
HOSTNAME: 0.0.0.0
BACKEND_API_BASE_URL: ${BACKEND_API_BASE_URL}
restart: unless-stopped
healthcheck:
test:
[
"CMD",
"node",
"-e",
"require('http').get('http://localhost:3000/', (res) => { process.exit(res.statusCode >= 200 && res.statusCode < 500 ? 0 : 1) }).on('error', () => process.exit(1)"
]
interval: 10s
timeout: 5s
retries: 3
start_period: 30s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
backend: image: ${BACKEND_IMAGE} network_mode: host
environment:
DB_USERNAME: ${DB_USERNAME}
DB_PASSWORD: ${DB_PASSWORD}
KAKAO_REST_API_KEY: ${KAKAO_REST_API_KEY}
KAKAO_CLIENT_SECRET: ${KAKAO_CLIENT_SECRET}
KAKAO_REDIRECT_URI: ${KAKAO_REDIRECT_URI}
KAKAO_FRIEND_REDIRECT_URI: ${KAKAO_FRIEND_REDIRECT_URI}
JWT_SECRET: ${JWT_SECRET}
AUTH_COOKIE_SECURE: ${AUTH_COOKIE_SECURE}
AI_SERVER_BASE_URL: ${AI_SERVER_BASE_URL}
AI_SERVICE_TOKEN: ${AI_SERVICE_TOKEN}
restart: unless-stopped
healthcheck:
test:
[
"CMD",
"curl",
"-f",
"http://localhost:8080/actuator/health"
]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
ai-server: image: ${AI_IMAGE} network_mode: host
environment:
APP_ENV: ${APP_ENV}
APP_VERSION: ${APP_VERSION}
LOG_LEVEL: ${LOG_LEVEL}
SERVICE_TOKEN: ${SERVICE_TOKEN}
REDIS_URL: ${REDIS_URL}
AI_DATABASE_URL: ${AI_DATABASE_URL}
RESPONSE_MODEL_NAME: ${RESPONSE_MODEL_NAME}
EXTRACTION_MODEL_NAME: ${EXTRACTION_MODEL_NAME}
OPENROUTER_API_KEY: ${OPENROUTER_API_KEY}
MODEL_API_KEY: ${MODEL_API_KEY}
EMBEDDING_BASE_URL: ${EMBEDDING_BASE_URL}
EMBEDDING_MODEL_NAME: ${EMBEDDING_MODEL_NAME}
EMBEDDING_DIMENSIONS: ${EMBEDDING_DIMENSIONS}
UPSTAGE_API_KEY: ${UPSTAGE_API_KEY}
RESPONSE_MODEL_TIMEOUT_SECONDS: ${RESPONSE_MODEL_TIMEOUT_SECONDS}
EXTRACTION_MODEL_TIMEOUT_SECONDS: ${EXTRACTION_MODEL_TIMEOUT_SECONDS}
SUMMARY_MODEL_TIMEOUT_SECONDS: ${SUMMARY_MODEL_TIMEOUT_SECONDS}
depends_on:
redis:
condition: service_healthy
restart: unless-stopped
healthcheck:
test:
[
"CMD",
"python",
"-c",
"import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"
]
interval: 10s
timeout: 3s
retries: 3
start_period: 30s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
redis: image: redis:7-alpine network_mode: host
command:
[
"redis-server",
"--bind",
"127.0.0.1",
"--protected-mode",
"yes",
"--save",
"",
"--appendonly",
"no"
]
restart: unless-stopped
healthcheck:
test:
[
"CMD",
"redis-cli",
"ping"
]
interval: 5s
timeout: 3s
retries: 5
start_period: 10s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
> Compose에서는 실제 Secret 값을 관리하지 않고 EC2의 `.env`에서 값을 주입한다.
>
<a id="5-environment-variable"></a>
## 5. Environment Variable
### Backend
현재 Backend에서 사용하는 변수는 다음과 같다.
DB_HOST DB_PORT DB_NAME DB_USERNAME DB_PASSWORD
KAKAO_REST_API_KEY KAKAO_CLIENT_SECRET KAKAO_REDIRECT_URI
JWT_SECRET JWT_ACCESS_TOKEN_EXPIRATION JWT_REFRESH_TOKEN_EXPIRATION
AI_SERVER_URL
### AI Server
현재 AI Server에서 사용하는 변수는 다음과 같다.
APP_ENV APP_VERSION LOG_LEVEL
SERVICE_TOKEN
REDIS_URL AI_DATABASE_URL
RESPONSE_MODEL_BASE_URL EXTRACTION_MODEL_BASE_URL MODEL_API_KEY RESPONSE_MODEL_NAME EXTRACTION_MODEL_NAME
AI Server의 현재 V1 구성은 외부 OpenAI-compatible Model API를 사용한다.
AI Server ├─ Redis ├─ PostgreSQL + pgvector └─ External Model API
### Frontend
Frontend의 실제 `.env` 변수명은 별도 확인 후 Compose에 반영한다.
Frontend에서 사용하는 환경변수 중 Client Bundle에 포함되는 값은 Build 시점에 결정될 수 있으므로 Secret을 `NEXT_PUBLIC_*` 변수로 전달하지 않는다.
<a id="6-health-check-및-service-dependency"></a>
## 6. Health Check 및 Service Dependency
각 Application의 실제 HTTP Endpoint를 기준으로 Health Check를 구성한다.
| Service | Endpoint / Command | 목적 |
| --- | --- | --- |
| Frontend | `localhost:3000/` | Next.js Server 응답 확인 |
| Backend | `/actuator/health` | Spring Boot Health 확인 |
| AI Server | `/health` | FastAPI Health 확인 |
| Redis | `redis-cli ping` | Redis 응답 확인 |
AI Server는 Redis Health Check가 성공한 이후 시작한다.
```yaml
depends_on:
redis:
condition: service_healthy
이는 Redis의 초기 실행 순서만 관리하며, Application 간 장애 처리는 각 Application에서 담당한다.
Frontend는 Redis에 직접 의존하지 않으므로 Dependency를 설정하지 않는다.
restart: unless-stopped예상하지 못한 Container 종료 시 자동 재시작한다.
단, EC2 자체 장애를 다른 Server로 복구하는 기능은 제공하지 않는다.
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"Container 하나당 최대 3개의 Log File을 유지하여 EC2 Disk 사용량을 제한한다.
V1에서는 하나의 EC2에서 여러 Application이 실행되므로 초기 Resource Limit을 설정할 수 있다.
frontend:
mem_limit: 512m
backend:
mem_limit: 1536m
ai-server:
mem_limit: 512m
redis:
mem_limit: 256m실제 운영 환경의 CPU 및 Memory 사용량을 확인한 후 조정한다.
Frontend, Backend, AI Server는 영구 데이터를 Container에 저장하지 않는다.
Redis 역시 V1에서는 Cache 및 임시 State 용도로 사용하므로 Persistence Volume을 사용하지 않는다.
영구 데이터는 다음 Database에 저장한다.
MySQL
→ Backend 영구 데이터
PostgreSQL + pgvector
→ AI Server Vector / Catalog 데이터
Frontend가 Container에서 실행되므로 Nginx는 정적 파일을 직접 제공하지 않고 Next.js Server로 요청을 전달한다.
server {
listen 80;
server_name _;
location /api/ {
proxy_pass http://127.0.0.1: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;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Client
↓
Nginx :443
├─ /api/* → Backend :8080
└─ /* → Frontend :3000
AI Server 8000 Port는 외부에 직접 노출하지 않는다.
Frontend, Backend, AI Server는 각각 Git Commit SHA를 Image Tag로 사용한다.
needu-frontend:<Frontend SHA>
needu-backend:<Backend SHA>
needu-ai:<AI SHA>
따라서 Application별로 Image를 독립적으로 교체할 수 있다.
docker compose pull frontend
docker compose up -d --no-deps frontenddocker compose pull backend
docker compose up -d --no-deps backenddocker compose pull ai-server
docker compose up -d --no-deps ai-serverRollback이 필요한 경우 이전 Commit SHA를 Image Tag로 지정하여 동일한 방식으로 재배포한다.
현재 Image
↓
문제 발생
↓
이전 Commit SHA 지정
↓
해당 Service만 재배포
Application별 독립 배포를 통해 Frontend 변경으로 Backend와 AI Server까지 함께 재배포하는 상황을 방지한다.
docker compose version
docker compose configdocker compose up -d
docker compose pscurl http://localhost:3000/
curl http://localhost:8080/actuator/health
curl http://localhost:8000/health
docker compose exec redis redis-cli pingdocker compose logs frontend
docker compose logs backend
docker compose logs ai-server
docker compose logs redisFrontend
→ Nginx → Backend
Backend
→ MySQL
→ AI Server
AI Server
→ Redis
→ PostgreSQL + pgvector
→ External Model API
문제
기존에는 Frontend Build 결과물을 Nginx에서 직접 제공하는 정적 배포 방식을 고려했으나, 현재 Frontend는 Next.js Server를 사용하는 구조이다.
해결
Frontend를 Docker Image로 관리하고 Compose에서 3000 Port의 Next.js Server를 실행하도록 변경했다.
M1에서 정의한 Frontend Docker Image 생성 방법은 M1 문서에서 관리하고, M2에서는 해당 Image를 실행하는 Compose 설정만 관리한다.
문제
Container가 먼저 실행되더라도 Redis가 실제 요청을 처리할 준비가 완료되지 않았을 수 있다.
해결
Redis Health Check를 기준으로 AI Server의 Dependency를 설정했다.
depends_on:
redis:
condition: service_healthy문제
Container Log가 계속 증가하면 EC2 Disk를 점유할 수 있다.
해결
모든 Compose Service에 Log Rotation을 적용했다.
max-size: "10m"
max-file: "3"- 모든 Application과 Database가 하나의 EC2에 있어 EC2 장애 시 전체 서비스가 영향을 받는다.
- Frontend, Backend, AI Server의 Scale-Out을 지원하지 않는다.
- Application 교체 시 짧은 Downtime이 발생할 수 있다.
- Production Secret을 EC2
.env에서 관리한다. - Redis 재생성 시 Cache와 임시 State가 초기화될 수 있다.
- Database도 EC2 Host에 존재하므로 EC2 장애의 영향을 받는다.
- Application 간 CPU 및 Memory Resource를 하나의 EC2에서 공유한다.
M3 — CI/CD 자동화
GitHub
↓
GitHub Actions
↓
Docker Build
↓
ECR Push
↓
EC2 배포
Frontend, Backend, AI Server 각각의 Image를 Commit SHA로 Build하고 ECR에 Push한 뒤 EC2에 배포한다.
M4 — Infrastructure as Code
V1에서 확정한 AWS Infrastructure를 IaC로 관리한다.
V2 — Scale-Out
실제 Traffic 및 Resource 사용량을 기준으로 EC2 Scale-Out, ECS/Fargate, Load Balancer, Managed Redis 등의 도입을 검토한다.
-
compose.yml작성 완료 - Frontend Container 실행 확인
- Backend Container 실행 확인
- AI Server Container 실행 확인
- Redis Container 실행 확인
- Frontend Health Check 확인
- Backend Health Check 확인
- AI Server Health Check 확인
- Redis Health Check 확인
- Backend → MySQL 연결 확인
- Backend → AI Server 연결 확인
- AI Server → PostgreSQL 연결 확인
- AI Server → Redis 연결 확인
- Environment Variable 주입 확인
- Rollback 확인
- Log Rotation 확인
- 관련 문서 업데이트
완료 / 부분 완료 / 보류
- Frontend Repository:
- Backend Repository:
- AI Server Repository:
- Frontend PR:
- Backend PR:
- AI Server PR:
- Commit:
- Need U V1 — 클라우드 공통 결정사항
- M1 Backend Docker Image
- M1 AI Server Docker Image
- M1 Frontend Docker Image
- M2 Docker Compose