Skip to content

Commit fcf4499

Browse files
authored
Merge pull request #131 from CareCode-Repo/feat/free-tier-deploy
fix: 1GB 인스턴스(프리티어)에서 배포·기동이 가능하도록
2 parents b940762 + f2676e4 commit fcf4499

5 files changed

Lines changed: 202 additions & 76 deletions

File tree

‎.github/workflows/ci-cd.yml‎

Lines changed: 55 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ jobs:
252252
IMAGE: ${{ needs.build-docker.outputs.image-tag }}
253253
run: |
254254
ssh -i ~/.ssh/deploy_key "$DEPLOY_USER@$DEPLOY_HOST" \
255-
"docker pull $IMAGE && (docker stop carecode-staging || true) && (docker rm carecode-staging || true) && docker run -d --name carecode-staging -p 8082:8082 --env-file /opt/carecode/.env $IMAGE"
255+
"docker pull $IMAGE && (docker stop carecode-staging || true) && (docker rm carecode-staging || true) && docker run -d --name carecode-staging --memory=640m --memory-swap=640m -p 8082:8082 --env-file /opt/carecode/.env $IMAGE"
256256
257257
- name: Run health check
258258
env:
@@ -369,66 +369,79 @@ jobs:
369369
set -euo pipefail
370370
371371
APP=carecode
372-
PROBE=carecode-probe
373372
PORT=8082
374-
# 예전 blue/green 이 8083 을 쓰므로 검증 포트는 겹치지 않는 곳으로 잡는다.
375-
PROBE_PORT=18082
376373
ENV_FILE=/opt/carecode/.env
377374
375+
# 컨테이너 메모리 한도. 없으면 JVM 이 호스트 전체를 기준으로 힙을 잡아(측정: 858MB)
376+
# 같은 머신의 MariaDB·Redis 가 쓸 메모리를 먹는다. 1GB 인스턴스 기준 기본값이며,
377+
# 서버 .env 에 APP_MEMORY 를 두면 그 값을 쓴다(여유 있는 인스턴스에서 올리면 된다).
378+
APP_MEMORY="$(grep -E '^APP_MEMORY=' "$ENV_FILE" 2>/dev/null | tail -1 | cut -d= -f2)"
379+
APP_MEMORY="${APP_MEMORY:-640m}"
380+
378381
if [ ! -f "$ENV_FILE" ]; then
379382
echo "$ENV_FILE 이 없습니다."
380383
exit 1
381384
fi
382385
386+
# 되돌릴 대상. 지금 돌고 있는 컨테이너가 쓰는 이미지 ID 를 먼저 붙잡아 둔다.
387+
# 태그가 아니라 ID 를 쓰는 이유: 같은 태그가 새 이미지로 덮여도 예전 것을 가리키게 하기 위해서다.
388+
PREV_IMAGE="$(docker inspect -f '{{.Image}}' "$APP" 2>/dev/null || true)"
389+
383390
echo "===== pull $IMAGE ====="
384391
docker pull "$IMAGE"
385392
386-
# 이전 실행이 남긴 검증 컨테이너 정리
387-
docker rm -f "$PROBE" >/dev/null 2>&1 || true
388-
389-
# 1) 예비 포트에서 먼저 띄워 본다. 살아 있는 컨테이너는 아직 그대로다.
390-
echo "===== 새 이미지 검증 (:$PROBE_PORT) ====="
391-
docker run -d --name "$PROBE" -p "127.0.0.1:$PROBE_PORT:8082" --env-file "$ENV_FILE" "$IMAGE"
392-
393-
ok=0
394-
for _ in $(seq 1 40); do
395-
if curl -fsS "http://127.0.0.1:$PROBE_PORT/actuator/health" 2>/dev/null | grep -q '"status":"UP"'; then
396-
ok=1; break
397-
fi
398-
sleep 5
393+
run_app() {
394+
docker run -d --name "$APP" --restart unless-stopped --memory="$APP_MEMORY" --memory-swap="$APP_MEMORY" -p "$PORT:8082" --env-file "$ENV_FILE" "$1" >/dev/null
395+
}
396+
397+
wait_healthy() {
398+
for _ in $(seq 1 48); do
399+
if curl -fsS "http://127.0.0.1:$PORT/actuator/health" 2>/dev/null | grep -q '"status":"UP"'; then
400+
return 0
401+
fi
402+
# 컨테이너가 이미 죽었으면 더 기다릴 이유가 없다 (OOM 이면 여기서 잡힌다).
403+
if [ -z "$(docker ps -q -f name="^${APP}$")" ]; then
404+
return 1
405+
fi
406+
sleep 5
407+
done
408+
return 1
409+
}
410+
411+
# 교체. 1GB 인스턴스에서는 새 컨테이너를 미리 띄워 검증할 메모리가 없다
412+
# (JVM 두 개 = 측정 기준 1.3GB). 그래서 짧은 순단을 받아들이고, 실패하면 되돌린다.
413+
echo "===== 교체 ($APP_MEMORY) ====="
414+
for name in "$APP" carecode-probe carecode-blue carecode-green; do
415+
docker rm -f "$name" >/dev/null 2>&1 || true
399416
done
400417
401-
if [ "$ok" -ne 1 ]; then
402-
echo "새 이미지가 기동하지 못했습니다. 운영 컨테이너는 건드리지 않습니다."
403-
echo "----- 컨테이너 로그 (마지막 100줄) -----"
404-
docker logs --tail 100 "$PROBE" 2>&1 || true
405-
docker rm -f "$PROBE" >/dev/null 2>&1 || true
406-
exit 1
418+
run_app "$IMAGE"
419+
420+
if wait_healthy; then
421+
echo "===== 교체 완료 ====="
422+
docker image prune -f >/dev/null 2>&1 || true
423+
exit 0
407424
fi
408425
409-
echo "검증 통과. 교체합니다."
410-
docker rm -f "$PROBE" >/dev/null 2>&1 || true
426+
echo "새 이미지가 기동하지 못했습니다."
427+
echo "----- 컨테이너 로그 (마지막 100줄) -----"
428+
docker logs --tail 100 "$APP" 2>&1 || true
411429
412-
# 2) 교체. 여기서부터 짧은 순단이 있다.
413-
# 예전 워크플로가 만들던 blue/green 이름도 함께 정리한다. 남아 있으면 포트를 잡고 있다.
414-
for name in "$APP" carecode-blue carecode-green; do
415-
docker rm -f "$name" >/dev/null 2>&1 || true
416-
done
430+
if [ -z "$PREV_IMAGE" ]; then
431+
echo "::error::되돌릴 이전 이미지가 없습니다(첫 배포로 보입니다). 서비스가 내려간 상태입니다."
432+
exit 1
433+
fi
417434
418-
docker run -d --name "$APP" --restart unless-stopped \
419-
-p "$PORT:8082" --env-file "$ENV_FILE" "$IMAGE"
435+
echo "===== 이전 이미지로 되돌립니다 ($PREV_IMAGE) ====="
436+
docker rm -f "$APP" >/dev/null 2>&1 || true
437+
run_app "$PREV_IMAGE"
420438
421-
for _ in $(seq 1 40); do
422-
if curl -fsS "http://127.0.0.1:$PORT/actuator/health" 2>/dev/null | grep -q '"status":"UP"'; then
423-
echo "===== 교체 완료 ====="
424-
docker image prune -f >/dev/null 2>&1 || true
425-
exit 0
426-
fi
427-
sleep 5
428-
done
439+
if wait_healthy; then
440+
echo "::error::새 이미지 기동 실패. 이전 이미지로 되돌렸고 서비스는 살아 있습니다."
441+
exit 1
442+
fi
429443
430-
echo "교체 후 기동에 실패했습니다."
431-
echo "----- 컨테이너 로그 (마지막 100줄) -----"
444+
echo "::error::되돌린 이미지도 기동하지 못했습니다. 서비스가 내려간 상태입니다 — 서버를 직접 확인하세요."
432445
docker logs --tail 100 "$APP" 2>&1 || true
433446
exit 1
434447
REMOTE

