본문으로 건너뛰기

NestJS 운영 가이드 (PostgreSQL 17 · MariaDB 11.4)

💡 요약 정리

  • Node.js 24 + NestJS 11 + TypeORM + TypeScript + pnpm 10 + PM2 6 (cluster x2) 환경의 운영 매뉴얼입니다.
  • DB는 PostgreSQL 17 또는 MariaDB 11.4를 사용합니다.
  • 앱은 127.0.0.1:3000에서 listen하며, nginx가 리버스 프록시로 외부 요청을 전달합니다.
  • systemd 서비스는 pm2-appuser.service 하나로 다중 앱을 관리합니다.
  • 배포 시 ecosystem.config.js와 환경변수 파일은 절대 업로드 금지입니다.

Node.js 24 + NestJS 11 + TypeORM + TypeScript + pnpm 10 + PM2 6 (cluster x2) 환경에서 PostgreSQL 17 또는 MariaDB 11.4를 사용하는 운영 매뉴얼.


1. 환경 매니페스트

항목
OSUbuntu 24.04 LTS
언어 / 런타임Node.js 24, TypeScript 5.x
프레임워크NestJS 11 + TypeORM
패키지 매니저pnpm 10
프로세스 매니저PM2 6 (cluster x2)
앱 listen127.0.0.1:3000
리버스 프록시nginx 1.24 (:80/:443, Let's Encrypt 자동)
앱 디렉토리/opt/[프로젝트]
환경변수/etc/[프로젝트]/env
로그/var/log/[프로젝트] (PM2 logs)
systemd 서비스pm2-appuser.service (공용 PM2 데몬)
ecosystem 파일/opt/[프로젝트]/ecosystem.config.js
빌드 산출물/opt/[프로젝트]/dist/main.js

DB 매니페스트

항목PostgreSQL 17MariaDB 11.4
인증peer auth (sudo psql)unix_socket auth (sudo mariadb)
포트54323306
서비스명postgresql@17-mainmariadb
TypeORM type'postgres''mysql' (또는 'mariadb')
드라이버pgmysql2

PM2 그룹 (Node.js — Express · NestJS · Next.js) 공통: systemd는 pm2-appuser.service 데몬 하나, 그 위에 ecosystem.config.js로 다중 앱 관리.


2. 서버 접속

2-1. SSH (root)

ssh root@[아이디].mycafe24.com
PROJECT_NAME=myapp; export PROJECT_NAME

2-2. SFTP / rsync

rsync -avz --exclude='node_modules/' --exclude='dist/' --exclude='.next/' \
  ./ root@[아이디].mycafe24.com:/opt/$PROJECT_NAME/
ssh root@... "chown -R appuser:appuser /opt/$PROJECT_NAME"

2-3. appuser로 작업 (디버깅)

sudo -u appuser bash
cd /opt/$PROJECT_NAME
pm2 status
pm2 logs $PROJECT_NAME --lines 50

3. 환경 확인

ls -la /opt/$PROJECT_NAME
sudo cat /etc/$PROJECT_NAME/env

# PM2 데몬 상태 (systemd)
sudo systemctl is-active pm2-appuser

# PM2 앱 목록
sudo -u appuser pm2 status
sudo -u appuser pm2 logs $PROJECT_NAME --lines 50

# 포트 listen (127.0.0.1만 정상)
ss -tlnp | grep 3000

# 헬스 체크
curl -sf http://127.0.0.1:3000/

4. DB 직접 접속

4-1. PostgreSQL 17

sudo -u postgres psql
\l
\du
\c [DB명]
\dt
\q

4-1'. MariaDB 11.4

sudo mariadb
SHOW DATABASES;
USE [DB명];
SHOW TABLES;
EXIT;

4-2. 앱 사용자로 접속

psql "postgresql://[DB사용자]:[비밀번호]@127.0.0.1:5432/[DB명]"
mariadb -u [DB사용자] -p [DB명]

4-3. 샘플 DDL

TypeORM이 자동 생성. 수동 점검:

PG:

SELECT table_name FROM information_schema.tables WHERE table_schema = 'public';

MariaDB:

SHOW TABLES;

5. 코드 배포 워크플로우

표준 시퀀스

# 1. 업로드
sudo -u appuser bash -c "cd /opt/$PROJECT_NAME && git pull"

# 2. 의존성 + 빌드
sudo -u appuser bash -c "cd /opt/$PROJECT_NAME && pnpm install && pnpm run build"

# 3. PM2 재시작 (env 자동 reload)
sudo -u appuser pm2 restart $PROJECT_NAME --update-env

# 4. 검증
curl -sf http://127.0.0.1:3000/
sudo -u appuser pm2 logs $PROJECT_NAME --lines 30

5-A. 배포 시 위험 회피

⚠️ 가장 중요: 자체 코드를 통째로 업로드하면 자동 구성된 DB·보안·PM2 ecosystem 설정이 사라져 서비스가 깨질 수 있습니다.

위험 5종 매트릭스

#위험증상NestJS 안전 패턴
1DB 환경변수 참조 깨짐HTTP 200은 떠도 모든 DB 쿼리 실패 (undefined / 인증 오류)process.env.DATABASE_URL 또는 process.env.DB_HOST 등 사용
2외부 IP에 listen외부에서 :3000 직접 접속 가능 → nginx 우회 → fail2ban·rate limit 무력화app.listen(3000, '127.0.0.1') 명시
3엔트리 포인트 변경PM2가 시작 시 파일 못 찾음 ("Cannot find module")dist/main.js 위치 유지 (ecosystem.config.js의 script)
4의존성 동기화 누락"Cannot find module" / TypeORM 매핑 실패업로드 후 pnpm install --frozen-lockfile && pnpm run build
5ecosystem.config.js 덮어씀env, instances 등 설정 손실 → DB 연결 실패ecosystem.config.js 절대 업로드 대상 포함 금지

안전한 main.ts 패턴

// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  // ✅ 127.0.0.1 명시 (외부 노출 차단)
  await app.listen(parseInt(process.env.PORT || '3000', 10), '127.0.0.1');
}
bootstrap();

