Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions .env.local
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# ===================================
# DevNogi Gateway Server - Local Environment Configuration
# ===================================
# 이 파일은 로컬 개발 환경용 설정입니다.
# 사용법: docker-compose -f docker-compose-local.yml --env-file .env.local up --build

# === Application Configuration ===
SPRING_PROFILES_ACTIVE=local
SERVER_PORT=8090

# === Downstream Service URLs ===
# 로컬 개발 시 호스트의 다른 서비스에 연결 (Docker 외부)
AUTH_SERVER_URL=http://host.docker.internal:8091
COMMUNITY_SERVER_URL=http://host.docker.internal:8093
OPEN_API_BATCH_SERVER_URL=http://host.docker.internal:8092

# === Security Configuration ===
JWT_SECRET_KEY=e4f1a5c8d2b7e9f0a6c3d1b8e5f2c7a9d4e6f3b1a2c8d5e9f0b3a7c2d1e8f5a4
JWT_ISSUER=devnogi

# === CORS Configuration ===
CORS_ALLOWED_ORIGINS=*

# === JVM Configuration (로컬 개발용 - 메모리 사용량 감소) ===
JAVA_OPTS_XMS=128m
JAVA_OPTS_XMX=256m
JAVA_OPTS_MAX_METASPACE_SIZE=128m
JAVA_OPTS_RESERVED_CODE_CACHE_SIZE=32m
JAVA_OPTS_MAX_DIRECT_MEMORY_SIZE=32m
JAVA_OPTS_XSS=512k
JAVA_OPTS_MAX_GC_PAUSE_MILLIS=200
JAVA_OPTS_G1_HEAP_REGION_SIZE=1m
JAVA_OPTS_INITIATING_HEAP_OCCUPANCY_PERCENT=45
JAVA_OPTS_TIERED_STOP_AT_LEVEL=1
JAVA_OPTS_CI_COMPILER_COUNT=2

# === Docker Resource Limits (로컬 개발용) ===
DOCKER_MEMORY_LIMIT=512m
DOCKER_MEMORY_RESERVATION=256m

# === Restart Policy ===
RESTART_POLICY_MAX_RETRIES=3

# === Health Check Configuration ===
HEALTHCHECK_INTERVAL=30s
HEALTHCHECK_TIMEOUT=10s
HEALTHCHECK_RETRIES=3
HEALTHCHECK_START_PERIOD=60s

# === Logging Configuration ===
LOGGING_MAX_SIZE=10m
LOGGING_MAX_FILE=3

# === Autoheal Configuration ===
AUTOHEAL_INTERVAL=30
AUTOHEAL_START_PERIOD=0
AUTOHEAL_DEFAULT_STOP_TIMEOUT=10
AUTOHEAL_MEMORY_LIMIT=50m
AUTOHEAL_MEMORY_RESERVATION=20m
4 changes: 4 additions & 0 deletions .github/workflows/push-cd-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ jobs:
run: |
ssh -i ~/.ssh/my-key.pem ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }} "mkdir -p /home/${{ secrets.SERVER_USER }}/app"

- name: Copy docker-compose file to server
run: |
scp -i ~/.ssh/my-key.pem docker-compose-dev.yaml ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}:/home/${{ secrets.SERVER_USER }}/app/

- name: Deploy and Restart Container
run: |
ssh -i ~/.ssh/my-key.pem ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }} << 'EOF'
Expand Down
5 changes: 1 addition & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,4 @@ out/
/.nb-gradle/

### VS Code ###
.vscode/

### Spring Boot ###
application.yml
.vscode/
81 changes: 76 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,76 @@
# Dockerfile
FROM openjdk:21-jdk-slim
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
# Multi-Stage Dockerfile for Spring Cloud Gateway
# Stage 1: Build Stage - Gradle을 사용하여 애플리케이션 빌드
# Stage 2: Extract Stage - Spring Boot Layered JAR 추출
# Stage 3: Runtime Stage - 최종 런타임 이미지

# Stage 1: Build Stage
FROM gradle:8.5-jdk21 AS builder

# 작업 디렉토리 설정
WORKDIR /app

# Gradle 의존성 다운로드를 위한 파일만 먼저 복사 (레이어 캐싱 최적화)
COPY gradle gradle
COPY gradlew .
COPY build.gradle.kts .
COPY settings.gradle.kts .

# gradle.properties가 없으면 빈 파일 생성
RUN touch gradle.properties 2>/dev/null || true

# 의존성 다운로드 (캐시 활용)
RUN gradle dependencies --no-daemon || true

