diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..869e63a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +node_modules +dist +build +.git +.gitignore +.env +npm-debug.log +uploads +*.xlsx +*.log diff --git a/.env b/.env deleted file mode 100644 index bd7a495..0000000 --- a/.env +++ /dev/null @@ -1,6 +0,0 @@ -DB_HOST=172.16.8.151 -DB_PORT=3306 -DB_USER=itam_admin -DB_PASS=itam1234 -DB_NAME=itam -PORT=3000 \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..363ed05 --- /dev/null +++ b/.env.example @@ -0,0 +1,17 @@ +# Database Configuration +DB_HOST=172.16.8.151 +DB_PORT=3306 +DB_USER=itam_admin +DB_PASS=itam1234 +DB_NAME=itam + +# Application Configuration +NODE_ENV=development +PORT=3000 + +# Logging (optional) +LOG_LEVEL=info + +# Security (for production) +# API_KEY=your_api_key_here +# JWT_SECRET=your_jwt_secret_here diff --git a/.gitea/coverage.json b/.gitea/coverage.json new file mode 100644 index 0000000..a80ae85 --- /dev/null +++ b/.gitea/coverage.json @@ -0,0 +1,7 @@ +{ + "Path": "./backend/coverage.out", + "Thresholds": { + "baron-sso-backend/internal/handler": 10, + "baron-sso-backend/internal/service": 10 + } +} diff --git a/.gitea/workflows/itam_code_check.yml b/.gitea/workflows/itam_code_check.yml new file mode 100644 index 0000000..fbbe71f --- /dev/null +++ b/.gitea/workflows/itam_code_check.yml @@ -0,0 +1,47 @@ +name: ITAM Code Check + +on: + push: + branches: + - Dockerizing + - main + pull_request: + workflow_dispatch: + +jobs: + build-and-config-check: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Prepare CI env file + run: | + cat <<'EOF' > .env + DB_HOST=127.0.0.1 + DB_PORT=3306 + DB_USER=itam_ci + DB_PASS=itam_ci_password + DB_NAME=itam + NODE_ENV=production + PORT=3000 + LOG_LEVEL=info + EOF + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + + - name: Install dependencies + run: npm ci + + - name: Frontend TypeScript and Vite build + run: npm run build + + - name: Validate test compose + run: docker compose -f docker-compose.test.yaml config + + - name: Validate prod compose + run: docker compose -f docker-compose.prod.yaml config diff --git a/.gitea/workflows/itam_docker_build_check.yml b/.gitea/workflows/itam_docker_build_check.yml new file mode 100644 index 0000000..2848516 --- /dev/null +++ b/.gitea/workflows/itam_docker_build_check.yml @@ -0,0 +1,69 @@ +name: ITAM Docker Build Check + +on: + push: + branches: + - Dockerizing + - main + paths: + - "Dockerfile.frontend.prod" + - "Dockerfile.backend.prod" + - "docker-compose.prod.yaml" + - "docker-compose.test.yaml" + - "docker/**" + - "src/**" + - "server.js" + - "package.json" + - "package-lock.json" + - "vite.config.ts" + - "index.html" + - "img/**" + - "public/**" + pull_request: + paths: + - "Dockerfile.frontend.prod" + - "Dockerfile.backend.prod" + - "docker-compose.prod.yaml" + - "docker-compose.test.yaml" + - "docker/**" + - "src/**" + - "server.js" + - "package.json" + - "package-lock.json" + - "vite.config.ts" + - "index.html" + - "img/**" + - "public/**" + workflow_dispatch: + +jobs: + docker-build-check: + runs-on: ubuntu-latest + env: + DOCKER_BUILDKIT: "1" + COMPOSE_DOCKER_CLI_BUILD: "1" + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Prepare CI env file + run: | + cat <<'EOF' > .env + DB_HOST=127.0.0.1 + DB_PORT=3306 + DB_USER=itam_ci + DB_PASS=itam_ci_password + DB_NAME=itam + NODE_ENV=production + PORT=3000 + LOG_LEVEL=info + EOF + + - name: Build backend production image + run: docker build -f Dockerfile.backend.prod -t itam-backend:ci . + + - name: Build frontend production image + run: docker build -f Dockerfile.frontend.prod -t itam-frontend:ci . + + - name: Validate production compose with CI env + run: docker compose -f docker-compose.prod.yaml config diff --git a/.gitea/workflows/itam_production_deploy.yml b/.gitea/workflows/itam_production_deploy.yml new file mode 100644 index 0000000..1cf90d2 --- /dev/null +++ b/.gitea/workflows/itam_production_deploy.yml @@ -0,0 +1,137 @@ +name: ITAM Production Deploy + +on: + workflow_dispatch: + inputs: + target_branch: + description: "Branch to deploy" + required: true + default: "Dockerizing" + type: string + +jobs: + deploy-production: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup SSH agent + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.PROD_SSH_PRIVATE_KEY }} + + - name: Validate required production variables + env: + PROD_HOST: ${{ vars.PROD_HOST }} + PROD_USER: ${{ vars.PROD_USER }} + PROD_DEPLOY_PATH: ${{ vars.PROD_DEPLOY_PATH }} + PROD_GIT_URL: ${{ vars.PROD_GIT_URL }} + DB_HOST: ${{ vars.PROD_DB_HOST }} + DB_PORT: ${{ vars.PROD_DB_PORT }} + DB_USER: ${{ vars.PROD_DB_USER }} + DB_PASS: ${{ secrets.PROD_DB_PASS }} + DB_NAME: ${{ vars.PROD_DB_NAME }} + run: | + set -euo pipefail + required_keys="PROD_HOST PROD_USER PROD_DEPLOY_PATH PROD_GIT_URL DB_HOST DB_PORT DB_USER DB_PASS DB_NAME" + for key in ${required_keys}; do + if [ -z "${!key:-}" ]; then + echo "::error::Missing required variable or secret: ${key}" + exit 1 + fi + done + + - name: Create production env file + env: + DB_HOST: ${{ vars.PROD_DB_HOST }} + DB_PORT: ${{ vars.PROD_DB_PORT }} + DB_USER: ${{ vars.PROD_DB_USER }} + DB_PASS: ${{ secrets.PROD_DB_PASS }} + DB_NAME: ${{ vars.PROD_DB_NAME }} + LOG_LEVEL: ${{ vars.PROD_LOG_LEVEL }} + run: | + set -euo pipefail + EFFECTIVE_LOG_LEVEL="${LOG_LEVEL:-info}" + cat > .env.deploy <> ~/.ssh/known_hosts + + ssh "${PROD_USER}@${PROD_HOST}" "mkdir -p '${PROD_DEPLOY_PATH}'" + + EFFECTIVE_BACKUP_ROOT="${PROD_BACKUP_ROOT:-${PROD_DEPLOY_PATH%/}_backups}" + + ssh "${PROD_USER}@${PROD_HOST}" "export DEPLOY_PATH='${PROD_DEPLOY_PATH}' BACKUP_ROOT='${EFFECTIVE_BACKUP_ROOT}'; sh -eu -s" <<'REMOTE_BACKUP' + case "$BACKUP_ROOT" in + "$DEPLOY_PATH"|"$DEPLOY_PATH"/*) + echo "Backup path must be outside deploy path: $BACKUP_ROOT" + exit 1 + ;; + esac + + if [ -d "$DEPLOY_PATH/.git" ]; then + mkdir -p "$BACKUP_ROOT" + cd "$DEPLOY_PATH" + + if [ -f Makefile ] && [ -f scripts/backup.sh ]; then + make predeploy-backup BACKUP_ROOT="$BACKUP_ROOT" + else + echo "Skipping pre-deploy backup because current deployed revision does not contain Makefile backup tooling." + fi + else + echo "Skipping pre-deploy backup because no existing deployment was found." + fi + REMOTE_BACKUP + + ssh "${PROD_USER}@${PROD_HOST}" "if [ ! -d '${PROD_DEPLOY_PATH}/.git' ]; then git clone '${PROD_GIT_URL}' '${PROD_DEPLOY_PATH}'; else cd '${PROD_DEPLOY_PATH}' && git remote set-url origin '${PROD_GIT_URL}'; fi" + + ssh "${PROD_USER}@${PROD_HOST}" "cd '${PROD_DEPLOY_PATH}' && git fetch origin '${TARGET_BRANCH}' && git checkout -B '${TARGET_BRANCH}' FETCH_HEAD && git reset --hard FETCH_HEAD && git clean -fd" + + ssh "${PROD_USER}@${PROD_HOST}" "cd '${PROD_DEPLOY_PATH}' && mkdir -p uploads logs/nginx" + + scp .env.deploy "${PROD_USER}@${PROD_HOST}:${PROD_DEPLOY_PATH}/.env" + + ssh "${PROD_USER}@${PROD_HOST}" "cd '${PROD_DEPLOY_PATH}' && chmod 600 .env && docker compose -f docker-compose.prod.yaml config && docker compose -f docker-compose.prod.yaml up -d --build" + + - name: Post-deploy status check + env: + PROD_HOST: ${{ vars.PROD_HOST }} + PROD_USER: ${{ vars.PROD_USER }} + PROD_DEPLOY_PATH: ${{ vars.PROD_DEPLOY_PATH }} + run: | + set -euo pipefail + ssh "${PROD_USER}@${PROD_HOST}" "cd '${PROD_DEPLOY_PATH}' && docker compose -f docker-compose.prod.yaml ps" + + - name: Post-deploy smoke checks + env: + PROD_HOST: ${{ vars.PROD_HOST }} + PROD_USER: ${{ vars.PROD_USER }} + PROD_DEPLOY_PATH: ${{ vars.PROD_DEPLOY_PATH }} + run: | + set -euo pipefail + ssh "${PROD_USER}@${PROD_HOST}" "curl -fsS http://localhost/health" + ssh "${PROD_USER}@${PROD_HOST}" "curl -fsS http://localhost/ > /dev/null" + ssh "${PROD_USER}@${PROD_HOST}" "cd '${PROD_DEPLOY_PATH}' && docker compose -f docker-compose.prod.yaml exec -T backend curl -fsS http://localhost:3000/ready" + + - name: Cleanup generated env file + if: ${{ always() }} + run: rm -f .env.deploy diff --git a/.gitignore b/.gitignore index f838511..cb187d3 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ dist/ *.log .DS_Store Thumbs.db +backups/ diff --git a/Dockerfile.backend b/Dockerfile.backend new file mode 100644 index 0000000..d04b4b0 --- /dev/null +++ b/Dockerfile.backend @@ -0,0 +1,12 @@ +FROM node:20-alpine + +WORKDIR /app + +COPY package*.json ./ +RUN npm ci + +COPY . . + +EXPOSE 3000 + +CMD ["npm", "run", "server"] \ No newline at end of file diff --git a/Dockerfile.backend.prod b/Dockerfile.backend.prod new file mode 100644 index 0000000..5d1f381 --- /dev/null +++ b/Dockerfile.backend.prod @@ -0,0 +1,48 @@ +FROM node:20-alpine + +LABEL maintainer="ITAM Team " + +# Set production environment +ENV NODE_ENV=production + +WORKDIR /app + +# Install curl for health checks and dumb-init for proper signal handling +RUN apk add --no-cache curl dumb-init + +# Copy package files +COPY package*.json ./ + +# Install production dependencies only +RUN npm ci --only=production + +# Copy application code +COPY server.js ./ +COPY src ./src + +# Create non-root user 'appuser' with UID 1001 (1000 already in use by node image) +RUN addgroup -g 1001 appuser && \ + adduser -D -u 1001 -G appuser appuser + +# Set ownership of application files to appuser +RUN chown -R appuser:appuser /app + +# Create logs directory +RUN mkdir -p /app/logs && \ + chown -R appuser:appuser /app/logs + +# Switch to non-root user +USER appuser + +# Expose port +EXPOSE 3000 + +# Health check - backend should implement /health endpoint +HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ + CMD curl -f http://localhost:3000/health || exit 1 + +# Use dumb-init from PATH to avoid distro-specific absolute path issues +ENTRYPOINT ["dumb-init", "--"] + +# Run application +CMD ["npm", "run", "server"] diff --git a/Dockerfile.frontend b/Dockerfile.frontend new file mode 100644 index 0000000..ba24cd6 --- /dev/null +++ b/Dockerfile.frontend @@ -0,0 +1,12 @@ +FROM node:20-alpine + +WORKDIR /app + +COPY package*.json ./ +RUN npm ci + +COPY . . + +EXPOSE 8080 + +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] \ No newline at end of file diff --git a/Dockerfile.frontend.prod b/Dockerfile.frontend.prod new file mode 100644 index 0000000..d1fa16e --- /dev/null +++ b/Dockerfile.frontend.prod @@ -0,0 +1,62 @@ +# Stage 1: Build +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package*.json ./ +COPY tsconfig*.json ./ +COPY vite.config.ts ./ + +# Install all dependencies (including devDependencies for build) +RUN npm ci + +# Copy source code +COPY src ./src +COPY public ./public +COPY index.html ./ + +# Build application +RUN npm run build + +# Verify build output +RUN ls -la dist/ && echo "Build completed successfully" + +# Stage 2: Runtime +FROM nginx:stable-alpine + +LABEL maintainer="ITAM Team " + +# Install curl for health checks +RUN apk add --no-cache curl + +WORKDIR /usr/share/nginx/html + +# Copy built assets from builder +COPY --from=builder /app/dist . + +# Copy static image assets referenced by literal /img/... paths +COPY img ./img + +# Copy root-level logo asset referenced directly by index.html +# COPY ["image 92.png", "./image 92.png"] + +# Copy Nginx static file serving configuration (not reverse proxy) +COPY docker/frontend/default.conf /etc/nginx/conf.d/default.conf + +# Create nginx runtime user and directories +RUN mkdir -p /var/log/nginx && \ + chown -R nginx:nginx /usr/share/nginx/html && \ + chown -R nginx:nginx /var/log/nginx && \ + chown -R nginx:nginx /var/cache/nginx && \ + chown -R nginx:nginx /etc/nginx/conf.d + +# Expose port +EXPOSE 80 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=20s --retries=3 \ + CMD curl -f http://localhost:80/ || exit 1 + +# Run nginx +CMD ["nginx", "-g", "daemon off;"] diff --git a/ITAM_임원보고_운영배포_요약.md b/ITAM_임원보고_운영배포_요약.md new file mode 100644 index 0000000..f3be050 --- /dev/null +++ b/ITAM_임원보고_운영배포_요약.md @@ -0,0 +1,103 @@ +# 자산관리 시스템 운영 오픈 보고 요약 + +## 1. 운영 방식 + +```mermaid +flowchart LR + A[개발 완료] --> B[개발 완료 검증] + B --> C[운영 서버 반영] + C --> D[최종 점검] + D --> E[서비스 오픈] + + linkStyle default stroke:#d32f2f,stroke-width:2px; +``` + +| 구분 | 운영 방향 | +| --- | --- | +| 반영 기준 | 개발 완료 및 검증 후 운영에 반영 | +| 반영 방식 | 운영 담당자 기준 통제 절차 | +| 데이터 연계 | 기존 외부 DB와 연동 | +| 안정성 확보 | 반영 전 점검, 반영 전 백업, 반영 후 확인 | + +임의 반영 배제, 개발 완료 및 검증 결과 기준 운영 반영 방식. + +--- + +## 2. 운영 배포 절차 + +```mermaid +flowchart TD + A[배포 준비 완료] --> B[사전 점검] + B --> C[백업 수행] + C --> D[운영 배포 실행] + D --> E[접속 및 기능 확인] + E --> F[오픈] + + B1[환경 설정 확인] + B2[외부 DB 연결 확인] + B3[배포 파일 확인] + + B --> B1 + B --> B2 + B --> B3 + + linkStyle default stroke:#d32f2f,stroke-width:2px; +``` + +| 단계 | 목적 | 담당 | +| --- | --- | --- | +| 사전 점검 | 운영 반영에 필요한 조건 확인 | 개발팀 + 운영 | +| 백업 수행 | 문제 발생 시 신속 복구 대비 | 운영 | +| 운영 배포 실행 | 운영 서버에 검증 완료 결과 반영 | 운영 | +| 접속 및 기능 확인 | 주요 화면과 서비스 상태 확인 | 개발팀 + 운영 | +| 오픈 | 사용자 대상 서비스 오픈 | 운영 | + +--- + +## 3. 예상 일정 + +```mermaid +gantt + title 자산관리 시스템 운영 오픈 일정 + dateFormat YYYY-MM-DD + axisFormat %m/%d + + section 준비 단계 + 배포 구성 정리 및 환경 보완 :done, a1, 2026-06-18, 2026-06-24 + + section 검증 단계 + 테스트 배포 및 점검 :a2, 2026-06-25, 2026-06-27 + 오픈 전 최종 확인 :a3, 2026-06-28, 2026-06-30 + + section 오픈 + 운영 오픈 :milestone, a4, 2026-07-01, 1d +``` + +| 구간 | 일정 | 주요 내용 | +| --- | --- | --- | +| 준비 단계 | 6월 18일 ~ 6월 24일 | 운영 환경 정리 및 배포 준비 완료 | +| 검증 단계 | 6월 25일 ~ 6월 30일 | 테스트 배포, 접속 확인, 최종 보완 | +| 오픈 시점 | 7월 1일 | 운영 서비스 시작 | + +6월 30일까지 준비 및 검증 완료, 7월 1일 오픈. + +--- + +## 4. 보고 요약 + +| 항목 | 보고 내용 | +| --- | --- | +| 통제된 배포 | 개발 완료 및 검증 후 운영 반영 | +| 운영 안정성 | 점검과 백업 후 반영 | +| 데이터 연속성 | 기존 외부 DB와 연계 유지 | +| 오픈 목표 | 7월 1일 서비스 개시 | + +1. 통제 절차 기반 운영 반영. +2. 오픈 전 점검 및 백업 기반 리스크 관리. +3. 6월 30일까지 준비 완료, 7월 1일 오픈. + +--- + +## 5. 결론 + +자산관리 시스템, 6월 30일까지 운영 배포 준비 및 검증 완료, 7월 1일 오픈. \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b781027 --- /dev/null +++ b/Makefile @@ -0,0 +1,33 @@ +SHELL := /bin/sh + +ENV_FILE ?= .env +BACKUP_ROOT ?= backups +RETENTION_DAYS ?= 14 +BACKUP_SCRIPT := scripts/backup.sh + +.PHONY: help db-dump files-backup full-backup predeploy-backup cleanup-backups + +help: + @echo "Usage: make [ENV_FILE=.env BACKUP_ROOT=backups RETENTION_DAYS=14]" + @echo "" + @echo "Targets:" + @echo " db-dump Create a gzip-compressed MySQL dump from .env settings" + @echo " files-backup Archive runtime files such as uploads/, map_config.json, and .env" + @echo " full-backup Run both db-dump and files-backup" + @echo " predeploy-backup Alias for the backup step executed before production deploy" + @echo " cleanup-backups Delete backup files older than RETENTION_DAYS" + +db-dump: + @ENV_FILE="$(ENV_FILE)" BACKUP_ROOT="$(BACKUP_ROOT)" sh "$(BACKUP_SCRIPT)" db + +files-backup: + @ENV_FILE="$(ENV_FILE)" BACKUP_ROOT="$(BACKUP_ROOT)" sh "$(BACKUP_SCRIPT)" files + +full-backup: + @ENV_FILE="$(ENV_FILE)" BACKUP_ROOT="$(BACKUP_ROOT)" sh "$(BACKUP_SCRIPT)" full + +predeploy-backup: + @ENV_FILE="$(ENV_FILE)" BACKUP_ROOT="$(BACKUP_ROOT)" sh "$(BACKUP_SCRIPT)" full + +cleanup-backups: + @BACKUP_ROOT="$(BACKUP_ROOT)" RETENTION_DAYS="$(RETENTION_DAYS)" sh "$(BACKUP_SCRIPT)" cleanup \ No newline at end of file diff --git a/TEST_LOCAL.md b/TEST_LOCAL.md new file mode 100644 index 0000000..c6effbd --- /dev/null +++ b/TEST_LOCAL.md @@ -0,0 +1,108 @@ +# 로컬 Docker 테스트 가이드 + +## 준비 사항 + +- Docker & Docker Compose 설치 (WSL2 Ubuntu 권장) +- Node.js 20 (로컬 빌드 테스트 시) + +## 테스트 단계 + +### 1. 파일 구조 확인 + +```bash +# docker-compose.test.yaml이 다음을 사용 확인 +ls -la docker/nginx/default.conf +ls -la docker/frontend/default.conf +ls -la Dockerfile.backend.prod +ls -la Dockerfile.frontend.prod +ls -la package.json +ls -la src/ +ls -la public/ +ls -la index.html +``` + +### 2. Compose 파일 검증 + +```bash +docker compose -f docker-compose.test.yaml config +``` + +### 3. 이미지 빌드 테스트 + +```bash +# 개별 빌드 테스트 +docker build -f Dockerfile.backend.prod -t itam-backend:test . +docker build -f Dockerfile.frontend.prod -t itam-frontend:test . +``` + +### 4. 컨테이너 시작 + +```bash +# WSL 터미널에서 +cd /mnt/c/Users/user/Desktop/안건\ 파일/itam + +# 또는 docker-compose.test.yaml을 사용하여 전체 스택 시작 +docker compose -f docker-compose.test.yaml up --build + +# 백그라운드에서 실행 +docker compose -f docker-compose.test.yaml up -d --build +``` + +### 5. 컨테이너 상태 확인 + +```bash +docker compose -f docker-compose.test.yaml ps +docker logs itam-backend-test +docker logs itam-frontend-test +docker logs itam-nginx-test +``` + +### 6. 브라우저 테스트 + +``` +http://localhost:8080/ # Frontend 접근 +http://localhost:8080/api/ # Backend API 테스트 +http://localhost:3000/health # 직접 backend health check +``` + +### 7. 정리 + +```bash +docker compose -f docker-compose.test.yaml down +``` + +## 예상되는 포트 매핑 + +- 8080: Nginx reverse proxy (frontend route to static + /api to backend) +- 3000: Backend Express API (내부, frontend와 nginx를 통해 접근) +- 80: Frontend Nginx (내부, 정적 파일 서빙) + +## 테스트 체크리스트 + +- [ ] docker compose config 성공 +- [ ] docker build 성공 (backend) +- [ ] docker build 성공 (frontend) +- [ ] 컨테이너 모두 실행 중 (ps 확인) +- [ ] http://localhost:8080 접근 가능 (frontend 페이지 로드) +- [ ] http://localhost:8080/api 응답 확인 (backend proxy 동작) +- [ ] backend health check 성공 (docker logs에서 /health 요청 확인) + +## 문제 해결 + +### npm run build 실패 +- Dockerfile.frontend.prod에서 tsc && vite build 실행 +- package.json과 tsconfig.json 확인 + +### nginx 포트 이미 사용 중 +```bash +docker ps +docker stop container_name +``` + +### DB 연결 실패 +- 정상 동작 (NODE_ENV 때문에 /health는 200 반환) +- 실제 API 호출 시 DB 오류 예상 + +### 권한 문제 +- logs/ 디렉토리 소유권 확인 +- 필요 시 mkdir -p logs/nginx && chmod 777 logs diff --git a/doc_readme.md b/doc_readme.md new file mode 100644 index 0000000..55964c0 --- /dev/null +++ b/doc_readme.md @@ -0,0 +1,940 @@ +# ITAM 도커라이징 실전 가이드 + +## 1. 문서 목적 + +이 문서는 Gitea에 올라가 있는 현재 저장소를 기준으로, 개발 PC에 WSL2와 Ubuntu만 설치되어 있는 상태에서 지금의 Docker 실행 구조를 재현하는 방법을 처음부터 끝까지 설명하는 실전 가이드다. + +이 문서는 아래 상황을 가정한다. + +1. 소스 코드는 아직 로컬에 없거나, Gitea에서 막 받아올 예정이다. +2. Windows에는 WSL2와 Ubuntu는 설치되어 있다. +3. 그 외 Docker 관련 세팅은 아직 안 되어 있을 수 있다. +4. 최종 목표는 현재 저장소 기준 `frontend + backend + external DB` 구조를 Docker로 재현하는 것이다. + +이 문서의 목적은 아래 네 가지다. + +1. 현재 시스템 구조와 Docker 구조를 먼저 이해하게 한다. +2. 기존 파일 중 무엇이 새로 추가되었고 무엇이 수정되었는지 정리한다. +3. 각 단계별로 정확히 어디에서 명령을 실행해야 하는지 명시한다. +4. Gitea 소스만 받은 상태에서 지금과 같은 Docker 실행 상태까지 도달하게 한다. + +--- + +## 2. 현재 시스템 구조 개요 + +## 2.1 애플리케이션 원래 구조 + +현재 저장소의 본래 실행 구조는 다음과 같다. + +1. 프런트엔드: Vite 기반 TypeScript 앱 +2. 백엔드: Express 기반 Node.js API 서버 +3. 데이터베이스: 외부 MySQL 서버 + +즉, 원래부터 MySQL이 Docker 안에 들어 있던 구조가 아니다. + +프런트와 백엔드는 각각 별도 프로세스로 실행되며, 프런트는 `/api` 상대 경로로 백엔드 API를 호출한다. + +--- + +## 2.2 현재 Docker 구조 + +현재 최종 Docker 구조는 아래와 같다. + +1. `frontend` 컨테이너 +2. `backend` 컨테이너 +3. 외부 MySQL DB + +즉, 지금은 내부 `db` 컨테이너가 없고, 내부 `db-bootstrap` 컨테이너도 없다. + +현재 구조를 문장으로 풀면 다음과 같다. + +1. 브라우저는 `http://localhost:8080`으로 `frontend` 컨테이너에 접속한다. +2. `frontend`는 `/api` 요청을 `backend:3000`으로 프록시한다. +3. `backend`는 `.env`에 적힌 외부 DB 정보로 외부 MySQL에 직접 접속한다. +4. 조회 결과 JSON을 프런트가 받아 화면에 렌더링한다. + +간단한 흐름은 아래와 같다. + +```text +Browser + -> frontend container :8080 + -> Vite proxy (/api) + -> backend container :3000 + -> external MySQL (.env) +``` + +--- + +## 2.3 왜 이 구조가 맞는가 + +현재 구조가 적절한 이유는 다음과 같다. + +1. 원래 시스템도 외부 MySQL을 쓰는 구조였다. +2. 지금 목표는 운영형 단일 배포가 아니라 현재 개발형 구조를 Docker로 재현하는 것이다. +3. 프런트는 Vite dev server 기반이라 운영형 nginx 정적 배포 구조로 억지로 바꾸는 것보다, 현 구조를 유지하는 편이 안전하다. +4. 실무 표준 관점에서도 앱 컨테이너는 무상태로 유지하고, DB는 외부 인프라를 사용하는 구성이 더 일반적이다. + +--- + +## 3. 이번 도커라이징에서 추가되거나 수정된 파일 정리 + +아래 파일들은 이번 Docker 재현 구조를 위해 새로 추가되었거나 수정된 핵심 파일이다. + +## 3.1 새로 추가된 파일 + +1. `Dockerfile.frontend` +2. `Dockerfile.backend` +3. `.dockerignore` +4. `docker-compose.yaml` +5. `start_docker_wsl.ps1` +6. `stop_docker_wsl.ps1` +7. `start_docker_wsl.bat` +8. `stop_docker_wsl.bat` +9. `docker/mysql/init/README.md` +10. `docker_task_plan.md` +11. `doc_readme2.md` + +--- + +## 3.2 기존 파일 중 수정된 핵심 파일 + +1. `server.js` +2. `vite.config.ts` +3. `doc_readme.md` + +--- + +## 3.3 각 파일의 역할 + +### `Dockerfile.frontend` + +역할: + +1. 프런트 Vite 개발 서버 이미지를 만든다. +2. 컨테이너 내부에서 `npm run dev -- --host 0.0.0.0`를 실행한다. + +### `Dockerfile.backend` + +역할: + +1. 백엔드 Express 서버 이미지를 만든다. +2. 컨테이너 내부에서 `npm run server`를 실행한다. + +### `.dockerignore` + +역할: + +1. `node_modules`, `build`, `.git`, `.env`, `uploads` 같은 불필요한 파일을 Docker build context에서 제외한다. + +### `docker-compose.yaml` + +역할: + +1. `frontend`, `backend` 두 컨테이너를 동시에 띄운다. +2. `backend`는 `.env`의 외부 DB를 사용한다. +3. `frontend`는 `backend:3000`으로 프록시한다. + +### `start_docker_wsl.ps1` + +역할: + +1. Windows 경로를 WSL 경로로 안전하게 바꾼다. +2. WSL 내부 Docker를 사용해 `docker compose up --build -d`를 실행한다. +3. 한글 경로와 공백 경로에서도 안정적으로 실행되게 한다. + +### `stop_docker_wsl.ps1` + +역할: + +1. 같은 방식으로 WSL 내부에서 `docker compose down`을 실행한다. + +### `start_docker_wsl.bat`, `stop_docker_wsl.bat` + +역할: + +1. PowerShell 스크립트를 쉽게 실행하는 래퍼 역할을 한다. + +### `server.js` + +중요 수정 사항: + +1. `dotenv.config({ override: true })`가 아니라 `dotenv.config()`를 사용한다. + +이유: + +1. Compose나 실행 환경이 주는 환경변수를 `.env`가 덮어써 버리면 안 된다. +2. 외부 DB 정보와 포트 설정 등 실행 환경 우선 구조를 유지해야 한다. + +### `vite.config.ts` + +중요 수정 사항: + +1. 프록시 타깃을 고정 `localhost:3000`이 아니라 환경변수 기반으로 받도록 바꿨다. + +현재 구조: + +```ts +const proxyTarget = process.env.VITE_DEV_PROXY_TARGET || 'http://localhost:3000'; +``` + +이유: + +1. 로컬에서 직접 프런트를 띄울 때는 `localhost:3000`이 맞다. +2. Docker 안에서는 `frontend` 컨테이너에서 보는 `localhost`가 백엔드가 아니므로 `backend:3000`을 써야 한다. + +--- + +## 4. 현재 `docker-compose.yaml` 기준 실제 동작 구조 + +현재 `docker-compose.yaml`은 아래 구조다. + +### `backend` + +1. `Dockerfile.backend`로 이미지를 빌드한다. +2. `.env`를 읽는다. +3. DB 관련 변수는 `${DB_HOST}`, `${DB_PORT}`, `${DB_USER}`, `${DB_PASS}`, `${DB_NAME}`를 그대로 사용한다. +4. 포트 `3000:3000`으로 노출한다. +5. `uploads`, `map_config.json`을 마운트한다. + +### `frontend` + +1. `Dockerfile.frontend`로 이미지를 빌드한다. +2. `VITE_DEV_PROXY_TARGET: http://backend:3000` 환경변수를 사용한다. +3. 포트 `8080:8080`으로 노출한다. +4. 브라우저의 `/api` 요청을 `backend`로 프록시한다. + +즉, 현재 Compose는 DB를 띄우지 않고 앱 두 개만 띄운다. + +--- + +## 5. 사전 준비 사항 + +이 섹션은 Gitea에서 코드를 받기 전 또는 받은 직후에 확인해야 한다. + +## 5.1 가정하는 기본 상태 + +이미 설치되어 있다고 가정하는 것: + +1. Windows +2. WSL2 +3. Ubuntu 배포판 + +아직 없을 수 있는 것: + +1. Docker Desktop 또는 WSL 내부 Docker 사용 환경 +2. Git 클라이언트 +3. 프로젝트 `.env` + +--- + +## 5.2 권장 Docker 실행 방식 + +현재 저장소 구조상 가장 권장하는 방식은 다음이다. + +1. Windows에 Docker Desktop 설치 +2. Docker Desktop에서 WSL2 통합 활성화 +3. Ubuntu WSL 내부에서 `docker` 명령을 사용할 수 있게 한다. + +이유: + +1. 현재 `start_docker_wsl.ps1`가 WSL 내부의 `docker`를 호출하는 구조다. +2. 실제 검증도 WSL 내부 Docker 기준으로 이루어졌다. + +--- + +## 5.3 외부 DB 정보 준비 + +현재 구조는 외부 MySQL을 사용하므로 `.env` 파일이 반드시 필요하다. + +최소한 아래 값이 필요하다. + +```env +DB_HOST=<외부 MySQL 호스트> +DB_PORT=3306 +DB_USER=<외부 MySQL 계정> +DB_PASS=<외부 MySQL 비밀번호> +DB_NAME=itam +``` + +필요 시 추가 환경변수는 현재 백엔드 코드 기준으로 함께 넣을 수 있다. + +--- + +## 6. Gitea에서 소스 받기 + +## 6.1 작업 실행 위치 + +이 단계는 **Windows PowerShell** 또는 **Windows 터미널의 PowerShell**에서 수행한다. + +실행 위치 이유: + +1. 이후 `start_docker_wsl.ps1`도 Windows PowerShell에서 실행하는 것이 가장 자연스럽다. +2. 로컬 작업 폴더를 Windows 경로 기준으로 준비할 수 있다. + +--- + +## 6.2 소스 클론 + +예시: + +```powershell +git clone +cd <클론된 저장소 경로> +``` + +현재 프로젝트처럼 한글 경로를 사용할 수도 있지만, 가능하면 너무 복잡한 경로는 피하는 것이 좋다. + +현재 실제 프로젝트 경로 예시는 아래였다. + +```text +c:\Users\user\Desktop\안건 파일\itam +``` + +이 경로도 현재 스크립트로는 동작 가능하다. + +--- + +## 7. Docker 환경 준비 + +## 7.1 작업 실행 위치 + +이 단계는 **Windows PowerShell**과 **WSL Ubuntu 터미널**을 둘 다 사용한다. + +1. 설치 확인은 Windows PowerShell에서 시작 +2. 실제 Docker 동작 확인은 WSL Ubuntu에서 수행 + +--- + +## 7.2 Docker Desktop 설치 여부 확인 + +**실행 위치: Windows PowerShell** + +```powershell +docker version +``` + +만약 여기서 바로 안 잡혀도 현재 프로젝트는 WSL 내부 Docker를 쓰므로, 다음 단계로 넘어가 WSL 내부 확인을 한다. + +--- + +## 7.3 WSL 내부 Docker 확인 + +**실행 위치: Windows PowerShell** + +```powershell +wsl -l -v +wsl sh -lc "docker --version" +``` + +정상 기대 결과: + +1. Ubuntu가 Running 상태 +2. `docker --version`이 정상 출력 + +만약 `docker --version`이 실패하면, Docker Desktop 설치 및 WSL 통합을 먼저 완료해야 한다. + +--- + +## 8. `.env` 파일 준비 + +## 8.1 작업 실행 위치 + +이 단계는 **Windows PowerShell**, **VS Code**, 또는 아무 텍스트 편집기**에서 수행한다. + +즉, 프로젝트 루트에 `.env` 파일을 만드는 작업이다. + +--- + +## 8.2 `.env` 작성 + +프로젝트 루트에 `.env`를 만든다. + +예시: + +```env +DB_HOST=your-external-db-host +DB_PORT=3306 +DB_USER=your-db-user +DB_PASS=your-db-password +DB_NAME=itam +``` + +주의: + +1. 현재 Compose는 내부 DB를 만들지 않는다. +2. 따라서 이 값이 곧 실제 운영/개발 외부 DB 연결 정보다. +3. 이 정보가 틀리면 `backend`는 기동해도 API에서 DB 오류가 난다. + +--- + +## 9. 현재 Docker 파일이 어떻게 동작하는지 이해하기 + +## 9.1 `Dockerfile.frontend` + +**확인 위치: 프로젝트 루트 / VS Code** + +현재 내용 핵심: + +```dockerfile +FROM node:20-alpine +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +EXPOSE 8080 +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] +``` + +의미: + +1. Node 20 Alpine 기반 +2. 의존성 설치 후 전체 소스 복사 +3. Vite 개발 서버 실행 + +--- + +## 9.2 `Dockerfile.backend` + +**확인 위치: 프로젝트 루트 / VS Code** + +현재 내용 핵심: + +```dockerfile +FROM node:20-alpine +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +EXPOSE 3000 +CMD ["npm", "run", "server"] +``` + +의미: + +1. Node 20 Alpine 기반 +2. Express 서버 실행 + +--- + +## 9.3 `vite.config.ts` + +**확인 위치: 프로젝트 루트 / VS Code** + +현재 핵심: + +```ts +const proxyTarget = process.env.VITE_DEV_PROXY_TARGET || 'http://localhost:3000'; +``` + +그리고 `/api`, `/uploads`가 모두 `proxyTarget`으로 프록시된다. + +의미: + +1. 로컬 실행 시 기본값은 `localhost:3000` +2. Docker 실행 시 Compose가 `http://backend:3000`을 주입 + +이 수정이 있어야 Docker 안에서도 화면에 데이터가 표시된다. + +--- + +## 10. Docker Compose 기동 + +## 10.1 작업 실행 위치 + +이 단계는 반드시 **Windows PowerShell**에서 수행하는 것을 권장한다. + +이유: + +1. `start_docker_wsl.ps1`가 Windows 경로를 받아 WSL 경로로 바꾸는 구조다. +2. 한글/공백 경로에서 가장 안전하다. + +--- + +## 10.2 권장 기동 방법 + +**실행 위치: 프로젝트 루트의 Windows PowerShell** + +```powershell +.\start_docker_wsl.ps1 +``` + +또는 + +```powershell +.\start_docker_wsl.bat +``` + +이 스크립트는 내부적으로 아래를 수행한다. + +1. PowerShell 출력 인코딩을 UTF-8로 설정 +2. 현재 Windows 경로를 WSL 경로로 변환 +3. WSL 동작 확인 +4. WSL 내부 Docker 동작 확인 +5. `docker compose up --build -d` 수행 + +--- + +## 10.3 직접 기동이 필요할 때 + +**실행 위치: WSL Ubuntu 터미널** + +직접 실행 예시는 아래와 같다. + +```bash +cd /mnt/c/Users/user/Desktop/안건\ 파일/itam +docker compose up --build -d +``` + +하지만 현재 프로젝트는 한글 경로 이슈가 있었기 때문에, 특별한 이유가 없으면 `start_docker_wsl.ps1`를 우선 사용한다. + +--- + +## 11. 컨테이너 기동 후 검증 + +## 11.1 컨테이너 상태 확인 + +**실행 위치: Windows PowerShell** + +```powershell +wsl sh -lc "docker ps -a --format 'table {{.Names}}\t{{.Status}}' | grep itam" +``` + +정상 기대 상태: + +1. `itam-backend` -> `Up` +2. `itam-frontend` -> `Up` + +현재는 `itam-db`, `itam-db-bootstrap`가 없어야 정상이다. + +--- + +## 11.2 백엔드 API 확인 + +**실행 위치: Windows PowerShell** + +```powershell +Invoke-WebRequest -Uri http://localhost:3000/api/assets/master -UseBasicParsing | Select-Object -ExpandProperty StatusCode +``` + +정상 기대값: + +1. `200` + +이 검사는 `backend`가 외부 DB에 정상 연결됐는지 보는 가장 직접적인 검사다. + +--- + +## 11.3 프런트 경유 API 확인 + +**실행 위치: Windows PowerShell** + +```powershell +Invoke-WebRequest -Uri http://localhost:8080/api/assets/master -UseBasicParsing | Select-Object -ExpandProperty StatusCode +``` + +정상 기대값: + +1. `200` + +이 검사는 프런트 프록시가 정상인지 확인한다. + +예전에 화면에 데이터가 안 보였던 것은 외부 DB 자체가 아니라, 이 프록시 경로가 잘못돼 있었기 때문이다. + +--- + +## 11.4 브라우저 화면 확인 + +**실행 위치: 브라우저** + +```text +http://localhost:8080 +``` + +확인 포인트: + +1. 화면이 열리는지 +2. 목록/대시보드/테이블 데이터가 비어 있지 않은지 +3. 모달 진입 시 데이터가 정상적으로 보이는지 + +--- + +## 12. 지금 데이터가 표시되는 원리 + +현재는 내부 DB로 데이터를 옮겨 담지 않는다. + +현재 실제 동작 원리는 다음과 같다. + +1. 브라우저가 `frontend`에 접속한다. +2. 프런트가 `/api/...`로 요청한다. +3. Vite 프록시가 `backend:3000`으로 요청을 넘긴다. +4. `backend`가 `.env`의 외부 MySQL에 직접 접속한다. +5. 조회 결과 JSON을 프런트가 받아 화면에 렌더링한다. + +즉, 현재는 아래 구조다. + +```text +Browser -> frontend -> backend -> external MySQL +``` + +예전 외부 DB 구조에서 화면에 데이터가 안 보였던 이유는 외부 DB 때문이 아니라, 프런트 컨테이너가 `localhost:3000`을 잘못 바라보고 있었기 때문이다. + +지금은 `VITE_DEV_PROXY_TARGET: http://backend:3000`으로 수정되어 있기 때문에 정상 표시된다. + +--- + +## 13. 자주 헷갈리는 포인트 + +## 13.1 현재는 내부 DB 컨테이너가 없다 + +현재 `docker-compose.yaml`에는 아래가 없다. + +1. `db` 서비스 +2. `db-bootstrap` 서비스 +3. `itam_mysql_data` 볼륨 + +즉, DB는 Docker 스택 밖에 있다. + +--- + +## 13.2 현재는 `.env`가 곧 실제 DB 연결 정보다 + +현재 `backend`는 아래처럼 Compose에서 그대로 받는다. + +1. `DB_HOST: ${DB_HOST}` +2. `DB_PORT: ${DB_PORT}` +3. `DB_USER: ${DB_USER}` +4. `DB_PASS: ${DB_PASS}` +5. `DB_NAME: ${DB_NAME}` + +즉, `.env`를 틀리게 적으면 화면도 데이터가 안 뜬다. + +--- + +## 13.3 `server.js`는 여전히 중요하게 수정된 상태다 + +현재 `server.js`는 `dotenv.config()`를 사용한다. + +이 구조는 이후 Compose나 실행 환경에서 변수를 주입할 때, 애플리케이션이 그 값을 받아들일 수 있게 하기 위해 유지해야 한다. + +--- + +## 14. 스택 중지 방법 + +## 14.1 작업 실행 위치 + +**Windows PowerShell / 프로젝트 루트** + +--- + +## 14.2 권장 종료 명령 + +```powershell +.\stop_docker_wsl.ps1 +``` + +또는 + +```powershell +.\stop_docker_wsl.bat +``` + +이 스크립트는 내부적으로 WSL 경로 변환 후 `docker compose down`을 수행한다. + +--- + +## 15. 장애 발생 시 점검 순서 + +## 15.1 `frontend` 화면은 뜨는데 데이터가 없을 때 + +**실행 위치: Windows PowerShell** + +먼저 아래 두 API를 분리해서 본다. + +```powershell +Invoke-WebRequest -Uri http://localhost:3000/api/assets/master -UseBasicParsing | Select-Object -ExpandProperty StatusCode +Invoke-WebRequest -Uri http://localhost:8080/api/assets/master -UseBasicParsing | Select-Object -ExpandProperty StatusCode +``` + +판단 기준: + +1. `3000`은 200이고 `8080`만 실패 -> 프런트 프록시 문제 +2. 둘 다 실패 -> 백엔드 또는 외부 DB 연결 문제 + +--- + +## 15.2 백엔드가 외부 DB에 연결되지 않을 때 + +**실행 위치: Windows PowerShell** + +```powershell +wsl sh -lc "docker logs --tail=200 itam-backend" +``` + +점검 항목: + +1. `.env`의 DB 정보가 정확한지 +2. 외부 DB 서버 접근이 가능한지 +3. 계정/비밀번호가 맞는지 +4. 방화벽 또는 네트워크 이슈가 없는지 + +--- + +## 16. 운영 수동 배포 플로우 + +이 섹션은 현재 ITAM 저장소 기준으로 운영 서버에 반영할 때의 전체 흐름을 설명한다. + +중요한 전제는 아래와 같다. + +1. 로컬 수정본을 서버에 직접 복사하지 않는다. +2. 반드시 Gitea에 올라간 커밋을 기준으로 배포한다. +3. 운영 반영은 자동 푸시 배포가 아니라 Gitea workflow 수동 실행으로 진행한다. +4. 현재 기준 운영 배포 workflow는 `.gitea/workflows/itam_production_deploy.yml`이다. + +--- + +## 16.1 전체 운영/배포 분기 흐름 + +운영 반영은 크게 세 상황으로 나뉜다. + +1. 최초 운영 서버 구축 후 첫 배포 +2. 코드 수정 후 일반 재배포 +3. 검증 실패 또는 배포 실패 후 수정 재배포 + +아래 분기 구조로 이해하면 된다. + +```mermaid +flowchart TD + START["배포 필요 발생"] --> CASE{"어떤 상황인가?"} + + CASE -->|초기 구축| INIT["초기 운영 배포 준비"] + CASE -->|수정 반영| CHANGE["수정 후 재배포 준비"] + CASE -->|실패 후 재시도| RETRY["실패 원인 분석 후 재배포 준비"] + + INIT --> INIT1["운영 서버 Docker / compose 확인"] + INIT1 --> INIT2["Gitea Variables / Secrets 등록"] + INIT2 --> INIT3["map_config.json / uploads 초기 데이터 준비"] + INIT3 --> MANUAL["Gitea에서 수동 배포 workflow 실행"] + + CHANGE --> CHANGE1["로컬 수정 및 테스트"] + CHANGE1 --> CHANGE2["Gitea 커밋 / push"] + CHANGE2 --> CHANGE3["Code Check / Docker Build Check 통과"] + CHANGE3 --> MANUAL + + RETRY --> RETRY1{"어디서 실패했는가?"} + RETRY1 -->|코드 체크 실패| FIX1["코드 또는 설정 수정"] + RETRY1 -->|배포 단계 실패| FIX2["서버 / 변수 / 권한 / 네트워크 수정"] + RETRY1 -->|Smoke Check 실패| FIX3["앱 기동 상태 / 프록시 / DB 상태 수정"] + FIX1 --> CHANGE2 + FIX2 --> MANUAL + FIX3 --> MANUAL + + MANUAL --> DEPLOY["운영 서버 반영 수행"] + DEPLOY --> RESULT{"최종 검증 통과?"} + RESULT -->|예| DONE["운영 반영 완료"] + RESULT -->|아니오| RETRY + linkStyle default stroke:#d32f2f,stroke-width:2px; +``` + +핵심은 아래와 같다. + +1. 초기 구축은 서버와 운영 데이터 준비가 먼저다. +2. 수정 반영은 반드시 커밋과 push가 먼저다. +3. 실패 후 재배포는 실패 지점에 따라 수정 위치가 달라진다. + +--- + +## 16.2 최초 운영 배포 플로우 + +최초 배포에서는 코드보다 운영 환경 준비가 더 중요하다. + +순서는 아래와 같다. + +1. 운영 서버에 Docker Engine과 `docker compose`를 설치한다. +2. 운영 서버에서 Gitea 저장소에 접근 가능한 SSH 키를 준비한다. +3. Gitea repository Variables / Secrets를 등록한다. +4. `PROD_DEPLOY_PATH` 경로를 정한다. +5. `map_config.json`, `uploads/` 초기 데이터를 준비한다. +6. Gitea에서 `itam_production_deploy.yml`을 수동 실행한다. +7. 배포 후 `ps`, `/health`, `/`, `/ready`를 확인한다. + +즉 최초 배포는 아래 조건이 먼저 충족되어야 한다. + +```text +서버 준비 완료 +-> Gitea 변수 / 시크릿 등록 완료 +-> 초기 데이터 준비 완료 +-> 수동 배포 실행 +``` + +--- + +## 16.3 수정 후 일반 재배포 플로우 + +일반적인 수정 반영은 아래 흐름이다. + +1. 개발자가 로컬에서 코드 또는 설정을 수정한다. +2. 로컬에서 필요한 테스트를 수행한다. +3. 변경사항을 Gitea에 커밋 후 push 한다. +4. `itam_code_check.yml`이 빌드와 compose 문법을 검사한다. +5. `itam_docker_build_check.yml`이 운영용 이미지 빌드 가능 여부를 검사한다. +6. 두 검증이 통과하면 운영자가 Gitea에서 `itam_production_deploy.yml`을 수동 실행한다. +7. 운영 서버가 최신 커밋으로 동기화되고 컨테이너가 다시 올라온다. +8. smoke check 통과 여부를 확인한다. + +아래 다이어그램은 이 일반 재배포 흐름을 보여준다. + +```mermaid +flowchart LR + DEV["로컬 수정"] --> TEST["로컬 확인"] + TEST --> PUSH["커밋 / push"] + PUSH --> CODE["ITAM Code Check"] + CODE --> BUILD["ITAM Docker Build Check"] + BUILD --> GATE{"검증 통과?"} + GATE -->|예| RUN["Gitea에서 수동 배포 실행"] + GATE -->|아니오| FIX["로컬 수정 후 재커밋"] + FIX --> PUSH + RUN --> PROD["운영 서버 배포"] + PROD --> SMOKE{"Smoke Check 통과?"} + SMOKE -->|예| OK["배포 완료"] + SMOKE -->|아니오| FIXDEPLOY["원인 수정 후 재배포"] + FIXDEPLOY --> RUN + linkStyle default stroke:#d32f2f,stroke-width:2px; +``` + +--- + +## 16.4 수동 배포 workflow 내부 실행 순서 + +Gitea에서 `itam_production_deploy.yml`을 수동 실행하면 내부적으로는 아래 순서로 진행된다. + +1. SSH agent를 설정한다. +2. 필수 Variables / Secrets가 모두 있는지 확인한다. +3. 운영용 `.env.deploy` 파일을 생성한다. +4. 운영 서버에 접속한다. +5. `PROD_DEPLOY_PATH`를 생성한다. +6. 저장소를 clone 또는 fetch 한다. +7. 선택한 브랜치의 최신 커밋으로 checkout, reset, clean 한다. +8. `uploads`, `logs/nginx` 디렉토리를 준비한다. +9. `.env.deploy`를 서버의 `.env`로 복사한다. +10. `docker compose -f docker-compose.prod.yaml config`를 수행한다. +11. `docker compose -f docker-compose.prod.yaml up -d --build`를 수행한다. +12. `docker compose ps`를 확인한다. +13. `/health`, `/`, backend `/ready` smoke check를 수행한다. + +아래 다이어그램은 workflow 내부 실행 순서다. + +```mermaid +flowchart TD + A["수동 배포 시작"] --> B["SSH agent 설정"] + B --> C["Variables / Secrets 검증"] + C --> D{"필수 값 누락 여부"} + D -->|예| E["즉시 실패 후 설정 보완"] + D -->|아니오| F[".env.deploy 생성"] + F --> G["운영 서버 SSH 접속"] + G --> H["배포 경로 생성"] + H --> I["git clone 또는 fetch"] + I --> J["지정 브랜치 checkout / reset / clean"] + J --> K["uploads / logs/nginx 준비"] + K --> L[".env 업로드 및 권한 설정"] + L --> M["compose config 검증"] + M --> N{"compose config 성공?"} + N -->|아니오| O["설정 수정 후 재실행"] + N -->|예| P["compose up -d --build"] + P --> Q["docker compose ps 확인"] + Q --> R["/health, /, /ready smoke check"] + R --> S{"smoke check 성공?"} + S -->|예| T["운영 배포 완료"] + S -->|아니오| U["원인 분석 후 재배포"] + linkStyle default stroke:#d32f2f,stroke-width:2px; +``` + +--- + +## 16.5 실패 후 검증 및 재배포 플로우 + +실패가 났다고 해서 항상 같은 방식으로 다시 배포하면 안 된다. + +실패 지점별 판단은 아래처럼 나눈다. + +1. Code Check 실패: TypeScript, build, compose 문법 문제를 먼저 수정한다. +2. Docker Build Check 실패: Dockerfile, 정적 자산 복사, 운영 빌드 컨텍스트 문제를 수정한다. +3. Deploy 단계 실패: SSH, Gitea 변수, 서버 권한, 경로, git 접근, Docker 권한을 수정한다. +4. Smoke Check 실패: Nginx 프록시, backend readiness, 외부 DB 연결, 앱 런타임 오류를 수정한다. + +즉 재배포 전 판단 기준은 아래와 같다. + +```text +CI 실패 -> 로컬 코드 / 설정 수정 후 재커밋 +배포 실패 -> 서버 환경 또는 배포 설정 수정 후 수동 재실행 +Smoke Check 실패 -> 앱 / 프록시 / DB 상태 수정 후 수동 재실행 +``` + +운영 관점에서는 아래 순서를 지키는 것이 안전하다. + +1. 실패 지점 확인 +2. 원인 수정 +3. 같은 실패가 다시 나는지 좁은 범위로 재검증 +4. 그 다음에만 수동 배포 재실행 + +--- + +## 16.6 문서 기준 요약 + +현재 ITAM 운영 배포는 아래 원칙으로 이해하면 된다. + +1. 수정은 로컬에서 한다. +2. 배포 기준점은 Gitea에 올라간 커밋이다. +3. 운영 반영은 Gitea 수동 workflow 실행으로 한다. +4. 초기 배포, 일반 재배포, 실패 후 재배포는 분기 기준이 다르다. +5. 최종 성공 여부는 컨테이너 상태가 아니라 smoke check까지 통과했는지로 판단한다. + +--- + +## 15.3 프런트 프록시가 의심될 때 + +**확인 위치: `vite.config.ts`, `docker-compose.yaml`** + +다음 두 설정이 유지되는지 확인한다. + +`vite.config.ts` + +```ts +const proxyTarget = process.env.VITE_DEV_PROXY_TARGET || 'http://localhost:3000'; +``` + +`docker-compose.yaml` + +```yaml +VITE_DEV_PROXY_TARGET: http://backend:3000 +``` + +이 둘 중 하나라도 바뀌면 Docker 안에서 화면 데이터가 다시 안 보일 수 있다. + +--- + +## 17. 현재 기준 재현 절차 요약 + +가장 짧게 정리하면 아래 순서다. + +1. Gitea에서 소스를 클론한다. +2. Windows PowerShell에서 프로젝트 루트로 이동한다. +3. `.env`에 외부 MySQL 정보를 작성한다. +4. Docker Desktop + WSL 통합 또는 WSL 내부 Docker 사용 가능 상태를 만든다. +5. `start_docker_wsl.ps1`를 실행한다. +6. `http://localhost:3000/api/assets/master`가 200인지 확인한다. +7. `http://localhost:8080/api/assets/master`가 200인지 확인한다. +8. 브라우저에서 `http://localhost:8080`을 열어 실제 데이터 표시를 확인한다. + +--- + +## 18. 현재 최종 결론 + +현재 저장소의 도커라이징 구조는 실무 표준에 맞는 `무상태 앱 컨테이너 + 외부 DB` 구조다. + +현재 핵심은 아래 세 가지다. + +1. `backend`는 외부 MySQL에 직접 연결한다. +2. `frontend`는 `backend:3000`으로 API 프록시한다. +3. WSL 경로 변환 스크립트를 통해 Windows 한글 경로에서도 안정적으로 실행한다. + +즉, 이 문서대로 진행하면 Gitea 소스만 받은 상태에서 지금과 같은 Docker 실행 구조를 재현할 수 있다. diff --git a/doc_readme2.md b/doc_readme2.md new file mode 100644 index 0000000..815829c --- /dev/null +++ b/doc_readme2.md @@ -0,0 +1,730 @@ +# ITAM 도커라이징 최종 재현 가이드 + +## 1. 문서 목적 + +이 문서는 현재 Git 저장소에 올라간 파일만 가지고, 지금과 동일한 수준으로 ITAM 시스템을 도커라이징하고 실행하는 절차를 처음부터 끝까지 정리한 최종 가이드다. + +이 문서만 읽어도 아래 목표를 달성할 수 있게 작성한다. + +1. 현재 저장소 구조를 이해한다. +2. 왜 이렇게 도커라이징했는지 판단 근거를 안다. +3. WSL2 기반으로 실제 스택을 기동한다. +4. 외부 MySQL에서 내부 MySQL 컨테이너로 초기 데이터를 bootstrap 한다. +5. 프런트 8080과 백엔드 3000이 모두 정상 동작하는지 검증한다. +6. 재초기화, 재기동, 장애 확인까지 수행한다. + +이 문서는 최종 성공 구조 기준이다. 실패 기록은 `doc_readme.md`를 본다. + +--- + +## 2. 최종 목표 구조 + +현재 최종 구조는 아래 4개 서비스/역할로 나뉜다. + +1. `frontend`: Vite 개발 서버 컨테이너, 포트 8080 +2. `backend`: Express API 서버 컨테이너, 포트 3000 +3. `db`: MySQL 8 컨테이너, 포트 3306 +4. `db-bootstrap`: 외부 MySQL -> 내부 MySQL로 1회성 복제 수행 후 종료되는 도우미 컨테이너 + +논리 흐름은 다음과 같다. + +```text +브라우저 -> frontend:8080 -> Vite proxy -> backend:3000 -> db:3306 + \ + -> /uploads -> backend 정적 경로 + +초기 1회 기동 시 +외부 MySQL(.env) -> db-bootstrap -> 내부 MySQL(db) +``` + +--- + +## 3. 왜 이 구조를 선택했는가 + +이 저장소는 처음부터 운영형 정적 배포 앱이 아니었다. 실제 구조는 다음과 같았다. + +1. 프런트는 Vite 개발 서버가 따로 돈다. +2. 백엔드는 Express API가 따로 돈다. +3. 프런트는 상대 경로 `/api`를 호출한다. +4. 백엔드는 프런트의 `dist`를 서빙하지 않는다. + +따라서 내일 바로 시연 가능한 수준까지 빠르게 안정화하려면 아래 전략이 가장 맞다. + +1. 프런트를 Vite dev server 그대로 컨테이너화한다. +2. 백엔드를 별도 컨테이너로 유지한다. +3. DB는 MySQL 8 컨테이너로 묶되, 초기 데이터는 외부 DB에서 복제한다. +4. 프런트 프록시는 컨테이너 네트워크 서비스명 `backend`로 붙게 한다. + +즉, 현재 구조는 "개발형 구조를 Docker로 재현한 시연/개발용 Compose"다. + +--- + +## 4. 저장소 내 최종 관련 파일 목록 + +현재 도커라이징과 직접 관련된 핵심 파일은 아래와 같다. + +1. `.dockerignore` +2. `Dockerfile.frontend` +3. `Dockerfile.backend` +4. `docker-compose.yaml` +5. `start_docker_wsl.ps1` +6. `stop_docker_wsl.ps1` +7. `start_docker_wsl.bat` +8. `stop_docker_wsl.bat` +9. `docker/mysql/init/README.md` +10. `server.js` +11. `vite.config.ts` + +각 파일 역할은 다음과 같다. + +### 4.1 `.dockerignore` + +Docker build context에서 제외할 파일을 정의한다. + +주요 제외 대상은 다음과 같다. + +1. `node_modules` +2. `dist` +3. `build` +4. `.git` +5. `.env` +6. `uploads` +7. `*.xlsx` +8. `*.log` + +### 4.2 `Dockerfile.frontend` + +프런트 컨테이너 이미지 정의다. + +```dockerfile +FROM node:20-alpine + +WORKDIR /app + +COPY package*.json ./ +RUN npm ci + +COPY . . + +EXPOSE 8080 + +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] +``` + +이 이미지는 Vite dev server를 컨테이너에서 띄우기 위한 것이다. + +### 4.3 `Dockerfile.backend` + +백엔드 컨테이너 이미지 정의다. + +```dockerfile +FROM node:20-alpine + +WORKDIR /app + +COPY package*.json ./ +RUN npm ci + +COPY . . + +EXPOSE 3000 + +CMD ["npm", "run", "server"] +``` + +### 4.4 `docker-compose.yaml` + +전체 스택의 핵심 파일이다. + +현재 최종 구성은 다음 논리를 가진다. + +1. `db`는 MySQL 8 내부 DB다. +2. `db-bootstrap`은 외부 DB 데이터를 내부 DB로 1회 복제한다. +3. `backend`는 내부 `db`에 붙는다. +4. `frontend`는 `backend` 서비스명으로 프록시한다. + +### 4.5 `start_docker_wsl.ps1` + +Windows에서 WSL 경유로 Docker Compose를 안전하게 기동하는 진입점이다. + +핵심은 다음 두 가지다. + +1. 프로젝트 Windows 경로를 `wslpath`로 WSL 경로로 바꾼다. +2. 그 경로로 이동한 뒤 `docker compose up --build -d`를 수행한다. + +### 4.6 `stop_docker_wsl.ps1` + +같은 방식으로 WSL 내부에서 `docker compose down`을 수행해 스택을 안전하게 내린다. + +### 4.7 `start_docker_wsl.bat`, `stop_docker_wsl.bat` + +더블클릭 또는 간단 실행용 래퍼다. 내부적으로 PowerShell 스크립트를 호출한다. + +### 4.8 `server.js` + +중요 포인트는 다음 두 가지다. + +1. `dotenv.config();`를 사용한다. +2. `dotenv.config({ override: true })`를 사용하지 않는다. + +이 차이로 Compose 환경변수 `DB_HOST=db`가 `.env`보다 우선하도록 보장한다. + +### 4.9 `vite.config.ts` + +현재 프록시는 환경변수 기반으로 동작한다. + +```ts +const proxyTarget = process.env.VITE_DEV_PROXY_TARGET || 'http://localhost:3000'; +``` + +로컬 PC에서 직접 Vite를 띄우면 기본값 `http://localhost:3000`을 쓴다. +컨테이너에서는 Compose가 `http://backend:3000`을 주입한다. + +--- + +## 5. 현재 최종 `docker-compose.yaml` 구조 설명 + +아래는 실제 동작 관점에서 읽어야 할 핵심 내용이다. + +### 5.1 `db` 서비스 + +역할: + +1. 내부 MySQL 데이터 저장소 +2. 앱이 최종적으로 붙는 DB + +핵심 설정: + +1. 이미지: `mysql:8.0` +2. DB 이름: `itam` +3. 앱 계정: `itam_admin` +4. 데이터 볼륨: `itam_mysql_data` +5. healthcheck 사용 + +healthcheck는 `mysqladmin ping`으로 동작하며, `backend`와 `db-bootstrap`은 이 상태를 기다린다. + +### 5.2 `db-bootstrap` 서비스 + +역할: + +1. 외부 원본 DB에서 내부 `db`로 초기 데이터 복제 +2. 1회성 작업 후 종료 + +핵심 포인트: + +1. `.env`를 읽어 외부 DB 접속 정보를 가져온다. +2. 내부 `db`에 `asset_core` 테이블이 이미 존재하면 아무 것도 하지 않고 종료한다. +3. 그렇지 않으면 `mysqldump | mysql` 파이프라인으로 복제한다. +4. `restart: "no"` 이므로 정상 종료 후 반복 실행하지 않는다. + +또한 source DB와 target DB 변수는 분리돼 있다. + +1. source: `SOURCE_DB_*` +2. target: `TARGET_DB_*` + +이 구조로 외부 원본 DB 자격증명과 내부 컨테이너 DB 자격증명이 섞이지 않는다. + +### 5.3 `backend` 서비스 + +역할: + +1. Express API 제공 +2. 내부 `db`에 연결 +3. `/uploads` 정적 제공 + +핵심 포인트: + +1. `env_file: .env`를 유지하지만, +2. Compose `environment`에서 `DB_HOST=db`, `DB_PORT=3306`, `DB_USER=itam_admin`, `DB_PASS=itam1234`, `DB_NAME=itam`를 다시 지정한다. +3. `depends_on`은 `db` healthy와 `db-bootstrap` 성공 종료를 모두 기다린다. + +즉, 백엔드는 DB bootstrap이 끝난 뒤 시작한다. + +### 5.4 `frontend` 서비스 + +역할: + +1. Vite dev server 제공 +2. 브라우저 요청 `/api`, `/uploads`를 `backend`로 프록시 + +핵심 포인트: + +1. `VITE_DEV_PROXY_TARGET: http://backend:3000` +2. `CHOKIDAR_USEPOLLING: "true"` +3. `npm run dev -- --host 0.0.0.0` + +중요한 이유는 컨테이너 안의 `localhost`가 호스트의 `localhost`가 아니기 때문이다. + +--- + +## 6. 사전 준비 조건 + +이 저장소를 지금처럼 기동하려면 다음 전제가 필요하다. + +### 6.1 운영체제와 런타임 + +1. Windows +2. WSL2 Ubuntu 설치 및 실행 중 +3. Docker CLI가 WSL 내부에서 동작 가능 + +권장 확인 명령: + +```powershell +wsl -l -v +wsl sh -lc "docker --version" +``` + +### 6.2 `.env` 파일 + +현재 최종 구조는 "첫 기동 시 외부 DB에서 내부 DB로 bootstrap" 하는 방식이므로 `.env`가 반드시 필요하다. + +최소한 다음 값은 외부 원본 DB를 가리켜야 한다. + +```env +DB_HOST= +DB_PORT=3306 +DB_USER= +DB_PASS= +DB_NAME=itam +``` + +주의: + +1. `.env`는 `db-bootstrap`이 외부 원본 DB에 접속할 때 사용한다. +2. `backend`는 최종적으로 내부 `db` 컨테이너를 쓰므로, 런타임에서는 Compose `environment`가 우선한다. + +### 6.3 한글 경로 주의 + +현재 프로젝트 경로는 한글과 공백을 포함한다. + +```text +c:\Users\user\Desktop\안건 파일\itam +``` + +이 때문에 Docker 관련 명령은 수동으로 경로를 조립하지 말고, `start_docker_wsl.ps1` / `stop_docker_wsl.ps1`을 우선 사용해야 한다. + +--- + +## 7. 첫 기동 절차 + +이 절차는 "Git에서 소스를 받은 뒤 처음 올리는 경우" 기준이다. + +### 7.1 저장소 준비 + +1. 저장소를 받는다. +2. `.env`가 올바른 외부 원본 DB를 가리키는지 확인한다. +3. WSL이 켜져 있는지 확인한다. + +### 7.2 권장 실행 방법 + +Windows PowerShell에서 프로젝트 루트로 이동한 뒤 아래 중 하나를 사용한다. + +방법 A: + +```powershell +.\start_docker_wsl.ps1 +``` + +방법 B: + +```powershell +.\start_docker_wsl.bat +``` + +### 7.3 내부 실행 순서 + +스크립트는 내부적으로 다음 순서로 동작한다. + +1. 현재 Windows 경로를 WSL 경로로 변환한다. +2. WSL 동작 여부를 확인한다. +3. WSL 내부 Docker 사용 가능 여부를 확인한다. +4. `docker compose up --build -d`를 수행한다. + +### 7.4 기대되는 컨테이너 순서 + +정상이라면 다음 순서로 올라온다. + +1. `itam-db` +2. `itam-db-bootstrap` +3. `itam-backend` +4. `itam-frontend` + +`itam-db-bootstrap`은 정상이라면 최종 상태가 `Exited (0)`이어야 한다. + +--- + +## 8. 첫 기동 후 검증 절차 + +기동 후에는 반드시 아래 검증을 수행한다. + +### 8.1 컨테이너 상태 확인 + +```powershell +wsl sh -lc "docker ps -a --format 'table {{.Names}}\t{{.Status}}' | grep itam" +``` + +정상 기대 상태: + +1. `itam-db` -> `Up ... (healthy)` +2. `itam-db-bootstrap` -> `Exited (0)` +3. `itam-backend` -> `Up` +4. `itam-frontend` -> `Up` + +### 8.2 백엔드 API 직접 확인 + +```powershell +Invoke-WebRequest -Uri http://localhost:3000/api/assets/master -UseBasicParsing | Select-Object -ExpandProperty StatusCode +``` + +정상 기대값: + +1. `200` + +### 8.3 프런트 경유 API 확인 + +```powershell +Invoke-WebRequest -Uri http://localhost:8080/api/assets/master -UseBasicParsing | Select-Object -ExpandProperty StatusCode +``` + +정상 기대값: + +1. `200` + +### 8.4 데이터가 실제로 들어왔는지 확인 + +```powershell +wsl sh -lc "docker exec itam-db mysql -uitam_admin -pitam1234 -D itam -e 'SHOW TABLES' | head -n 20" +``` + +정상이라면 아래와 같은 테이블들이 보여야 한다. + +1. `asset_core` +2. `asset_remote` +3. `asset_spec` +4. `asset_location` +5. `asset_history` +6. `asset_software_perpetual` +7. `asset_software_subscription` +8. `hardware_components_master` +9. `job_spec_standards` + +### 8.5 브라우저 화면 확인 + +브라우저에서 아래 주소를 연다. + +```text +http://localhost:8080 +``` + +목록/대시보드 데이터가 보이면 화면까지 정상 연결된 것이다. + +--- + +## 9. 재기동 절차 + +코드만 수정됐고 DB는 유지하고 싶다면 다음처럼 하면 된다. + +### 9.1 스택 종료 + +```powershell +.\stop_docker_wsl.ps1 +``` + +또는 + +```powershell +.\stop_docker_wsl.bat +``` + +### 9.2 스택 재기동 + +```powershell +.\start_docker_wsl.ps1 +``` + +이 경우 `itam_mysql_data` 볼륨이 유지되므로, `db-bootstrap`은 내부 DB에 `asset_core`가 이미 있음을 감지하고 빠르게 종료한다. + +--- + +## 10. DB를 완전히 다시 초기화하는 절차 + +외부 원본 DB에서 다시 처음부터 내부 DB를 복제하고 싶다면, MySQL 볼륨을 제거해야 한다. + +### 10.1 스택 중지 + +```powershell +.\stop_docker_wsl.ps1 +``` + +### 10.2 MySQL 데이터 볼륨 삭제 + +```powershell +wsl sh -lc "docker volume rm -f itam_itam_mysql_data" +``` + +### 10.3 다시 시작 + +```powershell +.\start_docker_wsl.ps1 +``` + +이때 `db-bootstrap`이 외부 DB에서 내부 DB로 전체를 다시 복제한다. + +--- + +## 11. 현재 구조에서 꼭 알아야 할 설계 포인트 + +### 11.1 `server.js`의 `dotenv.config()` 변경 이유 + +백엔드가 내부 DB로 붙게 하려면 Compose가 준 환경변수가 `.env`보다 우선해야 한다. + +만약 아래처럼 `override: true`를 쓰면 안 된다. + +```js +dotenv.config({ override: true }); +``` + +이렇게 되면 내부 `db`가 아니라 `.env`의 외부 DB로 다시 붙을 수 있다. + +현재는 아래가 맞다. + +```js +dotenv.config(); +``` + +### 11.2 왜 `docker-entrypoint-initdb.d` 기반 dump 파일을 안 쓰는가 + +처음에는 이 방식을 시도했지만, 실제 데이터의 긴 문자열/깨진 텍스트 때문에 import가 line 97에서 중단됐다. + +그래서 현재는 더 안정적인 아래 방식을 쓴다. + +1. 외부 DB에서 `mysqldump` +2. 파이프로 내부 `db`에 즉시 `mysql` import + +즉, 파일 중간 생성물을 신뢰하지 않는 구조다. + +### 11.3 왜 프런트 프록시 타깃을 환경변수화했는가 + +로컬 직접 실행과 컨테이너 실행의 네트워크 기준이 다르기 때문이다. + +1. 로컬 직접 실행: `localhost:3000`이 맞다. +2. 컨테이너 내부 실행: `backend:3000`이 맞다. + +그래서 `vite.config.ts`는 둘 다 수용할 수 있게 작성됐다. + +--- + +## 12. 문제 발생 시 진단 순서 + +이 프로젝트에서는 문제를 아래 순서로 자르면 가장 빠르다. + +### 12.1 브라우저 화면에 데이터가 없을 때 + +먼저 다음 둘을 분리해서 본다. + +1. `http://localhost:3000/api/assets/master` +2. `http://localhost:8080/api/assets/master` + +판단 기준: + +1. `3000`은 200이고 `8080`만 실패면 프런트 프록시 문제다. +2. 둘 다 실패면 백엔드 또는 DB 문제다. + +### 12.2 DB bootstrap이 성공했는지 확인할 때 + +```powershell +wsl sh -lc "docker ps -a --format 'table {{.Names}}\t{{.Status}}' | grep itam" +``` + +여기서 `itam-db-bootstrap`이 `Exited (0)`인지 본다. + +### 12.3 내부 DB에 실제 데이터가 있는지 확인할 때 + +```powershell +wsl sh -lc "docker exec itam-db mysql -uitam_admin -pitam1234 -D itam -e 'SHOW TABLES'" +``` + +### 12.4 백엔드 로그 확인 + +```powershell +wsl sh -lc "docker logs --tail=200 itam-backend" +``` + +### 12.5 DB 로그 확인 + +```powershell +wsl sh -lc "docker logs --tail=200 itam-db" +``` + +### 12.6 프런트 로그 확인 + +```powershell +wsl sh -lc "docker logs --tail=200 itam-frontend" +``` + +--- + +## 13. 자주 나올 수 있는 장애와 해석 + +### 13.1 `docker` 명령이 PowerShell에서 안 보임 + +의미: + +1. Windows 셸이 아니라 WSL에서 Docker를 쓰는 환경이다. + +대응: + +1. `start_docker_wsl.ps1` 사용 + +### 13.2 `asset_core` 테이블 없음 + +의미: + +1. 내부 DB 초기화가 안 됐거나 bootstrap이 안 끝났다. + +대응: + +1. `db-bootstrap` 상태 확인 +2. `.env` 외부 DB 접속 정보 확인 +3. 필요하면 볼륨 삭제 후 재초기화 + +### 13.3 `3000` API는 되는데 화면은 비어 있음 + +의미: + +1. DB는 정상이고 프런트 프록시 또는 화면 렌더링 문제다. + +대응: + +1. `8080/api/assets/master` 상태 먼저 확인 + +### 13.4 `db-bootstrap`가 실패 종료함 + +의미 후보: + +1. `.env` 외부 DB 접속 정보 오류 +2. 외부 DB 네트워크 접근 불가 +3. 외부 계정 권한 문제 + +대응: + +1. `docker logs itam-db-bootstrap` 확인 + +--- + +## 14. 현재 최종 검증 완료 상태 + +이 저장소는 아래 상태까지 검증이 완료됐다. + +1. WSL2 Ubuntu에서 Docker 실행 가능 +2. `start_docker_wsl.ps1`로 전체 스택 기동 가능 +3. `db` 컨테이너 healthcheck 통과 +4. `db-bootstrap`가 외부 DB에서 내부 DB로 데이터 복제 후 `Exited (0)` 종료 +5. `backend`가 내부 `db`를 사용해 API 응답 가능 +6. `frontend`가 `backend`를 프록시해 8080 기준 화면/API 동작 가능 +7. 내부 MySQL에 실데이터 적재 확인 + +즉, 현재 Git에 올라간 상태만으로도 WSL2와 외부 원본 DB 정보만 있으면 지금과 같은 수준의 Docker 실행 재현이 가능하다. + +--- + +## 15. 현재 구조의 한계와 다음 단계 + +현재 구조는 충분히 시연 가능하고 개발 재현도 가능하지만, 다음은 아직 별도 작업이 필요하다. + +1. 운영형 정적 배포 구조 전환 +2. 외부 DB 없이도 완전 독립 실행 가능한 정식 dump/backup 체계 +3. `.env.example` 정리 +4. DB bootstrap 전용 계정/권한 최소화 +5. 장기적으로 `map_config.json` 파일 저장 정책 정리 + +하지만 "현재 저장소만으로 지금과 같은 Docker 실행 상태 재현"이라는 목표는 이미 충족한다. + +--- + +## 16. 빠른 실행 요약 + +가장 짧게 요약하면 다음 순서다. + +1. `.env`에 외부 원본 MySQL 접속 정보를 넣는다. +2. WSL2 Ubuntu와 WSL 내부 Docker가 살아 있는지 확인한다. +3. `start_docker_wsl.ps1`를 실행한다. +4. `itam-db-bootstrap`가 `Exited (0)`인지 확인한다. +5. `http://localhost:3000/api/assets/master`와 `http://localhost:8080/api/assets/master`가 모두 200인지 확인한다. +6. 브라우저에서 `http://localhost:8080`을 열어 데이터가 보이는지 확인한다. + +이 순서대로 진행하면 현재 저장소 기준 Dockerized ITAM 시연 환경을 재현할 수 있다. + +--- + +## 17. 2026-06-16 최신 정정 + +이 문서의 상단 본문은 한동안 사용했던 `내부 db + db-bootstrap` 구조를 기준으로 작성됐다. 하지만 오늘 기준 현재 저장소의 실제 `docker-compose.yaml`은 다시 `무상태 앱 컨테이너 + 외부 DB` 구조로 되돌아가 있다. + +따라서 현재 시점의 정답 아키텍처는 아래다. + +1. `backend` 컨테이너 +2. `frontend` 컨테이너 +3. 외부 MySQL DB + +현재는 더 이상 아래 항목이 없다. + +1. `db` 서비스 없음 +2. `db-bootstrap` 서비스 없음 +3. `itam_mysql_data` 볼륨 없음 + +### 17.1 현재 실제 `docker-compose.yaml` 기준 backend 동작 + +현재 backend는 `.env`의 외부 DB 접속 정보를 그대로 사용한다. + +즉, 아래 환경변수 매핑이 현재 기준이다. + +1. `DB_HOST: ${DB_HOST}` +2. `DB_PORT: ${DB_PORT}` +3. `DB_USER: ${DB_USER}` +4. `DB_PASS: ${DB_PASS}` +5. `DB_NAME: ${DB_NAME}` + +`PORT: 3000`만 Compose에서 고정한다. + +### 17.2 현재 실제 기동 구조 + +현재 스택 기동 순서는 단순하다. + +1. `backend` 기동 +2. `frontend` 기동 +3. backend는 외부 DB에 직접 접속 +4. frontend는 `http://backend:3000`으로 프록시 + +즉, 현재는 DB 컨테이너 초기화 단계나 bootstrap 단계가 존재하지 않는다. + +### 17.3 현재 기준 첫 실행 체크리스트 + +오늘 기준으로는 아래 순서가 맞다. + +1. `.env`에 외부 DB 접속 정보 입력 +2. `start_docker_wsl.ps1` 또는 `start_docker_wsl.bat` 실행 +3. `http://localhost:3000/api/assets/master`가 200인지 확인 +4. `http://localhost:8080/api/assets/master`가 200인지 확인 +5. 브라우저에서 `http://localhost:8080` 접속 후 데이터 표시 확인 + +### 17.4 이 문서에서 현재 유효한 부분과 과거 이력 부분 + +현재도 그대로 유효한 내용은 아래다. + +1. WSL2 기반 실행 방식 +2. `start_docker_wsl.ps1` / `stop_docker_wsl.ps1` 사용 방식 +3. `server.js`에서 Compose 환경변수가 `.env`보다 우선되도록 `dotenv.config()`를 유지해야 한다는 점 +4. `vite.config.ts`에서 프록시 타깃을 환경변수화해야 한다는 점 + +현재는 과거 이력으로만 읽어야 하는 내용은 아래다. + +1. 내부 `db` 서비스 설명 +2. `db-bootstrap` 설명 +3. `itam_mysql_data` 볼륨 설명 +4. 내부 DB 재초기화 절차 +5. 내부 테이블 확인 절차 + +### 17.5 현재 최종 한 줄 요약 + +오늘 날짜 기준 현재 저장소의 실사용 Compose 구조는 `frontend + backend + external DB`이며, 이전의 내부 DB/bootstrap 구조는 역사적으로 한 번 사용했던 임시 해결책으로만 남아 있다. \ No newline at end of file diff --git a/doc_readme3.md b/doc_readme3.md new file mode 100644 index 0000000..a0a4ed9 --- /dev/null +++ b/doc_readme3.md @@ -0,0 +1,730 @@ +# ITAM Linux 운영 배포 가이드 + +## 1. 문서 목적 + +이 문서는 현재 ITAM 저장소를 기준으로 Linux 환경에서 운영 배포하는 방법을 정리한 가이드다. + +핵심 전제는 아래와 같다. + +1. 저장소 구조를 크게 재편하지 않는다. +2. 현재 워크스페이스 기준 파일 구조를 그대로 활용한다. +3. `docker-compose.test.yaml`과 `docker-compose.prod.yaml` 모두 현재 저장소 루트 기준으로 동작한다. +4. DB는 Docker 내부가 아니라 외부 MySQL을 사용한다. + +즉, 이 문서는 `/srv/itam` 같은 별도 운영 디렉터리 구조를 강제하는 문서가 아니라, 현재 저장소 구조를 기준으로 운영 전환하는 방법을 설명한다. + +--- + +## 2. 현재 운영 관련 파일 + +현재 저장소에서 운영 전환과 직접 관련된 파일은 아래와 같다. + +1. `docker-compose.yaml` +2. `docker-compose.test.yaml` +3. `docker-compose.prod.yaml` +4. `Dockerfile.frontend.prod` +5. `Dockerfile.backend.prod` +6. `docker/nginx/default.conf` +7. `docker/frontend/default.conf` +8. `.env.example` +9. `server.js` + +각 파일의 역할은 아래와 같다. + +1. `docker-compose.yaml`: 개발 재현용 구성 +2. `docker-compose.test.yaml`: 운영형 Dockerfile과 reverse proxy 구조를 로컬에서 검증하는 테스트용 구성 +3. `docker-compose.prod.yaml`: 현재 저장소 기준 운영용 구성 +4. `Dockerfile.frontend.prod`: 프런트 정적 빌드 및 Nginx 서빙 이미지 정의 +5. `Dockerfile.backend.prod`: 백엔드 API 운영 이미지 정의 +6. `docker/nginx/default.conf`: reverse proxy 설정 +7. `docker/frontend/default.conf`: frontend 컨테이너 내부 정적 파일 서빙 설정 +8. `.env.example`: 운영/테스트 환경변수 템플릿 +9. `server.js`: `/health`, `/ready` 엔드포인트 포함 + +--- + +## 3. 현재 기준 운영 아키텍처 + +현재 구조에서의 요청 흐름은 아래와 같다. + +1. 외부 요청은 Nginx 컨테이너로 들어온다. +2. `/` 요청은 frontend 컨테이너로 전달된다. +3. `/api/`와 `/uploads/` 요청은 backend 컨테이너로 전달된다. +4. backend는 외부 MySQL에 연결한다. +5. `uploads`, `map_config.json`, `.env`, 로그는 현재 저장소 기준 상대 경로를 사용한다. + +```mermaid +flowchart LR + U["User Browser"] --> RP["Reverse Proxy Nginx 80"] + RP -->|root| FE["Frontend Container Nginx Static 80"] + RP -->|api| BE["Backend Container Node 3000"] + RP -->|uploads| BE + BE --> DB["External MySQL 3306"] + BE --> UP["./uploads"] + BE --> CFG["./map_config.json"] + BE --> ENV["./.env"] + linkStyle default stroke:#d32f2f,stroke-width:2px; +``` + +현재 저장소 기준 파일/컨테이너 관계는 아래와 같다. + +```mermaid +flowchart TB + subgraph REPO["Current Repository Root"] + ENV[".env"] + UP["uploads/"] + MAP["map_config.json"] + LOGS["logs/nginx/"] + CONF["docker/nginx/default.conf"] + end + + subgraph CTR["Containers"] + NGINX["itam-nginx"] + FRONT["itam-frontend"] + BACK["itam-backend"] + end + + DB["External MySQL"] + + CONF --> NGINX + LOGS --> NGINX + ENV --> BACK + UP --> BACK + MAP --> BACK + NGINX --> FRONT + NGINX --> BACK + BACK --> DB + linkStyle default stroke:#d32f2f,stroke-width:2px; +``` + +--- + +## 4. 개발용, 테스트용, 운영용 차이 + +### 4.1 `docker-compose.yaml` + +용도: + +1. 개발 재현 +2. 소스 수정과 빠른 확인 +3. Vite dev server 기반 실행 + +특징: + +1. bind mount 중심 +2. 프런트는 개발 서버 기반 +3. 운영 배포보다는 개발 생산성에 초점 + +### 4.2 `docker-compose.test.yaml` + +용도: + +1. 운영형 Dockerfile 테스트 +2. reverse proxy 동작 테스트 +3. 로컬/WSL에서 8080 포트 검증 + +특징: + +1. frontend/backend를 `build`로 생성 +2. nginx는 8080 포트로 노출 +3. 현재 저장소 상대 경로를 그대로 사용 + +### 4.3 `docker-compose.prod.yaml` + +용도: + +1. 현재 저장소 구조 기준 운영 배포 +2. 운영 모드 환경변수와 health check 사용 +3. 현재 구조를 바꾸지 않고 운영 전환 + +특징: + +1. frontend/backend를 `build + image` 방식으로 정의 +2. `.env`, `uploads`, `map_config.json`, `logs/nginx`를 현재 저장소 기준 상대 경로로 사용 +3. nginx는 80 포트를 사용 +4. backend는 `NODE_ENV=production`으로 실행 + +--- + +## 5. 운영 파일 구조 기준 + +현재 운영 배포는 저장소 루트를 기준으로 아래 구조를 전제로 한다. + +```text +itam/ + .env + .env.example + docker-compose.prod.yaml + docker-compose.test.yaml + Dockerfile.frontend.prod + Dockerfile.backend.prod + map_config.json + uploads/ + logs/ + nginx/ + docker/ + nginx/ + default.conf + frontend/ + default.conf +``` + +운영에서 실제로 중요한 경로는 아래 네 가지다. + +1. `./.env` +2. `./uploads` +3. `./map_config.json` +4. `./logs/nginx` + +즉, 현재 구조를 유지하려면 이 경로들이 항상 함께 관리되어야 한다. + +--- + +## 6. 운영 환경변수 정책 + +현재 기준 운영 환경변수는 저장소 루트의 `.env`를 사용한다. + +`.env.example` 기준 예시는 아래와 같다. + +```env +DB_HOST=172.16.8.151 +DB_PORT=3306 +DB_USER=itam_admin +DB_PASS=change-this +DB_NAME=itam + +NODE_ENV=production +PORT=3000 +LOG_LEVEL=info +``` + +운영 원칙은 아래와 같다. + +1. 실제 운영 비밀번호가 들어간 `.env`는 Git에 올리지 않는다. +2. `.env.example`은 템플릿으로만 사용한다. +3. 운영 서버에서는 `.env` 파일 권한을 제한한다. +4. 운영 DB 계정과 개발 DB 계정은 분리한다. + +권장 권한 예시는 아래와 같다. + +```bash +chmod 600 .env +``` + +--- + +## 7. Frontend 운영 이미지 기준 + +`Dockerfile.frontend.prod`는 multi-stage build를 사용한다. + +구성은 아래와 같다. + +1. builder 단계에서 `npm ci` 수행 +2. `npm run build` 수행 +3. 결과물을 Nginx 이미지에 복사 +4. `docker/frontend/default.conf`로 정적 파일 서빙 + +운영 관점에서의 장점은 아래와 같다. + +1. runtime 이미지에 build 결과물만 포함된다. +2. dev server 없이 정적 파일만 제공한다. +3. frontend 컨테이너 자체도 health check 가능하다. + +--- + +## 8. Backend 운영 이미지 기준 + +`Dockerfile.backend.prod`는 아래 기준으로 작성되어 있다. + +1. `NODE_ENV=production` +2. production dependency만 설치 +3. `appuser` 비루트 사용자 사용 +4. `dumb-init` 사용 +5. `/health` health check 사용 + +backend 컨테이너는 아래 자원을 사용한다. + +1. `./.env` +2. `./uploads` +3. `./map_config.json` +4. 외부 MySQL + +--- + +## 9. Reverse Proxy 기준 + +`docker/nginx/default.conf`는 현재 아래처럼 동작한다. + +1. `/` -> `frontend:80` +2. `/api/` -> `backend:3000` +3. `/uploads/` -> `backend:3000` + +추가로 아래 설정을 포함한다. + +1. 기본 보안 헤더 +2. access/error 로그 +3. gzip 설정 +4. health endpoint + +중요한 점은, 현재 운영 기준에서 외부 사용자가 직접 frontend 컨테이너에 붙는 것이 아니라 반드시 nginx를 통해 들어간다는 점이다. + +--- + +## 10. 현재 기준 운영 배포 절차 + +### 10.1 사전 점검 + +아래 항목을 먼저 확인한다. + +1. `.env` 파일 존재 여부 +2. `uploads/` 디렉터리 존재 여부 +3. `logs/nginx/` 디렉터리 존재 여부 +4. `map_config.json` 존재 여부 +5. 외부 DB 접근 가능 여부 + +예시: + +```bash +ls -la .env +ls -la uploads +ls -la logs/nginx +ls -la map_config.json +``` + +### 10.2 Compose 검증 + +```bash +docker compose -f docker-compose.prod.yaml config +``` + +### 10.3 운영 기동 + +```bash +docker compose -f docker-compose.prod.yaml up -d --build +docker compose -f docker-compose.prod.yaml ps +``` + +### 10.4 운영 중지 + +```bash +docker compose -f docker-compose.prod.yaml down +``` + +### 10.5 운영/배포 분기 흐름 + +현재 운영 반영은 자동 push 배포가 아니라, Gitea에 올라간 커밋을 기준으로 수동 workflow를 실행하는 방식이다. + +즉 아래 원칙으로 이해하면 된다. + +1. 로컬 수정본을 서버에 직접 복사하지 않는다. +2. 반드시 Gitea에 올라간 커밋을 기준으로 배포한다. +3. 운영 반영은 `.gitea/workflows/itam_production_deploy.yml` 수동 실행으로 진행한다. +4. 실패 후 재배포는 실패 지점에 따라 수정 위치가 달라진다. + +운영 반영은 크게 세 상황으로 나뉜다. + +1. 최초 운영 서버 구축 후 첫 배포 +2. 코드 수정 후 일반 재배포 +3. 배포 실패 또는 검증 실패 후 재배포 + +```mermaid +flowchart TD + START["배포 필요 발생"] --> CASE{"어떤 상황인가?"} + + CASE -->|초기 구축| INIT["초기 운영 배포 준비"] + CASE -->|수정 반영| CHANGE["수정 후 재배포 준비"] + CASE -->|실패 후 재시도| RETRY["실패 원인 분석 후 재배포 준비"] + + INIT --> INIT1["운영 서버 Docker / compose 확인"] + INIT1 --> INIT2["Gitea Variables / Secrets 등록"] + INIT2 --> INIT3["map_config.json / uploads 초기 데이터 준비"] + INIT3 --> MANUAL["Gitea에서 수동 배포 workflow 실행"] + + CHANGE --> CHANGE1["로컬 수정 및 테스트"] + CHANGE1 --> CHANGE2["Gitea 커밋 / push"] + CHANGE2 --> CHANGE3["Code Check / Docker Build Check 통과"] + CHANGE3 --> MANUAL + + RETRY --> RETRY1{"어디서 실패했는가?"} + RETRY1 -->|코드 체크 실패| FIX1["코드 또는 설정 수정"] + RETRY1 -->|배포 단계 실패| FIX2["서버 / 변수 / 권한 / 네트워크 수정"] + RETRY1 -->|Smoke Check 실패| FIX3["앱 기동 상태 / 프록시 / DB 상태 수정"] + FIX1 --> CHANGE2 + FIX2 --> MANUAL + FIX3 --> MANUAL + + MANUAL --> BACKUP["기존 운영 상태가 있으면 배포 전 백업"] + BACKUP --> DEPLOY["운영 서버 반영 수행"] + DEPLOY --> RESULT{"최종 검증 통과?"} + RESULT -->|예| DONE["운영 반영 완료"] + RESULT -->|아니오| RETRY + linkStyle default stroke:#d32f2f,stroke-width:2px; +``` + +### 10.6 최초 운영 배포 플로우 + +최초 배포에서는 코드보다 운영 환경 준비가 먼저다. + +순서는 아래와 같다. + +1. 운영 서버에 Docker Engine과 `docker compose`를 설치한다. +2. 운영 서버에서 Gitea 저장소에 접근 가능한 SSH 키를 준비한다. +3. Gitea repository Variables / Secrets를 등록한다. +4. `PROD_DEPLOY_PATH` 경로를 확정한다. +5. `PROD_BACKUP_ROOT` 경로를 `PROD_DEPLOY_PATH` 바깥으로 확정한다. +6. `map_config.json`, `uploads/` 초기 데이터를 준비한다. +7. Gitea에서 `itam_production_deploy.yml`을 수동 실행한다. +8. 배포 후 `docker compose ps`, `/health`, `/`, `/ready`를 확인한다. + +즉 최초 배포는 아래 순서다. + +```text +서버 준비 완료 +-> Gitea 변수 / 시크릿 등록 완료 +-> 백업 경로 확정 완료 +-> 초기 데이터 준비 완료 +-> 수동 배포 실행 +``` + +### 10.7 수정 후 일반 재배포 플로우 + +일반적인 수정 반영은 아래 흐름이다. + +1. 개발자가 로컬에서 코드 또는 설정을 수정한다. +2. 로컬에서 필요한 테스트를 수행한다. +3. 변경사항을 Gitea에 커밋 후 push 한다. +4. `itam_code_check.yml`이 빌드와 compose 문법을 검사한다. +5. `itam_docker_build_check.yml`이 운영용 이미지 빌드 가능 여부를 검사한다. +6. 두 검증이 통과하면 운영자가 Gitea에서 `itam_production_deploy.yml`을 수동 실행한다. +7. 기존 운영 상태가 있으면 배포 전 백업을 먼저 수행한다. +8. 운영 서버가 최신 커밋으로 동기화되고 컨테이너가 다시 올라온다. +9. smoke check 통과 여부를 확인한다. + +```mermaid +flowchart LR + DEV["로컬 수정"] --> TEST["로컬 확인"] + TEST --> PUSH["커밋 / push"] + PUSH --> CODE["ITAM Code Check"] + CODE --> BUILD["ITAM Docker Build Check"] + BUILD --> GATE{"검증 통과?"} + GATE -->|예| RUN["Gitea에서 수동 배포 실행"] + GATE -->|아니오| FIX["로컬 수정 후 재커밋"] + FIX --> PUSH + RUN --> BACKUP["배포 전 백업"] + BACKUP --> PROD["운영 서버 배포"] + PROD --> SMOKE{"Smoke Check 통과?"} + SMOKE -->|예| OK["배포 완료"] + SMOKE -->|아니오| FIXDEPLOY["원인 수정 후 재배포"] + FIXDEPLOY --> RUN + linkStyle default stroke:#d32f2f,stroke-width:2px; +``` + +### 10.8 수동 배포 workflow 내부 실행 순서 + +Gitea에서 `itam_production_deploy.yml`을 수동 실행하면 내부적으로는 아래 순서로 진행된다. + +1. SSH agent를 설정한다. +2. 필수 Variables / Secrets가 모두 있는지 확인한다. +3. 운영용 `.env.deploy` 파일을 생성한다. +4. 운영 서버에 접속한다. +5. `PROD_DEPLOY_PATH`를 생성한다. +6. 기존 운영 상태가 있으면 `make predeploy-backup`을 실행한다. +7. 저장소를 clone 또는 fetch 한다. +8. 선택한 브랜치의 최신 커밋으로 checkout, reset, clean 한다. +9. `uploads`, `logs/nginx` 디렉토리를 준비한다. +10. `.env.deploy`를 서버의 `.env`로 복사한다. +11. `docker compose -f docker-compose.prod.yaml config`를 수행한다. +12. `docker compose -f docker-compose.prod.yaml up -d --build`를 수행한다. +13. `docker compose ps`를 확인한다. +14. `/health`, `/`, backend `/ready` smoke check를 수행한다. + +```mermaid +flowchart TD + A["수동 배포 시작"] --> B["SSH agent 설정"] + B --> C["Variables / Secrets 검증"] + C --> D{"필수 값 누락 여부"} + D -->|예| E["즉시 실패 후 설정 보완"] + D -->|아니오| F[".env.deploy 생성"] + F --> G["운영 서버 SSH 접속"] + G --> H["배포 경로 생성"] + H --> I["기존 운영 상태가 있으면 make predeploy-backup"] + I --> J["git clone 또는 fetch"] + J --> K["지정 브랜치 checkout / reset / clean"] + K --> L["uploads / logs/nginx 준비"] + L --> M[".env 업로드 및 권한 설정"] + M --> N["compose config 검증"] + N --> O{"compose config 성공?"} + O -->|아니오| P["설정 수정 후 재실행"] + O -->|예| Q["compose up -d --build"] + Q --> R["docker compose ps 확인"] + R --> S["/health, /, /ready smoke check"] + S --> T{"smoke check 성공?"} + T -->|예| U["운영 배포 완료"] + T -->|아니오| V["원인 분석 후 재배포"] + linkStyle default stroke:#d32f2f,stroke-width:2px; +``` + +### 10.9 실패 후 검증 및 재배포 플로우 + +실패가 났다고 해서 같은 방식으로 바로 다시 배포하면 안 된다. + +실패 지점별 판단은 아래처럼 나눈다. + +1. Code Check 실패: TypeScript, build, compose 문법 문제를 먼저 수정한다. +2. Docker Build Check 실패: Dockerfile, 정적 자산 복사, 운영 빌드 컨텍스트 문제를 수정한다. +3. Deploy 단계 실패: SSH, Gitea 변수, 서버 권한, 경로, 백업 경로, git 접근, Docker 권한을 수정한다. +4. Smoke Check 실패: Nginx 프록시, backend readiness, 외부 DB 연결, 앱 런타임 오류를 수정한다. + +즉 재배포 전 판단 기준은 아래와 같다. + +```text +CI 실패 -> 로컬 코드 / 설정 수정 후 재커밋 +배포 실패 -> 서버 환경 또는 배포 설정 수정 후 수동 재실행 +Smoke Check 실패 -> 앱 / 프록시 / DB 상태 수정 후 수동 재실행 +``` + +운영 관점에서는 아래 순서를 지키는 것이 안전하다. + +1. 실패 지점 확인 +2. 원인 수정 +3. 같은 실패가 다시 나는지 좁은 범위로 재검증 +4. 그 다음에만 수동 배포 재실행 + +--- + +## 11. 테스트 배포 절차 + +운영형 구성을 먼저 검증하려면 아래처럼 진행한다. + +```bash +docker compose -f docker-compose.test.yaml up -d --build +docker compose -f docker-compose.test.yaml ps +``` + +접속 기준은 아래와 같다. + +1. `http://localhost:8080` -> nginx reverse proxy +2. `http://localhost:3000/health` -> backend health 확인 + +테스트용은 운영과 매우 유사하지만, 외부 노출 포트와 일부 실행 목적이 다르다. + +--- + +## 12. Health Check 및 상태 판정 + +backend에는 아래 두 엔드포인트가 있다. + +1. `/health` +2. `/ready` + +판정 기준은 아래와 같다. + +1. `/health = 200`, `/ready = 200`: 정상 서비스 가능 상태 +2. `/health = 200`, `/ready = 503`: 프로세스는 살아 있으나 DB 또는 외부 의존성 미준비 + +확인 예시는 아래와 같다. + +```bash +curl http://localhost:3000/health +curl http://localhost:3000/ready +``` + +--- + +## 13. 운영 점검 체크리스트 + +### 13.1 애플리케이션 + +1. `docker compose -f docker-compose.prod.yaml config` 통과 여부 +2. frontend 이미지 빌드 성공 여부 +3. backend 이미지 빌드 성공 여부 +4. 모든 컨테이너가 `Up` 상태인지 +5. 메인 화면이 정상 렌더링되는지 +6. 데이터 조회가 정상 동작하는지 + +### 13.2 데이터 및 파일 + +1. `uploads/`에 쓰기 가능한지 +2. `map_config.json`을 backend가 읽을 수 있는지 +3. `.env` 파일 권한이 적절한지 +4. `logs/nginx/`에 로그가 쌓이는지 + +### 13.3 네트워크 + +1. 외부 DB 접근 가능한지 +2. nginx에서 backend upstream 연결이 되는지 +3. `/api` 요청이 정상 응답하는지 + +--- + +## 14. 로그 및 장애 대응 + +기본 점검 명령은 아래와 같다. + +```bash +docker compose -f docker-compose.prod.yaml ps +docker compose -f docker-compose.prod.yaml logs --tail=200 nginx +docker compose -f docker-compose.prod.yaml logs --tail=200 backend +docker compose -f docker-compose.prod.yaml logs --tail=200 frontend +``` + +장애 확인 순서는 아래가 좋다. + +1. 컨테이너가 살아 있는지 +2. nginx가 frontend/backend로 프록시하는지 +3. backend가 DB에 붙는지 +4. 업로드/설정 파일 권한 문제는 없는지 + +추가 확인 예시는 아래와 같다. + +```bash +curl -I http://localhost/ +curl http://localhost:3000/health +curl http://localhost:3000/ready +``` + +--- + +## 15. 백업 기준 + +현재 구조 기준 최소 백업 대상은 아래와 같다. + +1. `.env` +2. `uploads/` +3. `map_config.json` +4. 외부 MySQL 데이터베이스 + +현재 저장소에는 운영 백업을 직접 실행할 수 있도록 `Makefile`과 `scripts/backup.sh`가 추가되어 있다. + +기본 원칙은 아래와 같다. + +1. DB dump와 런타임 파일 백업을 분리해서 실행할 수 있어야 한다. +2. 기본 백업 산출물은 `backups/` 디렉터리에 쌓는다. +3. DB 접속 정보는 `.env`를 기준으로 읽는다. +4. 오래된 백업 파일은 보존 주기에 따라 정리한다. + +백업 실행 흐름은 아래와 같다. + +```mermaid +flowchart TD + START["운영 백업 실행"] --> TARGET{"무엇을 백업할 것인가?"} + TARGET -->|DB| DB["make db-dump"] + TARGET -->|운영 파일| FILES["make files-backup"] + TARGET -->|전체| FULL["make full-backup"] + DB --> OUT1["backups/db/*.sql.gz"] + FILES --> OUT2["backups/files/*.tar.gz"] + FULL --> OUT1 + FULL --> OUT2 + OUT1 --> CLEAN["make cleanup-backups"] + OUT2 --> CLEAN + linkStyle default stroke:#d32f2f,stroke-width:2px; +``` + +### 15.1 Make 명령 기준 + +사용 가능한 기본 명령은 아래와 같다. + +```bash +make db-dump +make files-backup +make full-backup +make cleanup-backups +``` + +각 명령의 역할은 아래와 같다. + +1. `make db-dump`: `.env` 기준 MySQL dump를 `backups/db/`에 `.sql.gz`로 저장 +2. `make files-backup`: `.env`, `uploads/`, `map_config.json`을 `backups/files/`에 `.tar.gz`로 저장 +3. `make full-backup`: DB dump와 파일 백업을 한 번에 수행 +4. `make cleanup-backups`: 기본 14일이 지난 백업 파일 정리 + +### 15.2 실행 예시 + +가장 단순한 전체 백업 예시는 아래와 같다. + +```bash +make full-backup +``` + +DB dump만 별도로 실행하려면 아래처럼 사용한다. + +```bash +make db-dump +``` + +운영 파일만 묶으려면 아래처럼 사용한다. + +```bash +make files-backup +``` + +보존 주기를 30일로 바꿔 정리하려면 아래처럼 사용한다. + +```bash +make cleanup-backups RETENTION_DAYS=30 +``` + +백업 경로를 별도 디스크나 마운트 경로로 바꾸려면 아래처럼 사용한다. + +```bash +make full-backup BACKUP_ROOT=/opt/itam-backups +``` + +### 15.3 현재 스크립트가 실제로 백업하는 대상 + +현재 `scripts/backup.sh`는 아래 규칙으로 동작한다. + +1. `.env` 파일에서 `DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASS`, `DB_NAME`을 읽는다. +2. `mysqldump --single-transaction --quick --routines --triggers` 옵션으로 dump를 생성한다. +3. DB dump는 gzip 압축본으로 저장한다. +4. 파일 백업은 `.env`, `uploads/`, `map_config.json` 중 실제로 존재하는 항목만 묶는다. +5. 백업 정리는 `find ... -mtime` 기준으로 수행한다. + +즉 현재 스크립트는 운영 서버 또는 백업 서버에서 바로 실행 가능한 최소 백업 도구로 보면 된다. + +### 15.4 운영 사용 권장 방식 + +운영에서는 아래 방식이 가장 현실적이다. + +1. 매일 새벽 cron 또는 systemd timer로 `make full-backup` 실행 +2. 백업 완료 후 `make cleanup-backups` 실행 +3. `backups/` 또는 별도 `BACKUP_ROOT` 경로를 NAS 또는 외부 백업 스토리지로 추가 복사 +4. 최소 월 1회 restore 테스트 수행 + +가장 단순한 예시는 아래와 같다. + +```bash +make full-backup BACKUP_ROOT=/opt/itam-backups +make cleanup-backups BACKUP_ROOT=/opt/itam-backups RETENTION_DAYS=30 +``` + +DB 백업 자체는 여전히 DB 서버 정책과 함께 관리하는 것이 가장 안전하지만, 현재 저장소 기준 운영 자동화 진입점은 위 `make` 명령으로 통일해도 된다. + +--- + +## 16. 롤백 기준 + +현재 구조에서는 가장 단순한 롤백 방식이 아래와 같다. + +1. 이전 정상 커밋 또는 파일 상태 확보 +2. 이미지 재빌드 또는 이전 이미지 재사용 +3. `docker compose -f docker-compose.prod.yaml up -d --build` 재실행 +4. `/health`, `/ready`, 메인 화면, 핵심 API 재검증 + +즉, 현재 구조에서는 별도 디렉터리 재배치보다 현재 저장소 상태 관리와 compose 재기동이 롤백의 중심이 된다. + +--- + +## 17. 결론 + +현재 ITAM 저장소는 별도 `/srv/itam` 구조로 옮기지 않아도, 지금 파일 구조를 유지한 채 운영형 배포 흐름으로 전환할 수 있다. + +정리하면 아래와 같다. + +1. test와 prod 모두 현재 저장소 구조 기준으로 통일한다. +2. `.env`, `uploads`, `map_config.json`, `logs/nginx`를 운영 핵심 경로로 본다. +3. reverse proxy는 현재 `docker/nginx/default.conf`를 기준으로 운영한다. +4. backend는 production 모드, health check, 외부 DB 연결 구조를 유지한다. +5. 큰 구조 변경 없이도 운영 전환이 가능하다. + +남은 작업은 TLS, 로그 로테이션, CI/CD, 보안 점검을 현재 구조 기준으로 계속 보강하는 것이다. \ No newline at end of file diff --git a/docker-compose.prod.yaml b/docker-compose.prod.yaml new file mode 100644 index 0000000..736fb23 --- /dev/null +++ b/docker-compose.prod.yaml @@ -0,0 +1,57 @@ +services: + backend: + image: itam-backend:prod + build: + context: . + dockerfile: Dockerfile.backend.prod + container_name: itam-backend + working_dir: /app + env_file: + - .env + environment: + NODE_ENV: production + PORT: 3000 + volumes: + - ./uploads:/app/uploads + - ./map_config.json:/app/map_config.json:ro + expose: + - "3000" + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + frontend: + image: itam-frontend:prod + build: + context: . + dockerfile: Dockerfile.frontend.prod + container_name: itam-frontend + expose: + - "80" + restart: unless-stopped + + nginx: + image: nginx:stable-alpine + container_name: itam-nginx + ports: + - "80:80" + volumes: + - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro + - ./logs/nginx:/var/log/nginx + depends_on: + backend: + condition: service_healthy + frontend: + condition: service_started + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:80/"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 20s + diff --git a/docker-compose.test.yaml b/docker-compose.test.yaml new file mode 100644 index 0000000..4d354dd --- /dev/null +++ b/docker-compose.test.yaml @@ -0,0 +1,62 @@ +# Local testing compose file - uses relative paths and build contexts +# Usage: docker compose -f docker-compose.test.yaml up --build + +services: + backend: + build: + context: . + dockerfile: Dockerfile.backend.prod + container_name: itam-backend-test + working_dir: /app + env_file: + - .env + environment: + NODE_ENV: development + PORT: 3000 + DB_HOST: ${DB_HOST:-172.16.8.151} + DB_PORT: ${DB_PORT:-3306} + DB_USER: ${DB_USER:-root} + DB_PASS: ${DB_PASS:-} + DB_NAME: ${DB_NAME:-itam} + ports: + - "3000:3000" + volumes: + - ./uploads:/app/uploads + - ./map_config.json:/app/map_config.json:ro + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + frontend: + build: + context: . + dockerfile: Dockerfile.frontend.prod + container_name: itam-frontend-test + expose: + - "80" + restart: unless-stopped + + nginx: + image: nginx:stable-alpine + container_name: itam-nginx-test + ports: + - "8080:80" + volumes: + - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro + - ./logs/nginx:/var/log/nginx + depends_on: + backend: + condition: service_healthy + frontend: + condition: service_started + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:80/"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 20s diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..9843fe5 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,48 @@ +services: + backend: + build: + context: . + dockerfile: Dockerfile.backend + container_name: itam-backend + working_dir: /app + env_file: + - .env + environment: + DB_HOST: ${DB_HOST} + DB_PORT: ${DB_PORT} + DB_USER: ${DB_USER} + DB_PASS: ${DB_PASS} + DB_NAME: ${DB_NAME} + PORT: 3000 + ports: + - "3000:3000" + volumes: + - ./:/app + - backend_node_modules:/app/node_modules + - ./uploads:/app/uploads + - ./map_config.json:/app/map_config.json + command: npm run server + restart: unless-stopped + + frontend: + build: + context: . + dockerfile: Dockerfile.frontend + container_name: itam-frontend + working_dir: /app + depends_on: + - backend + environment: + CHOKIDAR_USEPOLLING: "true" + VITE_DEV_PROXY_TARGET: http://backend:3000 + ports: + - "8080:8080" + volumes: + - ./:/app + - frontend_node_modules:/app/node_modules + command: npm run dev -- --host 0.0.0.0 + restart: unless-stopped + +volumes: + backend_node_modules: + frontend_node_modules: \ No newline at end of file diff --git a/docker/frontend/default.conf b/docker/frontend/default.conf new file mode 100644 index 0000000..d7f2a4f --- /dev/null +++ b/docker/frontend/default.conf @@ -0,0 +1,55 @@ +server { + listen 80; + listen [::]:80; + server_name _; + + root /usr/share/nginx/html; + + # Logging + access_log /var/log/nginx/frontend-access.log main; + error_log /var/log/nginx/frontend-error.log warn; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # Gzip compression + gzip on; + gzip_types text/plain text/css text/xml text/javascript + application/x-javascript application/xml+rss + application/json application/javascript; + gzip_min_length 1000; + + # Serve static files with SPA fallback + location / { + try_files $uri $uri/ /index.html; + } + + # Cache static assets (60 days) + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|webp|woff|woff2|ttf|eot)$ { + expires 60d; + add_header Cache-Control "public, immutable"; + } + + # Don't cache HTML files + location ~* \.html$ { + expires -1; + add_header Cache-Control "no-cache, no-store, must-revalidate"; + } + + # Health check + location /health { + access_log off; + return 200 "OK\n"; + add_header Content-Type text/plain; + } + + # Deny access to sensitive files + location ~ /\. { + deny all; + access_log off; + log_not_found off; + } +} diff --git a/docker/mysql/init/README.md b/docker/mysql/init/README.md new file mode 100644 index 0000000..71d5629 --- /dev/null +++ b/docker/mysql/init/README.md @@ -0,0 +1,16 @@ +# MySQL init directory + +This directory is kept as a legacy hook for file-based MySQL initialization. + +Current production path in this repository is not file-based import. +The live Docker flow uses the `db-bootstrap` service in `docker-compose.yaml` to stream data from the external source DB into the internal `db` container. + +Use this directory only if you intentionally switch back to `docker-entrypoint-initdb.d` style initialization. + +If you do that, typical naming would be: + +- `01_schema.sql` +- `02_seed.sql` +- or a single `01_itam_dump.sql` + +Remember that files in this directory are executed automatically by the MySQL container only on the first initialization of the data volume. \ No newline at end of file diff --git a/docker/nginx/default.conf b/docker/nginx/default.conf new file mode 100644 index 0000000..58e44b2 --- /dev/null +++ b/docker/nginx/default.conf @@ -0,0 +1,101 @@ +upstream backend { + server backend:3000; +} + +upstream frontend { + server frontend:80; +} + +server { + listen 80; + listen [::]:80; + server_name _; + + # Client upload size limit (adjust as needed) + client_max_body_size 100M; + + # Logging + access_log /var/log/nginx/access.log main; + error_log /var/log/nginx/error.log warn; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # Gzip compression + gzip on; + gzip_types text/plain text/css text/xml text/javascript + application/x-javascript application/xml+rss + application/json application/javascript; + gzip_min_length 1000; + + # Forward all app requests to the frontend container + location / { + proxy_pass http://frontend; + 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; + proxy_http_version 1.1; + } + + # API proxy to backend + location /api/ { + proxy_pass http://backend/api/; + + # Preserve original request information + 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; + proxy_set_header X-Forwarded-Host $server_name; + proxy_set_header X-Forwarded-Port $server_port; + + # Connection settings + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + + # Buffering settings + proxy_buffering on; + proxy_buffer_size 4k; + proxy_buffers 8 4k; + proxy_busy_buffers_size 8k; + } + + # Uploads proxy to backend + location /uploads/ { + proxy_pass http://backend/uploads/; + + 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; + + # Cache uploads + expires 30d; + add_header Cache-Control "public"; + } + + # Health check endpoint (for monitoring) + location /health { + access_log off; + return 200 "OK\n"; + add_header Content-Type text/plain; + } + + # Deny access to sensitive files + location ~ /\. { + deny all; + access_log off; + log_not_found off; + } + + location ~ ~$ { + deny all; + access_log off; + log_not_found off; + } +} diff --git a/docker_task_plan.md b/docker_task_plan.md new file mode 100644 index 0000000..f545657 --- /dev/null +++ b/docker_task_plan.md @@ -0,0 +1,330 @@ +# ITAM 도커라이징 작업 태스크 정리 + +## 1. 문서 목적 + +이 문서는 ITAM 자산관리 시스템의 도커라이징 작업을 실제 실행 단위로 쪼개서 정리한 태스크 문서다. + +이 문서의 목표는 아래와 같다. + +1. 내일까지 보여줄 시연 범위를 기준으로 우선순위를 정한다. +2. 시연용 작업과 운영형 전환 작업을 분리한다. +3. 개발 담당자가 바로 실행할 수 있는 체크리스트를 제공한다. + +관련 배경과 구조 분석은 [doc_readme.md](c:/Users/user/Desktop/안건%20파일/itam/doc_readme.md) 문서를 기준으로 한다. + +현재 구현/검증 상태: + +- `Dockerfile.frontend` 생성 완료 +- `Dockerfile.backend` 생성 완료 +- `docker-compose.yaml` 생성 완료 +- `.dockerignore` 생성 완료 +- WSL2 Ubuntu에서 `docker compose up --build -d` 검증 완료 +- frontend 8080 응답 확인 완료 +- backend `/api/assets/master` 응답 확인 완료 +- 현재 DB는 external MySQL 기준이며, DB 컨테이너 추가 작업은 다음 단계로 남아 있음 + +## 2. 이번 작업의 최우선 목표 + +이번 도커라이징의 1차 목표는 "운영 배포 완료"가 아니라 아래 상태를 재현하는 것이다. + +1. frontend 컨테이너가 정상 기동한다. +2. backend 컨테이너가 정상 기동한다. +3. backend가 기존 외부 MySQL 또는 MySQL 컨테이너에 정상 연결된다. +4. 브라우저에서 화면이 열린다. +5. 핵심 API 호출이 정상 동작한다. +6. 업로드 저장 경로가 유지된다. +7. 필요 시 DB까지 함께 포함된 재현 가능한 스택을 제공한다. + +## 3. 작업 범위 구분 + +### 3.1 이번 시연 범위에 포함 + +- Dockerfile.frontend 초안 작성 +- Dockerfile.backend 초안 작성 +- docker-compose.yaml 작성 +- `.dockerignore` 작성 +- MySQL 컨테이너 추가 설계 +- 초기 SQL dump 또는 init SQL 적재 방식 정의 +- `uploads` 볼륨 처리 +- `map_config.json` 영속성 처리 방식 반영 +- 컨테이너 기동 및 접속 확인 +- 핵심 API 및 화면 확인 + +### 3.2 이번 시연 범위에서 제외 + +- DB 전체 마이그레이션 자동화 +- nginx 기반 운영 배포 구조 +- 단일 이미지 운영 구조 전환 +- CI/CD 연계 + +## 4. 선행 확인 태스크 + +아래 태스크는 실제 Docker 파일 작성 전에 먼저 확인해야 한다. + +### Task 1. 외부 MySQL 접근 가능 여부 확인 + +- 목적: 컨테이너에서 외부 DB 접속이 가능한지 확인 +- 확인 항목: + - DB_HOST 접근 가능 여부 + - DB_PORT 3306 접속 가능 여부 + - 계정 권한 정상 여부 +- 완료 기준: + - backend 컨테이너 기준 DB 연결 에러가 발생하지 않음 + +### Task 2. 기준 스키마 상태 확인 + +- 목적: 현재 앱이 요구하는 테이블 구조가 실제 DB와 맞는지 확인 +- 확인 항목: + - `asset_core` + - `asset_spec` + - `asset_location` + - `asset_remote` + - `asset_history` + - `hardware_components_master` + - `job_spec_standards` +- 완료 기준: + - `/api/assets/master` 호출 시 쿼리 에러가 발생하지 않음 + +### Task 3. 파일 영속성 대상 확인 + +- 목적: 컨테이너 재시작 이후에도 유지되어야 할 파일/폴더 식별 +- 대상: + - `uploads` + - `map_config.json` +- 완료 기준: + - 볼륨 설계 대상이 명확하게 문서화됨 + +### Task 4. DB 기준 데이터 소스 확정 + +- 목적: MySQL 컨테이너 최초 기동 시 어떤 데이터로 초기화할지 결정 +- 선택지: + - 기존 사내 DB에서 추출한 SQL dump 사용 + - 정리된 스키마 SQL + seed SQL 사용 + - 수동 import 절차 사용 +- 완료 기준: + - `docker/mysql/init` 기준 적재 전략 또는 수동 복원 절차가 확정됨 + +## 5. 시연용 도커라이징 태스크 + +### Task 5. 프런트 Dockerfile 작성 + +- 목적: Vite 개발 서버를 컨테이너에서 구동 +- 작업 내용: + - Node 20 계열 이미지 사용 + - `package*.json` 복사 후 `npm install` + - 8080 포트 노출 + - `npm run dev -- --host 0.0.0.0` 실행 +- 산출물: + - `Dockerfile.frontend` +- 완료 기준: + - 컨테이너에서 8080 포트가 정상 listen 상태가 됨 + +### Task 6. 백엔드 Dockerfile 작성 + +- 목적: Express API 서버를 컨테이너에서 구동 +- 작업 내용: + - Node 20 계열 이미지 사용 + - `package*.json` 복사 후 `npm install` + - 3000 포트 노출 + - `npm run server` 실행 +- 산출물: + - `Dockerfile.backend` +- 완료 기준: + - 컨테이너에서 3000 포트가 정상 listen 상태가 됨 + +### Task 7. MySQL Docker 구성 추가 + +- 목적: DB까지 포함한 재현 가능한 스택 구성 +- 작업 내용: + - `mysql:8.0` 서비스 정의 + - `MYSQL_DATABASE`, `MYSQL_USER`, `MYSQL_PASSWORD` 설정 + - utf8mb4 문자셋 옵션 반영 + - MySQL 데이터 volume 연결 + - 초기 SQL 적재용 `docker/mysql/init` 디렉터리 설계 +- 산출물: + - `docker-compose.yaml` 내 `db` 서비스 또는 별도 DB compose 확장안 +- 완료 기준: + - MySQL 컨테이너가 정상 기동하고 3306 포트에서 응답 가능 + +### Task 8. backend DB 연결 전환 + +- 목적: backend가 external MySQL 대신 DB 컨테이너를 바라보도록 변경 +- 작업 내용: + - `DB_HOST`를 `db`로 전환 + - 필요 시 `.env.docker` 또는 compose 내부 환경변수 사용 + - backend `depends_on`에 db 추가 +- 산출물: + - DB 컨테이너용 backend 환경 정의 +- 완료 기준: + - backend 로그에서 DB 연결 성공 확인 + +### Task 9. docker-compose.yaml 확장 + +- 목적: frontend/backend를 함께 기동 +- 작업 내용: + - frontend 서비스 정의 + - backend 서비스 정의 + - db 서비스 정의 + - 포트 매핑 추가 + - `.env` 또는 docker 전용 환경변수 연결 + - MySQL 데이터 볼륨 연결 + - `uploads` 볼륨 연결 + - `map_config.json` 처리 방식 반영 +- 산출물: + - `docker-compose.yaml` +- 완료 기준: + - `docker compose up --build` 한 번으로 세 서비스가 모두 올라옴 + +### Task 10. `.dockerignore` 작성 + +- 목적: 불필요한 빌드 컨텍스트 제외 +- 제외 권장 항목: + - `node_modules` + - `dist` + - `build` + - `.git` + - `uploads` + - `*.xlsx` +- 산출물: + - `.dockerignore` +- 완료 기준: + - 이미지 빌드 컨텍스트가 과도하게 커지지 않음 + +## 6. 시연 검증 태스크 + +### Task 11. WSL 컨테이너 기동 검증 + +- 실행 명령: + +```bash +powershell -ExecutionPolicy Bypass -File .\start_docker_wsl.ps1 +``` + +- 확인 항목: + - frontend 로그 에러 여부 + - backend 로그 에러 여부 + - db 로그 에러 여부 + - backend와 db 연결 성공 여부 +- 완료 기준: + - 세 컨테이너 모두 종료 없이 유지됨 + +### Task 12. 웹 접속 검증 + +- 확인 항목: + - `http://localhost:8080` 접속 가능 여부 + - 첫 화면 로딩 여부 + - 콘솔 에러 여부 +- 완료 기준: + - 브라우저에서 초기 화면이 정상 표시됨 + +### Task 13. API 검증 + +- 확인 항목: + - `http://localhost:3000/api/assets/master` + - 프런트에서 `/api/assets/master` 호출 정상 여부 +- 완료 기준: + - 200 응답 또는 정상 데이터 응답 확인 + +### Task 14. DB 초기 데이터 검증 + +- 확인 항목: + - MySQL 컨테이너 내부에 목표 DB가 생성되었는지 + - 기준 테이블이 존재하는지 + - 샘플 데이터 또는 실데이터가 적재되었는지 +- 완료 기준: + - backend가 기대하는 최소 테이블과 데이터가 실제로 조회됨 + +### Task 15. 업로드/파일 저장 검증 + +- 확인 항목: + - `/api/upload` 호출 정상 여부 + - 업로드 파일이 `uploads`에 실제 저장되는지 + - `map_config.json` 수정 내용이 유지되는지 +- 완료 기준: + - 컨테이너 재시작 후에도 저장 데이터가 유지됨 + +## 7. 시연 후 후속 태스크 + +### Task 16. 운영형 프런트 배포 구조 전환 + +- 목표: Vite dev server 대신 정적 빌드 기반 구조로 전환 +- 후보: + - nginx 정적 서빙 + - Express 정적 서빙 + +### Task 17. DB 초기화/마이그레이션 전략 통합 + +- 목표: 기준 스키마와 실행 순서를 단일 정책으로 통일 +- 필요 작업: + - 기준 스키마 선정 + - 초기화 스크립트 확정 + - 마이그레이션 순서 정의 + +### Task 18. `.env.example` 및 배포 환경 분리 + +- 목표: 민감정보를 저장소에서 분리하고 배포별 설정 체계화 + +### Task 19. 운영 볼륨 및 백업 전략 정리 + +- 목표: 업로드 파일과 설정 파일, MySQL 데이터의 장기 보존 정책 정리 + +### Task 20. DB 백업/복원 절차 문서화 + +- 목표: 컨테이너 DB를 기준으로 dump/restore 절차를 문서화 + +## 8. 우선순위 정리 + +### P0: 내일까지 반드시 필요한 작업 + +1. Task 1. 외부 MySQL 접근 가능 여부 확인 +2. Task 2. 기준 스키마 상태 확인 +3. Task 4. DB 기준 데이터 소스 확정 +4. Task 7. MySQL Docker 구성 추가 +5. Task 8. backend DB 연결 전환 +6. Task 9. docker-compose.yaml 확장 +7. Task 11. WSL 컨테이너 기동 검증 +8. Task 12. 웹 접속 검증 +9. Task 13. API 검증 +10. Task 14. DB 초기 데이터 검증 + +### P1: 시연 안정화를 위해 권장되는 작업 + +1. Task 3. 파일 영속성 대상 확인 +2. Task 10. `.dockerignore` 작성 +3. Task 15. 업로드/파일 저장 검증 + +### P2: 시연 이후 진행할 작업 + +1. Task 16. 운영형 프런트 배포 구조 전환 +2. Task 17. DB 초기화/마이그레이션 전략 통합 +3. Task 18. `.env.example` 및 배포 환경 분리 +4. Task 19. 운영 볼륨 및 백업 전략 정리 +5. Task 20. DB 백업/복원 절차 문서화 + +## 9. 개발자용 최종 작업 순서 제안 + +개발 담당자에게는 아래 순서로 진행하라고 전달하면 된다. + +1. 외부 DB 연결 가능 여부부터 확인 +2. 현재 DB 스키마가 앱 요구사항과 맞는지 확인 +3. DB 기준 dump 또는 init SQL 확보 +4. MySQL 컨테이너 구성 추가 +5. backend의 DB 연결 대상을 `db`로 전환 +6. WSL에서 `docker compose config` 확인 +7. WSL에서 컨테이너 기동 테스트 +8. 웹 접속 및 API 확인 +9. 업로드 및 파일 영속성 확인 +10. 시연 완료 후 운영형 구조로 분리 작업 진행 + +## 10. 완료 판단 기준 + +이번 도커라이징 1차 작업은 아래 조건을 만족하면 완료로 본다. + +1. `docker compose up --build`로 프런트, 백엔드, DB가 모두 기동한다. +2. 브라우저에서 8080 화면이 열린다. +3. `/api/assets/master`가 정상 응답한다. +4. backend가 DB 컨테이너와 정상 연결된다. +5. DB 초기 테이블과 데이터가 기대 상태로 적재된다. +6. `uploads`, `map_config.json`, MySQL 데이터가 재시작 후에도 유지된다. + +이 문서는 실제 구현 작업의 체크리스트로 사용한다. \ No newline at end of file diff --git a/docs/itam_cicd_setup.md b/docs/itam_cicd_setup.md new file mode 100644 index 0000000..c6baad6 --- /dev/null +++ b/docs/itam_cicd_setup.md @@ -0,0 +1,226 @@ +# ITAM CI/CD 설정 가이드 + +## 1. 문서 목적 + +이 문서는 현재 ITAM 저장소에 구성된 CI/CD 파일을 실제 운영에 연결하기 위해 필요한 기준을 정리한 가이드다. + +대상 범위는 아래와 같다. + +1. Gitea Actions workflow 역할 +2. Gitea Variables / Secrets 설정값 +3. 운영 서버 배포 디렉토리 기준 +4. 운영 영속 경로 기준 +5. production deploy 실행 전 확인 사항 + +--- + +## 2. 현재 CI/CD 구성 + +현재 `.gitea/workflows`에는 ITAM 관련 workflow만 남겨둔 상태다. + +1. `itam_code_check.yml` +2. `itam_docker_build_check.yml` +3. `itam_production_deploy.yml` + +각 workflow의 역할은 아래와 같다. + +1. `itam_code_check.yml`: TypeScript/Vite build와 compose 문법 검증 +2. `itam_docker_build_check.yml`: 운영용 Docker 이미지 빌드 가능 여부 검증 +3. `itam_production_deploy.yml`: 운영 서버에 SSH 접속 후 실제 배포 수행 + +현재 배포 흐름은 아래와 같다. + +```mermaid +flowchart LR + DEV["Developer Push or Manual Run"] --> CODE["ITAM Code Check"] + CODE --> BUILD["ITAM Docker Build Check"] + BUILD --> DEPLOY["ITAM Production Deploy"] + DEPLOY --> HOST["Production Host"] + HOST --> APP["docker compose up -d --build"] + linkStyle default stroke:#d32f2f,stroke-width:2px; +``` + +--- + +## 3. Gitea Variables / Secrets 기준 + +`itam_production_deploy.yml`이 정상 동작하려면 아래 값이 필요하다. + +### 3.1 Variables + +아래 항목은 Gitea repository Variables에 등록한다. + +| Key | 설명 | 예시 | +| --- | --- | --- | +| `PROD_HOST` | 운영 서버 SSH 접속 호스트 | `10.0.0.25` | +| `PROD_USER` | 운영 서버 SSH 사용자 | `deploy` | +| `PROD_DEPLOY_PATH` | 서버에서 저장소를 배포할 경로 | `/opt/itam` | +| `PROD_BACKUP_ROOT` | 배포 전 백업 저장 경로, 배포 경로 바깥이어야 함 | `/opt/itam-backups` | +| `PROD_GIT_URL` | 운영 서버에서 pull 가능한 저장소 주소 | `git@gitea.example.com:team/itam.git` | +| `PROD_DB_HOST` | 외부 MySQL 호스트 | `172.16.8.151` | +| `PROD_DB_PORT` | 외부 MySQL 포트 | `3306` | +| `PROD_DB_USER` | 운영 DB 계정 | `itam_admin` | +| `PROD_DB_NAME` | 운영 DB 이름 | `itam` | +| `PROD_LOG_LEVEL` | 애플리케이션 로그 레벨 | `info` | + +### 3.2 Secrets + +아래 항목은 Gitea repository Secrets에 등록한다. + +| Key | 설명 | +| --- | --- | +| `PROD_SSH_PRIVATE_KEY` | 운영 서버 접속용 개인키 | +| `PROD_DB_PASS` | 운영 DB 비밀번호 | + +### 3.3 운영 원칙 + +1. `PROD_DB_PASS`는 Variables가 아니라 Secrets에만 둔다. +2. `PROD_SSH_PRIVATE_KEY`는 배포 전용 계정 키를 사용한다. +3. `PROD_GIT_URL`은 운영 서버에서 직접 pull 가능한 주소여야 한다. +4. 운영 서버의 `known_hosts`는 workflow에서 자동 등록되지만, 최초 운영 전 수동 접속 검증도 함께 수행하는 것이 안전하다. +5. `PROD_BACKUP_ROOT`는 `PROD_DEPLOY_PATH` 내부가 아니라 바깥 경로를 사용해야 한다. + +--- + +## 4. 운영 서버 배포 디렉토리 기준 + +현재 `itam_production_deploy.yml`은 운영 서버에서 아래 흐름으로 배포를 수행한다. + +1. `PROD_DEPLOY_PATH` 디렉토리를 생성한다. +2. 기존 운영 상태가 있으면 배포 전 백업을 수행한다. +3. 해당 경로에 저장소를 clone 또는 fetch 한다. +4. 지정 브랜치로 checkout 한다. +5. `uploads`, `logs/nginx` 디렉토리를 생성한다. +6. `.env.deploy`를 서버의 `.env`로 복사한다. +7. `docker compose -f docker-compose.prod.yaml up -d --build`를 실행한다. + +권장 디렉토리 구조는 아래와 같다. + +```text +/opt/itam/ + .env + docker-compose.prod.yaml + Dockerfile.frontend.prod + Dockerfile.backend.prod + map_config.json + uploads/ + logs/ + nginx/ + docker/ + nginx/ + default.conf + frontend/ + default.conf + src/ + public/ + img/ + +/opt/itam-backups/ + db/ + files/ +``` + +현재 구조 기준 배포 관계는 아래와 같다. + +```mermaid +flowchart TB + subgraph HOST["Production Host"] + REPO["PROD_DEPLOY_PATH"] + ENV[".env"] + UP["uploads/"] + LOGS["logs/nginx/"] + MAP["map_config.json"] + end + + subgraph CTR["Docker Services"] + NGINX["itam-nginx"] + FRONT["itam-frontend"] + BACK["itam-backend"] + end + + DB["External MySQL"] + + REPO --> NGINX + ENV --> BACK + UP --> BACK + MAP --> BACK + LOGS --> NGINX + REPO --> BAK["PROD_BACKUP_ROOT"] + NGINX --> FRONT + NGINX --> BACK + BACK --> DB + linkStyle default stroke:#d32f2f,stroke-width:2px; +``` + +--- + +## 5. 영속 경로 기준 + +현재 `docker-compose.prod.yaml` 기준으로 운영에서 유지되어야 하는 경로는 아래와 같다. + +1. `.env` +2. `uploads/` +3. `map_config.json` +4. `logs/nginx/` +5. `PROD_BACKUP_ROOT` + +각 경로의 의미는 아래와 같다. + +1. `.env`: backend 런타임 환경변수 +2. `uploads/`: 업로드 파일 데이터 +3. `map_config.json`: 위치/맵 구성 데이터 +4. `logs/nginx/`: reverse proxy 접근 로그 및 에러 로그 +5. `PROD_BACKUP_ROOT`: 배포 전 DB dump와 운영 파일 아카이브 저장 위치 + +운영 기준으로 보면 `uploads/`와 `map_config.json`은 애플리케이션 데이터이고, `.env`는 환경 설정이며, `logs/nginx/`는 운영 추적 데이터다. + +즉 서버 운영 시 컨테이너만 다시 띄우면 되는 구조가 아니라, 이 경로들이 유지되는 것을 전제로 배포가 성립한다. + +--- + +## 6. 배포 전 체크리스트 + +`itam_production_deploy.yml` 실행 전 아래 항목을 먼저 확인한다. + +1. 운영 서버에 Docker Engine과 `docker compose`가 설치되어 있어야 한다. +2. 운영 서버의 배포 계정이 Docker 실행 권한을 가져야 한다. +3. 운영 서버에서 `PROD_GIT_URL`로 직접 `git fetch`가 가능해야 한다. +4. 외부 MySQL 접속 정보가 실제 운영망 기준으로 열려 있어야 한다. +5. 운영 서버에 `map_config.json` 초기 파일이 존재해야 한다. +6. 방화벽 또는 보안 장비에서 80 포트 접근 정책이 정리되어 있어야 한다. + +권장 확인 명령 예시는 아래와 같다. + +```bash +docker --version +docker compose version +git ls-remote +test -f map_config.json +test -d uploads +``` + +--- + +## 7. 현재 구조에서의 해석 + +현재 ITAM CI/CD는 staging 없이도 운영 배포가 가능한 최소 구조로 정리되어 있다. + +이 구조의 장점은 아래와 같다. + +1. workflow 수가 적어서 관리가 단순하다. +2. 운영 배포에 필요한 변수와 시크릿 범위가 명확하다. +3. staging이 필요해지면 production deploy workflow를 복제해 별도 환경으로 확장하기 쉽다. + +즉, 지금 단계에서는 production 기준을 먼저 고정하고, staging은 동일 패턴으로 추후 추가하는 전략이 적절하다. + +--- + +## 8. 다음 권장 작업 + +현재 문서 기준으로 바로 이어서 할 작업은 아래 순서가 적절하다. + +1. `PROD_DEPLOY_PATH` 실제 서버 경로 확정 +2. 운영 서버 배포 계정 생성 및 SSH 키 등록 +3. `map_config.json`, `uploads/` 초기 데이터 준비 +4. production deploy workflow에 smoke check 추가 +5. 로그 로테이션과 백업/복구 절차 문서화 \ No newline at end of file diff --git a/index.html b/index.html index f836f0a..ec81e9a 100644 --- a/index.html +++ b/index.html @@ -24,7 +24,7 @@