‎Dockerfile‎

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,35 +6,57 @@ COPY . .
66

77
RUN gradle clean bootJar --no-daemon
88

9-
# 2단계: JDK 17로 실행용 이미지 구성
9+
# 스프링 부트 레이어로 쪼갠다. 의존성(156MB 중 대부분)은 거의 바뀌지 않으므로 별도 레이어로 두면
10+
# 재배포 때 애플리케이션 레이어(수 MB)만 내려받는다. 1GB 인스턴스에서 배포 시간이 크게 줄어든다.
11+
RUN java -Djarmode=tools -jar build/libs/carecode-app.jar extract --layers --launcher --destination build/extracted
12+
13+
# 2단계: 실행용 이미지
1014
#
11-
# openjdk 공식 이미지는 폐기되어 Docker Hub 에서 태그가 내려갔다. openjdk:17-jdk-slim 은
12-
# 더 이상 존재하지 않아 이미지 빌드가 "not found" 로 실패한다. Docker 가 후속으로 안내하는
13-
# eclipse-temurin 으로 옮긴다.
15+
# openjdk 공식 이미지는 폐기되어 Docker Hub 에서 태그가 내려갔다. Docker 가 후속으로 안내하는
16+
# eclipse-temurin 으로 옮겼다.
1417
#
15-
# JRE 가 아니라 JDK 를 쓰는 건 이전과 같다. 운영 중 jcmd·jstack 으로 들여다보던 걸
16-
# 이 교체 때문에 잃지 않도록 한다. (이미지 크기를 줄이려면 -jre-jammy 로 바꿀 수 있는데,
17-
# 아래 HEALTHCHECK 의 wget 과 addgroup/adduser 는 그쪽에도 모두 있다.)
18-
FROM eclipse-temurin:17-jdk-jammy
18+
# JDK 가 아니라 JRE 를 쓴다. 이미지가 1.26GB → 300MB 대로 줄어, 1GB 인스턴스에서 배포마다
19+
# 받는 양과 디스크 사용이 크게 줄어든다. 예전에 JDK 를 둔 이유는 운영 중 jcmd·jstack 이었는데,
20+
# 그건 필요할 때 JDK 컨테이너를 같은 PID 공간에 붙여 쓰면 된다(운영 문서에 명령을 적어 두었다).
21+
# docker run --rm --pid=container:carecode eclipse-temurin:17-jdk-jammy jcmd 1 VM.native_memory
22+
FROM eclipse-temurin:17-jre-jammy
1923