# 소스 코드 복사
COPY src src

# 애플리케이션 빌드 (테스트 제외)
RUN gradle clean bootJar -x test --no-daemon

# JAR 파일 위치 확인 및 이름 변경
RUN mkdir -p /app/build/extracted && \
cp /app/build/libs/*.jar /app/build/app.jar

# Stage 2: Extract Layers
FROM eclipse-temurin:21-jre-alpine AS extractor

WORKDIR /app

# 빌드된 JAR 파일 복사
COPY --from=builder /app/build/app.jar app.jar

# Spring Boot Layered JAR 추출 (레이어 최적화)
RUN java -Djarmode=layertools -jar app.jar extract

# Stage 3: Final Runtime Stage
FROM eclipse-temurin:21-jre-alpine

# 메타데이터 추가
LABEL maintainer="DevNogi Team"
LABEL description="DevNogi Gateway Server - API Gateway with JWT Authentication"
LABEL version="0.0.1"

# 보안: non-root 사용자 생성
RUN addgroup -S spring && adduser -S spring -G spring

# 작업 디렉토리 설정
WORKDIR /app

# 레이어별로 복사 (의존성 변경 시 캐시 활용)
COPY --from=extractor --chown=spring:spring /app/dependencies/ ./
COPY --from=extractor --chown=spring:spring /app/spring-boot-loader/ ./
COPY --from=extractor --chown=spring:spring /app/snapshot-dependencies/ ./
COPY --from=extractor --chown=spring:spring /app/application/ ./

# 로그 디렉토리 생성 및 권한 설정
RUN mkdir -p /app/logs /app/logs/archive && \
chown -R spring:spring /app/logs

# 사용자 전환
USER spring:spring

# JVM 메모리 설정 환경변수 (기본값, docker-compose에서 오버라이드)
ENV JAVA_OPTS="-Xms256m -Xmx512m -XX:+UseG1GC -XX:MaxGCPauseMillis=200"

# 애플리케이션 실행 (환경변수 JAVA_OPTS 사용)
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS org.springframework.boot.loader.launch.JarLauncher"]
6 changes: 3 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-webflux")

// ✅ JWT 인증용
implementation("io.jsonwebtoken:jjwt-api:0.11.5")
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.5")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.5")
implementation("io.jsonwebtoken:jjwt-api:0.12.5")
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.5")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.5")

// ✅ Actuator (모니터링 및 헬스 체크)
implementation("org.springframework.boot:spring-boot-starter-actuator")
Expand Down
127 changes: 112 additions & 15 deletions docker-compose-dev.yaml
Original file line number Diff line number Diff line change
@@ -1,31 +1,128 @@
version: "3.8"

services:
spring-app:
image: ${DOCKER_USERNAME}/${DOCKER_REPO}:latest
container_name: spring-app
gateway-app:
build:
context: .
dockerfile: Dockerfile
image: ${DOCKER_USERNAME}/${DOCKER_REPO}:${DOCKER_IMAGE_TAG:-latest}
container_name: gateway-app
ports:
- "${SERVER_PORT}:${SERVER_PORT}"
env_file:
- .env
labels:
# Autoheal: unhealthy 상태 시 자동 재시작 활성화
autoheal: "true"
environment:
# === Application Configuration ===
SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE}
LANG: C.UTF-8
LC_ALL: C.UTF-8
SERVER_PORT: ${SERVER_PORT}

# === Downstream Service URLs ===
AUTH_SERVER_URL: ${AUTH_SERVER_URL}
OPEN_API_BATCH_SERVER_URL: ${OPEN_API_BATCH_SERVER_URL}
COMMUNITY_SERVER_URL: ${COMMUNITY_SERVER_URL}

# === Security Configuration ===
JWT_SECRET_KEY: ${JWT_SECRET_KEY}
JWT_ISSUER: ${JWT_ISSUER}

# === CORS Configuration ===
CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS}

# === Docker Configuration ===
DOCKER_USERNAME: ${DOCKER_USERNAME}
DOCKER_REPO: ${DOCKER_REPO}

# === JVM Configuration ===
# All JVM options are now configurable via .env file
JAVA_OPTS: >-
-Xms${JAVA_OPTS_XMS}
-Xmx${JAVA_OPTS_XMX}
-XX:MaxMetaspaceSize=${JAVA_OPTS_MAX_METASPACE_SIZE}
-XX:ReservedCodeCacheSize=${JAVA_OPTS_RESERVED_CODE_CACHE_SIZE}
-XX:MaxDirectMemorySize=${JAVA_OPTS_MAX_DIRECT_MEMORY_SIZE}
-Xss${JAVA_OPTS_XSS}
-XX:+UseG1GC
-XX:MaxGCPauseMillis=${JAVA_OPTS_MAX_GC_PAUSE_MILLIS}
-XX:G1HeapRegionSize=${JAVA_OPTS_G1_HEAP_REGION_SIZE}
-XX:InitiatingHeapOccupancyPercent=${JAVA_OPTS_INITIATING_HEAP_OCCUPANCY_PERCENT}
-XX:+TieredCompilation
-XX:TieredStopAtLevel=${JAVA_OPTS_TIERED_STOP_AT_LEVEL}
-XX:CICompilerCount=${JAVA_OPTS_CI_COMPILER_COUNT}
-XX:+UseCompressedOops
-XX:+UseCompressedClassPointers
-Djava.security.egd=file:/dev/./urandom
-Dspring.jmx.enabled=false
volumes:
- ./logs:/app/logs
- ./config:/app/config:ro
restart: always
- gateway-logs:/app/logs # Named volume 사용 (권한 문제 해결)
# Restart Policy:
# - always: 항상 재시작 (수동 stop 포함)
# - unless-stopped: 수동 stop 제외하고 재시작
# - on-failure:N: 실패 시 최대 N번만 재시작 (무한 재시작 루프 방지)
restart: on-failure:${RESTART_POLICY_MAX_RETRIES}

# Docker Resource Limits (cgroup을 통한 강제 메모리 제한)
deploy:
resources:
limits:
memory: ${DOCKER_MEMORY_LIMIT} # 컨테이너 최대 메모리 (hard limit, OOM killer threshold)
reservations:
memory: ${DOCKER_MEMORY_RESERVATION} # 예약 메모리 (soft limit, guaranteed minimum)

networks:
- app-network
- gateway-network
# Health Check: 컨테이너 상태 감지 (autoheal과 연동)
# wget 사용 (Alpine Linux에 기본 설치되어 있음)
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:${SERVER_PORT}/actuator/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:${SERVER_PORT}/actuator/health"]
interval: ${HEALTHCHECK_INTERVAL} # 체크 주기
timeout: ${HEALTHCHECK_TIMEOUT} # 응답 타임아웃
retries: ${HEALTHCHECK_RETRIES} # 연속 실패 횟수
start_period: ${HEALTHCHECK_START_PERIOD} # 시작 유예 기간
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
max-size: ${LOGGING_MAX_SIZE} # 로그 파일 최대 크기
max-file: "${LOGGING_MAX_FILE}" # 로그 파일 보관 개수

# Autoheal: unhealthy 컨테이너 자동 재시작 서비스
# - gateway-app이 unhealthy 상태가 되면 자동으로 재시작
# - Docker 소켓을 마운트하여 컨테이너 관리 권한 획득
# - healthcheck와 독립적으로 동작 (healthcheck가 unhealthy 판정하면 autoheal이 재시작)
autoheal:
image: willfarrell/autoheal:latest
container_name: autoheal-gateway
restart: unless-stopped
environment:
# AUTOHEAL_INTERVAL: 체크 주기 (초 단위)
AUTOHEAL_INTERVAL: ${AUTOHEAL_INTERVAL} # unhealthy 컨테이너 체크 주기 (healthcheck interval과 동기화 권장)
# AUTOHEAL_START_PERIOD: 컨테이너 시작 후 체크 시작까지 유예 시간 (초)
AUTOHEAL_START_PERIOD: ${AUTOHEAL_START_PERIOD} # healthcheck의 start_period를 따르므로 0으로 설정
# AUTOHEAL_DEFAULT_STOP_TIMEOUT: 재시작 시 강제 종료까지 대기 시간 (초)
AUTOHEAL_DEFAULT_STOP_TIMEOUT: ${AUTOHEAL_DEFAULT_STOP_TIMEOUT} # graceful shutdown 대기 시간
# DOCKER_SOCK: Docker 소켓 경로 (컨테이너 제어용)
DOCKER_SOCK: /var/run/docker.sock
volumes:
# Docker 소켓 마운트 (컨테이너 재시작 권한 획득)
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- gateway-network
# autoheal은 매우 가벼운 서비스 (메모리 ~10MB)
deploy:
resources:
limits:
memory: ${AUTOHEAL_MEMORY_LIMIT}
reservations:
memory: ${AUTOHEAL_MEMORY_RESERVATION}

volumes:
gateway-logs:
driver: local

networks:
app-network:
gateway-network:
driver: bridge
Loading