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. 환경 매니페스트
| 항목 | 값 |
|---|---|
| OS | Ubuntu 24.04 LTS |
| 언어 / 런타임 | Node.js 24, TypeScript 5.x |
| 프레임워크 | NestJS 11 + TypeORM |
| 패키지 매니저 | pnpm 10 |
| 프로세스 매니저 | PM2 6 (cluster x2) |
| 앱 listen | 127.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 17 | MariaDB 11.4 |
|---|---|---|
| 인증 | peer auth (sudo psql) | unix_socket auth (sudo mariadb) |
| 포트 | 5432 | 3306 |
| 서비스명 | postgresql@17-main | mariadb |
| TypeORM type | 'postgres' | 'mysql' (또는 'mariadb') |
| 드라이버 | pg | mysql2 |
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 안전 패턴 |
|---|---|---|---|
| 1 | DB 환경변수 참조 깨짐 | 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 |
| 5 | ecosystem.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