2024
ENV TZ=Asia/Seoul
2125
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
22-
ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 -XX:+UseG1GC -XX:+ExitOnOutOfMemoryError"
2326

24-
WORKDIR /app
27+
# 작은 인스턴스(1GB)를 기준으로 잡은 기본값. 컨테이너에 --memory 가 걸려 있어야 의미가 있다
28+
# (제한이 없으면 JVM 이 호스트 전체를 기준으로 계산해 858MB 까지 썼다).
29+
#
30+
# - MaxRAMPercentage=55: --memory=512m 에서 힙 약 280MB. 힙 밖(메타스페이스·스레드·코드캐시·
31+
# 다이렉트 버퍼)이 150MB 가까이 되므로 70% 로 두면 컨테이너 한도를 넘겨 OOM 으로 죽는다.
32+
# - SerialGC: vCPU 1~2개에서는 G1 의 백그라운드 스레드가 오히려 부담이다.
33+
# - MaxMetaspaceSize: 상한이 없으면 메타스페이스가 조용히 늘어 컨테이너 한도를 밀어낸다.
34+
# - ExitOnOutOfMemoryError: 반쯤 죽은 상태로 버티는 대신 죽는다. 그래야 --restart 가 살린다.
35+
#
36+
# 여유 있는 인스턴스라면 배포 시 JAVA_OPTS 로 덮어쓴다 (예: -XX:+UseG1GC -XX:MaxRAMPercentage=75).
37+
ENV JAVA_OPTS="-XX:MaxRAMPercentage=55.0 -XX:MaxMetaspaceSize=192m -XX:+UseSerialGC -XX:+ExitOnOutOfMemoryError"
2538

26-
# 빌드된 JAR 복사 (하나만 있는 경우 자동 복사 가능)
27-
COPY --from=builder /app/build/libs/carecode-app.jar app.jar
39+
WORKDIR /app
2840

