diff --git a/apps/server/Dockerfile b/apps/server/Dockerfile new file mode 100644 index 0000000..249fd0a --- /dev/null +++ b/apps/server/Dockerfile @@ -0,0 +1,40 @@ +# syntax=docker/dockerfile:1 + +# +# 빌드 컨텍스트 = 저장소 루트 (packages/* 와 pnpm-lock.yaml 접근 필요): +# docker buildx build --platform linux/arm64 -f apps/server/Dockerfile -t . +# +# EC2가 Graviton(ARM)이므로 반드시 linux/arm64 로 빌드한다. +# x86 로 빌드하면 인스턴스에서 실행되지 않는다. + +FROM node:22.13.0-slim AS base +# Prisma 엔진이 요구하는 OpenSSL, 아웃바운드 HTTPS(SMTP/OAuth/ECR)용 CA 인증서 +RUN apt-get update \ + && apt-get install -y --no-install-recommends openssl ca-certificates \ + && rm -rf /var/lib/apt/lists/* +# corepack 번들 버전의 서명키가 오래돼 최신 pnpm 검증에 실패하므로 npm 으로 직접 설치. +RUN npm install -g pnpm@10.15.0 +WORKDIR /app + +# 워크스페이스 전체 복사 (.dockerignore 가 node_modules/dist/.env 등을 제외). +# frozen-lockfile 검증을 위해 모든 워크스페이스 package.json 이 필요하므로 전체 복사한다. +COPY . . + +# server 서브트리 의존성만 설치 (web/extension 미포함). 락파일 고정. +RUN pnpm install --frozen-lockfile --filter server... + +# Prisma 클라이언트 생성. generate 는 DB 연결이 불필요하지만 +# prisma.config.ts 가 process.env.DATABASE_URL 을 읽으므로 더미 값을 준다. +RUN DATABASE_URL="postgresql://user:pass@localhost:5432/db?schema=public" \ + pnpm --filter server exec prisma generate + +# webpack 번들 빌드 → apps/server/dist/main.js (단일 파일, @dookmark/* 인라인) +RUN pnpm --filter server build + +ENV NODE_ENV=production +ENV PORT=3001 +WORKDIR /app/apps/server +EXPOSE 3001 + +# 컨테이너는 평문 HTTP(:3001)만 처리한다. TLS 는 앞단 ALB 가 종단. +CMD ["node", "dist/main.js"] diff --git a/apps/server/package.json b/apps/server/package.json index 75fda27..b18c408 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -6,7 +6,7 @@ "private": true, "license": "UNLICENSED", "scripts": { - "build": "nest build", + "build": "nest build --webpack --webpackPath webpack.config.js", "dev": "nest start --watch", "start": "nest start", "start:dev": "nest start --watch", @@ -83,7 +83,8 @@ "ts-loader": "^9.5.2", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", - "typescript": "^5.7.3" + "typescript": "^5.7.3", + "webpack-node-externals": "^3.0.0" }, "jest": { "moduleFileExtensions": [ diff --git a/apps/server/src/app.module.ts b/apps/server/src/app.module.ts index ca0ef27..9228e17 100644 --- a/apps/server/src/app.module.ts +++ b/apps/server/src/app.module.ts @@ -11,6 +11,7 @@ import { BookmarkModule } from './bookmark/bookmark.module'; import { PrismaExceptionFilter } from './common/filters/prisma-exception.filter'; import { RedisModule } from './common/redis/redis.module'; import { validateEnv } from './config/env.validation'; +import { HealthController } from './health/health.controller'; import { PrismaModule } from './prisma/prisma.module'; import { SessionModule } from './session/session.module'; import { UserModule } from './user/user.module'; @@ -33,7 +34,7 @@ import { UserModule } from './user/user.module'; ]), ScheduleModule.forRoot(), ], - controllers: [AppController], + controllers: [AppController, HealthController], providers: [ AppService, { diff --git a/apps/server/src/health/health.controller.ts b/apps/server/src/health/health.controller.ts new file mode 100644 index 0000000..a7204c0 --- /dev/null +++ b/apps/server/src/health/health.controller.ts @@ -0,0 +1,12 @@ +import { Controller, Get } from '@nestjs/common'; +import { SkipThrottle } from '@nestjs/throttler'; + +// ALB 타깃 그룹 헬스체크용 경량 liveness 엔드포인트. +@SkipThrottle() +@Controller('health') +export class HealthController { + @Get() + check() { + return { status: 'ok', timestamp: new Date().toISOString() }; + } +} diff --git a/apps/server/webpack.config.js b/apps/server/webpack.config.js new file mode 100644 index 0000000..3367958 --- /dev/null +++ b/apps/server/webpack.config.js @@ -0,0 +1,29 @@ +const nodeExternals = require('webpack-node-externals'); + +/** + * `nest build --webpack` 전용 설정. + * + * 워크스페이스 패키지(@dookmark/*)는 컴파일되지 않은 raw TypeScript를 + * main 필드로 내보낸다(예: @dookmark/analytics의 DASHBOARD_PERIODS 같은 + * 런타임 값). + * + * tsc만 쓰면 dist에 require("@dookmark/analytics")가 남아 + * 런타임에 .ts를 로드하려다 크래시한다. + * + * => webpack으로 @dookmark/* 소스만 번들에 인라인하고, 나머지 실제 + * npm 의존성(@nestjs/*, prisma, socket.io, bcrypt 등)은 externalize해 + * node_modules에서 그대로 require하게 한다. + * ts-loader가 실제 tsc로 + * 컴파일하므로 NestJS 데코레이터 메타데이터(emitDecoratorMetadata)도 보존된다. + * + * dev(nest start)는 이 설정을 쓰지 않으므로 개발 경험은 그대로다. + */ +module.exports = (options) => ({ + ...options, + externals: [ + nodeExternals({ + // @dookmark/* 는 externalize하지 말고 번들에 포함 + allowlist: [/^@dookmark\//], + }), + ], +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 60abded..ac158a4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -274,6 +274,9 @@ importers: typescript: specifier: ^5.7.3 version: 5.9.3 + webpack-node-externals: + specifier: ^3.0.0 + version: 3.0.0 apps/web: dependencies: