Next.js 운영 가이드 (PostgreSQL 17 · MariaDB 11.4)
💡 요약 정리
- Node.js 24 + Next.js(App Router) + pnpm 10 + PM2 6(cluster) 환경의 운영 매뉴얼입니다.
- DB는 PostgreSQL 17 또는 MariaDB 11.4를 사용합니다.
- Next.js는 빌드 필수이며, DEV A(2GB)에서 OOM 가능 — DEV B(4GB) 이상 권장합니다.
- 배포 시
ecosystem.config.js와/etc/[프로젝트]/env는 절대 업로드 금지입니다. - 무중단 배포에는
pm2 restart가 아닌 pm2 reload를 사용합니다.
Node.js 24 + Next.js(App Router) + pnpm 10 + PM2 6(cluster) 환경에서 PostgreSQL 17 또는 MariaDB 11.4를 사용하는 운영 매뉴얼.
1. 환경 매니페스트
| 항목 | 값 |
|---|---|
| OS | Ubuntu 24.04 LTS |
| 언어 / 런타임 | Node.js 24, TypeScript 5.x |
| 프레임워크 | Next.js(App Router) |
| 패키지 매니저 | pnpm 10 |
| 프로세스 매니저 | PM2 6(cluster) |
| 앱 listen | 127.0.0.1:3000 |
| 리버스 프록시 | nginx 1.24 |
| 앱 디렉토리 | /opt/[프로젝트] |
| 환경변수 | /etc/[프로젝트]/env |
| 로그 | /var/log/[프로젝트](PM2 logs) |
| systemd 서비스 | pm2-appuser.service |
| ecosystem 파일 | /opt/[프로젝트]/ecosystem.config.js |
| 빌드 산출물 | /opt/[프로젝트]/.next/ |
⚠️ 메모리 주의: Next.js 빌드(next build)는 메모리를 많이 씁니다. DEV A(2GB)에서 OOM 가능. DEV B(4GB) 이상 권장.
DB 매니페스트
| 항목 | PostgreSQL 17 | MariaDB 11.4 |
|---|---|---|
| 인증 | peer auth | unix_socket auth |
| 포트 | 5432 | 3306 |
| 서비스명 | postgresql@17-main | mariadb |
| 드라이버 | pg, @vercel/postgres, prisma | mysql2, prisma |
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='.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
sudo systemctl is-active pm2-appuser
sudo -u appuser pm2 status
sudo -u appuser pm2 logs $PROJECT_NAME --lines 50
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(PG/Maria)
-- PG
CREATE TABLE posts (
id BIGSERIAL PRIMARY KEY,
slug TEXT UNIQUE NOT NULL,
title TEXT NOT NULL,
content TEXT,
metadata JSONB,
created_at TIMESTAMPTZ DEFAULT now()
);
-- MariaDB
CREATE TABLE posts (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
slug VARCHAR(255) UNIQUE NOT NULL,
title VARCHAR(500) NOT NULL,
content TEXT,
metadata JSON,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
5. 코드 배포 워크플 로우
표준 시퀀스(Next.js는 빌드 필수)
# 1. 업로드 (.next, node_modules 제외)
sudo -u appuser bash -c "cd /opt/$PROJECT_NAME && git pull"
# 2. 의존성
sudo -u appuser bash -c "cd /opt/$PROJECT_NAME && pnpm install --frozen-lockfile"
# 3. 빌드 (메모리 주의 — DEV A는 OOM 가능)
sudo -u appuser bash -c "cd /opt/$PROJECT_NAME && pnpm build"
# 4. PM2 reload (무중단 배포)
sudo -u appuser pm2 reload $PROJECT_NAME --update-env
# 5. 검증
curl -sf http://127.0.0.1:3000/
sudo -u appuser pm2 logs $PROJECT_NAME --lines 30
💡 Next.js는 무중단 배포에 pm2 reload(restart 아님) 권장.
5-A. ⚠️ 배포 시 위험 회피(★ 가장 중요)
자체 코드를 통째로 업로드하면 자동 구성된 DB·보안·PM2 ecosystem 설정이 사라져 서비스가 깨질 수 있습니다.
위험 5종 매트릭스
| # | 위험 | 증상 | Next.js 안전 패턴 |
|---|---|---|---|
| 1 | DB 환경변수 참조 깨짐 | HTTP 200은 떠도 모든 DB 쿼리 실패(undefined / 인증 오류) | process.env.DATABASE_URL(서버 컴포넌트/Route Handler에서) |
| 2 | 외부 IP에 listen | 외부에서 :3000 직접 접속 가능 → nginx 우회 → fail2ban·rate limit 무력화 | next start -H 127.0.0.1 -p 3000(ecosystem.config.js의 args) |
| 3 | 빌드 산출물 누락 | PM2 시작 실패 / 페이지 404 | .next/ 디렉토리 누락 → pnpm build 실행 |
| 4 | 의존성 동기화 누락 | "Cannot find module" / 빌드 실패 | 업로드 후 pnpm install --frozen-lockfile && pnpm build |
| 5 | ecosystem.config.js 덮어씀 | env, instances 등 설정 손실 → DB 연결 실패 | ecosystem.config.js 절대 업로드 대상 포함 금지 |
안전한 next.config.js 패턴
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone', // Docker/단독 배포에 유리 (선택)
// 외부 도메인 이미지 사용 시
images: {
remotePatterns: [{ protocol: 'https', hostname: 'example.com' }],
},
};
module.exports = nextConfig;
안전한 환경변수 사용
// app/api/posts/route.ts
import { Pool } from 'pg';
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export async function GET() {
const { rows } = await pool.query('SELECT * FROM posts LIMIT 100');
return Response.json(rows);
}
클라이언트 컴포넌트("use client")에서는 process.env.DATABASE_URL 같은 서버 비밀 변수를 참조하지 마세요. 빌드 시 클라이언트 번들에 노출됩니다. 클라이언트에서 사용해야 한다면 NEXT_PUBLIC_ 접두어를 붙여 환경변수를 별도 정의하세요.
ecosystem.config.js 표준
module.exports = {
apps: [{
name: "[프로젝트]",
script: "node_modules/next/dist/bin/next",
args: "start -H 127.0.0.1 -p 3000",
cwd: "/opt/[프로젝트]",
instances: 2,
exec_mode: "cluster",
env_file: "/etc/[프로젝트]/env",
max_memory_restart: "800M",
error_file: "/var/log/[프로젝트]/error.log",
out_file: "/var/log/[프로젝트]/out.log",
}]
};
절대 업로드 금지
-
/opt/[프로젝트]/ecosystem.config.js -
/etc/[프로젝트]/env
3가지 배포 방법(안전 순서)
| 방법 | 설명 | 적합 |
|---|---|---|
| 방법 1 — .next/ + 의존성만 업로드 | 로컬 빌드 후 .next/+package.json(가장 안전) | 일상 배포 |
| 방법 2 — 소스 통째 업로드 + 서버 빌드 | node_modules/, .next/ 제외 필수, 빌드 메모리 800MB+ | 큰 변경, 의존성 추가 |
| 방법 3 — 외부 설정 분리 | DB 등은 env 파일만, 코드는 자유 | 권장 베이스 |
⚠️ DEV A(2GB)에서 빌드 OOM 가능성 → swap 추가 또는 로컬 빌드 후 산출물 업로드 권장(공통 운영 가이드 §8 참고).
배포 후 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. Next.js 배포 7단계
# 1. 업로드 (.next, node_modules 제외)
# 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 build"
# 5. PM2 reload (무중단)
sudo -u appuser pm2 reload $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
자주 만나는 실수
| 실수 | 결과 | 회복 |
|---|---|---|
pnpm build 누락 | "ENOENT: .next" | pnpm build 후 reload |
| 빌드 중 OOM(DEV A) | JavaScript heap out of memory | swap 추가, 또는 DEV B+ 변경, 또는 NODE_OPTIONS=--max-old-space-size=1536 |
node_modules/ 통째 업로드 | OS/Node 불일치 | rm -rf node_modules && pnpm install |
| 클라이언트에서 비밀 env 참조 | 빌드 번들에 비밀값 노출 | 서버 컴포넌트/Route Handler로 이동 |
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 연동 실전 코드
Route Handler(App Router) — PG
// app/api/posts/route.ts
import { Pool } from 'pg';
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export async function GET() {
const { rows } = await pool.query(
'SELECT id, slug, title, created_at FROM posts ORDER BY created_at DESC LIMIT 100'
);
return Response.json(rows);
}
export async function POST(req: Request) {
const body = await req.json();
const { rows } = await pool.query(
'INSERT INTO posts (slug, title, content) VALUES ($1, $2, $3) RETURNING id',
[body.slug, body.title, body.content]
);
return Response.json({ id: rows[0].id });
}
Route Handler — MariaDB
import mysql from 'mysql2/promise';
const pool = mysql.createPool({
host: '127.0.0.1', port: 3306,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
charset: 'utf8mb4',
connectionLimit: 10,
});
export async function GET() {
const [rows] = await pool.execute(
'SELECT id, slug, title, created_at FROM posts ORDER BY created_at DESC LIMIT 100'
);
return Response.json(rows);
}
Server Component(PG)
// app/posts/page.tsx
import { Pool } from 'pg';
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export default async function PostsPage() {
const { rows } = await pool.query('SELECT * FROM posts ORDER BY created_at DESC LIMIT 20');
return (
<ul>
{rows.map((p: any) => <li key={p.id}>{p.title}</li>)}
</ul>
);
}
Prisma 사용 시(선택)
sudo -u appuser bash -c '
set -a; source /etc/$PROJECT_NAME/env; set +a
cd /opt/$PROJECT_NAME && pnpm prisma migrate deploy
'
7. 정적 파일 / 미디어
Next.js는 public/ 폴더의 파일을 자동 서빙합니다. 큰 정적 파일이 많으면 nginx에 위임 + Next 캐시 헤더 활용:
# Next.js 빌드 산출물 정적 파일은 nginx로 직접
location /_next/static/ {
alias /opt/[프로젝트]/.next/static/;
expires 365d;
immutable;
}
8. 로그 / 모니터링
sudo -u appuser pm2 logs $PROJECT_NAME
sudo -u appuser pm2 monit
sudo tail -f /var/log/[프로젝트]/out.log
sudo tail -f /var/log/[프로젝트]/error.log
sudo tail -f /var/log/nginx/[프로젝트]_access.log
logrotate
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
도메인 변경 시 Next.js의 next.config.js의 images.remotePatterns도 같이 갱신하세요.
10. 트러블슈팅 + FAQ
트러블슈팅 매트릭스
| 증상 | 원인 | 해결 |
|---|---|---|
| HTTP 502 | Next 다운 또는 빌드 실패 | pm2 logs + pnpm build |
| 빌드 OOM | DEV A 메모리 부족 | swap 추가, DEV B+, 또는 NODE_OPTIONS=--max-old-space-size=1536 |
Module not found | 의존성 미설치 | pnpm install |
| ECONNREFUSED(DB) | DB 미기동 | systemctl status postgresql@17-main 또는 mariadb |
| SSR 캐시 미반영 | 빌드 후 reload 안 함 | pm2 reload $PROJECT_NAME --update-env |
| 이미지 도메인 차단 | next.config.js 미등록 | images.remotePatterns에 도메인 추가 |
| 클라이언트에 비밀 노출 | process.env.SECRET 클라이언트에서 참조 | 서버 컴포넌트로 이동 |
FAQ
Q. 빌드 OOM 발생
# 임시 메모리 한도 상향
NODE_OPTIONS="--max-old-space-size=1536" pnpm build
# 또는 swap 추가 (DEV A)
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
# 또는 DEV B 이상으로 변경 (가장 안정적)
Q. next start 호스트 명시는 어디서?
ecosystem.config.js의 args: "start -H 127.0.0.1 -p 3000". 호스트 미명시 시 0.0.0.0에 listen → 외부 노출 위험.
Q. ISR / 캐시 디렉토리는?
.next/cache/. 배포 시 보존하면 ISR 페이지 재생성 비용 절감. rsync --exclude='.next/cache/' 하지 않기.
Q. output: 'standalone' 사용 시 배포가 다르나요?
.next/standalone/ 안에 자체 server.js + 필요한 node_modules가 들어갑니다. node .next/standalone/server.js로 실행. PM2 ecosystem의 script를 그쪽으로 변경 가능.
Q. 환경변수 변경 후 빌드 다시 해야 하나요?
- NEXT_PUBLIC_ 변경* → 빌드 필수(클라이언트 번들에 인라인됨)
- 서버용 변수 → 빌드 불필요,
pm2 restart --update-env만
12. 추가 운영 / 커스터마이징
추가적인 공통 운영·커스터마이징 사항은 공통 운영·커스터마이징 가이드를 참조해 주세요.
트러블슈팅 관련 상세 내용은 FAQ·트러블슈팅 문서를 참조해 주세요.