Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 60 additions & 14 deletions run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,12 @@ check_docker() {

# 서비스 중지
stop_services() {
print_step "0. 현재 사용 중 이미지 stop, down"
print_step "0. 현재 사용 중인 서비스 중지"
docker compose stop || true
docker compose down || true
}

# Docker 리소스 정리
cleanup_docker() {
print_step "0.5. 사용하지 않는 Docker 리소스 정리"

Expand All @@ -53,37 +54,82 @@ cleanup_docker() {
echo -e "${GREEN}Docker 정리 완료${NC}"
}

# 이미지 업데이트
# 모든 이미지 업데이트 (API 포함)
update_images() {
print_step "1. 외부 이미지 업데이트 (fe, nginx)..."
docker compose pull fe nginx
}

# API 빌드
build_api() {
print_step "2. 로컬 이미지 빌드 (api)..."
docker compose build api
print_step "1. Docker Hub에서 최신 이미지 다운로드 중..."

# 모든 서비스의 이미지를 Docker Hub에서 최신 버전으로 pull
docker compose pull

echo -e "${GREEN}모든 이미지 업데이트 완료${NC}"
}

# 서비스 시작
start_services() {
print_step "3. 서비스 재시작..."
print_step "2. 서비스 시작 중..."
docker compose up -d

echo -e "${GREEN}모든 서비스가 시작되었습니다${NC}"
}

# 서비스 상태 확인
check_services() {
print_step "3. 서비스 상태 확인"

# 잠시 대기 (서비스 시작 시간 확보)
sleep 5

# 실행 중인 컨테이너 확인
echo -e "${YELLOW}실행 중인 컨테이너:${NC}"
docker compose ps

# 각 서비스 헬스체크
echo -e "\n${YELLOW}서비스 헬스체크:${NC}"

# API 서비스 확인
if curl -f http://localhost:8080/health &>/dev/null; then
echo -e "✅ API 서비스: ${GREEN}정상${NC}"
else
echo -e "❌ API 서비스: ${RED}응답 없음${NC}"
fi

# Frontend 서비스 확인 (포트 3000)
if curl -f http://localhost:3000 &>/dev/null; then
echo -e "✅ Frontend 서비스: ${GREEN}정상${NC}"
else
echo -e "❌ Frontend 서비스: ${RED}응답 없음${NC}"
fi

# Nginx 서비스 확인 (포트 80)
if curl -f http://localhost &>/dev/null; then
echo -e "✅ Nginx 서비스: ${GREEN}정상${NC}"
else
echo -e "❌ Nginx 서비스: ${RED}응답 없음${NC}"
fi
}

# 메인 실행 로직
main() {
set -e # 스크립트 실행 중 오류 발생 시 종료

print_step "Velog Dashboard V2 배포 스크립트 시작"

check_docker
stop_services
cleanup_docker
update_images
build_api
start_services
check_services

print_step "모든 작업이 완료되었습니다! 로그 모니터링을 시작합니다."
sleep 1
print_step "🎉 모든 작업이 완료되었습니다!"
echo -e "${GREEN}서비스 접속 정보:${NC}"
echo -e "• 메인 사이트: ${YELLOW}http://localhost${NC}"
echo -e "• API 서버: ${YELLOW}http://localhost:8080${NC}"
echo -e "• Frontend: ${YELLOW}http://localhost:3000${NC}"
echo -e "• API Health Check: ${YELLOW}http://localhost:8080/health${NC}"

echo -e "\n${YELLOW}로그 모니터링을 시작합니다... (Ctrl+C로 종료)${NC}"
sleep 2
docker compose logs -f
}

Expand Down
68 changes: 59 additions & 9 deletions src/configs/db.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const poolConfig: pg.PoolConfig = {
port: Number(process.env.POSTGRES_PORT),
max: 10, // 최대 연결 수
idleTimeoutMillis: 30000, // 연결 유휴 시간 (30초)
connectionTimeoutMillis: 5000, // 연결 시간 초과 (5초)
connectionTimeoutMillis: 10000, // 연결 시간 초과 (10초)
};

if (process.env.NODE_ENV === 'production') {
Expand All @@ -27,15 +27,65 @@ if (process.env.NODE_ENV === 'production') {

const pool = new Pool(poolConfig);

(async () => {
const client = await pool.connect();
/**
* 데이터베이스 연결을 초기화하고 TimescaleDB 확장을 보장
* 최대 3회 재시도하며, 실패 시 서버를 종료
*
* @throws 연결에 3회 실패하면 서버가 종료
* @returns {Promise<void>} 연결 및 확장 완료 시 resolve되는 프로미스
*/
export async function initializeDatabase(): Promise<void> {
const maxRetries = 3;
let delay = 800;

for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
logger.info(`데이터베이스 연결 시도 ${attempt}/${maxRetries}`);

const client = await pool.connect();

try {
// 연결 테스트
await client.query('SELECT 1');
logger.info('데이터베이스 연결 성공');

// TimescaleDB 확장 (필수)
await client.query('CREATE EXTENSION IF NOT EXISTS timescaledb;');
logger.info('TimescaleDB 확장 성공');

return; // 성공
} finally {
client.release();
}
} catch (error) {
logger.error(`데이터베이스 연결 실패 (시도 ${attempt}/${maxRetries}):`, error);

if (attempt === maxRetries) {
logger.error('데이터베이스 연결에 완전히 실패했습니다. 서버를 종료합니다.');
process.exit(1); // 연결 실패시 서버 종료
}

logger.info(`${delay}ms 후 재시도...`);
await new Promise((resolve) => setTimeout(resolve, delay));
delay = Math.floor(delay * 1.5);
}
}
}

/**
* 데이터베이스 커넥션 풀을 안정적으로 직접 종료하는 함수
*
* @throws 데이터베이스 연결 종료에 실패할 경우 에러
* @returns {Promise<void>} 데이터베이스 연결이 종료되면 resolve되는 프로미스
*/
export async function closeDatabase(): Promise<void> {
try {
await client.query('CREATE EXTENSION IF NOT EXISTS timescaledb;');
logger.info('TimescaleDB 확장 성공');
await pool.end();
logger.info('데이터베이스 연결이 정상적으로 종료되었습니다.');
} catch (error) {
logger.error('TimescaleDB 초기화 실패 : ', error);
} finally {
client.release();
logger.error('데이터베이스 연결 종료 중 오류 발생:', error);
throw error;
}
})();
}

export default pool;
100 changes: 72 additions & 28 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,89 @@
import app from '@/app';
import logger from '@/configs/logger.config';
import { closeCache } from './configs/cache.config';
import { closeDatabase, initializeDatabase } from './configs/db.config';
import { Server } from 'http';

const port = parseInt(process.env.PORT || '8080', 10);

const server = app.listen(port, () => {
logger.info(`Server running on port ${port}`);
logger.info(`Environment: ${process.env.NODE_ENV}`);
if (process.env.NODE_ENV !== 'production') {
logger.info(`API Docs: http://localhost:${port}/api-docs`);
async function startServer() {
try {
// 데이터베이스 초기화
await initializeDatabase();
logger.info('데이터베이스 초기화 완료');

// 서버 시작
const server = app.listen(port, () => {
logger.info(`Server running on port ${port}`);
logger.info(`Environment: ${process.env.NODE_ENV}`);
if (process.env.NODE_ENV !== 'production') {
logger.info(`API Docs: http://localhost:${port}/api-docs`);
}
logger.info(`Health Check: http://localhost:${port}/health`);
});

// Graceful shutdown 핸들러 설정
setupGracefulShutdown(server);
} catch (error) {
logger.error('서버 시작 중 오류 발생:', error);
process.exit(1);
}
logger.info(`Health Check: http://localhost:${port}/health`);
});
}

function setupGracefulShutdown(server: Server) {
// 기본적인 graceful shutdown 추가
const gracefulShutdown = async (signal: string) => {
logger.info(`${signal} received, shutting down gracefully`);

try {
// HTTP 서버 종료
await new Promise<void>((resolve) => {
server.close(() => {
logger.info('HTTP server closed');
resolve();
});
});

// 데이터베이스 연결 종료
await closeDatabase();

// 기본적인 graceful shutdown 추가
const gracefulShutdown = async (signal: string) => {
logger.info(`${signal} received, shutting down gracefully`);
await closeCache();
// 캐시 연결 종료
await closeCache();

server.close(() => {
logger.info('HTTP server closed');
process.exit(0);
logger.info('Graceful shutdown completed');
process.exit(0);
} catch (error) {
logger.error('Graceful shutdown 중 오류 발생:', error);
process.exit(1);
}
};

// 시그널 핸들러 등록
process.on('SIGTERM', async () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', async () => gracefulShutdown('SIGINT'));

// 예상치 못한 에러 처리
process.on('uncaughtException', async (error) => {
logger.error('Uncaught Exception:', error);
await gracefulShutdown('UNCAUGHT_EXCEPTION');
});

process.on('unhandledRejection', async (reason, promise) => {
logger.error('Unhandled Rejection at:', promise, 'reason:', reason);
await gracefulShutdown('UNHANDLED_REJECTION');
});

// 강제 종료 타이머 (10초)
setTimeout(() => {
const forceExitTimer = setTimeout(() => {
logger.error('Could not close connections in time, forcefully shutting down');
process.exit(1);
}, 10000);
};

process.on('SIGTERM', async () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', async () => gracefulShutdown('SIGINT'));

// 예상치 못한 에러 처리
process.on('uncaughtException', async (error) => {
logger.error('Uncaught Exception:', error);
await gracefulShutdown('UNCAUGHT_EXCEPTION');
});
// graceful shutdown이 완료되면 타이머 해제
process.on('exit', () => {
clearTimeout(forceExitTimer);
});
}

process.on('unhandledRejection', async (reason, promise) => {
logger.error('Unhandled Rejection at:', promise, 'reason:', reason);
await gracefulShutdown('UNHANDLED_REJECTION');
});
// 서버 시작
startServer();