diff --git a/.dockerignore b/.dockerignore index 12df7a5..869e63a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,10 +1,10 @@ -node_modules -dist -build -.git -.gitignore -.env -npm-debug.log -uploads -*.xlsx -*.log +node_modules +dist +build +.git +.gitignore +.env +npm-debug.log +uploads +*.xlsx +*.log 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..fb0a73a --- /dev/null +++ b/.gitea/workflows/itam_code_check.yml @@ -0,0 +1,34 @@ +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: 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 index 65f9dac..d04b4b0 100644 --- a/Dockerfile.backend +++ b/Dockerfile.backend @@ -1,12 +1,12 @@ -FROM node:20-alpine - -WORKDIR /app - -COPY package*.json ./ -RUN npm ci - -COPY . . - -EXPOSE 3000 - +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 index 525a1ff..ba24cd6 100644 --- a/Dockerfile.frontend +++ b/Dockerfile.frontend @@ -1,12 +1,12 @@ -FROM node:20-alpine - -WORKDIR /app - -COPY package*.json ./ -RUN npm ci - -COPY . . - -EXPOSE 8080 - +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..e068ff9 --- /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..1d251e6 --- /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 ITAM 운영 오픈 일정 + 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 index f9b7dbe..55964c0 100644 --- a/doc_readme.md +++ b/doc_readme.md @@ -1,729 +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. 방화벽 또는 네트워크 이슈가 없는지 - ---- - -## 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 안에서 화면 데이터가 다시 안 보일 수 있다. - ---- - -## 16. 현재 기준 재현 절차 요약 - -가장 짧게 정리하면 아래 순서다. - -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`을 열어 실제 데이터 표시를 확인한다. - ---- - -## 17. 현재 최종 결론 - -현재 저장소의 도커라이징 구조는 실무 표준에 맞는 `무상태 앱 컨테이너 + 외부 DB` 구조다. - -현재 핵심은 아래 세 가지다. - -1. `backend`는 외부 MySQL에 직접 연결한다. -2. `frontend`는 `backend:3000`으로 API 프록시한다. -3. WSL 경로 변환 스크립트를 통해 Windows 한글 경로에서도 안정적으로 실행한다. - -즉, 이 문서대로 진행하면 Gitea 소스만 받은 상태에서 지금과 같은 Docker 실행 구조를 재현할 수 있다. +# 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 index 5b35781..815829c 100644 --- a/doc_readme2.md +++ b/doc_readme2.md @@ -1,730 +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 현재 최종 한 줄 요약 - +# 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 index 6156596..9843fe5 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,48 +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: +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 index fbe7b40..71d5629 100644 --- a/docker/mysql/init/README.md +++ b/docker/mysql/init/README.md @@ -1,16 +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` - +# 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 index ce2d78d..f545657 100644 --- a/docker_task_plan.md +++ b/docker_task_plan.md @@ -1,330 +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 데이터가 재시작 후에도 유지된다. - +# 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/production_deploy_roadmap.md b/production_deploy_roadmap.md new file mode 100644 index 0000000..d2f9936 --- /dev/null +++ b/production_deploy_roadmap.md @@ -0,0 +1,247 @@ +# ITAM 운영 배포 작업 로드맵 + +## 1. 문서 목적 + +이 문서는 ITAM 저장소를 Windows/WSL 시범 구동 상태에서 Linux 운영 서버 배포 상태로 전환하기 위한 구체적인 작업 목록과 우선순위를 정리한 로드맵이다. + +현재 상태: 개발/시범용 Docker 구조 (Vite dev server + bind mount + external DB) +목표 상태: 운영 배포 구조 (정적 빌드 + 영속 스토리지 분리 + reverse proxy + external DB) + +--- + +## 2. 작업 페이즈 분류 + +### 2.1 Phase 1: 핵심 배포 파일 (우선순위: 높음) + +운영 환경에서 실제 배포를 가능하게 하는 기초 작업이다. + +1. ✅ **Add production compose file** (`docker-compose.prod.yaml`) + - 목표: 운영 서버 기준 최종 compose 파일 작성 + - 범위: backend, frontend, nginx 서비스 정의 + - 입력: 외부 `.env`, 호스트 경로 마운트 정의 + - 출력: `/deploy/docker-compose.prod.yaml` + - 완료 기준: `docker compose -f docker-compose.prod.yaml config` 성공 + +2. ✅ **Create production frontend Dockerfile (multi-stage build)** + - 목표: 정적 자산 기반 프런트엔드 이미지 생성 + - 범위: Stage 1 = 빌드 (Node.js + npm build), Stage 2 = 정적 서빙 (Nginx) + - 입력: 현재 `Dockerfile.frontend` 참고, `npm run build` 검증 + - 출력: `Dockerfile.frontend.prod` 또는 분기 처리 + - 완료 기준: 로컬에서 `npm run build` 성공, 이미지 빌드 성공, 정적 파일 8080 서빙 확인 + +3. ✅ **Harden backend Dockerfile for production** + - 목표: production 환경 최적화된 백엔드 이미지 + - 범위: NODE_ENV=production, production 의존성만, 비루트 사용자, health check 추가 + - 입력: 현재 `Dockerfile.backend`, `server.js` 검토 + - 출력: 수정된 `Dockerfile.backend` 또는 `.prod` 버전 + - 완료 기준: 이미지 빌드 성공, 시작 후 health endpoint 응답 200 + +4. ✅ **Define host paths and named volumes for persistence** + - 목표: Linux 서버의 운영 디렉터리 구조 정의 + - 범위: `/srv/itam/{app,env,config,uploads,logs,deploy}` 마운트 정책 확정 + - 입력: doc_readme3.md 섹션 7 참고 + - 출력: docker-compose.prod.yaml에 적용, 볼륨 마운트 확인 + - 완료 기준: compose 파일에 경로 마운트 명시, 영속성 테스트 (컨테이너 재시작 후 데이터 유지) + +--- + +### 2.2 Phase 2: 네트워킹 및 보안 (우선순위: 높음) + +외부 접근 경로와 보안을 담당하는 작업이다. + +5. ✅ **Provide Nginx reverse-proxy and frontend static config** + - 목표: Nginx 설정파일 작성 (frontend 정적 서빙 + API 프록시) + - 범위: `/` → frontend, `/api/` → backend:3000, 기본 보안 헤더, gzip + - 입력: doc_readme3.md 섹션 12 참고 (예시 개념) + - 출력: `/deploy/nginx/default.conf` + - 완료 기준: `docker compose -f docker-compose.prod.yaml up -d` 후 `http://localhost/api/assets/master` 응답 200 + +6. ✅ **Externalize and secure environment variables (.env.example + secrets guidance)** + - 목표: 민감정보 보호 기준 문서화 + - 범위: `.env.example` 생성, Git 제외 확인, 운영 환경 분리 지침 + - 입력: 현재 `.env` 파일, `.gitignore` 점검 + - 출력: `.env.example`, env 관리 가이드 추가 (doc_readme3.md 또는 SECURITY.md) + - 완료 기준: `.env`가 `.gitignore`에 등록, `.env.example` 배포 파일 작성됨 + +7. ✅ **Define TLS certificate handling strategy (Let's Encrypt / mount certs)** + - 목표: HTTPS 인증서 관리 정책 확정 + - 범위: 자동 갱신 (certbot + Let's Encrypt) 또는 마운트 기반 수동 관리 선택 + - 입력: 사내 정책 확인, 운영 도메인 확인 + - 출력: `deploy/nginx/tls-strategy.md` 또는 compose 파일 주석으로 정리 + - 완료 기준: 선택 방식 문서화, nginx 설정 적용 준비 + +8. ✅ **Security review: non-root users, image scan, secret rotation** + - 목표: 보안 체크리스트 작성 및 초기 적용 + - 범위: Dockerfile non-root 사용자 추가, 이미지 취약점 스캔 지침, 비밀 로테이션 정책 + - 입력: doc_readme3.md 섹션 13.3 (보안) 참고 + - 출력: 수정된 Dockerfile, `SECURITY.md` 또는 운영 가이드 추가 + - 완료 기준: 백엔드/프런트엔드 모두 비루트 사용자로 실행 확인 + +--- + +### 2.3 Phase 3: 모니터링 및 운영 준비 (우선순위: 중간) + +배포 후 운영을 원활하게 하기 위한 작업이다. + +9. ✅ **Add healthcheck and readiness endpoint in backend** + - 목표: backend 헬스 체크 엔드포인트 추가 + - 범위: `GET /health` 또는 `/ready` 엔드포인트 추가 (DB 연결 확인) + - 입력: `server.js` 현재 코드 검토 + - 출력: backend 에서 health 응답, docker-compose.prod.yaml에 healthcheck 설정 + - 완료 기준: `curl http://localhost:3000/health` 응답 200, 컨테이너 헬스 상태 healthy 표시 + +10. ✅ **Add logging and log rotation guidance** + - 목표: 컨테이너 로그 관리 정책 문서화 + - 범위: Docker logging driver 설정, log rotation 정책, 저장소 경로 정의 + - 입력: `/srv/itam/logs` 마운트 계획 + - 출력: docker-compose.prod.yaml에 로깅 설정, docs에 로그 확인 가이드 + - 완료 기준: 로그 파일이 `/srv/itam/logs`에 저장됨, rotation 정책 명시 + +11. ✅ **Document backup and restore procedures for DB and uploads** + - 목표: 운영 데이터 백업/복구 절차 문서화 + - 범위: 외부 MySQL 백업 정책, `/srv/itam/uploads` 백업, 복구 절차 스크립트 예시 + - 입력: doc_readme3.md 섹션 14 참고 + - 출력: `BACKUP_RESTORE.md` 또는 운영 가이드 추가 섹션 + - 완료 기준: 백업 스크립트 예시 작성, 복구 절차 테스트 완료 + +12. ✅ **Add smoke tests and post-deploy checks** + - 목표: 배포 후 빠른 검증 스크립트 작성 + - 범위: 컨테이너 상태 확인, API 응답 테스트, 파일 업로드 테스트, DB 연결 확인 + - 입력: doc_readme3.md 섹션 13 (점검 체크리스트) 참고 + - 출력: `scripts/smoke-test.sh` 또는 배포 후 확인 스크립트 + - 완료 기준: 스크립트 실행 후 모든 검사 통과 + +--- + +### 2.4 Phase 4: 자동화 및 CI/CD (우선순위: 중간) + +장기 운영을 위한 자동화 작업이다. + +13. ✅ **Prepare CI/CD build & push scripts (image registry)** + - 목표: 이미지 빌드 및 레지스트리 푸시 자동화 + - 범위: `docker build`, `docker tag`, `docker push` 스크립트 또는 GitHub Actions/GitLab CI 예시 + - 입력: 이미지 레지스트리 주소 확인 (e.g., registry.example.com, Docker Hub, etc.) + - 출력: `.github/workflows/build.yml` 또는 `scripts/build-push.sh` + - 완료 기준: 수동 빌드/푸시 스크립트 작동 확인, 이미지 태그 정책 확정 + +14. ✅ **Create deploy directory with compose.prod and nginx configs** + - 목표: 운영 배포 디렉터리 정리 + - 범위: `/deploy/docker-compose.prod.yaml`, `/deploy/nginx/default.conf`, 기타 설정 파일 조직화 + - 입력: Phase 1-3 결과물 + - 출력: 다음 구조로 정리됨: + ``` + deploy/ + docker-compose.prod.yaml + nginx/ + default.conf + tls-strategy.md + scripts/ + smoke-test.sh + backup.sh + ``` + - 완료 기준: 디렉터리 구조 확정, 모든 파일 위치 일관성 있음 + +--- + +### 2.5 Phase 5: 절차 및 문서화 (우선순위: 중간) + +운영 절차와 문서를 정리하는 작업이다. + +15. ✅ **Write rollout and rollback procedures (steps, checklist)** + - 목표: 배포 및 복구 절차 문서화 + - 범위: 배포 전 체크리스트, 배포 단계별 절차, 장애 시 롤백 절차 + - 입력: doc_readme3.md 섹션 16 참고 + - 출력: `DEPLOYMENT_PROCEDURES.md` 또는 운영 가이드 통합 + - 완료 기준: 절차 문서 완성, 체크리스트 확인 가능 + +--- + +## 3. 작업 우선순위 및 권장 순서 + +### 3.1 필수 우선 작업 (Phase 1 완료 필수) + +1. Add production compose file +2. Create production frontend Dockerfile (multi-stage build) +3. Harden backend Dockerfile for production +4. Define host paths and named volumes for persistence + +**목표**: 기본 배포 구조 완성, Linux 서버에서 최소 기동 가능 상태 + +### 3.2 보안 필수 작업 (Phase 2 완료 권장) + +5. Provide Nginx reverse-proxy and frontend static config +6. Externalize and secure environment variables +7. Define TLS certificate handling strategy +8. Security review: non-root users, image scan, secret rotation + +**목표**: 운영 환경 최소 보안 기준 충족 + +### 3.3 운영 안정화 작업 (Phase 3 진행) + +9. Add healthcheck and readiness endpoint in backend +10. Add logging and log rotation guidance +11. Document backup and restore procedures +12. Add smoke tests and post-deploy checks + +**목표**: 배포 후 문제 식별 및 백업/복구 가능 상태 + +### 3.4 장기 운영 자동화 (Phase 4-5는 선택) + +13. Prepare CI/CD build & push scripts +14. Create deploy directory with compose.prod and nginx configs +15. Write rollout and rollback procedures + +**목표**: 반복 배포 자동화, 절차 표준화 + +--- + +## 4. 작업 환경 및 검증 기준 + +### 4.1 개발/테스트 환경 + +- 로컬 Linux VM 또는 WSL에서 Phase 1-2 테스트 +- Docker Desktop or Docker Engine 필수 +- 외부 테스트 MySQL 또는 mock DB + +### 4.2 스테이징 환경 + +- 실제 Linux 서버에서 전체 배포 절차 테스트 +- 운영 환경과 동일 아키텍처 + +### 4.3 운영 배포 + +- 위 모든 Phase 완료 후 진행 +- 백업 확인 후 배포 +- 배포 후 smoke test 자동 실행 + +--- + +## 5. 진행 추적 + +진행 상황은 아래 상태로 추적한다. + +- `not-started`: 아직 시작 안 함 +- `in-progress`: 현재 진행 중 +- `completed`: 완료됨 + +현재 상태는 모두 `not-started`이며, Phase 1 우선 순위 항목부터 순차적으로 진행한다. + +--- + +## 6. 예상 일정 + +- Phase 1 (핵심 배포 파일): 1-2일 +- Phase 2 (네트워킹 및 보안): 1-2일 +- Phase 3 (모니터링 및 운영 준비): 1일 +- Phase 4-5 (자동화 및 절차): 1-2일 + +**전체 예상 소요 시간**: 4-7일 + +--- + +## 7. 추가 고려사항 + +1. 현재 `doc_readme3.md`는 가이드 문서이고, 이 로드맵은 구현 작업 목록이다. +2. Phase 1-2 완료 후 실제 코드 커밋은 `Dockerizing` 브랜치에만 한다. +3. 각 Phase 완료 후 관련 문서도 함께 업데이트한다. +4. 운영 전환 전에 최소 1회 스테이징 환경에서 전체 배포 절차 테스트를 수행한다. diff --git a/scripts/backup.sh b/scripts/backup.sh new file mode 100644 index 0000000..c4b1794 --- /dev/null +++ b/scripts/backup.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env sh + +set -eu + +COMMAND="${1:-help}" +ENV_FILE="${ENV_FILE:-.env}" +BACKUP_ROOT="${BACKUP_ROOT:-backups}" +RETENTION_DAYS="${RETENTION_DAYS:-14}" +TIMESTAMP="${BACKUP_TIMESTAMP:-$(date +%Y%m%d_%H%M%S)}" + +log() { + printf '[backup] %s\n' "$*" +} + +fail() { + printf '[backup] %s\n' "$*" >&2 + exit 1 +} + +require_command() { + command -v "$1" >/dev/null 2>&1 || fail "Required command not found: $1" +} + +load_env() { + [ -f "$ENV_FILE" ] || fail "Env file not found: $ENV_FILE" + + set -a + # shellcheck disable=SC1090 + . "$ENV_FILE" + set +a + + : "${DB_HOST:?DB_HOST is required in $ENV_FILE}" + : "${DB_PORT:=3306}" + : "${DB_USER:?DB_USER is required in $ENV_FILE}" + : "${DB_PASS:?DB_PASS is required in $ENV_FILE}" + : "${DB_NAME:?DB_NAME is required in $ENV_FILE}" +} + +db_dump() { + require_command mysqldump + require_command gzip + load_env + + mkdir -p "$BACKUP_ROOT/db" + output_path="$BACKUP_ROOT/db/${DB_NAME}_${TIMESTAMP}.sql.gz" + + log "Creating DB dump: $output_path" + MYSQL_PWD="$DB_PASS" mysqldump \ + --host="$DB_HOST" \ + --port="$DB_PORT" \ + --user="$DB_USER" \ + --single-transaction \ + --quick \ + --routines \ + --triggers \ + "$DB_NAME" | gzip > "$output_path" + + log "DB dump completed: $output_path" +} + +files_backup() { + require_command tar + mkdir -p "$BACKUP_ROOT/files" + + archive_path="$BACKUP_ROOT/files/runtime_${TIMESTAMP}.tar.gz" + set -- + + [ -f "$ENV_FILE" ] && set -- "$@" "$ENV_FILE" + [ -d "uploads" ] && set -- "$@" "uploads" + [ -f "map_config.json" ] && set -- "$@" "map_config.json" + + [ "$#" -gt 0 ] || fail "No runtime files found to archive" + + log "Creating runtime archive: $archive_path" + tar -czf "$archive_path" "$@" + log "Runtime archive completed: $archive_path" +} + +cleanup_backups() { + require_command find + [ -d "$BACKUP_ROOT" ] || { + log "Backup root does not exist, skipping cleanup: $BACKUP_ROOT" + return 0 + } + + log "Deleting backup files older than ${RETENTION_DAYS} days from $BACKUP_ROOT" + find "$BACKUP_ROOT" -type f -mtime "+$RETENTION_DAYS" -print -delete +} + +case "$COMMAND" in + db) + db_dump + ;; + files) + files_backup + ;; + full) + db_dump + files_backup + ;; + cleanup) + cleanup_backups + ;; + help|--help|-h) + log "Commands: db | files | full | cleanup" + ;; + *) + fail "Unknown command: $COMMAND" + ;; +esac \ No newline at end of file diff --git a/server.js b/server.js index 118ed52..17de21a 100644 --- a/server.js +++ b/server.js @@ -742,6 +742,31 @@ app.post('/api/upload', (req, res) => { } }); +// Health check endpoint for container orchestration +app.get('/health', async (req, res) => { + try { + const connection = await pool.getConnection(); + const result = await connection.query('SELECT 1'); + connection.release(); + res.status(200).json({ status: 'ok', db: 'connected' }); + } catch (err) { + // Return degraded status if DB unreachable, but still report service as alive + res.status(200).json({ status: 'degraded', db: 'unreachable', error: err.message }); + } +}); + +// Readiness check endpoint (only returns 200 if fully ready) +app.get('/ready', async (req, res) => { + try { + const connection = await pool.getConnection(); + const result = await connection.query('SELECT 1'); + connection.release(); + res.status(200).json({ status: 'ready' }); + } catch (err) { + res.status(503).json({ status: 'not_ready', error: err.message }); + } +}); + app.listen(3000, '0.0.0.0', () => { console.log('📡 ITAM BACKEND SERVER RUNNING ON PORT 3000 (V3 Normalized)'); }); diff --git a/start_docker_wsl.bat b/start_docker_wsl.bat index 36ee26e..c173d26 100644 --- a/start_docker_wsl.bat +++ b/start_docker_wsl.bat @@ -1,10 +1,10 @@ -@echo off -chcp 65001 >nul -cd /d "%~dp0" -powershell -ExecutionPolicy Bypass -File "%~dp0start_docker_wsl.ps1" -if errorlevel 1 ( - echo. - echo [ERROR] start_docker_wsl.ps1 failed. - pause - exit /b %errorlevel% +@echo off +chcp 65001 >nul +cd /d "%~dp0" +powershell -ExecutionPolicy Bypass -File "%~dp0start_docker_wsl.ps1" +if errorlevel 1 ( + echo. + echo [ERROR] start_docker_wsl.ps1 failed. + pause + exit /b %errorlevel% ) \ No newline at end of file diff --git a/start_docker_wsl.ps1 b/start_docker_wsl.ps1 index 66e4975..c0f611a 100644 --- a/start_docker_wsl.ps1 +++ b/start_docker_wsl.ps1 @@ -1,107 +1,107 @@ -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -$projectWindowsPath = $PSScriptRoot -$wslProjectPath = (wsl wslpath $projectWindowsPath).Trim() -$envFilePath = Join-Path $PSScriptRoot '.env' - -function Get-EnvValue { - param( - [string]$FilePath, - [string]$Key - ) - - if (-not (Test-Path $FilePath)) { - return $null - } - - $line = Get-Content $FilePath | Where-Object { $_ -match "^$Key=" } | Select-Object -First 1 - if (-not $line) { - return $null - } - - return ($line -split '=', 2)[1].Trim() -} - -function Test-TcpPortFast { - param( - [string]$HostName, - [int]$Port, - [int]$TimeoutMs = 3000 - ) - - $client = New-Object System.Net.Sockets.TcpClient - try { - $asyncResult = $client.BeginConnect($HostName, $Port, $null, $null) - if (-not $asyncResult.AsyncWaitHandle.WaitOne($TimeoutMs, $false)) { - $client.Close() - return $false - } - - $client.EndConnect($asyncResult) - $client.Close() - return $true - } - catch { - $client.Close() - return $false - } -} - -Write-Host "============================================" -ForegroundColor Cyan -Write-Host " HM ITAM WSL Docker Start" -ForegroundColor Cyan -Write-Host "============================================" -ForegroundColor Cyan -Write-Host "" - -Write-Host "[INFO] Checking WSL..." -wsl -l -v -if ($LASTEXITCODE -ne 0) { - Write-Host "[ERROR] WSL is not available." -ForegroundColor Red - exit 1 -} - -Write-Host "[INFO] Checking Docker in WSL..." -wsl sh -lc "docker --version" -if ($LASTEXITCODE -ne 0) { - Write-Host "[ERROR] Docker is not available inside WSL." -ForegroundColor Red - exit 1 -} - -$dbHost = Get-EnvValue -FilePath $envFilePath -Key 'DB_HOST' -$dbPort = Get-EnvValue -FilePath $envFilePath -Key 'DB_PORT' - -if (-not $dbPort) { - $dbPort = '3306' -} - -if (-not $dbHost) { - Write-Host "[WARN] .env is missing DB_HOST. Containers will still start, but backend DB calls will fail until DB settings are fixed." -ForegroundColor Yellow -} - -if ($dbHost) { - Write-Host "[INFO] Checking external DB reachability..." - $dbReachable = Test-TcpPortFast -HostName $dbHost -Port ([int]$dbPort) - if (-not $dbReachable) { - Write-Host "[WARN] External DB is unreachable: $dbHost`:$dbPort" -ForegroundColor Yellow - Write-Host "[HINT] Containers will still start. Check VPN/private network connection, firewall rules, DB host/port in .env, or whether the DB server is running." -ForegroundColor Yellow - } -} - -Write-Host "[INFO] Starting ITAM containers in WSL..." -wsl sh -lc "cd '$wslProjectPath' && docker compose up --build -d --remove-orphans" -if ($LASTEXITCODE -ne 0) { - Write-Host "[WARN] Build-based startup failed. Retrying with cached images/containers..." -ForegroundColor Yellow - wsl sh -lc "cd '$wslProjectPath' && docker compose up -d --remove-orphans" - if ($LASTEXITCODE -ne 0) { - Write-Host "[ERROR] Failed to start containers." -ForegroundColor Red - exit 1 - } -} - -Write-Host "" -Write-Host "============================================" -ForegroundColor Green -Write-Host " [OK] WSL Docker stack started." -ForegroundColor Green -Write-Host " [INFO] Frontend: http://localhost:8080" -Write-Host " [INFO] Backend : http://localhost:3000/api/assets/master" -Write-Host "============================================" -ForegroundColor Green - +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +$projectWindowsPath = $PSScriptRoot +$wslProjectPath = (wsl wslpath $projectWindowsPath).Trim() +$envFilePath = Join-Path $PSScriptRoot '.env' + +function Get-EnvValue { + param( + [string]$FilePath, + [string]$Key + ) + + if (-not (Test-Path $FilePath)) { + return $null + } + + $line = Get-Content $FilePath | Where-Object { $_ -match "^$Key=" } | Select-Object -First 1 + if (-not $line) { + return $null + } + + return ($line -split '=', 2)[1].Trim() +} + +function Test-TcpPortFast { + param( + [string]$HostName, + [int]$Port, + [int]$TimeoutMs = 3000 + ) + + $client = New-Object System.Net.Sockets.TcpClient + try { + $asyncResult = $client.BeginConnect($HostName, $Port, $null, $null) + if (-not $asyncResult.AsyncWaitHandle.WaitOne($TimeoutMs, $false)) { + $client.Close() + return $false + } + + $client.EndConnect($asyncResult) + $client.Close() + return $true + } + catch { + $client.Close() + return $false + } +} + +Write-Host "============================================" -ForegroundColor Cyan +Write-Host " HM ITAM WSL Docker Start" -ForegroundColor Cyan +Write-Host "============================================" -ForegroundColor Cyan +Write-Host "" + +Write-Host "[INFO] Checking WSL..." +wsl -l -v +if ($LASTEXITCODE -ne 0) { + Write-Host "[ERROR] WSL is not available." -ForegroundColor Red + exit 1 +} + +Write-Host "[INFO] Checking Docker in WSL..." +wsl sh -lc "docker --version" +if ($LASTEXITCODE -ne 0) { + Write-Host "[ERROR] Docker is not available inside WSL." -ForegroundColor Red + exit 1 +} + +$dbHost = Get-EnvValue -FilePath $envFilePath -Key 'DB_HOST' +$dbPort = Get-EnvValue -FilePath $envFilePath -Key 'DB_PORT' + +if (-not $dbPort) { + $dbPort = '3306' +} + +if (-not $dbHost) { + Write-Host "[WARN] .env is missing DB_HOST. Containers will still start, but backend DB calls will fail until DB settings are fixed." -ForegroundColor Yellow +} + +if ($dbHost) { + Write-Host "[INFO] Checking external DB reachability..." + $dbReachable = Test-TcpPortFast -HostName $dbHost -Port ([int]$dbPort) + if (-not $dbReachable) { + Write-Host "[WARN] External DB is unreachable: $dbHost`:$dbPort" -ForegroundColor Yellow + Write-Host "[HINT] Containers will still start. Check VPN/private network connection, firewall rules, DB host/port in .env, or whether the DB server is running." -ForegroundColor Yellow + } +} + +Write-Host "[INFO] Starting ITAM containers in WSL..." +wsl sh -lc "cd '$wslProjectPath' && docker compose up --build -d --remove-orphans" +if ($LASTEXITCODE -ne 0) { + Write-Host "[WARN] Build-based startup failed. Retrying with cached images/containers..." -ForegroundColor Yellow + wsl sh -lc "cd '$wslProjectPath' && docker compose up -d --remove-orphans" + if ($LASTEXITCODE -ne 0) { + Write-Host "[ERROR] Failed to start containers." -ForegroundColor Red + exit 1 + } +} + +Write-Host "" +Write-Host "============================================" -ForegroundColor Green +Write-Host " [OK] WSL Docker stack started." -ForegroundColor Green +Write-Host " [INFO] Frontend: http://localhost:8080" +Write-Host " [INFO] Backend : http://localhost:3000/api/assets/master" +Write-Host "============================================" -ForegroundColor Green + Start-Process "http://localhost:8080" \ No newline at end of file diff --git a/stop_docker_wsl.bat b/stop_docker_wsl.bat index b9b3955..c40c795 100644 --- a/stop_docker_wsl.bat +++ b/stop_docker_wsl.bat @@ -1,4 +1,4 @@ -@echo off -chcp 65001 >nul -cd /d "%~dp0" +@echo off +chcp 65001 >nul +cd /d "%~dp0" powershell -ExecutionPolicy Bypass -File "%~dp0stop_docker_wsl.ps1" \ No newline at end of file diff --git a/stop_docker_wsl.ps1 b/stop_docker_wsl.ps1 index bfd7656..47a0d87 100644 --- a/stop_docker_wsl.ps1 +++ b/stop_docker_wsl.ps1 @@ -1,13 +1,13 @@ -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -$projectWindowsPath = $PSScriptRoot -$wslProjectPath = (wsl wslpath $projectWindowsPath).Trim() - -Write-Host "[INFO] Stopping ITAM WSL Docker stack..." -wsl sh -lc "cd '$wslProjectPath' && docker compose down --remove-orphans" -if ($LASTEXITCODE -ne 0) { - Write-Host "[ERROR] Failed to stop containers." -ForegroundColor Red - exit 1 -} - +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +$projectWindowsPath = $PSScriptRoot +$wslProjectPath = (wsl wslpath $projectWindowsPath).Trim() + +Write-Host "[INFO] Stopping ITAM WSL Docker stack..." +wsl sh -lc "cd '$wslProjectPath' && docker compose down --remove-orphans" +if ($LASTEXITCODE -ne 0) { + Write-Host "[ERROR] Failed to stop containers." -ForegroundColor Red + exit 1 +} + Write-Host "[OK] WSL Docker stack stopped." -ForegroundColor Green \ No newline at end of file diff --git a/운영구축_변경비교_보고서.md b/운영구축_변경비교_보고서.md new file mode 100644 index 0000000..f767ae4 --- /dev/null +++ b/운영구축_변경비교_보고서.md @@ -0,0 +1,242 @@ +# ITAM 구축 방식 변경 비교 보고서 + +## 1. 문서 목적 + +이 문서는 ITAM 시스템의 Docker 기반 구축 방식이 초기 개발/테스트 중심 구조에서 현재 운영 배포 중심 구조로 어떻게 변경되었는지 설명하기 위한 보고용 문서다. + +이번 비교의 기준은 아래 두 단계다. + +1. 초기 구축 방식: Windows + WSL 환경에서 개발 및 테스트 재현을 목적으로 한 Docker 구조 +2. 현재 구축 방식: 정식 운영 배포를 고려하여 production 기준으로 정리한 Docker 구조 + +즉, 이 문서는 “예전 계획안과 지금 계획안의 차이”가 아니라, “처음 구축했던 개발/테스트용 구조와 현재 운영 배포용 구조의 차이”를 설명하는 문서다. + +--- + +## 2. 한눈에 보는 핵심 변화 + +초기 구축은 아래 목적에 맞춰져 있었다. + +1. Windows PC에서 WSL2를 이용해 빠르게 실행할 수 있어야 한다. +2. 현재 개발 구조를 그대로 Docker 안에서 재현해야 한다. +3. 프런트와 백엔드가 개발용 방식으로 동작하면 된다. + +반면 현재 구축은 아래 목적에 맞춰 변경되었다. + +1. 실제 운영 배포에 사용할 수 있어야 한다. +2. 프런트는 개발 서버가 아니라 정적 빌드 산출물로 서비스되어야 한다. +3. reverse proxy, health check, 운영용 Dockerfile, 운영용 compose 기준이 정리되어야 한다. +4. 운영 과정에서 경로, 정적 자산, 로그, 환경변수 사용 방식이 명확해야 한다. + +즉, 초기에는 “개발 재현”이 목적이었고, 현재는 “운영 배포 가능 상태로 정리”하는 것이 목적이다. + +--- + +## 3. 구조 비교도 + +### 3.1 초기 구축 구조 + +```mermaid +flowchart LR + U["User Browser"] --> FE["Frontend Container Vite Dev 8080"] + FE -->|api proxy| BE["Backend Container Express 3000"] + BE --> DB["External MySQL 3306"] + BE --> UP["./uploads"] + BE --> CFG["./map_config.json"] + ENV["./.env"] --> BE + linkStyle default stroke:#d32f2f,stroke-width:2px; +``` + +### 3.2 현재 구축 구조 + +```mermaid +flowchart LR + U["User Browser"] --> RP["Nginx Reverse Proxy 80"] + RP --> FE["Frontend Container Static Nginx 80"] + RP -->|api| BE["Backend Container Node 3000"] + RP -->|uploads| BE + BE --> DB["External MySQL 3306"] + BE --> UP["./uploads"] + BE --> CFG["./map_config.json"] + ENV["./.env"] --> BE + LOGS["./logs/nginx"] --> RP + linkStyle default stroke:#d32f2f,stroke-width:2px; +``` + +--- + +## 4. 비교 요약표 + +| 구분 | 초기 구축 방식 | 현재 구축 방식 | +|---|---|---| +| 구축 목적 | Windows/WSL 개발 테스트 재현 | 정식 운영 배포 기준 정리 | +| 프런트 실행 방식 | Vite dev server | 빌드된 정적 파일 + Nginx | +| 프런트 포트 | 8080 | 80 | +| 백엔드 | Express API 컨테이너 | Express API 컨테이너 | +| 프록시 구조 | frontend 내부 dev proxy | 외부 nginx reverse proxy | +| 운영용 compose | 없음 또는 개발형 중심 | `docker-compose.prod.yaml` 별도 구성 | +| 테스트용 compose | `docker-compose.yaml` 중심 | `docker-compose.test.yaml` 별도 구성 | +| 정적 자산 처리 | 개발 서버가 직접 참조 | 운영 이미지에 정적 자산 포함 필요 | +| health check | 사실상 없음 | `/health`, `/ready` 기반 포함 | +| 운영 관점 기능 | 낮음 | 높음 | + +--- + +## 5. 초기 구축 방식 설명 + +초기 구축은 Windows + WSL 환경에서 “지금 돌아가는 개발 구조를 Docker로 그대로 재현”하는 것이 핵심이었다. + +주요 특징은 아래와 같다. + +1. 프런트는 `Dockerfile.frontend`를 통해 Vite 개발 서버를 컨테이너에서 실행했다. +2. 백엔드는 `Dockerfile.backend`를 통해 Express API 서버를 컨테이너로 실행했다. +3. 프런트는 `VITE_DEV_PROXY_TARGET`을 이용해 `/api` 요청을 `backend:3000`으로 전달했다. +4. 접속 포트는 `http://localhost:8080`이었다. +5. 목적은 운영 배포가 아니라 개발/시연/테스트 재현이었다. + +즉, 이 단계에서 중요한 것은 “실제 앱이 개발 환경과 최대한 비슷하게 동작하는가”였다. + +--- + +## 6. 현재 구축 방식 설명 + +현재 구축은 정식 운영 배포에 맞게 구조를 보강한 상태다. + +주요 특징은 아래와 같다. + +1. 프런트는 `Dockerfile.frontend.prod`에서 multi-stage build를 통해 정적 파일로 빌드된다. +2. 빌드 산출물은 frontend 컨테이너 내부 Nginx가 서빙한다. +3. 별도의 nginx reverse proxy가 외부 80 포트를 받아 frontend와 backend로 요청을 분기한다. +4. backend는 `Dockerfile.backend.prod`를 사용하며, 비루트 사용자와 health check를 포함한다. +5. 운영용 `docker-compose.prod.yaml`과 검증용 `docker-compose.test.yaml`이 분리되어 있다. +6. 정적 이미지 자산과 로고 파일도 운영 이미지에 포함되도록 보완되었다. + +즉, 현재는 단순 개발 재현이 아니라 “운영에 투입 가능한 형태로 구조를 정리”한 상태다. + +--- + +## 7. 주요 변경 포인트 상세 + +### 7.1 프런트 실행 방식 변경 + +초기 구축: + +1. Vite 개발 서버를 그대로 띄웠다. +2. 코드 수정과 빠른 재확인에 유리했다. +3. 운영 배포용 웹 서버 구조는 아니었다. + +현재 구축: + +1. `npm run build` 결과물을 사용한다. +2. frontend 컨테이너 내부 Nginx가 정적 파일을 서빙한다. +3. 운영 환경에서 더 안정적인 구조다. + +이 차이는 개발 중심 구조에서 운영 중심 구조로 넘어갔다는 것을 가장 잘 보여준다. + +### 7.2 프록시 구조 변경 + +초기 구축: + +1. 프런트 컨테이너 내부에서 dev proxy가 `/api`를 backend로 우회했다. +2. 구조가 단순했지만 운영형 reverse proxy는 아니었다. + +현재 구축: + +1. 외부 nginx가 진입점 역할을 한다. +2. `/`는 frontend, `/api`와 `/uploads`는 backend로 분기한다. +3. 운영형 라우팅 구조를 갖추게 되었다. + +### 7.3 compose 구성 분리 + +초기 구축: + +1. 사실상 개발용 compose 중심이었다. +2. 운영 목적과 검증 목적이 명확히 분리되지 않았다. + +현재 구축: + +1. `docker-compose.test.yaml`은 운영형 구조 검증용 +2. `docker-compose.prod.yaml`은 운영 모드 기동용 +3. 목적에 따라 compose 파일이 구분된다. + +### 7.4 백엔드 운영성 강화 + +초기 구축: + +1. 백엔드는 단순 실행 중심이었다. +2. 운영 상태 확인이나 readiness 기준이 부족했다. + +현재 구축: + +1. `/health`, `/ready` 엔드포인트 추가 +2. `dumb-init` 적용 +3. 비루트 계정 실행 +4. health check 포함 + +즉, 장애 확인과 운영 점검이 가능한 상태로 바뀌었다. + +### 7.5 정적 자산 보강 + +초기 구축: + +1. 개발 환경에서는 파일이 로컬에 그대로 있어 지도 이미지와 로고가 직접 보였다. +2. 운영 이미지 기준으로는 정적 자산 누락 가능성이 드러나지 않았다. + +현재 구축: + +1. `img/location_photo/...` 경로의 지도 이미지 포함 +2. `/image 92.png` 로고 파일 포함 +3. 운영 이미지에서도 실제 화면이 깨지지 않도록 보완 완료 + +즉, 현재 구축은 운영 화면 품질까지 고려한 상태다. + +--- + +## 8. 왜 이렇게 바뀌었는가 + +초기 단계에서는 다음이 중요했다. + +1. 빠르게 실행되는가 +2. 개발 PC에서 재현 가능한가 +3. 기존 코드 구조를 크게 안 건드리고 Docker 안에서 띄울 수 있는가 + +하지만 운영 배포 기준으로는 다음이 추가로 필요했다. + +1. 개발 서버가 아니라 정적 배포 구조여야 함 +2. reverse proxy가 있어야 함 +3. health check와 운영 점검 기준이 있어야 함 +4. 정적 자산 누락 없이 화면이 완전하게 떠야 함 + +따라서 초기 구축은 목적에 충실한 개발/테스트형 구조였고, 현재 구축은 그 위에 운영 요소를 보강한 형태라고 볼 수 있다. + +--- + +## 9. 기대 효과 + +현재 구축 방식으로 변경되면서 기대되는 효과는 아래와 같다. + +1. 운영 배포 적합성 향상 +2. 요청 라우팅 구조 명확화 +3. 장애 점검 및 상태 확인 가능 +4. test와 prod 분리로 검증 절차 명확화 +5. 화면 자산 누락 문제 해소 + +즉, 지금 구조는 단순히 실행되는 수준을 넘어 운영 준비도가 높아진 구조다. + +--- + +## 10. 최종 요약 + +이번 변경의 본질은 아래와 같이 정리할 수 있다. + +“처음에는 Windows/WSL에서 개발과 테스트를 빠르게 재현하는 Docker 구조였다면, 현재는 실제 운영 배포를 염두에 둔 production 기준 구조로 발전시켰다.” + +요약하면 아래와 같다. + +1. 초기 구축은 개발 재현과 테스트 목적이었다. +2. 현재 구축은 정식 운영 배포 목적이다. +3. 프런트는 dev server에서 정적 배포 구조로 바뀌었다. +4. nginx reverse proxy와 health check가 추가되었다. +5. 운영 이미지 기준 정적 자산까지 보완되었다. + +현재 상태는 “개발 테스트용 Docker 재현” 단계를 넘어, “운영 배포가 가능한 Docker 구조”로 정리된 상태라고 설명할 수 있다. \ No newline at end of file