機能 #585 » vps-root-deployment-specs.md
VPS-rootデプロイ仕様書 - 機材予約システム
システム概要
基本情報
- ドメイン: rental.call2arm.com
- VPS環境: Ubuntu 24.04.2 LTS @ 85.131.243.51
- デプロイ日: 2025年6月18日
- 担当: 開発チーム・運用チーム
アーキテクチャ
[Internet]
↓ HTTPS/443
[nginx reverse proxy]
↓ HTTP/3000
[Docker: rental-app]
↓
[SQLite Database]
↓
[LINE WORKS APIs]
Docker構成
docker-compose.yml
version: '3.8'
services:
rental-app:
build:
context: ./app
dockerfile: Dockerfile
container_name: rental-app
restart: unless-stopped
environment:
- NODE_ENV=production
- PORT=3000
volumes:
- ./data:/app/data:rw
- ./logs:/app/logs:rw
- ./uploads:/app/uploads:rw
networks:
- proxy-network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
depends_on:
- rental-db-init
labels:
- "traefik.enable=false" # nginx経由のみ
rental-db-init:
build:
context: ./db
dockerfile: Dockerfile
container_name: rental-db-init
volumes:
- ./data:/data:rw
command: >
sh -c "
if [ ! -f /data/rental_system.db ]; then
echo 'Initializing database...';
sqlite3 /data/rental_system.db < /sql/schema.sql;
sqlite3 /data/rental_system.db < /sql/initial_data.sql;
chmod 644 /data/rental_system.db;
echo 'Database initialized successfully';
else
echo 'Database already exists, skipping initialization';
fi
"
networks:
- proxy-network
networks:
proxy-network:
external: true
volumes:
rental-data:
driver: local
rental-logs:
driver: local
Dockerfile (アプリケーション用)
# rental-system/app/Dockerfile
FROM node:18-alpine
# 作業ディレクトリ設定
WORKDIR /app
# セキュリティ: 非rootユーザー作成
RUN addgroup -g 1001 -S nodejs && adduser -S nextjs -u 1001
# システム依存関係インストール
RUN apk add --no-cache \
curl \
sqlite \
&& rm -rf /var/cache/apk/*
# package.json とpackage-lock.json をコピー
COPY package*.json ./
# 依存関係インストール(本番環境のみ)
RUN npm ci --only=production && npm cache clean --force
# アプリケーションコードをコピー
COPY . .
# ディレクトリ権限設定
RUN mkdir -p /app/data /app/logs /app/uploads && \
chown -R nextjs:nodejs /app
# 非rootユーザーに切り替え
USER nextjs
# ポート公開
EXPOSE 3000
# ヘルスチェック用エンドポイント
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
# アプリケーション起動
CMD ["npm", "start"]
Dockerfile (データベース初期化用)
# rental-system/db/Dockerfile
FROM alpine:3.18
# SQLite インストール
RUN apk add --no-cache sqlite
# SQL ファイルをコピー
COPY sql/ /sql/
# 実行権限設定
RUN chmod +x /sql/*.sql
WORKDIR /data
nginx設定
rental.call2arm.com.conf
# /etc/nginx/sites-available/rental.call2arm.com.conf
# HTTP → HTTPS リダイレクト
server {
listen 80;
server_name rental.call2arm.com;
return 301 https://$server_name$request_uri;
}
# HTTPS メインサーバー
server {
listen 443 ssl http2;
server_name rental.call2arm.com;
# SSL証明書設定(wildcard証明書使用)
ssl_certificate /etc/letsencrypt/live/call2arm.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/call2arm.com/privkey.pem;
# SSL設定強化
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# セキュリティヘッダー
add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://auth.worksmobile.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https://auth.worksmobile.com https://www.worksapis.com;" always;
# 社内IP制限(必要に応じて有効化)
# allow 192.168.0.0/16;
# allow 10.0.0.0/8;
# allow 172.16.0.0/12;
# deny all;
# ログ設定
access_log /var/log/nginx/rental.call2arm.com.access.log combined;
error_log /var/log/nginx/rental.call2arm.com.error.log warn;
# クライアント設定
client_max_body_size 50m;
client_body_timeout 60s;
client_header_timeout 60s;
# メインアプリケーション
location / {
proxy_pass http://rental-app:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
# タイムアウト設定
proxy_connect_timeout 30s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# API専用設定
location /api/ {
proxy_pass http://rental-app:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Content-Type application/json;
# API専用タイムアウト
proxy_connect_timeout 10s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
}
# 静的ファイル(キャッシュ最適化)
location /static/ {
proxy_pass http://rental-app:3000;
expires 30d;
add_header Cache-Control "public, immutable";
access_log off;
}
# ヘルスチェック
location /health {
proxy_pass http://rental-app:3000;
access_log off;
}
# LINE WORKS Webhook
location /api/lineworks/webhook {
proxy_pass http://rental-app:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Webhook専用設定
proxy_buffering off;
proxy_request_buffering off;
}
# 404エラーページ
error_page 404 /404.html;
location = /404.html {
proxy_pass http://rental-app:3000;
internal;
}
# 5xxエラーページ
error_page 500 502 503 504 /50x.html;
location = /50x.html {
proxy_pass http://rental-app:3000;
internal;
}
}
環境設定
.env.production
# アプリケーション基本設定
NODE_ENV=production
PORT=3000
APP_NAME=機材予約システム
APP_URL=https://rental.call2arm.com
COMPANY_NAME=株式会社サンプル
# データベース設定
DB_PATH=/app/data/rental_system.db
DB_TIMEOUT=30000
DB_MAX_CONNECTIONS=10
# JWT認証設定
JWT_SECRET=<GENERATE_RANDOM_SECRET_64_CHARS>
JWT_EXPIRES_IN=24h
JWT_REFRESH_EXPIRES_IN=7d
# セッション設定
SESSION_SECRET=<GENERATE_RANDOM_SECRET_32_CHARS>
SESSION_TIMEOUT=1800000
# LINE WORKS OAuth設定
LINEWORKS_CLIENT_ID=<YOUR_LINEWORKS_CLIENT_ID>
LINEWORKS_CLIENT_SECRET=<YOUR_LINEWORKS_CLIENT_SECRET>
LINEWORKS_REDIRECT_URI=https://rental.call2arm.com/auth/callback
LINEWORKS_BOT_ID=<YOUR_BOT_ID>
LINEWORKS_BOT_SECRET=<YOUR_BOT_SECRET>
# セキュリティ設定
BCRYPT_ROUNDS=12
RATE_LIMIT_WINDOW=900000
RATE_LIMIT_MAX=100
ALLOWED_ORIGINS=https://rental.call2arm.com
CORS_CREDENTIALS=true
# ログ設定
LOG_LEVEL=info
LOG_FILE=/app/logs/application.log
LOG_MAX_SIZE=10485760
LOG_MAX_FILES=5
ACCESS_LOG_ENABLED=true
# ファイルアップロード設定
UPLOAD_MAX_SIZE=52428800
UPLOAD_ALLOWED_TYPES=image/jpeg,image/png,image/gif,application/pdf
UPLOAD_DIR=/app/uploads
# 通知設定
NOTIFICATION_ENABLED=true
REMINDER_HOURS_BEFORE=24
MAX_NOTIFICATIONS_PER_USER_PER_DAY=10
NOTIFICATION_RETRY_ATTEMPTS=3
# メンテナンス設定
MAINTENANCE_MODE=false
MAINTENANCE_MESSAGE=システムメンテナンス中です
# バックアップ設定
BACKUP_ENABLED=true
BACKUP_SCHEDULE=0 2 * * *
BACKUP_RETENTION_DAYS=30
BACKUP_DIR=/app/backup
# 監視設定
HEALTH_CHECK_ENABLED=true
METRICS_ENABLED=true
MONITORING_PORT=9090
# パフォーマンス設定
CACHE_TTL=3600
COMPRESSION_ENABLED=true
STATIC_CACHE_MAX_AGE=2592000
# デバッグ設定(本番では無効)
DEBUG_MODE=false
VERBOSE_LOGGING=false
SQL_LOGGING=false
データベース設計
schema.sql
-- rental-system/db/sql/schema.sql
-- 機材予約システム データベーススキーマ
-- ユーザーテーブル
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
name TEXT NOT NULL,
department TEXT NOT NULL,
email TEXT,
role TEXT DEFAULT 'user',
lineworks_id TEXT,
avatar_url TEXT,
last_login DATETIME,
login_count INTEGER DEFAULT 0,
notification_enabled BOOLEAN DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- カテゴリテーブル
CREATE TABLE IF NOT EXISTS categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT,
icon TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- サブカテゴリテーブル
CREATE TABLE IF NOT EXISTS subcategories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category_id INTEGER,
name TEXT NOT NULL,
description TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(category_id) REFERENCES categories(id)
);
-- 機材テーブル
CREATE TABLE IF NOT EXISTS equipment (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
category_id INTEGER,
subcategory_id INTEGER,
status TEXT DEFAULT 'available',
location TEXT,
specifications TEXT,
last_maintenance DATE,
purchase_date DATE,
warranty_date DATE,
image_url TEXT,
qr_code TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(category_id) REFERENCES categories(id),
FOREIGN KEY(subcategory_id) REFERENCES subcategories(id)
);
-- 予約テーブル
CREATE TABLE IF NOT EXISTS reservations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
equipment_id TEXT,
user_id INTEGER,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
purpose TEXT NOT NULL,
notes TEXT,
status TEXT DEFAULT 'confirmed',
priority TEXT DEFAULT 'normal',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(equipment_id) REFERENCES equipment(id),
FOREIGN KEY(user_id) REFERENCES users(id)
);
-- LINE WORKSトークンテーブル
CREATE TABLE IF NOT EXISTS lineworks_tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
access_token TEXT NOT NULL,
refresh_token TEXT,
expires_at INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id)
);
-- 通知履歴テーブル
CREATE TABLE IF NOT EXISTS notification_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
reservation_id INTEGER,
notification_type TEXT NOT NULL,
title TEXT NOT NULL,
message TEXT NOT NULL,
sent_at DATETIME DEFAULT CURRENT_TIMESTAMP,
delivery_status TEXT DEFAULT 'pending',
lineworks_message_id TEXT,
retry_count INTEGER DEFAULT 0,
FOREIGN KEY(user_id) REFERENCES users(id),
FOREIGN KEY(reservation_id) REFERENCES reservations(id)
);
-- Bot設定テーブル
CREATE TABLE IF NOT EXISTS bot_settings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
setting_key TEXT UNIQUE NOT NULL,
setting_value TEXT NOT NULL,
description TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- ユーザー通知設定テーブル
CREATE TABLE IF NOT EXISTS user_notification_settings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
reservation_reminders BOOLEAN DEFAULT 1,
maintenance_notifications BOOLEAN DEFAULT 1,
popular_equipment_alerts BOOLEAN DEFAULT 0,
system_announcements BOOLEAN DEFAULT 1,
reminder_days_before INTEGER DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id)
);
-- QRアクセスログテーブル
CREATE TABLE IF NOT EXISTS qr_access_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
equipment_id TEXT,
access_type TEXT NOT NULL,
source TEXT,
user_agent TEXT,
ip_address TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id),
FOREIGN KEY(equipment_id) REFERENCES equipment(id)
);
-- 予約履歴テーブル(アーカイブ用)
CREATE TABLE IF NOT EXISTS reservation_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
reservation_id INTEGER,
equipment_id TEXT,
user_id INTEGER,
start_date DATE,
end_date DATE,
purpose TEXT,
notes TEXT,
status TEXT,
completed_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(reservation_id) REFERENCES reservations(id),
FOREIGN KEY(equipment_id) REFERENCES equipment(id),
FOREIGN KEY(user_id) REFERENCES users(id)
);
-- インデックス作成
CREATE INDEX IF NOT EXISTS idx_users_lineworks_id ON users(lineworks_id);
CREATE INDEX IF NOT EXISTS idx_users_department ON users(department);
CREATE INDEX IF NOT EXISTS idx_equipment_category_id ON equipment(category_id);
CREATE INDEX IF NOT EXISTS idx_equipment_status ON equipment(status);
CREATE INDEX IF NOT EXISTS idx_equipment_name ON equipment(name);
CREATE INDEX IF NOT EXISTS idx_reservations_user_id ON reservations(user_id);
CREATE INDEX IF NOT EXISTS idx_reservations_equipment_id ON reservations(equipment_id);
CREATE INDEX IF NOT EXISTS idx_reservations_date_range ON reservations(start_date, end_date);
CREATE INDEX IF NOT EXISTS idx_reservations_status ON reservations(status);
CREATE INDEX IF NOT EXISTS idx_lineworks_tokens_user_id ON lineworks_tokens(user_id);
CREATE INDEX IF NOT EXISTS idx_lineworks_tokens_expires_at ON lineworks_tokens(expires_at);
CREATE INDEX IF NOT EXISTS idx_notification_history_user_id ON notification_history(user_id);
CREATE INDEX IF NOT EXISTS idx_notification_history_sent_at ON notification_history(sent_at);
-- 複合インデックス
CREATE INDEX IF NOT EXISTS idx_equipment_category_status ON equipment(category_id, status);
CREATE INDEX IF NOT EXISTS idx_reservations_equipment_date ON reservations(equipment_id, start_date, end_date);
-- ビュー作成
CREATE VIEW IF NOT EXISTS user_summary AS
SELECT
u.id,
u.username,
u.name,
u.department,
u.email,
u.lineworks_id,
u.last_login,
u.login_count,
COUNT(DISTINCT r.id) as total_reservations,
COUNT(DISTINCT CASE WHEN r.status = 'confirmed' THEN r.id END) as active_reservations,
MAX(r.created_at) as last_reservation_date
FROM users u
LEFT JOIN reservations r ON u.id = r.user_id
GROUP BY u.id;
CREATE VIEW IF NOT EXISTS equipment_usage_stats AS
SELECT
e.id,
e.name,
e.category_id,
e.status,
COUNT(DISTINCT r.id) as total_reservations,
COUNT(DISTINCT r.user_id) as unique_users,
AVG(julianday(r.end_date) - julianday(r.start_date)) as avg_rental_days,
MAX(r.created_at) as last_reserved_date,
ROUND(COUNT(DISTINCT r.id) * 100.0 / (SELECT COUNT(*) FROM reservations), 2) as popularity_percentage
FROM equipment e
LEFT JOIN reservations r ON e.id = r.equipment_id AND r.status = 'confirmed'
GROUP BY e.id;
-- トリガー作成
CREATE TRIGGER IF NOT EXISTS reservation_notification_trigger
AFTER INSERT ON reservations
FOR EACH ROW
WHEN NEW.status = 'confirmed'
BEGIN
INSERT INTO notification_history (user_id, reservation_id, notification_type, title, message)
VALUES (
NEW.user_id,
NEW.id,
'reservation_created',
'予約完了通知',
'機材の予約が完了しました。'
);
END;
CREATE TRIGGER IF NOT EXISTS login_count_trigger
AFTER UPDATE OF last_login ON users
FOR EACH ROW
WHEN NEW.last_login IS NOT NULL AND (OLD.last_login IS NULL OR NEW.last_login > OLD.last_login)
BEGIN
UPDATE users SET login_count = login_count + 1 WHERE id = NEW.id;
END;
運用手順書
システム起動手順
# 1. VPS-rootにSSH接続
ssh vps-root
# 2. プロジェクトディレクトリに移動
cd /root/projects/rental-system
# 3. 設定ファイル確認
ls -la .env.production
# 4. Dockerコンテナ起動
docker-compose up -d
# 5. 起動確認
docker-compose ps
docker-compose logs rental-app
# 6. ヘルスチェック
curl -f https://rental.call2arm.com/health
# 7. nginx設定確認・リロード
nginx -t
systemctl reload nginx
システム停止手順
# 1. メンテナンスモード有効化(必要に応じて)
# maintenance.htmlをnginxで配信
# 2. Dockerコンテナ停止
docker-compose down
# 3. 停止確認
docker-compose ps
バックアップ手順
# 1. データベースバックアップ
sqlite3 /root/projects/rental-system/data/rental_system.db ".backup /root/backup/rental_system_$(date +%Y%m%d_%H%M%S).db"
# 2. 設定ファイルバックアップ
tar -czf /root/backup/rental_config_$(date +%Y%m%d_%H%M%S).tar.gz \
/root/projects/rental-system/.env.production \
/root/projects/rental-system/docker-compose.yml \
/etc/nginx/sites-available/rental.call2arm.com.conf
# 3. ログファイルアーカイブ
tar -czf /root/backup/rental_logs_$(date +%Y%m%d_%H%M%S).tar.gz \
/root/projects/rental-system/logs/
# 4. バックアップファイルの古いものを削除(30日以上)
find /root/backup -name "rental_*" -type f -mtime +30 -delete
ログ確認手順
# 1. アプリケーションログ
tail -f /root/projects/rental-system/logs/application.log
# 2. Dockerコンテナログ
docker-compose logs -f rental-app
# 3. nginxログ
tail -f /var/log/nginx/rental.call2arm.com.access.log
tail -f /var/log/nginx/rental.call2arm.com.error.log
# 4. システムログ
journalctl -u docker.service -f
トラブルシューティング
起動しない場合
# 1. Dockerサービス確認
systemctl status docker
# 2. ポート競合確認
netstat -tulpn | grep :3000
# 3. ディスク容量確認
df -h
# 4. メモリ使用量確認
free -h
# 5. 設定ファイル構文確認
docker-compose config
パフォーマンス問題
# 1. リソース使用量確認
docker stats rental-app
# 2. データベースサイズ確認
ls -lh /root/projects/rental-system/data/
# 3. ログサイズ確認
du -sh /root/projects/rental-system/logs/
# 4. nginx接続数確認
nginx -T | grep worker_connections
セキュリティインシデント
# 1. 不正アクセス確認
grep "403\|404\|401" /var/log/nginx/rental.call2arm.com.access.log
# 2. 大量リクエスト確認
awk '{print $1}' /var/log/nginx/rental.call2arm.com.access.log | sort | uniq -c | sort -nr | head -10
# 3. システム緊急停止
docker-compose down
systemctl stop nginx
この仕様書により、VPS-root環境での機材予約システムの安全で確実なデプロイが可能になります。