2941
RUN addgroup --system carecode && adduser --system --ingroup carecode carecode
30-
# 업로드 저장소. 이미지에 폴더가 있어야 볼륨을 처음 붙일 때 소유권이 이어진다.
31-
# 없으면 볼륨이 root 소유로 생겨 carecode 사용자가 파일을 쓰지 못한다.
32-
RUN mkdir -p /app/uploads && chown -R carecode:carecode /app
42+
43+
# 업로드 저장소. 이미지에 폴더가 있어야 볼륨을 처음 붙일 때 소유권이 이어진다
44+
# (없으면 볼륨이 root 소유로 생겨 carecode 사용자가 파일을 쓰지 못한다).
45+
RUN mkdir -p /app/uploads && chown carecode:carecode /app /app/uploads
46+
47+
# 바뀌지 않는 것부터 복사해 레이어 캐시를 살린다. --chown 으로 복사하는 이유는
48+
# 나중에 chown -R 을 걸면 156MB JAR 이 레이어에 한 번 더 복사돼 이미지가 두 배가 되기 때문이다.
49+
COPY --from=builder --chown=carecode:carecode /app/build/extracted/dependencies/ ./
50+
COPY --from=builder --chown=carecode:carecode /app/build/extracted/spring-boot-loader/ ./
51+
COPY --from=builder --chown=carecode:carecode /app/build/extracted/snapshot-dependencies/ ./
52+
COPY --from=builder --chown=carecode:carecode /app/build/extracted/application/ ./
53+
3354
USER carecode
3455

3556
EXPOSE 8082
3657

3758
HEALTHCHECK --interval=30s --timeout=5s --start-period=180s --retries=3 \
3859
CMD wget -qO- http://127.0.0.1:8082/actuator/health | grep -q '"status":"UP"' || exit 1
3960

40-
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
61+
# 레이어로 쪼갠 실행 파일은 JarLauncher 로 띄운다 (app.jar 이 그대로 있지 않다).
62+
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS org.springframework.boot.loader.launch.JarLauncher"]

‎docker-compose.yml‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,16 @@ services:
2323
- --collation-server=utf8mb4_unicode_ci
2424
# 운영(Linux MariaDB)과 같게. 테이블 이름 대소문자를 구분해야 대문자 매핑 불일치가 여기서 잡힌다.
2525
- --lower-case-table-names=0
26+
# 1GB 인스턴스에서 앱과 같이 사는 전제. 측정값은 유휴 113MB 였다.
27+
- --innodb-buffer-pool-size=96M
28+
- --performance-schema=OFF
29+
- --max-connections=30
2630
ports:
2731
- "${DB_PORT:-3307}:3306"
2832
volumes:
2933
- mariadb-data:/var/lib/mysql
34+
# 작은 인스턴스에 맞춘 값. 기본 설정은 버퍼 풀만 128MB 를 잡는다.
35+
mem_limit: ${DB_MEMORY:-320m}
3036
healthcheck:
3137
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
3238
interval: 5s
@@ -35,6 +41,10 @@ services:
3541

3642
redis:
3743
image: redis:7-alpine
44+
mem_limit: ${REDIS_MEMORY:-64m}
45+
# 리프레시 토큰·인증코드가 들어 있어 함부로 내보내면 로그아웃이 깨진다.
46+
# 한도에 닿으면 새 쓰기를 거절하는 편이 낫다(noeviction 이 기본이지만 명시한다).
47+
command: ["redis-server", "--maxmemory", "48mb", "--maxmemory-policy", "noeviction"]
3848
ports:
3949
- "${REDIS_PORT:-6380}:6379"
4050
healthcheck:
@@ -96,6 +106,10 @@ services:
96106
CLAMD_HOST: ${CLAMD_HOST:-clamav}
97107
volumes:
98108
- uploads:/app/uploads
109+
# 운영(1GB 인스턴스)과 같은 한도로 돌린다. 한도가 없으면 JVM 이 개발 PC 메모리를 기준으로
110+
# 힙을 잡아(측정: 858MB) 운영에서만 OOM 으로 죽는 차이가 생긴다.
111+
# 512m 에서도 뜨지만(측정 464MB) 여유가 없어 640m 로 둔다.
112+
mem_limit: ${APP_MEMORY:-640m}
99113

100114
# 선택: 업로드 악성코드 검사기. 시그니처 DB 를 받느라 첫 기동에 몇 분 걸리고 메모리를 1GB 넘게 쓴다.
101115
clamav:

0 commit comments

Comments
 (0)