M2 ‐ Docker Compose 기반 서비스 실행 환경 구성 - 100-hours-a-week/KTB4-4th-wiki GitHub Wiki

목차

1. 목적

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한다.

2. V1 실행 구조

                         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를 통해 호출한다.

3. Compose 구성 결정

항목 결정
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에는 사용하지 않음

Host Network를 사용하는 이유

V1에서는 모든 Application과 Database가 하나의 EC2에 존재한다.

따라서 별도의 Bridge Network 대신 Host Network를 사용하여 다음과 같이 단순하게 통신한다.

Backend
→ localhost:3306  MySQL
→ localhost:8000  AI Server

AI Server
→ localhost:5432  PostgreSQL
→ localhost:6379  Redis

4. Docker Compose

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를 설정하지 않는다.

7. 운영 설정

Restart Policy

restart: unless-stopped

예상하지 못한 Container 종료 시 자동 재시작한다.

단, EC2 자체 장애를 다른 Server로 복구하는 기능은 제공하지 않는다.

Logging

logging:
  driver: json-file
  options:
    max-size: "10m"
    max-file: "3"

Container 하나당 최대 3개의 Log File을 유지하여 EC2 Disk 사용량을 제한한다.

Resource Limit

V1에서는 하나의 EC2에서 여러 Application이 실행되므로 초기 Resource Limit을 설정할 수 있다.

frontend:
  mem_limit: 512m

backend:
  mem_limit: 1536m

ai-server:
  mem_limit: 512m

redis:
  mem_limit: 256m

실제 운영 환경의 CPU 및 Memory 사용량을 확인한 후 조정한다.

Volume

Frontend, Backend, AI Server는 영구 데이터를 Container에 저장하지 않는다.

Redis 역시 V1에서는 Cache 및 임시 State 용도로 사용하므로 Persistence Volume을 사용하지 않는다.

영구 데이터는 다음 Database에 저장한다.

MySQL
→ Backend 영구 데이터

PostgreSQL + pgvector
→ AI Server Vector / Catalog 데이터

8. Nginx Reverse Proxy

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는 외부에 직접 노출하지 않는다.

9. 독립 배포 및 Rollback

Frontend, Backend, AI Server는 각각 Git Commit SHA를 Image Tag로 사용한다.

needu-frontend:<Frontend SHA>
needu-backend:<Backend SHA>
needu-ai:<AI SHA>

따라서 Application별로 Image를 독립적으로 교체할 수 있다.

Frontend

docker compose pull frontend
docker compose up -d --no-deps frontend

Backend

docker compose pull backend
docker compose up -d --no-deps backend

AI Server

docker compose pull ai-server
docker compose up -d --no-deps ai-server

Rollback이 필요한 경우 이전 Commit SHA를 Image Tag로 지정하여 동일한 방식으로 재배포한다.

현재 Image
    ↓
문제 발생
    ↓
이전 Commit SHA 지정
    ↓
해당 Service만 재배포

Application별 독립 배포를 통해 Frontend 변경으로 Backend와 AI Server까지 함께 재배포하는 상황을 방지한다.

10. 검증

Compose 설정

docker compose version
docker compose config

전체 실행

docker compose up -d
docker compose ps

Health Check

curl http://localhost:3000/
curl http://localhost:8080/actuator/health
curl http://localhost:8000/health
docker compose exec redis redis-cli ping

Log 확인

docker compose logs frontend
docker compose logs backend
docker compose logs ai-server
docker compose logs redis

주요 연결 확인

Frontend
→ Nginx → Backend

Backend
→ MySQL
→ AI Server

AI Server
→ Redis
→ PostgreSQL + pgvector
→ External Model API

11. 문제 및 해결

11.1 Frontend 정적 배포 방식과 실제 실행 환경 불일치

문제

기존에는 Frontend Build 결과물을 Nginx에서 직접 제공하는 정적 배포 방식을 고려했으나, 현재 Frontend는 Next.js Server를 사용하는 구조이다.

해결

Frontend를 Docker Image로 관리하고 Compose에서 3000 Port의 Next.js Server를 실행하도록 변경했다.

M1에서 정의한 Frontend Docker Image 생성 방법은 M1 문서에서 관리하고, M2에서는 해당 Image를 실행하는 Compose 설정만 관리한다.

11.2 Redis 준비 전에 Application이 실행될 가능성

문제

Container가 먼저 실행되더라도 Redis가 실제 요청을 처리할 준비가 완료되지 않았을 수 있다.

해결

Redis Health Check를 기준으로 AI Server의 Dependency를 설정했다.

depends_on:
  redis:
    condition: service_healthy

11.3 Container Log 증가로 인한 Disk 사용

문제

Container Log가 계속 증가하면 EC2 Disk를 점유할 수 있다.

해결

모든 Compose Service에 Log Rotation을 적용했다.

max-size: "10m"
max-file: "3"

12. 현재 한계 및 다음 단계

현재 한계

  • 모든 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 등의 도입을 검토한다.

13. 완료 기준

  • 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 확인
  • 관련 문서 업데이트

최종 결과

완료 / 부분 완료 / 보류

관련 자료

GitHub

  • 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

참고 자료

⚠️ **GitHub.com Fallback** ⚠️