diff --git a/.gitignore b/.gitignore index 2970737d..43bdc892 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ pnpm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* +*.log.* # OS .DS_Store @@ -53,5 +54,10 @@ lerna-debug.log* !.vscode/extensions.json db.sqlite +# Secret Keys *.crt -*.key \ No newline at end of file +*.key +*.pem + +# Production +data/* diff --git a/apps/backend/src/app.controller.ts b/apps/backend/src/app.controller.ts index cce879ee..cb318d6e 100644 --- a/apps/backend/src/app.controller.ts +++ b/apps/backend/src/app.controller.ts @@ -9,4 +9,12 @@ export class AppController { getHello(): string { return this.appService.getHello(); } + + @Get('health') + healthCheck() { + return { + status: 'ok', + timestamp: new Date().toISOString() + }; + } } diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts index d3dfdc0a..720b1d37 100644 --- a/apps/backend/src/app.module.ts +++ b/apps/backend/src/app.module.ts @@ -41,7 +41,8 @@ import { RedLockModule } from './red-lock/red-lock.module'; database: configService.get('DB_NAME'), entities: [Node, Page, Edge, User, Workspace, Role], logging: process.env.NODE_ENV === 'development', - synchronize: process.env.NODE_ENV === 'development', + // synchronize: process.env.NODE_ENV === 'development', + synchronize: true, }), }), NodeModule, diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts index c0cb1903..6eb8d71e 100644 --- a/apps/backend/src/main.ts +++ b/apps/backend/src/main.ts @@ -27,7 +27,7 @@ async function bootstrap() { app.enableCors({ origin: process.env.NODE_ENV === 'production' - ? ['https://octodocs.com', 'https://www.octodocs.com'] + ? ['https://octodocs.site', 'https://www.octodocs.site'] : process.env.origin, credentials: true, }); diff --git a/apps/websocket/package.json b/apps/websocket/package.json index b79cefc5..091d3088 100644 --- a/apps/websocket/package.json +++ b/apps/websocket/package.json @@ -17,22 +17,36 @@ "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.3.0", "@nestjs/core": "^10.0.0", + "@nestjs/mapped-types": "*", + "@nestjs/platform-express": "^10.0.0", "@nestjs/platform-socket.io": "^10.4.8", + "@nestjs/platform-ws": "^10.4.7", + "@nestjs/schedule": "^4.1.1", "@nestjs/typeorm": "^10.0.2", "@nestjs/websockets": "^10.4.8", + "@theinternetfolks/snowflake": "^1.3.0", + "@types/multer": "^1.4.12", + "@types/redlock": "^4.0.7", "axios": "^1.7.8", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "cookie-parser": "^1.4.7", "ioredis": "^5.4.1", "lib0": "^0.2.98", + "path": "^0.12.7", "pg": "^8.13.1", + "prosemirror-view": "^1.37.0", + "redlock": "^5.0.0-beta.2", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", "socket.io": "^4.8.1", "typeorm": "^0.3.20", + "uuid": "^11.0.3", + "ws": "^8.14.2", "y-prosemirror": "^1.2.12", "y-protocols": "^1.0.6", "y-socket.io": "^1.1.3", + "y-websocket": "^1.5.0", "yjs": "^13.6.8" }, "devDependencies": { diff --git a/apps/websocket/src/app.controller.ts b/apps/websocket/src/app.controller.ts new file mode 100644 index 00000000..3dd14bd9 --- /dev/null +++ b/apps/websocket/src/app.controller.ts @@ -0,0 +1,15 @@ +import { Controller, Get } from '@nestjs/common'; +import { AppService } from './app.service'; + +@Controller() +export class AppController { + constructor(private readonly appService: AppService) {} + + @Get('health') + healthCheck() { + return { + status: 'ok', + timestamp: new Date().toISOString() + }; + } +} diff --git a/apps/websocket/src/app.module.ts b/apps/websocket/src/app.module.ts index a999664a..63c9faac 100644 --- a/apps/websocket/src/app.module.ts +++ b/apps/websocket/src/app.module.ts @@ -2,6 +2,8 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import * as path from 'path'; import { YjsModule } from './yjs/yjs.module'; +import { AppController } from './app.controller'; +import { AppService } from './app.service'; @Module({ imports: [ @@ -11,5 +13,7 @@ import { YjsModule } from './yjs/yjs.module'; }), YjsModule, ], + controllers: [AppController], + providers: [AppService], }) export class AppModule {} diff --git a/apps/websocket/src/app.service.ts b/apps/websocket/src/app.service.ts new file mode 100644 index 00000000..7263d33a --- /dev/null +++ b/apps/websocket/src/app.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class AppService {} diff --git a/apps/websocket/src/main.ts b/apps/websocket/src/main.ts index 1d9386a8..e332b5e1 100644 --- a/apps/websocket/src/main.ts +++ b/apps/websocket/src/main.ts @@ -8,7 +8,7 @@ async function bootstrap() { app.enableCors({ origin: process.env.NODE_ENV === 'production' - ? ['https://octodocs.com', 'https://www.octodocs.com'] + ? ['https://octodocs.site', 'https://www.octodocs.site'] : process.env.origin, credentials: true, }); diff --git a/compose.init.yml b/compose.init.yml index b37fd7f4..c6119c3c 100644 --- a/compose.init.yml +++ b/compose.init.yml @@ -24,8 +24,8 @@ services: --email hihj070914@icloud.com --agree-tos --no-eff-email - -d octodocs.com - -d www.octodocs.com + -d octodocs.site + -d www.octodocs.site networks: frontend: diff --git a/compose.prod.yml b/compose.prod.yml index de893755..072e7f78 100644 --- a/compose.prod.yml +++ b/compose.prod.yml @@ -15,7 +15,10 @@ services: networks: - frontend depends_on: - - backend + backend: + condition: service_healthy + websocket: + condition: service_healthy backend: build: @@ -28,12 +31,46 @@ services: - .env.prod:/app/.env expose: - "3000" - - "1234" networks: - frontend - backend depends_on: - - redis + redis: + condition: service_healthy + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/api/health"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + restart: always + + websocket: + build: + context: . + dockerfile: ./services/websocket/Dockerfile.prod + image: websocket:latest + env_file: + - .env.prod + volumes: + - .env.prod:/app/apps/websocket/.env + expose: + - "4242" + networks: + - frontend + - backend + depends_on: + backend: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:4242/health"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + restart: always redis: image: redis:latest @@ -42,6 +79,13 @@ services: REDIS_PORT: ${REDIS_PORT} networks: - backend + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 30s + retries: 3 + start_period: 10s + timeout: 5s + restart: always certbot-renewer: image: certbot/certbot:latest @@ -50,6 +94,7 @@ services: - ./data/certbot/www:/var/www/certbot - ./data/certbot/log:/var/log/letsencrypt entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew --webroot --webroot-path=/var/www/certbot; sleep 12h & wait $${!}; done;'" + restart: always networks: frontend: diff --git a/package.json b/package.json index 3600ad00..d625b9e7 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,8 @@ "dev": "turbo run dev --parallel", "build": "turbo run build", "start": "node apps/backend/dist/main.js", + "start:backend": "node apps/backend/dist/main.js", + "start:websocket": "node apps/websocket/dist/main.js", "lint": "turbo run lint", "test": "turbo run test", "docker:dev": "docker compose -f compose.local.yml up", diff --git a/services/backend/Dockerfile.prod b/services/backend/Dockerfile.prod index 7d54212a..9ae8c037 100644 --- a/services/backend/Dockerfile.prod +++ b/services/backend/Dockerfile.prod @@ -3,14 +3,20 @@ FROM node:20-alpine as builder WORKDIR /app +# yarn 설정 추가 +RUN yarn config set network-timeout 300000 && \ + yarn config set network-concurrency 1 + # 의존성 파일 복사 COPY package.json yarn.lock ./ COPY turbo.json ./ COPY apps/backend/package.json ./apps/backend/ COPY apps/frontend/package.json ./apps/frontend/ -# 의존성 설치 -RUN yarn install --frozen-lockfile +# 의존성 설치 (재시도 옵션 추가) +RUN yarn install --frozen-lockfile --network-timeout 300000 || \ + yarn install --frozen-lockfile --network-timeout 300000 || \ + yarn install --frozen-lockfile --network-timeout 300000 # 소스 코드 복사 COPY . . @@ -23,16 +29,18 @@ FROM node:20-alpine WORKDIR /app +# wget 설치 +RUN apk add --no-cache wget + # 프로덕션에 필요한 파일만 복사 COPY --from=builder /app/package.json /app/yarn.lock ./ COPY --from=builder /app/apps/backend/package.json ./apps/backend/ COPY --from=builder /app/apps/backend/dist ./apps/backend/dist - -# 프로덕션 의존성만 설치 -RUN yarn install --frozen-lockfile --production +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/apps/backend/node_modules ./apps/backend/node_modules ENV NODE_ENV=production -EXPOSE 3000 1234 +EXPOSE 3000 -CMD ["yarn", "start"] +CMD ["yarn", "start:backend"] diff --git a/services/nginx/conf.d/prod_nginx.conf b/services/nginx/conf.d/prod_nginx.conf new file mode 100644 index 00000000..9ceb2602 --- /dev/null +++ b/services/nginx/conf.d/prod_nginx.conf @@ -0,0 +1,77 @@ +server { + listen 80; + server_name octodocs.site www.octodocs.site; + + # Certbot 인증용 경로 (최상단에 위치) + location ^~ /.well-known/acme-challenge/ { + root /var/www/certbot; + try_files $uri =404; + break; + } + + # 나머지 모든 HTTP 트래픽은 HTTPS로 리다이렉트 + location / { + return 301 https://$server_name$request_uri; + } +} + +server { + listen 443 ssl; + server_name octodocs.site www.octodocs.site; + + # Let's Encrypt 인증서 경로 + ssl_certificate /etc/letsencrypt/live/octodocs.site/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/octodocs.site/privkey.pem; + + # SSL 설정 + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers on; + + # 인증서가 없을 때 fallback + ssl_trusted_certificate /etc/letsencrypt/live/octodocs.site/chain.pem; + ssl_stapling on; + ssl_stapling_verify on; + + # 에러 페이지 설정 + error_page 497 https://$server_name$request_uri; + + # gzip 압축 설정 + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + + # 프론트엔드 정적 파일 + location / { + root /usr/share/nginx/html; + try_files $uri $uri/ /index.html; + expires 30d; + } + + # API 프록시 + location /api { + proxy_pass http://backend:3000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + } + + # Socket.IO 프록시 (일반 웹소켓) + location /socket.io { + proxy_pass http://websocket:4242; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; + } + + # Y-Socket.IO 프록시 (YJS 웹소켓) + location /flow-room { + proxy_pass http://websocket:4242; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; + } +} diff --git a/services/nginx/conf.d/prod_nginx_init.conf b/services/nginx/conf.d/prod_nginx_init.conf new file mode 100644 index 00000000..bb0ac612 --- /dev/null +++ b/services/nginx/conf.d/prod_nginx_init.conf @@ -0,0 +1,14 @@ +server { + listen 80; + server_name octodocs.site www.octodocs.site; + + # Certbot 인증용 경로 + location ^~ /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + # 나머지 요청에 대한 처리 (필요에 따라 수정) + location / { + return 200 '인증서 발급을 위한 임시 nginx 서버입니다.'; + } +} diff --git a/services/websocket/Dockerfile.prod b/services/websocket/Dockerfile.prod new file mode 100644 index 00000000..832dca51 --- /dev/null +++ b/services/websocket/Dockerfile.prod @@ -0,0 +1,43 @@ +# 빌드 스테이지 +FROM node:20-alpine as builder + +WORKDIR /app + +# yarn 설정 추가 +RUN yarn config set network-timeout 300000 && \ + yarn config set network-concurrency 1 + +# 패키지 파일 복사 +COPY package.json yarn.lock ./ +COPY apps/websocket/package.json ./apps/websocket/ +COPY turbo.json ./ + +# 의존성 설치 (재시도 옵션 추가) +RUN yarn install --frozen-lockfile --network-timeout 300000 || \ + yarn install --frozen-lockfile --network-timeout 300000 || \ + yarn install --frozen-lockfile --network-timeout 300000 + +# 소스 코드 복사 +COPY . . + +# 빌드 +RUN yarn turbo run build --filter=websocket + +# 프로덕션 스테이지 +FROM node:20-alpine + +WORKDIR /app + +# 빌드된 파일과 필요한 의존성만 복사 +COPY --from=builder /app/package.json /app/yarn.lock ./ +COPY --from=builder /app/apps/websocket/package.json ./apps/websocket/ +COPY --from=builder /app/apps/websocket/dist ./apps/websocket/dist +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/apps/websocket/node_modules ./apps/websocket/node_modules + +# 프로덕션 모드로 실행 +ENV NODE_ENV=production + +EXPOSE 4242 + +CMD ["yarn", "start:websocket"] \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index f595d040..62d36371 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2892,22 +2892,22 @@ resolved "https://registry.yarnpkg.com/@tanstack/history/-/history-1.85.3.tgz#c137958805e761659f97a2af9825a98a90d95a04" integrity sha512-62z1qXIILvjdkQyMTVPFQedHOc6kQgunz9GHV9jSy2z1ixsDqyI9GxNj3AWx8Ucmhjwd5/P+v3XN10bsb+FzRA== -"@tanstack/query-core@5.62.1": - version "5.62.1" - resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.62.1.tgz#0ef80db0832f7d96cf3f93b81470ce5a36e84478" - integrity sha512-thYv90GkMcfumgmtp6sptC18SqxWwXTCKUuk7jyeHHn7kYouh0VJrowuuBffAIBiR3Z8OnsccmPUnP1leKJBVQ== +"@tanstack/query-core@5.62.2": + version "5.62.2" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.62.2.tgz#4eef3201422f246788fb41d01662c2dea3136d9a" + integrity sha512-LcwVcC5qpsDpHcqlXUUL5o9SaOBwhNkGeV+B06s0GBoyBr8FqXPuXT29XzYXR36lchhnerp6XO+CWc84/vh7Zg== "@tanstack/react-query@^5.59.19": - version "5.62.1" - resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.62.1.tgz#90f3558a7a7c45e4387172df2ff15fe7b371d9ad" - integrity sha512-gb4eglrgW+yOeiNPkpqFyN8oLrFafHrHE+q2LzVl7TfyA4fuQluH92NTl6Jed7ae35v+BNtAQng9mykywWLzfA== + version "5.62.2" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.62.2.tgz#fbcb8f991ddcf484ce7968fb58bb4790d6c98cd3" + integrity sha512-fkTpKKfwTJtVPKVR+ag7YqFgG/7TRVVPzduPAUF9zRCiiA8Wu305u+KJl8rCrh98Qce77vzIakvtUyzWLtaPGA== dependencies: - "@tanstack/query-core" "5.62.1" + "@tanstack/query-core" "5.62.2" "@tanstack/react-router@^1.82.12": - version "1.85.3" - resolved "https://registry.yarnpkg.com/@tanstack/react-router/-/react-router-1.85.3.tgz#3cb1fc3fa7a6efdd0e1ee5e2ca98b86e9e9c7baf" - integrity sha512-QgX3fuc0W941TE6PDM2YmMdF2WrDFIv7DN3ATuCiezUF4Fn5xvB0QUy9Uabdn/FQWugbnplG/REGCHtzem59uQ== + version "1.85.4" + resolved "https://registry.yarnpkg.com/@tanstack/react-router/-/react-router-1.85.4.tgz#c3e242005fcb29d480ea5175305ef446069714be" + integrity sha512-CmrgrMtIIVnXS/og5W3glP1noBlFSD3mRaBgP6nTEbkZZ25nN1s52C35dnsMNuAQayQsVJNlkuSNZv3elfrBmA== dependencies: "@tanstack/history" "1.85.3" "@tanstack/react-store" "^0.6.1" @@ -2924,9 +2924,9 @@ use-sync-external-store "^1.2.2" "@tanstack/router-devtools@^1.82.12": - version "1.85.3" - resolved "https://registry.yarnpkg.com/@tanstack/router-devtools/-/router-devtools-1.85.3.tgz#bd03f2766909c766678284acfa913b0696c8b5a0" - integrity sha512-t9YrxnEhKEAXHfLQOgpDkqgDu479PoOXeFjfcti5Iok/oH5taxKXAZ0+2r/1yfU2TzcY4QFFNJJYt9PUFlprjA== + version "1.85.4" + resolved "https://registry.yarnpkg.com/@tanstack/router-devtools/-/router-devtools-1.85.4.tgz#891398920189f520a9aa04aed66e2c48e1a593c8" + integrity sha512-Io8qyv5WMYa+gk83/dQ0OlcOsvCxSY2PTNEYUQ4wHfbEV1RW3IjwOVL6/n7p7TDhjD5ou4eKufF9zDykdyFldQ== dependencies: clsx "^2.1.1" goober "^2.1.16" @@ -4815,9 +4815,9 @@ camelcase@^6.2.0: integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001669: - version "1.0.30001685" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001685.tgz#2d10d36c540a9a5d47ad6ab9e1ed5f61fdeadd8c" - integrity sha512-e/kJN1EMyHQzgcMEEgoo+YTCO1NGCmIYHk5Qk8jT6AazWemS5QFKJ5ShCJlH3GZrNIdZofcNCEwZqbMjjKzmnA== + version "1.0.30001686" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001686.tgz#0e04b8d90de8753188e93c9989d56cb19d902670" + integrity sha512-Y7deg0Aergpa24M3qLC5xjNklnKnhsmSyR/V89dLZ1n0ucJIFNs7PgR2Yfa/Zf6W79SbBicgtGxZr2juHkEUIA== chalk@4.1.2, chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: version "4.1.2" @@ -5487,11 +5487,16 @@ dotenv-expand@10.0.0: resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-10.0.0.tgz#12605d00fb0af6d0a592e6558585784032e4ef37" integrity sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A== -dotenv@16.4.5, dotenv@^16.0.3: +dotenv@16.4.5: version "16.4.5" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== +dotenv@^16.0.3: + version "16.4.6" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.6.tgz#fc88e8a664087abf3e19d61e21f7feee1849bbb1" + integrity sha512-JhcR/+KIjkkjiU8yEpaB/USlzVi3i5whwOjpIRNGi9svKEXZSe+Qp6IWAjFjv+2GViAoDRCUv/QLNziQxsLqDg== + eastasianwidth@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" @@ -7565,9 +7570,9 @@ lib0@^0.2.31, lib0@^0.2.42, lib0@^0.2.52, lib0@^0.2.85, lib0@^0.2.98: isomorphic.js "^0.2.4" libphonenumber-js@^1.10.53: - version "1.11.15" - resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.11.15.tgz#0947ba02208cf6c44fdf3b07e097a98b3ec945f4" - integrity sha512-M7+rtYi9l5RvMmHyjyoF3BHHUpXTYdJ0PezZGHNs0GyW1lO+K7jxlXpbdIb7a56h0nqLYdjIw+E+z0ciGaJP7g== + version "1.11.16" + resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.11.16.tgz#3aa64a8a95ffc59253a5df3009940a9604a02102" + integrity sha512-Noyazmt0yOvnG0OeRY45Cd1ur8G7Z0HWVkuCuKe+yysGNxPQwBAODBQQ40j0AIagi9ZWurfmmZWNlpg4h4W+XQ== lilconfig@^2.1.0: version "2.1.0"