본문으로 건너뛰기

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

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

💡 요약 정리

  • 런타임: Node.js 24 + Express 5, 프로세스 매니저는 PM2 6 클러스터 모드
  • DB: PostgreSQL 17 또는 MariaDB 11.4 선택 사용
  • 앱 listen: 127.0.0.1:3000 — 외부 직접 접속 차단, nginx 리버스 프록시 경유
  • 배포 시 핵심: ecosystem.config.js와 env 파일은 절대 업로드 금지
  • 환경변수: /etc/[프로젝트]/env에서 관리, 코드에서는 process.env로 참조

1. 환경 매니페스트

항목
OSUbuntu 24.04 LTS
언어 / 런타임Node.js 24
프레임워크Express 5
패키지 매니저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/[프로젝트]/index.js

DB 매니페스트

항목PostgreSQL 17MariaDB 11.4
인증peer authunix_socket auth
포트54323306
서비스명postgresql@17-mainmariadb
드라이버pgmysql2

2. 서버 접속

2-1. SSH (root)

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

2-2. SFTP / rsync

rsync -avz --exclude='node_modules/' \
  ./ 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:

CREATE TABLE items (
  id BIGSERIAL PRIMARY KEY,
  name TEXT NOT NULL,
  payload JSONB,
  created_at TIMESTAMPTZ DEFAULT now()
);

MariaDB:

CREATE TABLE items (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(255) NOT NULL,
  payload JSON,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

5. 코드 배포 워크플로우

표준 시퀀스

sudo -u appuser bash -c "cd /opt/$PROJECT_NAME && git pull && pnpm install"
sudo -u appuser pm2 restart $PROJECT_NAME --update-env
curl -sf http://127.0.0.1:3000/

5-A. 배포 시 위험 회피

⚠️ 가장 중요한 주의사항

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

위험 5종 매트릭스

#위험증상Express 안전 패턴
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")/opt/[프로젝트]/index.js 위치 유지 (ecosystem script)
4의존성 동기화 누락"Cannot find module" / 런타임 ReferenceError업로드 후 pnpm install --frozen-lockfile
5ecosystem.config.js 덮어씀env 변수 주입 안 됨 → DB 연결 실패ecosystem.config.js 절대 업로드 대상 포함 금지

안전한 index.js 패턴

// index.js
const express = require('express');
const app = express();

const PORT = parseInt(process.env.PORT || '3000', 10);
const HOST = '127.0.0.1';                            // ✅ 외부 노출 차단

// DB 클라이언트 (PG 예시)
const { Pool } = require('pg');
const pool = new Pool({ connectionString: process.env.DATABASE_URL });

app.use(express.json());

app.get('/', (req, res) => res.json({ status: 'ok' }));
app.get('/health', async (req, res) => {
  try {
    await pool.query('SELECT 1');
    res.json({ status: 'ok', db: 'ok' });
  } catch (e) {
    res.status(500).json({ status: 'error', error: e.message });
  }
});

app.listen(PORT, HOST, () => console.log(`listening on ${HOST}:${PORT}`));

MariaDB 사용 시

const mysql = require('mysql2/promise');
const pool = mysql.createPool({
  host: process.env.DB_HOST || '127.0.0.1',
  port: parseInt(process.env.DB_PORT || '3306', 10),
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME,
  charset: 'utf8mb4',
  connectionLimit: 10,
});

절대 업로드 금지

🚫 절대 업로드 금지 파일

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

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

방법설명적합
방법 1 — 변경 파일만 업로드rsync로 .js 파일만 (가장 안전, DB·PM2 설정 안전)일상 배포
방법 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. 외부 응답

배포 후 반드시 확인할 4가지

  1. pm2 status에서 online 상태 확인
  2. ss -tlnp에서 127.0.0.1로 listen 중인지 확인
  3. curl로 로컬 헬스체크 응답 확인
  4. 외부 도메인으로 HTTPS 응답 확인

5-B. Express 배포 7단계

# 1. 업로드 (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. (Express는 빌드 단계 없음)
# 5. PM2 재시작
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 버전 불일치rm -rf node_modules && pnpm install
app.listen(3000) 호스트 미명시0.0.0.0에 listen → 외부 노출app.listen(3000, '127.0.0.1')
pool 미사용, 매 요청 new Client()커넥션 누수Pool 사용 + 재사용

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 연동 실전 코드

CRUD (PG)

const express = require('express');
const { Pool } = require('pg');
const app = express();
const pool = new Pool({ connectionString: process.env.DATABASE_URL });

app.use(express.json());

app.get('/items', async (req, res) => {
  const { rows } = await pool.query(
    'SELECT id, name, payload, created_at FROM items ORDER BY created_at DESC LIMIT 100'
  );
  res.json(rows);
});

app.post('/items', async (req, res) => {
  const { name, payload } = req.body;
  const { rows } = await pool.query(
    'INSERT INTO items (name, payload) VALUES ($1, $2) RETURNING id',
    [name, payload]
  );
  res.json({ id: rows[0].id });
});

app.listen(3000, '127.0.0.1');

CRUD (MariaDB)

const mysql = require('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,
});

