diff --git a/backend/Dockerfile b/backend/Dockerfile index c92a37b..2557746 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -43,8 +43,8 @@ COPY backend/ ./backend/ # Build NestJS → backend/dist RUN cd backend && pnpm run build -# Prune dev dependencies -RUN pnpm prune --prod --filter backend... +# Deploy with production deps only (pnpm workspace isolation) +RUN pnpm --filter backend deploy --prod --legacy /app/backend-prod # ========================= # Stage 3: Production Runtime @@ -65,8 +65,8 @@ RUN addgroup -g 1001 -S nestjs && \ # Copy production artifacts only COPY --from=build --chown=nestjs:nestjs /app/backend/dist ./dist -COPY --from=build --chown=nestjs:nestjs /app/backend/node_modules ./node_modules -COPY --from=build --chown=nestjs:nestjs /app/backend/package.json ./ +COPY --from=build --chown=nestjs:nestjs /app/backend-prod/node_modules ./node_modules +COPY --from=build --chown=nestjs:nestjs /app/backend-prod/package.json ./ # Create uploads directory (Two-Phase Storage) RUN mkdir -p /app/uploads/temp /app/uploads/permanent && \ diff --git a/backend/package.json b/backend/package.json index dbeae49..453d28c 100644 --- a/backend/package.json +++ b/backend/package.json @@ -56,6 +56,7 @@ "cache-manager-redis-yet": "^5.1.5", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", + "express": "^5.1.0", "fs-extra": "^11.3.2", "helmet": "^8.1.0", "ioredis": "^5.8.2", diff --git a/backend/src/common/file-storage/file-storage.service.ts b/backend/src/common/file-storage/file-storage.service.ts index 37c8722..bfe5cf8 100644 --- a/backend/src/common/file-storage/file-storage.service.ts +++ b/backend/src/common/file-storage/file-storage.service.ts @@ -18,21 +18,26 @@ import { ForbiddenException } from '@nestjs/common'; // ✅ Import เพิ่ @Injectable() export class FileStorageService { private readonly logger = new Logger(FileStorageService.name); - private readonly uploadRoot: string; + private readonly tempDir: string; + private readonly permanentDir: string; constructor( @InjectRepository(Attachment) private attachmentRepository: Repository, - private configService: ConfigService, + private configService: ConfigService ) { - // ใช้ Path จริงถ้าอยู่บน Server (Production) หรือใช้ ./uploads ถ้าอยู่ Local - this.uploadRoot = - this.configService.get('NODE_ENV') === 'production' - ? '/share/dms-data' - : path.join(process.cwd(), 'uploads'); + // ใช้ env vars จาก docker-compose สำหรับ Production + // ถ้าไม่ได้กำหนดจะ fallback เป็น ./uploads/temp และ ./uploads/permanent + this.tempDir = + this.configService.get('UPLOAD_TEMP_DIR') || + path.join(process.cwd(), 'uploads', 'temp'); + this.permanentDir = + this.configService.get('UPLOAD_PERMANENT_DIR') || + path.join(process.cwd(), 'uploads', 'permanent'); - // สร้างโฟลเดอร์ temp รอไว้เลยถ้ายังไม่มี - fs.ensureDirSync(path.join(this.uploadRoot, 'temp')); + // สร้างโฟลเดอร์ temp และ permanent รอไว้เลยถ้ายังไม่มี + fs.ensureDirSync(this.tempDir); + fs.ensureDirSync(this.permanentDir); } /** @@ -42,7 +47,7 @@ export class FileStorageService { const tempId = uuidv4(); const fileExt = path.extname(file.originalname); const storedFilename = `${uuidv4()}${fileExt}`; - const tempPath = path.join(this.uploadRoot, 'temp', storedFilename); + const tempPath = path.join(this.tempDir, storedFilename); // 1. คำนวณ Checksum (SHA-256) เพื่อความปลอดภัยและความถูกต้องของไฟล์ const checksum = this.calculateChecksum(file.buffer); @@ -89,7 +94,7 @@ export class FileStorageService { // แจ้งเตือนแต่อาจจะไม่ throw ถ้าต้องการให้ process ต่อไปได้บางส่วน (ขึ้นอยู่กับ business logic) // แต่เพื่อความปลอดภัยควรแจ้งว่าไฟล์ไม่ครบ this.logger.warn( - `Expected ${tempIds.length} files to commit, but found ${attachments.length}`, + `Expected ${tempIds.length} files to commit, but found ${attachments.length}` ); throw new NotFoundException('Some files not found or already committed'); } @@ -100,7 +105,7 @@ export class FileStorageService { const month = (today.getMonth() + 1).toString().padStart(2, '0'); // โฟลเดอร์ถาวรแยกตาม ปี/เดือน - const permanentDir = path.join(this.uploadRoot, 'permanent', year, month); + const permanentDir = path.join(this.permanentDir, year, month); await fs.ensureDir(permanentDir); for (const att of attachments) { @@ -122,16 +127,16 @@ export class FileStorageService { } else { this.logger.error(`File missing during commit: ${oldPath}`); throw new NotFoundException( - `File not found on disk: ${att.originalFilename}`, + `File not found on disk: ${att.originalFilename}` ); } } catch (error) { this.logger.error( `Failed to move file from ${oldPath} to ${newPath}`, - error, + error ); throw new BadRequestException( - `Failed to commit file: ${att.originalFilename}`, + `Failed to commit file: ${att.originalFilename}` ); } } @@ -144,7 +149,7 @@ export class FileStorageService { * ดึงไฟล์มาเป็น Stream เพื่อส่งกลับไปให้ Controller */ async download( - id: number, + id: number ): Promise<{ stream: fs.ReadStream; attachment: Attachment }> { // 1. ค้นหาข้อมูลไฟล์จาก DB const attachment = await this.attachmentRepository.findOne({ @@ -191,7 +196,7 @@ export class FileStorageService { // (ในอนาคตอาจเพิ่มเงื่อนไข OR User เป็น Admin/Document Control) if (attachment.uploadedByUserId !== userId) { this.logger.warn( - `User ${userId} tried to delete file ${id} owned by ${attachment.uploadedByUserId}`, + `User ${userId} tried to delete file ${id} owned by ${attachment.uploadedByUserId}` ); throw new ForbiddenException('You are not allowed to delete this file'); } @@ -202,13 +207,13 @@ export class FileStorageService { await fs.remove(attachment.filePath); } else { this.logger.warn( - `File not found on disk during deletion: ${attachment.filePath}`, + `File not found on disk during deletion: ${attachment.filePath}` ); } } catch (error) { this.logger.error( `Failed to delete file from disk: ${attachment.filePath}`, - error, + error ); throw new BadRequestException('Failed to delete file from storage'); } diff --git a/backend/src/config/database.config.ts b/backend/src/config/database.config.ts index 0959342..10a6b86 100644 --- a/backend/src/config/database.config.ts +++ b/backend/src/config/database.config.ts @@ -1,7 +1,4 @@ import { TypeOrmModuleOptions } from '@nestjs/typeorm'; -import { config } from 'dotenv'; - -config(); export const databaseConfig: TypeOrmModuleOptions = { type: 'mysql', diff --git a/backend/src/main.ts b/backend/src/main.ts index ab42e41..41f20b3 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -48,8 +48,10 @@ async function bootstrap() { app.use(json({ limit: '50mb' })); app.use(urlencoded({ extended: true, limit: '50mb' })); - // 🌐 4. Global Prefix - app.setGlobalPrefix('api'); + // 🌐 4. Global Prefix (ยกเว้น /health, /metrics สำหรับ monitoring) + app.setGlobalPrefix('api', { + exclude: ['health', 'metrics'], + }); // ⚙️ 5. Global Pipes & Interceptors & Filters app.useGlobalPipes( diff --git a/backend/tsconfig.build.json b/backend/tsconfig.build.json index 96c2964..d9d3c4d 100644 --- a/backend/tsconfig.build.json +++ b/backend/tsconfig.build.json @@ -1,4 +1,11 @@ { "extends": "./tsconfig.json", - "exclude": ["node_modules", "test", "dist", "**/*spec.ts", "documentation"] + "exclude": [ + "node_modules", + "test", + "dist", + "scripts", + "**/*spec.ts", + "documentation" + ] } diff --git a/lcbp3-backend.tar b/lcbp3-backend.tar new file mode 100644 index 0000000..9d04468 Binary files /dev/null and b/lcbp3-backend.tar differ diff --git a/lcbp3-frontend.tar b/lcbp3-frontend.tar new file mode 100644 index 0000000..74ac18d Binary files /dev/null and b/lcbp3-frontend.tar differ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 15b02a6..a1e4804 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -109,6 +109,9 @@ importers: class-validator: specifier: ^0.14.2 version: 0.14.3 + express: + specifier: ^5.1.0 + version: 5.1.0 fs-extra: specifier: ^11.3.2 version: 11.3.2 @@ -4708,6 +4711,10 @@ packages: resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} engines: {node: '>= 0.10'} + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + cosmiconfig@8.3.6: resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} engines: {node: '>=14'} @@ -7312,10 +7319,6 @@ packages: engines: {node: '>=10'} hasBin: true - send@1.2.0: - resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} - engines: {node: '>= 18'} - send@1.2.1: resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} @@ -9790,7 +9793,7 @@ snapshots: chokidar: 3.6.0 colors: 1.4.0 connect: 3.7.0 - cors: 2.8.5 + cors: 2.8.6 event-stream: 4.0.1 faye-websocket: 0.11.4 http-auth: 4.1.9 @@ -13476,6 +13479,11 @@ snapshots: object-assign: 4.1.1 vary: 1.1.2 + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + cosmiconfig@8.3.6(typescript@5.9.3): dependencies: import-fresh: 3.3.1 @@ -13949,8 +13957,8 @@ snapshots: '@typescript-eslint/parser': 8.48.0(eslint@8.57.1)(typescript@5.9.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) eslint-plugin-react: 7.37.5(eslint@8.57.1) eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1) @@ -13973,7 +13981,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -13984,22 +13992,22 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.48.0(eslint@8.57.1)(typescript@5.9.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -14010,7 +14018,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -14295,7 +14303,7 @@ snapshots: qs: 6.14.0 range-parser: 1.2.1 router: 2.2.0 - send: 1.2.0 + send: 1.2.1 serve-static: 2.2.0 statuses: 2.0.2 type-is: 2.0.1 @@ -16492,22 +16500,6 @@ snapshots: semver@7.7.3: {} - send@1.2.0: - dependencies: - debug: 4.4.3 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - fresh: 2.0.0 - http-errors: 2.0.1 - mime-types: 3.0.2 - ms: 2.1.3 - on-finished: 2.4.1 - range-parser: 1.2.1 - statuses: 2.0.2 - transitivePeerDependencies: - - supports-color - send@1.2.1: dependencies: debug: 4.4.3 @@ -16547,7 +16539,7 @@ snapshots: encodeurl: 2.0.0 escape-html: 1.0.3 parseurl: 1.3.3 - send: 1.2.0 + send: 1.2.1 transitivePeerDependencies: - supports-color diff --git a/specs/08-infrastructure/09_app_deployment.md b/specs/08-infrastructure/09_app_deployment.md index b2ba91f..23539be 100644 --- a/specs/08-infrastructure/09_app_deployment.md +++ b/specs/08-infrastructure/09_app_deployment.md @@ -22,25 +22,25 @@ ## 1. Build Docker Images -### Option A: Build บน Dev Machine แล้ว Transfer +### Option A: Build บน Dev Machine (Windows) แล้ว Transfer -```bash +```powershell # อยู่ที่ workspace root (nap-dms.lcbp3/) # Build Backend docker build -f backend/Dockerfile -t lcbp3-backend:latest . # Build Frontend (NEXT_PUBLIC_API_URL bake เข้าไปตอน build) -docker build -f frontend/Dockerfile \ - --build-arg NEXT_PUBLIC_API_URL=https://backend.np-dms.work/api \ +docker build -f frontend/Dockerfile ` + --build-arg NEXT_PUBLIC_API_URL=https://backend.np-dms.work/api ` -t lcbp3-frontend:latest . # Export เป็น .tar เพื่อ Transfer -docker save lcbp3-backend:latest | gzip > lcbp3-backend.tar.gz -docker save lcbp3-frontend:latest | gzip > lcbp3-frontend.tar.gz +docker save lcbp3-backend:latest -o lcbp3-backend.tar +docker save lcbp3-frontend:latest -o lcbp3-frontend.tar -# Transfer ไปยัง QNAP (ผ่าน SCP หรือ Shared Folder) -scp lcbp3-*.tar.gz admin@192.168.10.8:/share/np-dms/app/ +# Transfer ไปยัง QNAP (ผ่าน SMB Shared Folder) +# Copy lcbp3-backend.tar และ lcbp3-frontend.tar ไปที่ \\192.168.10.8\np-dms\app\ ``` ### Option B: Build บน QNAP โดยตรง (SSH) @@ -69,8 +69,8 @@ docker build -f frontend/Dockerfile \ ssh admin@192.168.10.8 # Load images -docker load < /share/np-dms/app/lcbp3-backend.tar.gz -docker load < /share/np-dms/app/lcbp3-frontend.tar.gz +docker load < /share/np-dms/app/lcbp3-backend.tar +docker load < /share/np-dms/app/lcbp3-frontend.tar # ตรวจสอบ docker images | grep lcbp3 @@ -82,15 +82,15 @@ docker images | grep lcbp3 ```bash # สร้าง directories สำหรับ volumes -mkdir -p /share/dms-data/uploads/temp -mkdir -p /share/dms-data/uploads/permanent -mkdir -p /share/dms-data/logs/backend +mkdir -p /share/np-dms/data/uploads/temp +mkdir -p /share/np-dms/data/uploads/permanent +mkdir -p /share/np-dms/data/logs/backend mkdir -p /share/np-dms/app # กำหนดสิทธิ์ให้ non-root user ใน container (UID 1001) -chown -R 1001:1001 /share/dms-data/uploads -chown -R 1001:1001 /share/dms-data/logs/backend -chmod -R 750 /share/dms-data/uploads +chown -R 1001:1001 /share/np-dms/data/uploads +chown -R 1001:1001 /share/np-dms/data/logs/backend +chmod -R 750 /share/np-dms/data/uploads ``` --- @@ -160,20 +160,22 @@ docker logs -f frontend เมื่อต้องการ deploy version ใหม่: -```bash -# 1. Build images ใหม่ (บน Dev Machine) +```powershell +# 1. Build images ใหม่ (บน Dev Machine - PowerShell) docker build -f backend/Dockerfile -t lcbp3-backend:latest . -docker build -f frontend/Dockerfile -t lcbp3-frontend:latest . +docker build -f frontend/Dockerfile ` + --build-arg NEXT_PUBLIC_API_URL=https://backend.np-dms.work/api ` + -t lcbp3-frontend:latest . # 2. Export & Transfer -docker save lcbp3-backend:latest | gzip > lcbp3-backend.tar.gz -docker save lcbp3-frontend:latest | gzip > lcbp3-frontend.tar.gz -scp lcbp3-*.tar.gz admin@192.168.10.8:/share/np-dms/app/ +docker save lcbp3-backend:latest -o lcbp3-backend.tar +docker save lcbp3-frontend:latest -o lcbp3-frontend.tar +# Copy ไปที่ \\192.168.10.8\np-dms\app\ ผ่าน SMB Shared Folder -# 3. Load บน QNAP +# 3. Load บน QNAP (SSH) ssh admin@192.168.10.8 -docker load < /share/np-dms/app/lcbp3-backend.tar.gz -docker load < /share/np-dms/app/lcbp3-frontend.tar.gz +docker load < /share/np-dms/app/lcbp3-backend.tar +docker load < /share/np-dms/app/lcbp3-frontend.tar # 4. Restart ใน Container Station # Applications → lcbp3-app → Restart diff --git a/specs/08-infrastructure/docker-compose-app.yml b/specs/08-infrastructure/docker-compose-app.yml index c6fb791..c970dec 100644 --- a/specs/08-infrastructure/docker-compose-app.yml +++ b/specs/08-infrastructure/docker-compose-app.yml @@ -48,8 +48,8 @@ services: # --- Database --- DB_HOST: 'mariadb' DB_PORT: '3306' - DB_NAME: 'lcbp3' - DB_USER: 'center' + DB_DATABASE: 'lcbp3' + DB_USERNAME: 'center' DB_PASSWORD: 'Center#2025' # --- Redis --- REDIS_HOST: 'cache' @@ -60,7 +60,8 @@ services: ELASTICSEARCH_PORT: '9200' # --- JWT --- JWT_SECRET: 'eebc122aa65adde8c76c6a0847d9649b2b67a06db1504693e6c912e51499b76e' - JWT_EXPIRES_IN: '8h' + JWT_EXPIRATION: '8h' + JWT_REFRESH_SECRET: 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2' # --- Numbering --- NUMBERING_LOCK_TIMEOUT: '5000' NUMBERING_RESERVATION_TTL: '300' @@ -72,9 +73,9 @@ services: - lcbp3 volumes: # Two-Phase Storage: จัดเก็บไฟล์นอก container - - '/share/dms-data/uploads/temp:/app/uploads/temp' - - '/share/dms-data/uploads/permanent:/app/uploads/permanent' - - '/share/dms-data/logs/backend:/app/logs' + - '/share/np-dms/data/uploads/temp:/app/uploads/temp' + - '/share/np-dms/data/uploads/permanent:/app/uploads/permanent' + - '/share/np-dms/data/logs/backend:/app/logs' healthcheck: test: ['CMD', 'curl', '-f', 'http://localhost:3000/health'] interval: 30s