안전한 TypeORM 모듈 (PG)

// src/app.module.ts
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      url: process.env.DATABASE_URL,
      // 또는 host/port/username 분리
      // host: process.env.DB_HOST,
      // port: parseInt(process.env.DB_PORT || '5432', 10),
      // username: process.env.DB_USER,
      // password: process.env.DB_PASSWORD,
      // database: process.env.DB_NAME,
      autoLoadEntities: true,
      synchronize: false,                          // 운영 환경에서 false 권장
    }),
  ],
})
export class AppModule {}

MariaDB 사용 시

TypeOrmModule.forRoot({
  type: 'mysql',
  url: process.env.DATABASE_URL,    // mysql://user:pass@127.0.0.1:3306/db
  charset: 'utf8mb4',
  // ...
})

절대 업로드 금지 파일

  • /opt/[프로젝트]/ecosystem.config.js
  • /etc/[프로젝트]/env

ecosystem.config.js 표준 (참고)

// /opt/[프로젝트]/ecosystem.config.js
module.exports = {
  apps: [{
    name: "[프로젝트]",
    script: "./dist/main.js",
    instances: 2,                                  // cluster x2
    exec_mode: "cluster",
    env_file: "/etc/[프로젝트]/env",
    max_memory_restart: "500M",
    error_file: "/var/log/[프로젝트]/error.log",
    out_file: "/var/log/[프로젝트]/out.log",
  }]
};

3가지 배포 방법 (안전 순서)

방법설명적합
방법 1 — dist/만 업로드로컬 빌드 후 dist만 (가장 안전, 의존성 동기화 불필요)일상 배포
방법 2 — 소스 통째 업로드 + 서버 빌드node_modules/, dist/ 제외 필수큰 변경, 의존성 추가
방법 3 — 외부 설정 분리DB 등은 env 파일만, 코드는 자유권장 베이스

배포 후 4가지 체크

sudo -u appuser pm2 status                      # 1. online 상태
ss -tlnp | grep 3000 | grep '127.0.0.1'         # 2. 127.0.0.1 listen
curl -sf http://127.0.0.1:3000/                 # 3. 헬스 OK
curl -sI https://[도메인]/                      # 4. 외부 응답

5-B. NestJS 배포 7단계

# 1. 업로드 (node_modules·dist 제외)
# 2. 권한
ssh root@... "chown -R appuser:appuser /opt/$PROJECT_NAME"

# 3. 의존성
sudo -u appuser bash -c "cd /opt/$PROJECT_NAME && pnpm install --frozen-lockfile"

# 4. 빌드
sudo -u appuser bash -c "cd /opt/$PROJECT_NAME && pnpm run build"

# 5. (env 변경 시) ecosystem 갱신은 PM2 reload + --update-env 필요
sudo -u appuser pm2 restart $PROJECT_NAME --update-env

# 6. PM2 save (재부팅 자동 복원)
sudo -u appuser pm2 save

# 7. 검증
curl -sI https://[도메인]/
sudo -u appuser pm2 logs $PROJECT_NAME --lines 30

자주 만나는 실수

실수결과회복
node_modules/ 통째 업로드OS/Node 버전 불일치로 native 모듈 깨짐rm -rf node_modules && pnpm install
dist/ 업로드 잊음 + 서버 빌드 안 함"Cannot find module './dist/main'"pnpm run build 실행
pm2 restart 만 하고 --update-env 누락변경된 env 미반영pm2 restart $PROJECT_NAME --update-env
synchronize: true 운영에 켬DB 스키마 의도치 않은 변경false로 변경, migration 사용

5-C. 환영 페이지 끄고 내 코드 띄우기 (필독)

설치 직후 도메인으로 접속하면 "서버가 정상 동작 중입니다" 환영 페이지가 표시됩니다. 이는 Nginx가 /var/www/cafe24-welcome/index.html을 우선 서빙하기 때문입니다. 본인 코드의 / 라우트가 보이게 하려면 이 환영 파일 1개만 정리하면 됩니다.

5-C-1. 환영 페이지 끄기