app.get('/items', async (req, res) => {
  const [rows] = await pool.execute(
    'SELECT id, name, payload, created_at FROM items ORDER BY created_at DESC LIMIT 100'
  );
  res.json(rows);
});

app.post('/items', async (req, res) => {
  const { name, payload } = req.body;
  const [r] = await pool.execute(
    'INSERT INTO items (name, payload) VALUES (?, ?)',
    [name, JSON.stringify(payload)]
  );
  res.json({ id: r.insertId });
});

7. 정적 파일 / 미디어

// app.use(express.static('public'));   // 작은 사이트만

큰 트래픽이면 nginx 직접 서빙:

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

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

📋 로그 관리 참고

  • PM2 로그는 /var/log/[프로젝트] 경로에 저장됩니다.
  • pm2-logrotate를 설치하면 로그 파일이 자동으로 로테이션됩니다.
  • max_size를 50M, retain을 30으로 설정하면 50MB 초과 시 로테이션되며 최근 30개 파일이 유지됩니다.

9. HTTPS / 도메인

sudo certbot --nginx -d example.com -d www.example.com

10. 트러블슈팅 + FAQ

트러블슈팅 매트릭스

증상원인해결
HTTP 502Express 다운pm2 status + pm2 logs
ECONNREFUSED (DB)DB 미기동systemctl status postgresql@17-main 또는 mariadb
메모리 누수pool 미사용 / closure 캐시max_memory_restart 임시 대처 + 코드 점검
EADDRINUSE :3000포트 점유pm2 list → 중복 앱 정리
env 변경 미반영--update-env 누락pm2 restart $PROJECT_NAME --update-env

FAQ

Q. cluster 모드에서 메모리 공유 안 됨

정상입니다. cluster 워커는 독립 프로세스이므로, Redis 등 외부 캐시로 공유해야 합니다.

Q. WebSocket 사용 가능한가요?

가능합니다. socket.io + nginx upgrade 헤더 설정이 필요합니다. 자세한 내용은 공통 운영 가이드의 nginx 레시피를 참조하세요.

Q. Express 5 변경점 (vs 4)

async 핸들러 native 지원으로, router.get('/', async (req, res) => {...}) 안에서 throw하면 automatic next(err)가 동작합니다. 다른 변경 사항은 Express 5 마이그레이션 가이드를 참조하세요.


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

추가적인 운영 및 커스터마이징이 필요한 경우, 공통 운영·커스터마이징 가이드와 FAQ·트러블슈팅 문서를 참조하세요.

💬 추가 도움이 필요하신가요?

운영 중 해결되지 않는 문제가 있다면 1:1 문의게시판을 통해 문의해 주세요.