build: Docker 이미지와 176 서버 배포 정의 추가
멀티스테이지로 프런트(React)까지 이미지 안에서 빌드해 단일 jar로 만든다. 빌드 머신에만 인터넷이 필요하고(Node v20.17.0·npm·Maven 내려받음) 실행 서버에는 JDK도 Node도 깔 필요가 없다. - 컨테이너 시간대를 Asia/Seoul로 고정한다. UTC로 두면 업무단계 도달 일시, 칸반 카드의 D+n, 연락 이력 날짜가 9시간 어긋난다. - 비루트 사용자로 실행하고, actuator가 없으므로 헬스체크는 /login 200으로 한다. - 이미지 빌드 중에는 테스트를 돌리지 않는다(-DskipTests). Testcontainers가 Docker를 다시 요구해 Docker-in-Docker가 되기 때문이며, 테스트는 CI에서 별도 단계로 돌린다. - .dockerignore로 run-local.ps1을 제외한다(DB 비밀번호가 들어 있다). deploy/는 192.168.0.176 운영 정의다. DB는 기존 192.168.0.60을 그대로 쓰므로 컨테이너로 띄우지 않고, 앱이 로컬 디스크에 쓰는 것이 없어(업로드는 DB와 Mattermost로 간다) 영속 볼륨도 두지 않는다. 실제 값은 서버의 /etc/itnhub/itnhub.env 에만 두고 저장소에는 .example만 남긴다. 로컬에서 이미지 빌드·기동까지 확인했다: jar 안에 static/index.html 포함, 컨테이너 시간대 KST, /login 200, HEALTHCHECK healthy, Flyway "schema itnhub: 12, up to date". Co-Authored-By: Claude Opus 5 (1M context)
@19677698abbbc6edb38ee53db3bb43cee64f0f8b
+++ .dockerignore
... | ... | @@ -0,0 +1,16 @@ |
| 1 | +# 빌드 컨텍스트에서 뺄 것들. node_modules/target을 넣으면 전송만 수백 MB 늘고, | |
| 2 | +# 컨테이너 안에서 어차피 새로 만든다. | |
| 3 | +target/ | |
| 4 | +frontend/node_modules/ | |
| 5 | +frontend/node/ | |
| 6 | +frontend/.vite/ | |
| 7 | +src/main/resources/static/ | |
| 8 | +.git/ | |
| 9 | +.idea/ | |
| 10 | +.superpowers/ | |
| 11 | +*.iml | |
| 12 | + | |
| 13 | +# 로컬 실행용 스크립트에는 DB 비밀번호가 들어 있다. 이미지에 절대 넣지 않는다. | |
| 14 | +run-local.ps1 | |
| 15 | +archive-smoke.ps1 | |
| 16 | +.env |
+++ Dockerfile
... | ... | @@ -0,0 +1,48 @@ |
| 1 | +# ITN-HUB 컨테이너 이미지. | |
| 2 | +# | |
| 3 | +# 1단계에서 프런트(React)까지 함께 빌드한다 - frontend-maven-plugin이 Node v20.17.0을 | |
| 4 | +# 내려받아 `npm run build`를 돌리고, 그 결과가 src/main/resources/static 으로 들어가 | |
| 5 | +# 단일 jar에 패키징된다. 따라서 빌드 머신에 JDK/Node를 따로 깔 필요가 없고, | |
| 6 | +# 대신 <빌드할 때> 인터넷(nodejs.org, npm registry, Maven Central)이 열려 있어야 한다. | |
| 7 | +# | |
| 8 | +# 테스트는 여기서 돌리지 않는다(-DskipTests). Testcontainers가 Docker를 다시 필요로 해서 | |
| 9 | +# 이미지 빌드 안에서 돌리면 Docker-in-Docker가 되기 때문이다. 테스트는 CI에서 별도 단계로 돌린다. | |
| 10 | +FROM maven:3.9-eclipse-temurin-21 AS build | |
| 11 | +WORKDIR /build | |
| 12 | + | |
| 13 | +# pom을 먼저 넣어 의존성 레이어를 캐시한다(소스만 바뀌면 이 레이어는 재사용된다). | |
| 14 | +COPY pom.xml ./ | |
| 15 | +RUN mvn -B -q dependency:go-offline -DskipTests || true | |
| 16 | + | |
| 17 | +COPY . . | |
| 18 | +RUN mvn -B -DskipTests package \ | |
| 19 | + && cp target/itnhub-*.jar /build/app.jar | |
| 20 | + | |
| 21 | + | |
| 22 | +FROM eclipse-temurin:21-jre-alpine | |
| 23 | + | |
| 24 | +# 한국 시간대. 이걸 안 넣으면 컨테이너가 UTC로 돌아서 업무단계 도달 일시, 카드의 D+n, | |
| 25 | +# 연락 이력 날짜가 9시간 어긋난다. | |
| 26 | +RUN apk add --no-cache tzdata \ | |
| 27 | + && cp /usr/share/zoneinfo/Asia/Seoul /etc/localtime \ | |
| 28 | + && echo "Asia/Seoul" > /etc/timezone | |
| 29 | +ENV TZ=Asia/Seoul | |
| 30 | + | |
| 31 | +# 루트로 돌리지 않는다. | |
| 32 | +RUN addgroup -S itnhub && adduser -S -G itnhub itnhub | |
| 33 | +USER itnhub | |
| 34 | + | |
| 35 | +WORKDIR /app | |
| 36 | +COPY --from=build /build/app.jar /app/app.jar | |
| 37 | + | |
| 38 | +# 컨테이너 안에서는 스프링 기본 포트를 그대로 쓴다. 바깥 포트는 실행할 때 -p 로 정한다. | |
| 39 | +EXPOSE 8080 | |
| 40 | + | |
| 41 | +# actuator를 넣지 않아서 헬스체크는 로그인 화면 200으로 확인한다(익명 접근 허용 경로). | |
| 42 | +HEALTHCHECK --interval=30s --timeout=5s --start-period=90s --retries=3 \ | |
| 43 | + CMD wget -q --spider http://127.0.0.1:8080/login || exit 1 | |
| 44 | + | |
| 45 | +# 컨테이너 메모리 한도에 맞춰 힙을 잡는다. | |
| 46 | +ENV JAVA_OPTS="-XX:MaxRAMPercentage=75 -XX:+UseContainerSupport" | |
| 47 | + | |
| 48 | +ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar /app/app.jar"] |
+++ deploy/README.md
... | ... | @@ -0,0 +1,106 @@ |
| 1 | +# 배포 (Docker · 192.168.0.176) | |
| 2 | + | |
| 3 | +이 앱은 **단일 Spring Boot jar**다. React는 빌드 시점에 jar 안(`static/`)으로 들어가므로 | |
| 4 | +서버에는 Node도 웹서버도 필요 없다. 컨테이너 하나만 띄우면 된다. | |
| 5 | + | |
| 6 | +| 항목 | 값 | | |
| 7 | +|---|---| | |
| 8 | +| 런타임 | JDK 21 (이미지에 포함) | | |
| 9 | +| 컨테이너 내부 포트 | 8080 | | |
| 10 | +| 서버에 노출할 포트 | 8091 (compose에서 변경 가능) | | |
| 11 | +| DB | 외부 PostgreSQL 16 `192.168.0.60:5432/itnhub` (컨테이너로 띄우지 않음) | | |
| 12 | +| 영속 볼륨 | **불필요** — 업로드 파일은 DB(`org_report`)와 Mattermost에 저장된다 | | |
| 13 | +| 스키마 생성 | 기동 시 Flyway가 `itnhub` 스키마와 V1~V12를 자동 적용 | | |
| 14 | +| 헬스체크 | `GET /login` 200 (actuator 미포함) | | |
| 15 | + | |
| 16 | +## 1. 176 서버 준비 (최초 1회) | |
| 17 | + | |
| 18 | +```bash | |
| 19 | +# Docker / compose 플러그인 | |
| 20 | +sudo dnf install -y docker docker-compose-plugin # 또는 apt install docker.io docker-compose-plugin | |
| 21 | +sudo systemctl enable --now docker | |
| 22 | + | |
| 23 | +# 배포 파일 위치 | |
| 24 | +sudo install -d -m 755 /opt/itnhub | |
| 25 | +sudo install -d -m 750 /etc/itnhub | |
| 26 | + | |
| 27 | +# 환경변수 파일 (값은 직접 채운다) | |
| 28 | +sudo cp deploy/itnhub.env.example /etc/itnhub/itnhub.env | |
| 29 | +sudo chmod 600 /etc/itnhub/itnhub.env | |
| 30 | +sudo vi /etc/itnhub/itnhub.env | |
| 31 | + | |
| 32 | +# compose 파일 | |
| 33 | +sudo cp deploy/docker-compose.yml /opt/itnhub/ | |
| 34 | + | |
| 35 | +# 방화벽 | |
| 36 | +sudo firewall-cmd --add-port=8091/tcp --permanent && sudo firewall-cmd --reload | |
| 37 | +``` | |
| 38 | + | |
| 39 | +서버 밖에서 챙길 것 두 가지: | |
| 40 | + | |
| 41 | +- **DB**: `192.168.0.60`의 `pg_hba.conf`에 176 IP를 허용하고, `itnhub` 계정에 스키마 생성 권한을 준다. | |
| 42 | +- **아웃바운드**: 176에서 `https://hub.iten.co.kr`(Mattermost API)로 나갈 수 있어야 한다. | |
| 43 | + | |
| 44 | +## 2. 이미지 만들기 | |
| 45 | + | |
| 46 | +저장소 루트에서: | |
| 47 | + | |
| 48 | +```bash | |
| 49 | +docker build -t itnhub:$(git rev-parse --short HEAD) -t itnhub:latest . | |
| 50 | +``` | |
| 51 | + | |
| 52 | +빌드 컨테이너가 Node v20.17.0과 npm 패키지, Maven 의존성을 내려받으므로 **빌드하는 머신에 | |
| 53 | +인터넷이 열려 있어야 한다**(실행 서버는 필요 없다). 사내망이 막혀 있으면 Maven `settings.xml`의 | |
| 54 | +미러와 `frontend-maven-plugin`의 `nodeDownloadRoot`를 사내 저장소로 돌려야 한다. | |
| 55 | + | |
| 56 | +## 3. 176으로 옮겨 실행 | |
| 57 | + | |
| 58 | +레지스트리가 있으면 push/pull이 제일 깔끔하다. | |
| 59 | + | |
| 60 | +```bash | |
| 61 | +# (A) 사내 레지스트리가 있을 때 | |
| 62 | +docker tag itnhub:latest registry.iten.co.kr/itnhub:latest | |
| 63 | +docker push registry.iten.co.kr/itnhub:latest | |
| 64 | +ssh deploy@192.168.0.176 "cd /opt/itnhub && ITNHUB_IMAGE=registry.iten.co.kr/itnhub:latest docker compose pull && ITNHUB_IMAGE=registry.iten.co.kr/itnhub:latest docker compose up -d" | |
| 65 | + | |
| 66 | +# (B) 레지스트리가 없을 때 - 이미지를 파일로 넘긴다 | |
| 67 | +docker save itnhub:latest | gzip | ssh deploy@192.168.0.176 "gunzip | sudo docker load" | |
| 68 | +ssh deploy@192.168.0.176 "cd /opt/itnhub && sudo docker compose up -d" | |
| 69 | +``` | |
| 70 | + | |
| 71 | +## 4. 확인 | |
| 72 | + | |
| 73 | +```bash | |
| 74 | +ssh deploy@192.168.0.176 "sudo docker compose -f /opt/itnhub/docker-compose.yml ps" | |
| 75 | +curl -f http://192.168.0.176:8091/login && echo OK | |
| 76 | +ssh deploy@192.168.0.176 "sudo docker logs --tail 100 itnhub" | |
| 77 | +``` | |
| 78 | + | |
| 79 | +기동 로그에 Flyway가 어디까지 적용했는지 찍힌다. `Successfully applied N migrations` 또는 | |
| 80 | +`Schema itnhub is up to date` 가 보이면 정상이다. | |
| 81 | + | |
| 82 | +## 되돌리기 | |
| 83 | + | |
| 84 | +이미지 태그를 커밋 해시로 남겨두면 한 줄로 돌아간다. | |
| 85 | + | |
| 86 | +```bash | |
| 87 | +ssh deploy@192.168.0.176 "cd /opt/itnhub && ITNHUB_IMAGE=registry.iten.co.kr/itnhub:<이전해시> docker compose up -d" | |
| 88 | +``` | |
| 89 | + | |
| 90 | +**주의**: 애플리케이션은 되돌아가지만 Flyway가 이미 적용한 DB 마이그레이션은 되돌아가지 않는다. | |
| 91 | +스키마를 바꾸는 버전을 배포할 때는 이전 버전 앱이 새 스키마에서도 동작하는지 먼저 확인한다. | |
| 92 | + | |
| 93 | +## nginx를 앞에 둘 경우 | |
| 94 | + | |
| 95 | +- **서브패스(`/itnhub/`)로는 붙일 수 없다.** Vite `base`가 `/`라 에셋 경로가 절대경로다. | |
| 96 | + 도메인 루트 또는 전용 포트로 노출한다. | |
| 97 | +- 정적 파일을 nginx가 직접 서빙하려 하지 말고 전부 앱으로 프록시한다(SPA 경로 포워딩이 앱에 있다). | |
| 98 | +- TLS를 nginx가 종단하면 `/etc/itnhub/itnhub.env`에 | |
| 99 | + `SERVER_FORWARD_HEADERS_STRATEGY=framework` 를 넣는다. | |
| 100 | + | |
| 101 | +## CI(Jenkins)에서 돌릴 때 | |
| 102 | + | |
| 103 | +- 테스트(`mvn test`)는 Testcontainers로 PostgreSQL을 띄우므로 **에이전트에 Docker 소켓이 필요**하다. | |
| 104 | + 이미지 빌드 안에서는 테스트를 돌리지 않는다(Docker-in-Docker가 되므로). | |
| 105 | +- 권장 순서: `mvn -B verify`(테스트) → `docker build` → 레지스트리 push → 176에서 `compose up -d` | |
| 106 | + → `curl -f http://192.168.0.176:8091/login` 로 확인. |
+++ deploy/docker-compose.yml
... | ... | @@ -0,0 +1,30 @@ |
| 1 | +# 176 서버(192.168.0.176)에서 쓰는 실행 정의. | |
| 2 | +# | |
| 3 | +# DB는 컨테이너로 띄우지 않는다 - 기존 PostgreSQL(192.168.0.60)을 그대로 쓴다. | |
| 4 | +# 앱이 로컬 디스크에 쓰는 것이 없어(업로드 파일은 DB와 Mattermost로 간다) 영속 볼륨도 필요 없다. | |
| 5 | +services: | |
| 6 | + itnhub: | |
| 7 | + image: ${ITNHUB_IMAGE:-itnhub:latest} | |
| 8 | + container_name: itnhub | |
| 9 | + restart: unless-stopped | |
| 10 | + | |
| 11 | + # 비밀값은 이미지에도 이 파일에도 넣지 않는다. 서버의 /etc/itnhub/itnhub.env 만 읽는다. | |
| 12 | + env_file: | |
| 13 | + - /etc/itnhub/itnhub.env | |
| 14 | + | |
| 15 | + # 왼쪽이 서버에서 열리는 포트. 로컬 개발과 같은 8091로 맞춰 둔다. | |
| 16 | + ports: | |
| 17 | + - "8091:8080" | |
| 18 | + | |
| 19 | + # 컨테이너 로그가 무한정 쌓이지 않게 한다. | |
| 20 | + logging: | |
| 21 | + driver: json-file | |
| 22 | + options: | |
| 23 | + max-size: "20m" | |
| 24 | + max-file: "5" | |
| 25 | + | |
| 26 | + # Dockerfile의 HEALTHCHECK를 그대로 쓴다(/login 200 확인). | |
| 27 | + deploy: | |
| 28 | + resources: | |
| 29 | + limits: | |
| 30 | + memory: 1500m |
+++ deploy/itnhub.env.example
... | ... | @@ -0,0 +1,31 @@ |
| 1 | +# 176 서버의 /etc/itnhub/itnhub.env 로 복사해 실제 값을 채운다. | |
| 2 | +# sudo install -d -m 750 /etc/itnhub | |
| 3 | +# sudo cp itnhub.env.example /etc/itnhub/itnhub.env | |
| 4 | +# sudo chmod 600 /etc/itnhub/itnhub.env | |
| 5 | +# | |
| 6 | +# 이 8개가 하나라도 비면 애플리케이션이 기동하지 않는다(application.yml이 ${...}로 요구한다). | |
| 7 | +# 이 파일은 절대 커밋하지 않는다. 커밋되는 것은 .example 뿐이다. | |
| 8 | + | |
| 9 | +# --- 데이터베이스 (기존 PostgreSQL 16) --- | |
| 10 | +# 176 컨테이너에서 접속하므로 localhost가 아니라 실제 호스트를 적는다. | |
| 11 | +DB_URL=jdbc:postgresql://192.168.0.60:5432/itnhub | |
| 12 | +DB_USERNAME=itnhub | |
| 13 | +DB_PASSWORD= | |
| 14 | + | |
| 15 | +# --- Mattermost --- | |
| 16 | +# 토큰은 system-admin 권한 계정의 Personal Access Token이어야 한다(비공개 채널 전체 조회 때문). | |
| 17 | +MATTERMOST_URL=https://hub.iten.co.kr | |
| 18 | +MATTERMOST_TOKEN= | |
| 19 | +MATTERMOST_TEAM_ID= | |
| 20 | +MATTERMOST_TEAM_NAME=itn-hub | |
| 21 | +# 채널 생성 시 자동으로 만드는 담당자 계정의 공통 초기 비밀번호. | |
| 22 | +MATTERMOST_DEFAULT_USER_PASSWORD= | |
| 23 | + | |
| 24 | +# --- 앱 관리자 로그인 --- | |
| 25 | +# 로컬 개발값을 그대로 쓰지 말 것. 충분히 긴 무작위 문자열을 쓴다. | |
| 26 | +APP_ADMIN_USERNAME=admin | |
| 27 | +APP_ADMIN_PASSWORD= | |
| 28 | + | |
| 29 | +# --- 선택 --- | |
| 30 | +# nginx 등 TLS 종단 프록시를 앞에 두면 켠다(리다이렉트·쿠키 Secure 판정이 정상화된다). | |
| 31 | +# SERVER_FORWARD_HEADERS_STRATEGY=framework |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?