방법명령
방법 A — 삭제sudo rm /var/www/cafe24-welcome/index.html
방법 B — 백업 후 비활성 (원복 가능)sudo mv /var/www/cafe24-welcome/index.html /var/www/cafe24-welcome/index.html.bak
현재 상태 확인ls /var/www/cafe24-welcome/

5-C-2. 확인

도메인 재접속 또는 curl -sI https://[도메인]/ 결과로 본인 앱이 응답하는지 확인합니다. 캐시가 남으면 시크릿 창 또는 강제 새로고침을 권장합니다.

Tip: 환영 파일을 백업해두면 트러블슈팅 시 "Nginx는 살아있나" 확인용으로 다시 활성화할 수 있습니다.


6. DB 연동 실전 코드

Entity (TypeORM)

// src/entities/item.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm';

@Entity('items')
export class Item {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @Column({ type: 'jsonb', nullable: true })       // PG: jsonb / Maria: json
  payload: object;

  @CreateDateColumn({ name: 'created_at' })
  createdAt: Date;
}

Service / Controller

// src/items/items.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Item } from '../entities/item.entity';

@Injectable()
export class ItemsService {
  constructor(@InjectRepository(Item) private repo: Repository<Item>) {}

  list() {
    return this.repo.find({ order: { createdAt: 'DESC' }, take: 100 });
  }

  create(data: Partial<Item>) {
    return this.repo.save(data);
  }
}
// src/items/items.controller.ts
@Controller('items')
export class ItemsController {
  constructor(private svc: ItemsService) {}
  @Get() list() { return this.svc.list(); }
  @Post() create(@Body() body: any) { return this.svc.create(body); }
}

Migration

# 생성
sudo -u appuser bash -c "cd /opt/$PROJECT_NAME && pnpm typeorm migration:generate -- src/migrations/Init"

# 실행
sudo -u appuser bash -c '
  set -a; source /etc/$PROJECT_NAME/env; set +a
  cd /opt/$PROJECT_NAME && pnpm typeorm migration:run
'

7. 정적 파일 / 미디어

NestJS는 정적 파일을 ServeStaticModule로 처리할 수 있으나, nginx 직접 서빙 권장:

location /static/ {
    alias /opt/[프로젝트]/static/;
    expires 7d;
}

8. 로그 / 모니터링

# PM2 로그
sudo -u appuser pm2 logs $PROJECT_NAME            # 실시간
sudo -u appuser pm2 logs $PROJECT_NAME --lines 100
sudo -u appuser pm2 monit                         # CPU/메모리 모니터링

# 파일 로그
sudo tail -f /var/log/[프로젝트]/out.log
sudo tail -f /var/log/[프로젝트]/error.log

# nginx
sudo tail -f /var/log/nginx/[프로젝트]_access.log
sudo tail -f /var/log/nginx/[프로젝트]_error.log

logrotate (PM2)

sudo -u appuser pm2 install pm2-logrotate
sudo -u appuser pm2 set pm2-logrotate:max_size 50M
sudo -u appuser pm2 set pm2-logrotate:retain 30

9. HTTPS / 도메인

(공통)

sudo certbot --nginx -d example.com -d www.example.com
sudo systemctl status certbot.timer

10. 트러블슈팅 + FAQ

트러블슈팅 매트릭스

증상원인해결
HTTP 502NestJS 다운sudo -u appuser pm2 status + pm2 logs $PROJECT_NAME
Cannot find module './dist/main'빌드 누락pnpm run build
ECONNREFUSED (DB)DB 미기동PG: systemctl status postgresql@17-main / Maria: systemctl status mariadb
Native 모듈 빌드 실패OS/Node 버전 불일치rm -rf node_modules && pnpm install (서버에서)
env 변경 미반영--update-env 누락pm2 restart $PROJECT_NAME --update-env
메모리 누수 (점진적 OOM)코드 영역 / max_memory_restart 활용ecosystem max_memory_restart: '500M'

FAQ

Q. PM2가 재부팅 후 자동 시작 안 돼요

pm2 savepm2 startup으로 systemd 등록 (이미 pm2-appuser.service로 등록되어 있음). 새 앱 추가 시:

sudo -u appuser pm2 save

Q. cluster 모드인데 인스턴스 수 늘리고 싶어요

ecosystem.config.jsinstances 값 변경 후:

sudo -u appuser pm2 reload $PROJECT_NAME
sudo -u appuser pm2 save

주의: instances를 CPU 코어 수 이상으로 올리면 성능 저하. DEV A·B는 2 권장.

Q. PM2 명령은 root에서 안 되나요?

PM2는 사용자별 데몬. 반드시 sudo -u appuser pm2 ...로 실행.

Q. NestJS 시작 시 "synchronize: true" 위험

운영에서는 반드시 false. 스키마 변경은 migration으로.


12. 추가 운영 / 커스터마이징

추가 운영 및 커스터마이징이 필요한 경우, 공통 운영·커스터마이징 가이드와 FAQ·트러블슈팅 문서를 참고하시기 바랍니다. 그 외 궁금한 사항은 1:1 문의게시판을 통해 문의해 주세요.