- Express.js 기반 백엔드 API 서버 - MariaDB, Redis, phpMyAdmin Docker 환경 - Device 관리 기본 CRUD 구현 - Mac Mini M4 Pro 전용 설정 및 배포 스크립트 - 자동화된 설치 및 배포 시스템 - 완전한 문서화 및 실행 가이드
50 KiB
50 KiB
홈 관리 시스템 DB 구축 계획서
🎯 프로젝트 개요
Git 저장소
- Repository: https://git.hyungi.net/hyungi/myhome-server.git
- Gitea Server: git.hyungi.net (DS1525+ 호스팅)
목표
- 홈 IoT 기기 모니터링 및 관리
- 전력 소비, 네트워크 트래픽 추적
- 시스템 리소스 모니터링
- 향후 개인 서비스 확장 기반 마련
기술 스택
Backend: Express.js (Node.js)
Database: MariaDB 11.x
Admin Tool: phpMyAdmin
Architecture: MVC + Service Layer
Code Style: 모듈형 구조 (500자 이하 함수)
🗄️ 데이터베이스 설계
데이터베이스 생성
CREATE DATABASE home_management
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
CREATE USER 'homeuser'@'localhost' IDENTIFIED BY 'secure_password';
GRANT ALL PRIVILEGES ON home_management.* TO 'homeuser'@'localhost';
FLUSH PRIVILEGES;
테이블 설계
1. 디바이스 관리 (devices)
CREATE TABLE devices (
id INT AUTO_INCREMENT PRIMARY KEY,
device_id VARCHAR(50) UNIQUE NOT NULL,
name VARCHAR(100) NOT NULL,
device_type ENUM('server', 'nas', 'router', 'smart_plug', 'other') NOT NULL,
location VARCHAR(50),
ip_address VARCHAR(45),
mac_address VARCHAR(17),
power_rating_watts INT,
monitoring_enabled BOOLEAN DEFAULT TRUE,
metadata JSON,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_device_id (device_id),
INDEX idx_type_enabled (device_type, monitoring_enabled)
) ENGINE=InnoDB;
2. 전력 소비 데이터 (power_consumption)
CREATE TABLE power_consumption (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
device_id VARCHAR(50) NOT NULL,
timestamp TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3),
watts DECIMAL(8,2) NOT NULL,
voltage DECIMAL(6,2),
current DECIMAL(6,3),
kwh_total DECIMAL(10,4),
metadata JSON,
INDEX idx_device_time (device_id, timestamp),
INDEX idx_timestamp (timestamp),
INDEX idx_watts (watts),
FOREIGN KEY (device_id) REFERENCES devices(device_id) ON DELETE CASCADE
) ENGINE=InnoDB
PARTITION BY RANGE (UNIX_TIMESTAMP(timestamp)) (
PARTITION p_2025_q1 VALUES LESS THAN (UNIX_TIMESTAMP('2025-04-01')),
PARTITION p_2025_q2 VALUES LESS THAN (UNIX_TIMESTAMP('2025-07-01')),
PARTITION p_2025_q3 VALUES LESS THAN (UNIX_TIMESTAMP('2025-10-01')),
PARTITION p_2025_q4 VALUES LESS THAN (UNIX_TIMESTAMP('2026-01-01')),
PARTITION p_future VALUES LESS THAN MAXVALUE
);
3. 네트워크 트래픽 (network_traffic)
CREATE TABLE network_traffic (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
device_mac VARCHAR(17) NOT NULL,
device_name VARCHAR(100),
timestamp TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3),
bytes_in BIGINT UNSIGNED NOT NULL,
bytes_out BIGINT UNSIGNED NOT NULL,
packets_in INT UNSIGNED,
packets_out INT UNSIGNED,
connection_count INT UNSIGNED,
metadata JSON,
INDEX idx_mac_time (device_mac, timestamp),
INDEX idx_timestamp (timestamp)
) ENGINE=InnoDB;
4. 시스템 리소스 (system_resources)
CREATE TABLE system_resources (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
server_name VARCHAR(50) NOT NULL,
timestamp TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3),
cpu_percent DECIMAL(5,2),
memory_used_gb DECIMAL(6,2),
memory_total_gb DECIMAL(6,2),
disk_used_gb DECIMAL(8,2),
disk_total_gb DECIMAL(8,2),
network_io JSON,
temperature DECIMAL(4,1),
load_average JSON,
processes_count INT,
uptime_seconds BIGINT,
INDEX idx_server_time (server_name, timestamp)
) ENGINE=InnoDB;
5. 서비스 상태 (service_status)
CREATE TABLE service_status (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
service_name VARCHAR(50) NOT NULL,
server_name VARCHAR(50) NOT NULL,
timestamp TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3),
status ENUM('online', 'offline', 'degraded', 'maintenance') NOT NULL,
response_time_ms INT UNSIGNED,
cpu_usage DECIMAL(5,2),
memory_usage_mb INT,
error_message TEXT,
metadata JSON,
INDEX idx_service_time (service_name, timestamp),
INDEX idx_server_service (server_name, service_name),
INDEX idx_status (status, timestamp)
) ENGINE=InnoDB;
6. 사용자 관리 (users)
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE,
password_hash VARCHAR(255),
full_name VARCHAR(100),
role ENUM('admin', 'family', 'guest') DEFAULT 'family',
preferences JSON,
last_login TIMESTAMP NULL,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_username (username),
INDEX idx_email (email),
INDEX idx_role (role)
) ENGINE=InnoDB;
7. 알림 규칙 (alert_rules)
CREATE TABLE alert_rules (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
description TEXT,
condition_type ENUM('threshold', 'change', 'pattern', 'custom') NOT NULL,
target_table VARCHAR(50) NOT NULL,
target_field VARCHAR(50) NOT NULL,
operator ENUM('>', '<', '>=', '<=', '=', '!=', 'contains') NOT NULL,
threshold_value DECIMAL(15,4),
time_window_minutes INT DEFAULT 5,
severity ENUM('info', 'warning', 'critical') DEFAULT 'warning',
notification_methods JSON,
is_active BOOLEAN DEFAULT TRUE,
cooldown_minutes INT DEFAULT 60,
last_triggered TIMESTAMP NULL,
created_by INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (created_by) REFERENCES users(id),
INDEX idx_active (is_active),
INDEX idx_target (target_table, target_field)
) ENGINE=InnoDB;
8. 알림 로그 (alert_logs)
CREATE TABLE alert_logs (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
alert_rule_id INT NOT NULL,
triggered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
trigger_value DECIMAL(15,4),
message TEXT,
severity ENUM('info', 'warning', 'critical') NOT NULL,
notification_sent BOOLEAN DEFAULT FALSE,
resolved_at TIMESTAMP NULL,
metadata JSON,
FOREIGN KEY (alert_rule_id) REFERENCES alert_rules(id) ON DELETE CASCADE,
INDEX idx_rule_time (alert_rule_id, triggered_at),
INDEX idx_severity (severity, triggered_at)
) ENGINE=InnoDB;
🚀 Express.js 프로젝트 구조
폴더 구조
home-management-api/
├── src/
│ ├── config/
│ │ ├── database.js # DB 연결 설정
│ │ ├── redis.js # Redis 캐시 설정
│ │ └── environment.js # 환경변수 관리
│ ├── models/
│ │ ├── index.js # 모델 인덱스
│ │ ├── Device.js # 디바이스 모델
│ │ ├── PowerConsumption.js # 전력 소비 모델
│ │ ├── NetworkTraffic.js # 네트워크 트래픽 모델
│ │ ├── SystemResource.js # 시스템 리소스 모델
│ │ ├── ServiceStatus.js # 서비스 상태 모델
│ │ ├── User.js # 사용자 모델
│ │ ├── AlertRule.js # 알림 규칙 모델
│ │ └── AlertLog.js # 알림 로그 모델
│ ├── controllers/
│ │ ├── deviceController.js # 디바이스 CRUD
│ │ ├── powerController.js # 전력 데이터 처리
│ │ ├── networkController.js # 네트워크 데이터 처리
│ │ ├── systemController.js # 시스템 모니터링
│ │ ├── dashboardController.js # 대시보드 데이터
│ │ ├── authController.js # 인증/권한
│ │ └── alertController.js # 알림 관리
│ ├── services/
│ │ ├── deviceService.js # 디바이스 비즈니스 로직
│ │ ├── powerService.js # 전력 분석 서비스
│ │ ├── networkService.js # 네트워크 분석 서비스
│ │ ├── statisticsService.js # 통계 계산 서비스
│ │ ├── alertService.js # 알림 처리 서비스
│ │ ├── cacheService.js # 캐시 관리 서비스
│ │ └── schedulerService.js # 스케줄 작업 서비스
│ ├── routes/
│ │ ├── index.js # 라우터 인덱스
│ │ ├── devices.js # 디바이스 라우터
│ │ ├── power.js # 전력 라우터
│ │ ├── network.js # 네트워크 라우터
│ │ ├── system.js # 시스템 라우터
│ │ ├── dashboard.js # 대시보드 라우터
│ │ ├── auth.js # 인증 라우터
│ │ └── alerts.js # 알림 라우터
│ ├── middleware/
│ │ ├── auth.js # 인증 미들웨어
│ │ ├── validation.js # 데이터 검증
│ │ ├── rateLimit.js # 요청 제한
│ │ ├── errorHandler.js # 에러 처리
│ │ ├── logger.js # 로깅 미들웨어
│ │ └── cors.js # CORS 설정
│ ├── utils/
│ │ ├── logger.js # 로깅 유틸
│ │ ├── validation.js # 검증 함수들
│ │ ├── dateUtils.js # 날짜 처리 유틸
│ │ ├── mathUtils.js # 수학 계산 유틸
│ │ ├── encryption.js # 암호화 유틸
│ │ └── responseUtils.js # 응답 포매팅
│ ├── collectors/
│ │ ├── powerCollector.js # 전력 데이터 수집
│ │ ├── networkCollector.js # 네트워크 데이터 수집
│ │ ├── systemCollector.js # 시스템 데이터 수집
│ │ └── serviceCollector.js # 서비스 상태 수집
│ └── app.js # Express 앱 설정
├── tests/
│ ├── unit/ # 단위 테스트
│ ├── integration/ # 통합 테스트
│ └── fixtures/ # 테스트 데이터
├── docs/
│ ├── api/ # API 문서
│ └── database/ # DB 스키마 문서
├── scripts/
│ ├── setup-db.sql # 초기 DB 설정
│ ├── seed-data.sql # 샘플 데이터
│ └── migrate.js # 마이그레이션 스크립트
├── docker-compose.yml # Docker 구성
├── package.json
├── .env.example
└── README.md
📦 Package.json 구성
주요 의존성
{
"name": "home-management-api",
"version": "1.0.0",
"description": "홈 관리 시스템 백엔드 API",
"main": "src/app.js",
"scripts": {
"start": "node src/app.js",
"dev": "nodemon src/app.js",
"test": "jest",
"test:watch": "jest --watch",
"db:migrate": "node scripts/migrate.js",
"db:seed": "node scripts/seed.js",
"lint": "eslint src/",
"lint:fix": "eslint src/ --fix"
},
"dependencies": {
"express": "^4.18.2",
"mysql2": "^3.6.0",
"sequelize": "^6.32.1",
"redis": "^4.6.7",
"bcryptjs": "^2.4.3",
"jsonwebtoken": "^9.0.2",
"joi": "^17.9.2",
"helmet": "^7.0.0",
"cors": "^2.8.5",
"compression": "^1.7.4",
"express-rate-limit": "^6.8.1",
"winston": "^3.10.0",
"node-cron": "^3.0.2",
"dotenv": "^16.3.1",
"express-validator": "^7.0.1",
"moment": "^2.29.4"
},
"devDependencies": {
"nodemon": "^3.0.1",
"jest": "^29.6.1",
"supertest": "^6.3.3",
"eslint": "^8.45.0",
"prettier": "^3.0.0"
}
}
🐳 Docker Compose 구성
docker-compose.yml
version: '3.8'
services:
mariadb:
image: mariadb:11-jammy
container_name: home_mariadb
environment:
MYSQL_ROOT_PASSWORD: root_password
MYSQL_DATABASE: home_management
MYSQL_USER: homeuser
MYSQL_PASSWORD: home_password
ports:
- "3306:3306"
volumes:
- mariadb_data:/var/lib/mysql
- ./scripts/setup-db.sql:/docker-entrypoint-initdb.d/setup.sql
- ./config/mariadb.cnf:/etc/mysql/conf.d/custom.cnf
command: >
--character-set-server=utf8mb4
--collation-server=utf8mb4_unicode_ci
--innodb-buffer-pool-size=2G
--innodb-log-file-size=256M
--max-connections=200
--query-cache-size=256M
restart: unless-stopped
networks:
- home_network
phpmyadmin:
image: phpmyadmin/phpmyadmin:latest
container_name: home_phpmyadmin
environment:
PMA_HOST: mariadb
PMA_PORT: 3306
PMA_USER: homeuser
PMA_PASSWORD: home_password
UPLOAD_LIMIT: 2G
MEMORY_LIMIT: 512M
ports:
- "8080:80"
depends_on:
- mariadb
restart: unless-stopped
networks:
- home_network
redis:
image: redis:7-alpine
container_name: home_redis
ports:
- "6379:6379"
volumes:
- redis_data:/data
- ./config/redis.conf:/usr/local/etc/redis/redis.conf
command: redis-server /usr/local/etc/redis/redis.conf
restart: unless-stopped
networks:
- home_network
api:
build: .
container_name: home_api
environment:
NODE_ENV: development
DB_HOST: mariadb
DB_PORT: 3306
DB_NAME: home_management
DB_USER: homeuser
DB_PASSWORD: home_password
REDIS_HOST: redis
REDIS_PORT: 6379
JWT_SECRET: your-jwt-secret-key
API_PORT: 3000
ports:
- "3000:3000"
volumes:
- ./src:/app/src
- ./logs:/app/logs
depends_on:
- mariadb
- redis
restart: unless-stopped
networks:
- home_network
volumes:
mariadb_data:
redis_data:
networks:
home_network:
driver: bridge
🔧 환경 설정
.env.example
# 서버 설정
NODE_ENV=development
API_PORT=3000
API_HOST=0.0.0.0
# 데이터베이스 설정
DB_HOST=localhost
DB_PORT=3306
DB_NAME=home_management
DB_USER=homeuser
DB_PASSWORD=home_password
DB_POOL_MIN=5
DB_POOL_MAX=20
DB_TIMEOUT=30000
# Redis 설정
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_DB=0
REDIS_CACHE_TTL=3600
# JWT 설정
JWT_SECRET=your-super-secret-jwt-key
JWT_EXPIRES_IN=24h
JWT_REFRESH_EXPIRES_IN=7d
# 로깅 설정
LOG_LEVEL=info
LOG_FILE_PATH=./logs/app.log
LOG_MAX_SIZE=10m
LOG_MAX_FILES=5
# 알림 설정
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your-email@gmail.com
SMTP_PASSWORD=your-app-password
EMAIL_FROM=Home Management System <noreply@home.local>
# 데이터 수집 설정
POWER_COLLECTION_INTERVAL=300000 # 5분
NETWORK_COLLECTION_INTERVAL=60000 # 1분
SYSTEM_COLLECTION_INTERVAL=30000 # 30초
# API 제한 설정
RATE_LIMIT_WINDOW=900000 # 15분
RATE_LIMIT_MAX_REQUESTS=100
# 보안 설정
BCRYPT_ROUNDS=12
CORS_ORIGIN=http://localhost:3001,https://yourdomain.com
📊 MariaDB 최적화 설정
config/mariadb.cnf
[mariadb]
# 기본 설정
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
init-connect = 'SET NAMES utf8mb4'
# 메모리 최적화 (Mac Mini M4 Pro 64GB 기준)
innodb_buffer_pool_size = 4G
innodb_log_buffer_size = 64M
innodb_log_file_size = 512M
key_buffer_size = 256M
sort_buffer_size = 4M
read_buffer_size = 2M
read_rnd_buffer_size = 8M
thread_cache_size = 50
table_open_cache = 4000
# 연결 설정
max_connections = 200
max_user_connections = 180
wait_timeout = 600
interactive_timeout = 600
# 쿼리 캐시
query_cache_type = 1
query_cache_size = 256M
query_cache_limit = 2M
# InnoDB 최적화
innodb_flush_log_at_trx_commit = 2
innodb_flush_method = O_DIRECT
innodb_file_per_table = 1
innodb_io_capacity = 2000
innodb_io_capacity_max = 4000
innodb_read_io_threads = 4
innodb_write_io_threads = 4
# 시계열 데이터 최적화
innodb_compression_default = ON
innodb_page_compression = ON
innodb_adaptive_hash_index = ON
# 로깅
general_log = OFF
slow_query_log = ON
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 2
# 바이너리 로그 (백업/복제용)
log_bin = mysql-bin
binlog_format = ROW
expire_logs_days = 7
max_binlog_size = 100M
🔍 초기 데이터 및 인덱스
scripts/setup-db.sql
-- 기본 관리자 사용자 생성
INSERT INTO users (username, email, password_hash, full_name, role) VALUES
('admin', 'admin@home.local', '$2b$12$encrypted_password_hash', '시스템 관리자', 'admin'),
('family', 'family@home.local', '$2b$12$encrypted_password_hash', '가족 사용자', 'family');
-- 기본 디바이스 등록
INSERT INTO devices (device_id, name, device_type, location, monitoring_enabled) VALUES
('mac_mini_m4', 'Mac Mini M4 Pro', 'server', '서재', TRUE),
('ds1525plus', 'Synology DS1525+', 'nas', '서재', TRUE),
('rt6600ax', 'Synology RT6600ax', 'router', '거실', TRUE);
-- 기본 알림 규칙
INSERT INTO alert_rules (name, description, condition_type, target_table, target_field, operator, threshold_value, severity, notification_methods, created_by) VALUES
('높은 CPU 사용률', 'CPU 사용률이 80% 이상일 때 알림', 'threshold', 'system_resources', 'cpu_percent', '>', 80.0, 'warning', '["email"]', 1),
('높은 전력 소비', '전력 소비가 평소보다 50% 이상 증가했을 때', 'change', 'power_consumption', 'watts', '>', 50.0, 'warning', '["email"]', 1),
('서비스 다운', '서비스가 오프라인 상태일 때', 'threshold', 'service_status', 'status', '=', 'offline', 'critical', '["email", "push"]', 1);
-- 파티션 관리 프로시저
DELIMITER //
CREATE PROCEDURE CreateMonthlyPartitions()
BEGIN
DECLARE partition_date DATE;
DECLARE partition_name VARCHAR(20);
DECLARE partition_value BIGINT;
SET partition_date = DATE_ADD(NOW(), INTERVAL 3 MONTH);
SET partition_name = CONCAT('p_', DATE_FORMAT(partition_date, '%Y_%m'));
SET partition_value = UNIX_TIMESTAMP(DATE_ADD(partition_date, INTERVAL 1 MONTH));
SET @sql = CONCAT(
'ALTER TABLE power_consumption ADD PARTITION (',
'PARTITION ', partition_name,
' VALUES LESS THAN (', partition_value, '))'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END //
DELIMITER ;
-- 월별 파티션 자동 생성 이벤트
CREATE EVENT monthly_partition_maintenance
ON SCHEDULE EVERY 1 MONTH
STARTS CURRENT_TIMESTAMP
DO CALL CreateMonthlyPartitions();
-- 성능 모니터링용 뷰
CREATE VIEW device_power_summary AS
SELECT
d.device_id,
d.name,
d.device_type,
AVG(pc.watts) as avg_watts,
MAX(pc.watts) as max_watts,
COUNT(*) as reading_count,
MAX(pc.timestamp) as last_reading
FROM devices d
LEFT JOIN power_consumption pc ON d.device_id = pc.device_id
WHERE d.monitoring_enabled = TRUE
GROUP BY d.device_id, d.name, d.device_type;
CREATE VIEW system_health_summary AS
SELECT
server_name,
AVG(cpu_percent) as avg_cpu,
AVG(memory_used_gb / memory_total_gb * 100) as avg_memory_pct,
AVG(disk_used_gb / disk_total_gb * 100) as avg_disk_pct,
MAX(timestamp) as last_update
FROM system_resources
WHERE timestamp >= DATE_SUB(NOW(), INTERVAL 1 HOUR)
GROUP BY server_name;
🔄 CI/CD 파이프라인 구성
개발 환경 구성
MacBook Pro M3 Pro (개발/빌드)
Mac Mini M4 Pro (개발/빌드)
↓ git push
Gitea Server (git.hyungi.net - DS1525+ Container)
↓ webhook
개발 머신 (MacBook Pro / Mac Mini)
↓ docker build & test
Synology DS1525+ (프로덕션 배포)
개발 환경 특징
- MacBook Pro M3 Pro: 이동성이 필요한 개발 작업
- Mac Mini M4 Pro: 고정된 개발 환경, 장시간 빌드/테스트
- 공통 환경: 동일한 ARM64 아키텍처로 일관된 빌드 환경
- 유연한 작업: 두 머신 모두에서 완전한 개발 사이클 가능
CI/CD 워크플로우
1. Gitea Actions 설정 (.gitea/workflows/deploy.yml)
name: Home Management CI/CD
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: self-hosted
container:
image: node:18-alpine
options: --network host
services:
mariadb:
image: mariadb:11-jammy
env:
MARIADB_ROOT_PASSWORD: test_password
MARIADB_DATABASE: home_management_test
MARIADB_USER: testuser
MARIADB_PASSWORD: test_password
ports:
- 3307:3306
options: >-
--health-cmd="mariadb-admin ping -h localhost"
--health-interval=10s
--health-timeout=5s
--health-retries=5
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Cache Node modules
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install dependencies
run: npm ci
- name: Wait for MariaDB
run: |
for i in {1..30}; do
if nc -z localhost 3307; then break; fi
echo "Waiting for MariaDB... ($i/30)"
sleep 2
done
- name: Run database migrations
run: npm run db:migrate
env:
NODE_ENV: test
DB_HOST: localhost
DB_PORT: 3307
DB_NAME: home_management_test
DB_USER: testuser
DB_PASSWORD: test_password
- name: Run unit tests
run: npm run test:unit
env:
NODE_ENV: test
DB_HOST: localhost
DB_PORT: 3307
- name: Run integration tests
run: npm run test:integration
env:
NODE_ENV: test
DB_HOST: localhost
DB_PORT: 3307
- name: Generate coverage report
run: npm run test:coverage
- name: Lint check
run: npm run lint
- name: Security audit
run: npm audit --audit-level high
build:
needs: test
runs-on: self-hosted
if: gitea.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set build info
run: |
echo "BUILD_TIME=$(date -Iseconds)" >> $GITHUB_ENV
echo "COMMIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
echo "BRANCH_NAME=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV
- name: Build Docker image
run: |
docker build \
--build-arg BUILD_TIME=$BUILD_TIME \
--build-arg COMMIT_HASH=$COMMIT_HASH \
--build-arg BRANCH_NAME=$BRANCH_NAME \
-t home-management-api:$COMMIT_HASH \
-t home-management-api:latest .
- name: Run container security scan
run: |
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy image home-management-api:latest
- name: Save Docker image
run: |
docker save home-management-api:latest | gzip > home-management-api.tar.gz
- name: Record deployment start
run: |
curl -X POST http://localhost:3000/api/deployments/start \
-H "Content-Type: application/json" \
-d '{
"deployment_id": "${{ gitea.run_id }}",
"commit_hash": "${{ env.COMMIT_HASH }}",
"branch": "${{ env.BRANCH_NAME }}",
"stage": "build"
}'
- name: Deploy to NAS
run: |
scp -o StrictHostKeyChecking=no \
home-management-api.tar.gz \
admin@ds1525plus:/volume1/docker/images/
ssh -o StrictHostKeyChecking=no admin@ds1525plus \
"cd /volume1/docker && ./deploy-api.sh $COMMIT_HASH ${{ gitea.run_id }}"
- name: Record deployment result
if: always()
run: |
STATUS=${{ job.status == 'success' && 'success' || 'failed' }}
curl -X POST http://localhost:3000/api/deployments/complete \
-H "Content-Type: application/json" \
-d '{
"deployment_id": "${{ gitea.run_id }}",
"status": "'$STATUS'",
"stage": "deploy"
}'
2. Gitea Webhook 설정
# Gitea 서버에서 webhook 설정
# git.hyungi.net > myhome-server > Settings > Webhooks > Add Webhook
Payload URL: http://developer-machine:9000/webhook # MacBook Pro 또는 Mac Mini IP
Content Type: application/json
Secret: your-webhook-secret
Events: Push events, Pull request events
# 개발 머신에서 webhook 리스너 설정 (webhook.js)
const express = require('express');
const crypto = require('crypto');
const { execSync } = require('child_process');
const app = express();
app.use(express.json());
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
const GITEA_ACTIONS_PATH = '/Users/admin/gitea-actions';
function verifySignature(payload, signature) {
const computedSignature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(`sha256=${computedSignature}`),
Buffer.from(signature)
);
}
app.post('/webhook', (req, res) => {
const signature = req.headers['x-gitea-signature'];
const payload = JSON.stringify(req.body);
if (!verifySignature(payload, signature)) {
return res.status(401).send('Unauthorized');
}
const { repository, ref, commits } = req.body;
if (ref === 'refs/heads/main' || ref === 'refs/heads/develop') {
console.log(`Received push to ${ref} in ${repository.full_name}`);
try {
// Gitea Actions Runner 트리거
execSync(`cd ${GITEA_ACTIONS_PATH} && ./act_runner exec`,
{ stdio: 'inherit' });
res.status(200).send('Workflow triggered');
} catch (error) {
console.error('Failed to trigger workflow:', error);
res.status(500).send('Workflow trigger failed');
}
} else {
res.status(200).send('No action needed');
}
});
app.listen(9000, () => {
console.log('Webhook server listening on port 9000');
});
2. DS1525+ 배포 스크립트 (/volume1/docker/deploy-api.sh)
#!/bin/bash
# 배포 설정
IMAGE_NAME="home-management-api"
CONTAINER_NAME="home-api"
BACKUP_DIR="/volume1/backups/deployments"
LOG_FILE="/volume1/logs/deploy.log"
echo "$(date): Starting deployment..." >> $LOG_FILE
# 1. 기존 컨테이너 백업
if [ "$(docker ps -q -f name=$CONTAINER_NAME)" ]; then
echo "Creating backup of current deployment..." >> $LOG_FILE
docker commit $CONTAINER_NAME $IMAGE_NAME:backup-$(date +%Y%m%d-%H%M%S)
docker stop $CONTAINER_NAME
docker rm $CONTAINER_NAME
fi
# 2. 새 이미지 로드
echo "Loading new Docker image..." >> $LOG_FILE
gunzip -c /volume1/docker/images/home-management-api.tar.gz | docker load
# 3. 데이터베이스 마이그레이션 체크
echo "Checking database migrations..." >> $LOG_FILE
docker run --rm --network=host -e NODE_ENV=production $IMAGE_NAME:latest npm run db:migrate:check
# 4. 새 컨테이너 시작
echo "Starting new container..." >> $LOG_FILE
docker run -d \
--name $CONTAINER_NAME \
--network=host \
--restart=unless-stopped \
-v /volume1/docker/config/api.env:/app/.env \
-v /volume1/logs/api:/app/logs \
-v /volume1/data/uploads:/app/uploads \
$IMAGE_NAME:latest
# 5. 헬스체크
echo "Performing health check..." >> $LOG_FILE
sleep 10
for i in {1..30}; do
if curl -f http://localhost:3000/health > /dev/null 2>&1; then
echo "$(date): Deployment successful!" >> $LOG_FILE
# 이전 백업 이미지 정리 (7일 이상 된 것)
docker images | grep "$IMAGE_NAME:backup" | awk '{print $2}' | tail -n +8 | xargs -I {} docker rmi $IMAGE_NAME:{}
exit 0
fi
echo "Waiting for service to start... ($i/30)" >> $LOG_FILE
sleep 2
done
echo "$(date): Deployment failed! Rolling back..." >> $LOG_FILE
# 롤백 로직
LATEST_BACKUP=$(docker images | grep "$IMAGE_NAME:backup" | head -1 | awk '{print $2}')
if [ ! -z "$LATEST_BACKUP" ]; then
docker stop $CONTAINER_NAME 2>/dev/null
docker rm $CONTAINER_NAME 2>/dev/null
docker run -d --name $CONTAINER_NAME --network=host --restart=unless-stopped $IMAGE_NAME:backup-$LATEST_BACKUP
fi
exit 1
3. Gitea Actions Runner 설정 (개발 머신)
# Gitea Actions Runner 설치 및 설정 (MacBook Pro / Mac Mini 공통)
cd /Users/$(whoami)
wget https://gitea.com/gitea/act_runner/releases/download/v0.2.6/act_runner-0.2.6-darwin-arm64
chmod +x act_runner-0.2.6-darwin-arm64
sudo mv act_runner-0.2.6-darwin-arm64 /usr/local/bin/act_runner
# Runner 등록 (Gitea에서 토큰 생성 후)
act_runner register \
--instance https://git.hyungi.net \
--token YOUR_RUNNER_TOKEN \
--name $(hostname)-runner \
--labels self-hosted,macOS,ARM64
# 설정 파일 생성 (.runner 파일이 생성됨)
# 데몬으로 실행하기 위한 LaunchDaemon 설정
sudo tee /Library/LaunchDaemons/com.gitea.act_runner.plist << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.gitea.act_runner</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/act_runner</string>
<string>daemon</string>
<string>--config</string>
<string>/Users/$(whoami)/.runner</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/usr/local/var/log/act_runner.log</string>
<key>StandardErrorPath</key>
<string>/usr/local/var/log/act_runner.error.log</string>
<key>WorkingDirectory</key>
<string>/Users/$(whoami)</string>
<key>UserName</key>
<string>$(whoami)</string>
</dict>
</plist>
EOF
# 서비스 시작
sudo launchctl load /Library/LaunchDaemons/com.gitea.act_runner.plist
sudo launchctl start com.gitea.act_runner
4. DS1525+ 배포 스크립트 업데이트
#!/bin/bash
# /volume1/docker/deploy-api.sh
# 배포 설정
IMAGE_NAME="home-management-api"
CONTAINER_NAME="home-api"
BACKUP_DIR="/volume1/backups/deployments"
LOG_FILE="/volume1/logs/deploy.log"
COMMIT_HASH=$1
DEPLOYMENT_ID=$2
echo "$(date): Starting deployment $DEPLOYMENT_ID (commit: $COMMIT_HASH)..." >> $LOG_FILE
# 배포 상태를 API에 보고
function report_status() {
local status=$1
local message=$2
curl -s -X POST http://localhost:3000/api/deployments/update \
-H "Content-Type: application/json" \
-d "{
\"deployment_id\": \"$DEPLOYMENT_ID\",
\"status\": \"$status\",
\"message\": \"$message\",
\"timestamp\": \"$(date -Iseconds)\"
}" || true
}
# 1. 현재 컨테이너 상태 확인 및 백업
if [ "$(docker ps -q -f name=$CONTAINER_NAME)" ]; then
echo "Creating backup of current deployment..." >> $LOG_FILE
BACKUP_TAG="backup-$(date +%Y%m%d-%H%M%S)"
docker commit $CONTAINER_NAME $IMAGE_NAME:$BACKUP_TAG
report_status "backing_up" "Creating backup: $BACKUP_TAG"
# Graceful shutdown
docker exec $CONTAINER_NAME npm run graceful-shutdown 2>/dev/null || true
sleep 5
docker stop $CONTAINER_NAME
docker rm $CONTAINER_NAME
fi
# 2. 새 이미지 로드
echo "Loading new Docker image..." >> $LOG_FILE
report_status "loading_image" "Loading Docker image"
if ! gunzip -c /volume1/docker/images/home-management-api.tar.gz | docker load; then
echo "Failed to load Docker image" >> $LOG_FILE
report_status "failed" "Failed to load Docker image"
exit 1
fi
# 3. 데이터베이스 마이그레이션 체크
echo "Checking database migrations..." >> $LOG_FILE
report_status "migrating" "Running database migrations"
if ! docker run --rm --network=host \
-v /volume1/docker/config/api.env:/app/.env \
$IMAGE_NAME:latest npm run db:migrate; then
echo "Database migration failed" >> $LOG_FILE
report_status "failed" "Database migration failed"
exit 1
fi
# 4. 새 컨테이너 시작
echo "Starting new container..." >> $LOG_FILE
report_status "starting" "Starting new container"
docker run -d \
--name $CONTAINER_NAME \
--network=host \
--restart=unless-stopped \
-v /volume1/docker/config/api.env:/app/.env \
-v /volume1/logs/api:/app/logs \
-v /volume1/data/uploads:/app/uploads \
-e COMMIT_HASH=$COMMIT_HASH \
-e DEPLOYMENT_ID=$DEPLOYMENT_ID \
$IMAGE_NAME:latest
if [ $? -ne 0 ]; then
echo "Failed to start container" >> $LOG_FILE
report_status "failed" "Failed to start container"
exit 1
fi
# 5. 헬스체크 및 성능 테스트
echo "Performing health check..." >> $LOG_FILE
report_status "health_check" "Performing health check"
sleep 15
for i in {1..30}; do
if curl -f -s http://localhost:3000/health > /dev/null 2>&1; then
# 기본 기능 테스트
if curl -f -s http://localhost:3000/api/devices > /dev/null 2>&1; then
echo "$(date): Deployment successful!" >> $LOG_FILE
report_status "success" "Deployment completed successfully"
# 성능 벤치마크 실행
/volume1/docker/scripts/performance-test.sh $DEPLOYMENT_ID &
# 오래된 백업 이미지 정리 (7개 이상)
docker images | grep "$IMAGE_NAME:backup" | tail -n +8 | \
awk '{print $1":"$2}' | xargs -r docker rmi
exit 0
fi
fi
echo "Waiting for service to start... ($i/30)" >> $LOG_FILE
sleep 2
done
# 6. 실패시 롤백
echo "$(date): Deployment failed! Rolling back..." >> $LOG_FILE
report_status "rolling_back" "Deployment failed, rolling back"
docker stop $CONTAINER_NAME 2>/dev/null || true
docker rm $CONTAINER_NAME 2>/dev/null || true
# 최신 백업으로 롤백
LATEST_BACKUP=$(docker images | grep "$IMAGE_NAME:backup" | head -1 | awk '{print $2}')
if [ ! -z "$LATEST_BACKUP" ]; then
docker run -d \
--name $CONTAINER_NAME \
--network=host \
--restart=unless-stopped \
-v /volume1/docker/config/api.env:/app/.env \
-v /volume1/logs/api:/app/logs \
-v /volume1/data/uploads:/app/uploads \
$IMAGE_NAME:backup-$LATEST_BACKUP
report_status "rolled_back" "Rolled back to: backup-$LATEST_BACKUP"
else
report_status "failed" "Rollback failed - no backup available"
fi
exit 1
4. Docker 컨테이너 모니터링 (DS1525+)
# /volume1/docker/monitor-containers.sh
#!/bin/bash
SERVICES=("home-api" "home-mariadb" "home-redis" "home-phpmyadmin")
WEBHOOK_URL="http://developer-machine:3000/api/alerts/webhook"
for service in "${SERVICES[@]}"; do
if ! docker ps --format "table {{.Names}}" | grep -q "^${service}$"; then
# 서비스 다운 알림
curl -X POST $WEBHOOK_URL \
-H "Content-Type: application/json" \
-d "{
\"service\": \"$service\",
\"status\": \"down\",
\"timestamp\": \"$(date -Iseconds)\",
\"server\": \"ds1525plus\"
}"
# 자동 재시작 시도
echo "$(date): Attempting to restart $service..." >> /volume1/logs/monitor.log
docker start $service
fi
done
Gitea 연동 최적화
1. Gitea 서버 설정 (DS1525+ 컨테이너)
# docker-compose.yml에 추가
gitea:
image: gitea/gitea:1.21
container_name: home_gitea
environment:
- USER_UID=1000
- USER_GID=1000
- GITEA__database__DB_TYPE=mysql
- GITEA__database__HOST=mariadb:3306
- GITEA__database__NAME=gitea
- GITEA__database__USER=gitea
- GITEA__database__PASSWD=gitea_password
- GITEA__server__DOMAIN=git.hyungi.net
- GITEA__server__HTTP_PORT=3000
- GITEA__server__ROOT_URL=https://git.hyungi.net
- GITEA__actions__ENABLED=true
- GITEA__actions__DEFAULT_ACTIONS_URL=https://github.com
restart: unless-stopped
networks:
- home_network
volumes:
- gitea_data:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "3000:3000"
- "222:22"
depends_on:
- mariadb
2. 배포 상태 추적 API
// routes/deployments.js
const express = require('express');
const router = express.Router();
const DeploymentService = require('../services/deploymentService');
// 배포 시작 기록
router.post('/start', async (req, res) => {
try {
const { deployment_id, commit_hash, branch, stage } = req.body;
const logId = await DeploymentService.recordDeployment({
id: deployment_id,
stage: stage || 'build',
commit: commit_hash,
branch: branch,
user: req.user?.username || 'system'
});
res.json({ log_id: logId, status: 'recorded' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 배포 상태 업데이트
router.post('/update', async (req, res) => {
try {
const { deployment_id, status, message } = req.body;
await DeploymentService.updateDeploymentStatus(
deployment_id, status, message
);
// 실시간 알림 (WebSocket)
req.app.get('io').emit('deployment_update', {
deployment_id,
status,
message,
timestamp: new Date()
});
res.json({ status: 'updated' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 배포 완료 처리
router.post('/complete', async (req, res) => {
try {
const { deployment_id, status, stage } = req.body;
await DeploymentService.completeDeployment(
deployment_id, status, stage
);
if (status === 'success') {
// 성공시 현재 버전 업데이트
await DeploymentService.updateActiveVersion(deployment_id);
}
res.json({ status: 'completed' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 배포 히스토리 조회
router.get('/history', async (req, res) => {
try {
const { limit = 20, offset = 0 } = req.query;
const history = await DeploymentService.getDeploymentHistory(
parseInt(limit), parseInt(offset)
);
res.json(history);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;
3. 브랜치별 배포 전략
# .gitea/workflows/deploy.yml 추가 구성
on:
push:
branches: [ main, develop, feature/* ]
pull_request:
branches: [ main, develop ]
jobs:
test:
runs-on: self-hosted
# 모든 브랜치에서 테스트 실행
build-develop:
needs: test
runs-on: self-hosted
if: gitea.ref == 'refs/heads/develop'
steps:
- name: Deploy to Staging
run: |
docker build -t home-management-api:develop .
# 스테이징 환경 배포 (포트 3001)
ssh admin@ds1525plus \
"cd /volume1/docker && ./deploy-staging.sh develop"
build-main:
needs: test
runs-on: self-hosted
if: gitea.ref == 'refs/heads/main'
steps:
- name: Deploy to Production
run: |
docker build -t home-management-api:latest .
# 프로덕션 배포 (포트 3000)
ssh admin@ds1525plus \
"cd /volume1/docker && ./deploy-api.sh latest ${{ gitea.run_id }}"
feature-test:
needs: test
runs-on: self-hosted
if: startsWith(gitea.ref, 'refs/heads/feature/')
steps:
- name: Feature Branch Tests
run: |
# 추가 테스트만 수행, 배포 안함
npm run test:e2e
npm run test:security
4. Gitea Actions 캐시 최적화
# 캐시 설정으로 빌드 시간 단축
steps:
- name: Cache Node modules
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Cache Docker layers
uses: actions/cache@v3
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ gitea.sha }}
restore-keys: |
${{ runner.os }}-buildx-
🔧 환경 설정
1. 개발 환경 (MacBook Pro / Mac Mini)
# .env.development
NODE_ENV=development
DB_HOST=ds1525plus # NAS의 개발용 DB 연결
DB_PORT=3306
API_PORT=3000
CORS_ORIGIN=http://localhost:3001
LOG_LEVEL=debug
REDIS_HOST=ds1525plus
2. 로컬 개발 환경 (개발 머신)
# .env.local
NODE_ENV=development
DB_HOST=localhost # 로컬 Docker DB
DB_PORT=3306
API_PORT=3000
CORS_ORIGIN=http://localhost:3001
LOG_LEVEL=debug
REDIS_HOST=localhost
3. 테스트 환경 (Mac Mini CI)
# .env.test
NODE_ENV=test
DB_HOST=localhost
DB_PORT=3307
DB_NAME=home_management_test
API_PORT=3001
REDIS_HOST=localhost
REDIS_DB=1
4. 프로덕션 환경 (DS1525+)
# /volume1/docker/config/api.env
NODE_ENV=production
DB_HOST=localhost
DB_PORT=3306
DB_NAME=home_management
API_PORT=3000
CORS_ORIGIN=https://home.yourdomain.com
LOG_LEVEL=info
REDIS_HOST=localhost
# 보안 설정
JWT_SECRET=production-jwt-secret-key
BCRYPT_ROUNDS=14
# 프로덕션 최적화
NODE_OPTIONS=--max-old-space-size=2048
PM2_INSTANCES=2
데이터베이스 마이그레이션 관리
1. 마이그레이션 스크립트 구조
migrations/
├── 001_initial_schema.sql
├── 002_add_alert_system.sql
├── 003_add_user_preferences.sql
├── 004_optimize_indexes.sql
└── 005_add_ci_cd_logs.sql
2. 마이그레이션 실행기 (scripts/migrate.js)
// 500자 이하로 간단하게 구성
const mysql = require('mysql2/promise');
const fs = require('fs').promises;
const path = require('path');
async function runMigrations() {
const connection = await mysql.createConnection({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
multipleStatements: true
});
// 마이그레이션 테이블 생성
await connection.execute(`
CREATE TABLE IF NOT EXISTS migrations (
id INT AUTO_INCREMENT PRIMARY KEY,
filename VARCHAR(255) UNIQUE NOT NULL,
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
const migrationsDir = path.join(__dirname, '../migrations');
const files = await fs.readdir(migrationsDir);
for (const file of files.sort()) {
if (!file.endsWith('.sql')) continue;
const [rows] = await connection.execute(
'SELECT id FROM migrations WHERE filename = ?', [file]
);
if (rows.length === 0) {
console.log(`Running migration: ${file}`);
const sql = await fs.readFile(path.join(migrationsDir, file), 'utf8');
await connection.execute(sql);
await connection.execute(
'INSERT INTO migrations (filename) VALUES (?)', [file]
);
}
}
await connection.end();
}
module.exports = { runMigrations };
로그 및 모니터링 통합
1. 배포 로그 수집 테이블
-- 005_add_ci_cd_logs.sql
CREATE TABLE deployment_logs (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
deployment_id VARCHAR(100) NOT NULL,
stage ENUM('test', 'build', 'deploy', 'rollback') NOT NULL,
status ENUM('started', 'success', 'failed') NOT NULL,
commit_hash VARCHAR(40),
branch_name VARCHAR(100),
deployed_by VARCHAR(100),
start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
end_time TIMESTAMP NULL,
duration_seconds INT GENERATED ALWAYS AS (
CASE WHEN end_time IS NOT NULL
THEN TIMESTAMPDIFF(SECOND, start_time, end_time)
ELSE NULL END
) STORED,
error_message TEXT,
metadata JSON,
INDEX idx_deployment_stage (deployment_id, stage),
INDEX idx_status_time (status, start_time)
) ENGINE=InnoDB;
CREATE TABLE service_deployments (
id INT AUTO_INCREMENT PRIMARY KEY,
service_name VARCHAR(50) NOT NULL,
version VARCHAR(50) NOT NULL,
commit_hash VARCHAR(40) NOT NULL,
deployed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deployed_by VARCHAR(100),
rollback_version VARCHAR(50) NULL,
is_active BOOLEAN DEFAULT TRUE,
performance_baseline JSON, -- 배포 후 성능 지표
INDEX idx_service_version (service_name, version),
INDEX idx_active (is_active, deployed_at)
) ENGINE=InnoDB;
2. 배포 성능 모니터링
// services/deploymentService.js
class DeploymentService {
async recordDeployment(deploymentData) {
// 배포 기록 저장 (500자 이하)
const deployment = await this.db.query(`
INSERT INTO deployment_logs
(deployment_id, stage, status, commit_hash, branch_name, deployed_by)
VALUES (?, ?, ?, ?, ?, ?)
`, [deploymentData.id, deploymentData.stage, 'started',
deploymentData.commit, deploymentData.branch, deploymentData.user]);
return deployment.insertId;
}
async updateDeploymentStatus(logId, status, errorMessage = null) {
// 배포 상태 업데이트
await this.db.query(`
UPDATE deployment_logs
SET status = ?, end_time = CURRENT_TIMESTAMP, error_message = ?
WHERE id = ?
`, [status, errorMessage, logId]);
}
async getDeploymentHistory(limit = 50) {
// 최근 배포 이력 조회
const [rows] = await this.db.query(`
SELECT * FROM deployment_logs
ORDER BY start_time DESC LIMIT ?
`, [limit]);
return rows;
}
}
성능 벤치마크 자동화
1. 배포 후 성능 테스트
# scripts/performance-test.sh
#!/bin/bash
API_URL="http://localhost:3000"
RESULTS_FILE="/tmp/perf-results.json"
echo "Running performance tests after deployment..."
# API 응답 시간 테스트
ab -n 1000 -c 10 -g /tmp/ab-results.tsv $API_URL/api/devices/ > /tmp/ab-output.txt
# 메모리 사용량 체크
MEMORY_USAGE=$(docker stats --no-stream --format "table {{.Container}}\t{{.MemUsage}}" | grep home-api)
# 결과를 JSON으로 저장
echo "{
\"timestamp\": \"$(date -Iseconds)\",
\"memory_usage\": \"$MEMORY_USAGE\",
\"ab_results\": \"$(tail -5 /tmp/ab-output.txt)\"
}" > $RESULTS_FILE
# 성능 저하 감지시 알림
if [ $(awk '/Time per request/ {print $4}' /tmp/ab-output.txt | head -1 | cut -d. -f1) -gt 100 ]; then
curl -X POST http://developer-machine:3000/api/alerts/webhook \
-d '{"type":"performance_degradation","details":"'$(cat $RESULTS_FILE)'"}'
fi
🚀 개발 시작 체크리스트
1. 환경 준비
- Node.js 18+ 설치
- Docker & Docker Compose 설치
- Git 저장소 클론:
git clone https://git.hyungi.net/hyungi/myhome-server.git - 프로젝트 폴더 구조 생성
2. 데이터베이스 설정
docker-compose up -d mariadb실행- phpMyAdmin 접속 확인 (localhost:8080)
- 테이블 생성 및 초기 데이터 삽입
- 인덱스 및 파티션 설정 확인
3. 백엔드 개발 순서
- 기본 설정: Express 앱, DB 연결, 미들웨어 설정
- 모델 개발: Sequelize 모델 정의 (devices → power_consumption → network_traffic)
- 컨트롤러 개발: 기본 CRUD 작업 (GET, POST, PUT, DELETE)
- 서비스 계층: 비즈니스 로직 분리 (통계, 분석, 집계)
- 라우터 연결: API 엔드포인트 구성
- 데이터 수집기: 실제 하드웨어 연동 모듈
- 알림 시스템: 실시간 모니터링 및 알림
- 테스트 작성: 단위 테스트 및 통합 테스트
4. 성능 최적화
- Redis 캐싱 구현
- DB 쿼리 최적화
- API 응답 시간 모니터링
- 메모리 사용량 추적
5. CI/CD 파이프라인 구축
- Gitea Actions 활성화 및 설정
- 개발 머신(MacBook Pro/Mac Mini)에 Gitea Actions Runner 설치
- DS1525+에 Docker 환경 및 Gitea 서버 구성
- 배포 스크립트 작성 및 테스트 (프로덕션/스테이징)
- 데이터베이스 마이그레이션 자동화
- 브랜치별 배포 전략 설정 (main→production, develop→staging)
- 성능 모니터링 및 배포 알림 설정
- Webhook 서버 구성 (개발 머신)
6. 보안 설정
- JWT 인증 구현
- API 속도 제한 설정
- HTTPS 인증서 구성 (Let's Encrypt)
- 입력 데이터 검증 강화
- SSH Key 기반 서버 간 통신 설정
- Gitea 보안 설정 (2FA, 브랜치 보호)
7. 모니터링 및 알림
- 배포 성공/실패 실시간 알림 (WebSocket)
- 서비스 상태 모니터링 대시보드
- 성능 저하 감지 및 자동 알림
- 로그 중앙화 (Mac Mini → DS1525+)
- 배포 메트릭 수집 및 분석
🎯 Gitea 기반 CI/CD 장점
완전 프라이빗 환경
- 내부 네트워크: 모든 CI/CD 프로세스가 홈 네트워크 내에서 실행
- 데이터 보안: 소스코드와 빌드 아티팩트가 외부로 유출되지 않음
- 비용 효율: 외부 CI/CD 서비스 비용 절약
하드웨어 최적화
- MacBook Pro: 모바일 개발 환경, 외부 작업 가능
- Mac Mini: 고정 개발 환경, 안정적인 빌드 서버 역할
- 공통 ARM64: 두 머신 모두 네이티브 빌드 환경
- DS1525+: 안정적인 Git 서버 및 프로덕션 환경
- 통합 관리: 홈 관리 시스템과 개발 도구 통합
확장성
- 멀티 브랜치: feature/develop/main 브랜치별 배포 전략
- 스테이징: develop 브랜치로 스테이징 환경 자동 배포
- 롤백: 실패시 자동 롤백 및 알림
이제 완전히 프라이빗한 환경에서 기업급 CI/CD 파이프라인을 구축할 수 있습니다. Gitea 서버로 완전한 DevOps 환경을 홈에서 운영하는 것이 가능하겠네요!
이 계획서를 따라 단계별로 구현하면 확장성과 유지보수성을 갖춘 견고한 홈 관리 시스템과 함께 전문적인 CI/CD 파이프라인을 구축할 수 있습니다.