본문으로 건너뛰기

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. 환경 매니페스트

항목
OSUbuntu 24.04 LTS
언어 / 런타임Node.js 24, TypeScript 5.x
프레임워크Next.js(App Router)
패키지 매니저pnpm 10
프로세스 매니저PM2 6(cluster)
앱 listen127.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 17MariaDB 11.4
인증peer authunix_socket auth
포트54323306
서비스명postgresql@17-mainmariadb
드라이버pg, @vercel/postgres, prismamysql2, 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 안전 패턴
1DB 환경변수 참조 깨짐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
5ecosystem.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 memoryswap 추가, 또는 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.jsimages.remotePatterns도 같이 갱신하세요.


10. 트러블슈팅 + FAQ

트러블슈팅 매트릭스

증상원인해결
HTTP 502Next 다운 또는 빌드 실패pm2 logs + pnpm build
빌드 OOMDEV 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.jsargs: "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·트러블슈팅 문서를 참조해 주세요.