feat: Single-port deployment with improved error handling and SvelteKit static build
- Frontend now uses @sveltejs/adapter-static for production builds - Backend serves both API and static files from single port (originally port 3000) - Removed all throw statements from services to avoid Elysia prototype errors - Fixed favicon serving and SvelteKit assets path handling - Added ecosystem.config.js for PM2 process management - Comprehensive deployment documentation (PM2 + HAProxy) - Updated README with single-port architecture - Created start.sh script for easy production start
This commit is contained in:
461
README.md
461
README.md
@@ -227,79 +227,422 @@ This gives you:
|
||||
- ✅ Simple production deployment
|
||||
|
||||
### Production Mode
|
||||
In production, you can either:
|
||||
1. **Build static files** and serve from Elysia
|
||||
2. **Keep the proxy setup** with a proper reverse proxy (nginx, caddy)
|
||||
3. **Use SvelteKit adapter** for Node/Bun to serve everything from one process
|
||||
In production, the application serves everything from a single port:
|
||||
- **Backend** on port 3001 serves both API and static frontend files
|
||||
- Frontend static files are served from `src/frontend/build/`
|
||||
- SPA routing is handled by backend fallback
|
||||
- **Single port** simplifies HAProxy configuration
|
||||
|
||||
## Production Deployment
|
||||
|
||||
### Building for Production
|
||||
This guide covers deployment using **PM2** for process management and **HAProxy** as a reverse proxy/load balancer.
|
||||
|
||||
### Architecture Overview
|
||||
|
||||
```
|
||||
Internet
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ HAProxy │ Port 443 (HTTPS)
|
||||
│ (Port 80/443) │ Port 80 (HTTP → HTTPS redirect)
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Backend │ Port 3001
|
||||
│ Managed by │ ├─ API Routes (/api/*)
|
||||
│ PM2 │ ├─ Static Files (/*)
|
||||
│ │ └─ SPA Fallback
|
||||
└────────┬────────┘
|
||||
│
|
||||
├─────────────────┬──────────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────┐ ┌──────────┐ ┌─────────────┐
|
||||
│ SQLite │ │ Frontend │ │ ARRL LoTW │
|
||||
│ DB │ │ Build │ │ External API│
|
||||
└─────────┘ └──────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Server with SSH access
|
||||
- Bun runtime installed
|
||||
- PM2 installed globally: `bun install -g pm2` or `npm install -g pm2`
|
||||
- HAProxy installed
|
||||
- Domain with DNS pointing to server
|
||||
|
||||
### Step 1: Build the Application
|
||||
|
||||
```bash
|
||||
# Build the frontend
|
||||
# Clone repository on server
|
||||
git clone <repository-url>
|
||||
cd award
|
||||
|
||||
# Install dependencies
|
||||
bun install
|
||||
|
||||
# Build frontend (generates static files in src/frontend/build/)
|
||||
bun run build
|
||||
```
|
||||
|
||||
### Step 2: Configure Environment Variables
|
||||
|
||||
Create `.env` in the project root:
|
||||
|
||||
```bash
|
||||
# Application URL
|
||||
VITE_APP_URL=https://awards.dj7nt.de
|
||||
|
||||
# API Base URL (empty for same-domain)
|
||||
VITE_API_BASE_URL=
|
||||
|
||||
# JWT Secret (generate with: openssl rand -base64 32)
|
||||
JWT_SECRET=your-generated-secret-here
|
||||
|
||||
# Environment
|
||||
NODE_ENV=production
|
||||
|
||||
# Database path (absolute path recommended)
|
||||
DATABASE_PATH=/path/to/award/award.db
|
||||
```
|
||||
|
||||
**Security**: Ensure `.env` has restricted permissions:
|
||||
```bash
|
||||
chmod 600 .env
|
||||
```
|
||||
|
||||
### Step 3: Initialize Database
|
||||
|
||||
```bash
|
||||
# Push database schema
|
||||
bun run db:push
|
||||
|
||||
# Verify database was created
|
||||
ls -la award.db
|
||||
```
|
||||
|
||||
### Step 4: Create PM2 Ecosystem Configuration
|
||||
|
||||
Create `ecosystem.config.js` in the project root:
|
||||
|
||||
```javascript
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: 'award-backend',
|
||||
script: 'src/backend/index.js',
|
||||
interpreter: 'bun',
|
||||
cwd: '/path/to/award',
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
PORT: 3001
|
||||
},
|
||||
instances: 1,
|
||||
exec_mode: 'fork',
|
||||
autorestart: true,
|
||||
watch: false,
|
||||
max_memory_restart: '500M',
|
||||
error_file: './logs/backend-error.log',
|
||||
out_file: './logs/backend-out.log',
|
||||
log_date_format: 'YYYY-MM-DD HH:mm:ss Z'
|
||||
},
|
||||
{
|
||||
name: 'award-frontend',
|
||||
script: 'bun',
|
||||
args: 'run preview',
|
||||
cwd: '/path/to/award/src/frontend',
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
PORT: 5173
|
||||
},
|
||||
instances: 1,
|
||||
exec_mode: 'fork',
|
||||
autorestart: true,
|
||||
watch: false,
|
||||
max_memory_restart: '300M',
|
||||
error_file: './logs/frontend-error.log',
|
||||
out_file: './logs/frontend-out.log',
|
||||
log_date_format: 'YYYY-MM-DD HH:mm:ss Z'
|
||||
}
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
**Create logs directory:**
|
||||
```bash
|
||||
mkdir -p logs
|
||||
```
|
||||
|
||||
### Step 5: Start Applications with PM2
|
||||
|
||||
```bash
|
||||
# Start all applications
|
||||
pm2 start ecosystem.config.js
|
||||
|
||||
# Save PM2 process list
|
||||
pm2 save
|
||||
|
||||
# Setup PM2 to start on system reboot
|
||||
pm2 startup
|
||||
# Follow the instructions output by the command above
|
||||
```
|
||||
|
||||
**Useful PM2 Commands:**
|
||||
|
||||
```bash
|
||||
# View status
|
||||
pm2 status
|
||||
|
||||
# View logs
|
||||
pm2 logs
|
||||
|
||||
# Restart all apps
|
||||
pm2 restart all
|
||||
|
||||
# Restart specific app
|
||||
pm2 restart award-backend
|
||||
|
||||
# Stop all apps
|
||||
pm2 stop all
|
||||
|
||||
# Monitor resources
|
||||
pm2 monit
|
||||
```
|
||||
|
||||
### Step 6: Configure HAProxy
|
||||
|
||||
Edit `/etc/haproxy/haproxy.cfg`:
|
||||
|
||||
```haproxy
|
||||
global
|
||||
log /dev/log local0
|
||||
log /dev/log local1 notice
|
||||
chroot /var/lib/haproxy
|
||||
stats socket /run/haproxy/admin.sock mode 660 level admin
|
||||
stats timeout 30s
|
||||
user haproxy
|
||||
group haproxy
|
||||
daemon
|
||||
|
||||
# Default SSL material locations
|
||||
ca-base /etc/ssl/certs
|
||||
crt-base /etc/ssl/private
|
||||
|
||||
# Default ciphers to use
|
||||
ssl-default-bind-ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS
|
||||
ssl-default-bind-options no-sslv3
|
||||
|
||||
defaults
|
||||
log global
|
||||
mode http
|
||||
option httplog
|
||||
option dontlognull
|
||||
timeout connect 5000
|
||||
timeout client 50000
|
||||
timeout server 50000
|
||||
errorfile 400 /etc/haproxy/errors/400.http
|
||||
errorfile 403 /etc/haproxy/errors/403.http
|
||||
errorfile 408 /etc/haproxy/errors/408.http
|
||||
errorfile 500 /etc/haproxy/errors/500.http
|
||||
errorfile 502 /etc/haproxy/errors/502.http
|
||||
errorfile 503 /etc/haproxy/errors/503.http
|
||||
errorfile 504 /etc/haproxy/errors/504.http
|
||||
|
||||
# Statistics page (optional - secure with auth)
|
||||
listen stats
|
||||
bind *:8404
|
||||
mode http
|
||||
stats enable
|
||||
stats uri /stats
|
||||
stats refresh 30s
|
||||
stats realm HAProxy\ Statistics
|
||||
stats auth admin:your-secure-password
|
||||
|
||||
# Frontend redirect HTTP to HTTPS
|
||||
frontend http-in
|
||||
bind *:80
|
||||
http-request redirect scheme https
|
||||
|
||||
# Frontend HTTPS
|
||||
frontend https-in
|
||||
bind *:443 ssl crt /etc/ssl/private/awards.dj7nt.de.pem
|
||||
default_backend award-backend
|
||||
|
||||
# Backend configuration
|
||||
backend award-backend
|
||||
# Health check
|
||||
option httpchk GET /api/health
|
||||
|
||||
# Single server serving both frontend and API
|
||||
server award-backend 127.0.0.1:3001 check
|
||||
```
|
||||
|
||||
**SSL Certificate Setup:**
|
||||
|
||||
Using Let's Encrypt with Certbot:
|
||||
|
||||
```bash
|
||||
# Install certbot
|
||||
apt install certbot
|
||||
|
||||
# Generate certificate
|
||||
certbot certonly --standalone -d awards.dj7nt.de
|
||||
|
||||
# Combine certificate and key for HAProxy
|
||||
cat /etc/letsencrypt/live/awards.dj7nt.de/fullchain.pem > /etc/ssl/private/awards.dj7nt.de.pem
|
||||
cat /etc/letsencrypt/live/awards.dj7nt.de/privkey.pem >> /etc/ssl/private/awards.dj7nt.de.pem
|
||||
|
||||
# Set proper permissions
|
||||
chmod 600 /etc/ssl/private/awards.dj7nt.de.pem
|
||||
```
|
||||
|
||||
### Step 7: Start HAProxy
|
||||
|
||||
```bash
|
||||
# Test configuration
|
||||
haproxy -c -f /etc/haproxy/haproxy.cfg
|
||||
|
||||
# Restart HAProxy
|
||||
systemctl restart haproxy
|
||||
|
||||
# Enable HAProxy on boot
|
||||
systemctl enable haproxy
|
||||
|
||||
# Check status
|
||||
systemctl status haproxy
|
||||
```
|
||||
|
||||
### Step 8: Verify Deployment
|
||||
|
||||
```bash
|
||||
# Check PM2 processes
|
||||
pm2 status
|
||||
|
||||
# Check HAProxy stats (if enabled)
|
||||
curl http://localhost:8404/stats
|
||||
|
||||
# Test health endpoint
|
||||
curl https://awards.dj7nt.de/api/health
|
||||
|
||||
# Check logs
|
||||
pm2 logs
|
||||
tail -f /var/log/haproxy.log
|
||||
```
|
||||
|
||||
### Updating the Application
|
||||
|
||||
```bash
|
||||
# Pull latest changes
|
||||
git pull
|
||||
|
||||
# Install updated dependencies
|
||||
bun install
|
||||
|
||||
# Rebuild frontend (if UI changed)
|
||||
bun run build
|
||||
|
||||
# Preview the production build locally
|
||||
bun run preview
|
||||
# Restart PM2
|
||||
pm2 restart award-backend
|
||||
```
|
||||
|
||||
### Deployment Options
|
||||
### Database Backups
|
||||
|
||||
#### Option 1: Static Site + Backend Server
|
||||
|
||||
1. Build the frontend: `bun run build`
|
||||
2. Serve `src/frontend/build/` with Elysia using `@elysiajs/static`
|
||||
3. Backend runs on one port serving both frontend and API
|
||||
|
||||
#### Option 2: Reverse Proxy (Recommended)
|
||||
|
||||
Use nginx or Caddy to proxy:
|
||||
- `/` → SvelteKit frontend (port 5173 or static files)
|
||||
- `/api` → Elysia backend (port 3001)
|
||||
|
||||
**Example nginx configuration:**
|
||||
```nginx
|
||||
server {
|
||||
server_name awards.dj7nt.de;
|
||||
|
||||
# Frontend
|
||||
location / {
|
||||
proxy_pass http://localhost:5173;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
|
||||
# Backend API
|
||||
location /api {
|
||||
proxy_pass http://localhost:3001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Option 3: Single Process with SvelteKit Node Adapter
|
||||
|
||||
Use `@sveltejs/adapter-node` to build for Node/Bun:
|
||||
- Everything runs in one process
|
||||
- API routes handled by SvelteKit (need to migrate from Elysia)
|
||||
|
||||
### Environment Variables for Production
|
||||
|
||||
Make sure to set these in your production environment:
|
||||
Set up automated backups:
|
||||
|
||||
```bash
|
||||
VITE_APP_URL=https://awards.dj7nt.de
|
||||
VITE_API_BASE_URL= # Leave empty for same-domain
|
||||
JWT_SECRET=<strong-random-string>
|
||||
NODE_ENV=production
|
||||
# Create backup script
|
||||
cat > /usr/local/bin/backup-award.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
BACKUP_DIR="/backups/award"
|
||||
DATE=$(date +%Y%m%d_%H%M%S)
|
||||
mkdir -p $BACKUP_DIR
|
||||
|
||||
# Backup database
|
||||
cp /path/to/award/award.db $BACKUP_DIR/award_$DATE.db
|
||||
|
||||
# Keep last 30 days
|
||||
find $BACKUP_DIR -name "award_*.db" -mtime +30 -delete
|
||||
EOF
|
||||
|
||||
chmod +x /usr/local/bin/backup-award.sh
|
||||
|
||||
# Add to crontab (daily at 2 AM)
|
||||
crontab -e
|
||||
# Add line: 0 2 * * * /usr/local/bin/backup-award.sh
|
||||
```
|
||||
|
||||
### Monitoring
|
||||
|
||||
**PM2 Monitoring:**
|
||||
```bash
|
||||
# Real-time monitoring
|
||||
pm2 monit
|
||||
|
||||
# View logs
|
||||
pm2 logs --lines 100
|
||||
```
|
||||
|
||||
**HAProxy Monitoring:**
|
||||
- Access stats page: `http://your-server:8404/stats`
|
||||
- Check logs: `tail -f /var/log/haproxy.log`
|
||||
|
||||
**Log Files Locations:**
|
||||
- PM2 logs: `./logs/backend-error.log`, `./logs/frontend-error.log`
|
||||
- HAProxy logs: `/var/log/haproxy.log`
|
||||
- System logs: `journalctl -u haproxy -f`
|
||||
|
||||
### Security Checklist
|
||||
|
||||
- [ ] HTTPS enabled with valid SSL certificate
|
||||
- [ ] Firewall configured (ufw/firewalld)
|
||||
- [ ] JWT_SECRET is strong and randomly generated
|
||||
- [ ] .env file has proper permissions (600)
|
||||
- [ ] Database backups automated
|
||||
- [ ] PM2 stats page secured with authentication
|
||||
- [ ] HAProxy stats page secured (if publicly accessible)
|
||||
- [ ] Regular security updates applied
|
||||
- [ ] Log rotation configured for application logs
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
**Application won't start:**
|
||||
```bash
|
||||
# Check PM2 logs
|
||||
pm2 logs --err
|
||||
|
||||
# Check if ports are in use
|
||||
netstat -tulpn | grep -E ':(3001|5173)'
|
||||
|
||||
# Verify environment variables
|
||||
pm2 env 0
|
||||
```
|
||||
|
||||
**HAProxy not forwarding requests:**
|
||||
```bash
|
||||
# Test backend directly
|
||||
curl http://localhost:3001/api/health
|
||||
curl http://localhost:5173/
|
||||
|
||||
# Check HAProxy configuration
|
||||
haproxy -c -f /etc/haproxy/haproxy.cfg
|
||||
|
||||
# View HAProxy logs
|
||||
tail -f /var/log/haproxy.log
|
||||
```
|
||||
|
||||
**Database issues:**
|
||||
```bash
|
||||
# Check database file permissions
|
||||
ls -la award.db
|
||||
|
||||
# Verify database integrity
|
||||
bun run db:studio
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Features in Detail
|
||||
|
||||
### Background Job Queue
|
||||
|
||||
Reference in New Issue
Block a user