qhttpx 2.0.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -6
- package/dist/package.json +1 -1
- package/docs/API_REFERENCE.md +749 -0
- package/docs/MIGRATION_1.9_TO_2.0.md +495 -0
- package/docs/PRODUCTION_DEPLOYMENT.md +798 -0
- package/docs/SECURITY.md +876 -0
- package/package.json +1 -1
|
@@ -0,0 +1,798 @@
|
|
|
1
|
+
# QHttpX Production Deployment Guide
|
|
2
|
+
|
|
3
|
+
**Complete Guide to Deploying QHttpX in Production**
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Pre-Deployment Checklist
|
|
8
|
+
|
|
9
|
+
- [ ] All tests passing (`npm test`)
|
|
10
|
+
- [ ] Zero TypeScript errors (`npm run build`)
|
|
11
|
+
- [ ] No npm security vulnerabilities (`npm audit`)
|
|
12
|
+
- [ ] Environment variables documented
|
|
13
|
+
- [ ] Database migrations applied
|
|
14
|
+
- [ ] Backups scheduled
|
|
15
|
+
- [ ] Monitoring configured
|
|
16
|
+
- [ ] Alerting setup
|
|
17
|
+
- [ ] Logging aggregation ready
|
|
18
|
+
- [ ] Load testing completed
|
|
19
|
+
- [ ] Security review done
|
|
20
|
+
- [ ] Team trained on deployment
|
|
21
|
+
- [ ] Rollback plan documented
|
|
22
|
+
- [ ] Incident response plan ready
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Environment Setup
|
|
27
|
+
|
|
28
|
+
### Environment Variables
|
|
29
|
+
|
|
30
|
+
Create `.env.production`:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Server
|
|
34
|
+
NODE_ENV=production
|
|
35
|
+
PORT=3000
|
|
36
|
+
WORKERS=4
|
|
37
|
+
|
|
38
|
+
# Database
|
|
39
|
+
DATABASE_URL=postgresql://user:pass@prod-db:5432/qhttpx_prod
|
|
40
|
+
DB_POOL_MIN=5
|
|
41
|
+
DB_POOL_MAX=20
|
|
42
|
+
|
|
43
|
+
# Authentication
|
|
44
|
+
JWT_SECRET=your-super-secret-key-min-32-chars
|
|
45
|
+
JWT_EXPIRES_IN=7d
|
|
46
|
+
BCRYPT_ROUNDS=12
|
|
47
|
+
|
|
48
|
+
# Security
|
|
49
|
+
CORS_ORIGIN=https://example.com,https://www.example.com
|
|
50
|
+
RATE_LIMIT_WINDOW_MS=900000
|
|
51
|
+
RATE_LIMIT_MAX=100
|
|
52
|
+
|
|
53
|
+
# HTTPS
|
|
54
|
+
TLS_KEY_PATH=/etc/ssl/private/key.pem
|
|
55
|
+
TLS_CERT_PATH=/etc/ssl/certs/cert.pem
|
|
56
|
+
|
|
57
|
+
# Logging
|
|
58
|
+
LOG_LEVEL=info
|
|
59
|
+
LOG_FORMAT=json
|
|
60
|
+
|
|
61
|
+
# Monitoring
|
|
62
|
+
SENTRY_DSN=https://xxx@xxx.ingest.sentry.io/123456
|
|
63
|
+
DATADOG_API_KEY=your-datadog-key
|
|
64
|
+
PROMETHEUS_PORT=9090
|
|
65
|
+
|
|
66
|
+
# Secrets
|
|
67
|
+
VAULT_ADDR=https://vault.example.com
|
|
68
|
+
VAULT_TOKEN=s.xxxxxx
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Environment Validation
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
// src/env.ts
|
|
75
|
+
import { z } from 'zod';
|
|
76
|
+
|
|
77
|
+
const envSchema = z.object({
|
|
78
|
+
NODE_ENV: z.enum(['development', 'production', 'test']),
|
|
79
|
+
PORT: z.coerce.number().min(1).max(65535),
|
|
80
|
+
DATABASE_URL: z.string().url(),
|
|
81
|
+
JWT_SECRET: z.string().min(32),
|
|
82
|
+
CORS_ORIGIN: z.string(),
|
|
83
|
+
TLS_KEY_PATH: z.string().optional(),
|
|
84
|
+
TLS_CERT_PATH: z.string().optional(),
|
|
85
|
+
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']),
|
|
86
|
+
SENTRY_DSN: z.string().url().optional(),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
export const env = envSchema.parse(process.env);
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Deployment Strategies
|
|
95
|
+
|
|
96
|
+
### 1. Traditional VPS/EC2 Deployment
|
|
97
|
+
|
|
98
|
+
#### Initial Setup
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
# SSH into server
|
|
102
|
+
ssh ubuntu@your-server.com
|
|
103
|
+
|
|
104
|
+
# Update system
|
|
105
|
+
sudo apt update && sudo apt upgrade -y
|
|
106
|
+
|
|
107
|
+
# Install Node.js 18+
|
|
108
|
+
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
|
|
109
|
+
sudo apt install -y nodejs
|
|
110
|
+
|
|
111
|
+
# Install PM2 (process manager)
|
|
112
|
+
sudo npm install -g pm2
|
|
113
|
+
|
|
114
|
+
# Clone repository
|
|
115
|
+
git clone https://github.com/yourusername/qhttpx-app.git
|
|
116
|
+
cd qhttpx-app
|
|
117
|
+
|
|
118
|
+
# Install dependencies
|
|
119
|
+
npm ci --production
|
|
120
|
+
|
|
121
|
+
# Build
|
|
122
|
+
npm run build
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
#### PM2 Configuration
|
|
126
|
+
|
|
127
|
+
Create `ecosystem.config.js`:
|
|
128
|
+
|
|
129
|
+
```javascript
|
|
130
|
+
module.exports = {
|
|
131
|
+
apps: [
|
|
132
|
+
{
|
|
133
|
+
name: 'qhttpx-app',
|
|
134
|
+
script: 'dist/index.js',
|
|
135
|
+
instances: 4,
|
|
136
|
+
exec_mode: 'cluster',
|
|
137
|
+
env: {
|
|
138
|
+
NODE_ENV: 'production',
|
|
139
|
+
PORT: 3000,
|
|
140
|
+
},
|
|
141
|
+
// Restart if memory exceeds 500MB
|
|
142
|
+
max_memory_restart: '500M',
|
|
143
|
+
// Restart crashed apps
|
|
144
|
+
autorestart: true,
|
|
145
|
+
// Max restarts before waiting
|
|
146
|
+
max_restarts: 5,
|
|
147
|
+
min_uptime: '10s',
|
|
148
|
+
// Logging
|
|
149
|
+
out_file: '/var/log/qhttpx/out.log',
|
|
150
|
+
error_file: '/var/log/qhttpx/error.log',
|
|
151
|
+
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
};
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
#### Startup
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
# Start with PM2
|
|
161
|
+
pm2 start ecosystem.config.js
|
|
162
|
+
|
|
163
|
+
# Make it start on reboot
|
|
164
|
+
pm2 startup
|
|
165
|
+
pm2 save
|
|
166
|
+
|
|
167
|
+
# Monitor
|
|
168
|
+
pm2 monit
|
|
169
|
+
pm2 logs
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
#### Nginx Reverse Proxy
|
|
173
|
+
|
|
174
|
+
```nginx
|
|
175
|
+
upstream qhttpx {
|
|
176
|
+
server 127.0.0.1:3000;
|
|
177
|
+
server 127.0.0.1:3001;
|
|
178
|
+
server 127.0.0.1:3002;
|
|
179
|
+
server 127.0.0.1:3003;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
server {
|
|
183
|
+
listen 443 ssl http2;
|
|
184
|
+
server_name api.example.com;
|
|
185
|
+
|
|
186
|
+
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
|
|
187
|
+
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
|
|
188
|
+
|
|
189
|
+
# Strong SSL config
|
|
190
|
+
ssl_protocols TLSv1.2 TLSv1.3;
|
|
191
|
+
ssl_ciphers HIGH:!aNULL:!MD5;
|
|
192
|
+
ssl_prefer_server_ciphers on;
|
|
193
|
+
|
|
194
|
+
# Security headers
|
|
195
|
+
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
|
|
196
|
+
add_header X-Content-Type-Options "nosniff" always;
|
|
197
|
+
add_header X-Frame-Options "DENY" always;
|
|
198
|
+
add_header X-XSS-Protection "1; mode=block" always;
|
|
199
|
+
|
|
200
|
+
# Proxy settings
|
|
201
|
+
location / {
|
|
202
|
+
proxy_pass http://qhttpx;
|
|
203
|
+
proxy_set_header Host $host;
|
|
204
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
205
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
206
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
207
|
+
|
|
208
|
+
# WebSocket support
|
|
209
|
+
proxy_http_version 1.1;
|
|
210
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
211
|
+
proxy_set_header Connection "upgrade";
|
|
212
|
+
|
|
213
|
+
# Timeouts
|
|
214
|
+
proxy_connect_timeout 60s;
|
|
215
|
+
proxy_send_timeout 60s;
|
|
216
|
+
proxy_read_timeout 60s;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
# Health check endpoint
|
|
220
|
+
location /health {
|
|
221
|
+
access_log off;
|
|
222
|
+
proxy_pass http://qhttpx;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
# Redirect HTTP to HTTPS
|
|
227
|
+
server {
|
|
228
|
+
listen 80;
|
|
229
|
+
server_name api.example.com;
|
|
230
|
+
return 301 https://$server_name$request_uri;
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
#### Certbot for SSL
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
# Install Let's Encrypt
|
|
238
|
+
sudo apt install certbot python3-certbot-nginx
|
|
239
|
+
|
|
240
|
+
# Get certificate
|
|
241
|
+
sudo certbot certonly --standalone -d api.example.com
|
|
242
|
+
|
|
243
|
+
# Auto-renewal
|
|
244
|
+
0 12 * * * certbot renew --quiet && nginx -s reload
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
### 2. Docker Deployment
|
|
250
|
+
|
|
251
|
+
#### Dockerfile
|
|
252
|
+
|
|
253
|
+
```dockerfile
|
|
254
|
+
# Build stage
|
|
255
|
+
FROM node:18-alpine AS builder
|
|
256
|
+
|
|
257
|
+
WORKDIR /app
|
|
258
|
+
|
|
259
|
+
COPY package*.json ./
|
|
260
|
+
RUN npm ci
|
|
261
|
+
|
|
262
|
+
COPY . .
|
|
263
|
+
RUN npm run build
|
|
264
|
+
|
|
265
|
+
# Production stage
|
|
266
|
+
FROM node:18-alpine
|
|
267
|
+
|
|
268
|
+
WORKDIR /app
|
|
269
|
+
|
|
270
|
+
# Install dumb-init to handle signals properly
|
|
271
|
+
RUN apk add --no-cache dumb-init
|
|
272
|
+
|
|
273
|
+
# Copy from builder
|
|
274
|
+
COPY --from=builder /app/dist ./dist
|
|
275
|
+
COPY --from=builder /app/node_modules ./node_modules
|
|
276
|
+
COPY package*.json ./
|
|
277
|
+
|
|
278
|
+
# Non-root user
|
|
279
|
+
RUN addgroup -g 1001 -S nodejs && \
|
|
280
|
+
adduser -S nodejs -u 1001
|
|
281
|
+
|
|
282
|
+
USER nodejs
|
|
283
|
+
|
|
284
|
+
# Health check
|
|
285
|
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
|
286
|
+
CMD node -e "require('http').get('http://localhost:3000/health', (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})"
|
|
287
|
+
|
|
288
|
+
# Use dumb-init to forward signals
|
|
289
|
+
ENTRYPOINT ["dumb-init", "--"]
|
|
290
|
+
|
|
291
|
+
# Start app
|
|
292
|
+
CMD ["node", "dist/index.js"]
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
#### docker-compose.yml
|
|
296
|
+
|
|
297
|
+
```yaml
|
|
298
|
+
version: '3.8'
|
|
299
|
+
|
|
300
|
+
services:
|
|
301
|
+
app:
|
|
302
|
+
build: .
|
|
303
|
+
ports:
|
|
304
|
+
- "3000:3000"
|
|
305
|
+
environment:
|
|
306
|
+
NODE_ENV: production
|
|
307
|
+
DATABASE_URL: postgresql://user:pass@postgres:5432/qhttpx
|
|
308
|
+
JWT_SECRET: ${JWT_SECRET}
|
|
309
|
+
depends_on:
|
|
310
|
+
postgres:
|
|
311
|
+
condition: service_healthy
|
|
312
|
+
restart: unless-stopped
|
|
313
|
+
healthcheck:
|
|
314
|
+
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
|
|
315
|
+
interval: 30s
|
|
316
|
+
timeout: 10s
|
|
317
|
+
retries: 3
|
|
318
|
+
start_period: 5s
|
|
319
|
+
networks:
|
|
320
|
+
- qhttpx-network
|
|
321
|
+
|
|
322
|
+
postgres:
|
|
323
|
+
image: postgres:15-alpine
|
|
324
|
+
environment:
|
|
325
|
+
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
|
326
|
+
POSTGRES_DB: qhttpx
|
|
327
|
+
volumes:
|
|
328
|
+
- postgres_data:/var/lib/postgresql/data
|
|
329
|
+
restart: unless-stopped
|
|
330
|
+
healthcheck:
|
|
331
|
+
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
|
332
|
+
interval: 10s
|
|
333
|
+
timeout: 5s
|
|
334
|
+
retries: 5
|
|
335
|
+
networks:
|
|
336
|
+
- qhttpx-network
|
|
337
|
+
|
|
338
|
+
nginx:
|
|
339
|
+
image: nginx:alpine
|
|
340
|
+
ports:
|
|
341
|
+
- "80:80"
|
|
342
|
+
- "443:443"
|
|
343
|
+
volumes:
|
|
344
|
+
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
|
345
|
+
- ./ssl:/etc/nginx/ssl:ro
|
|
346
|
+
depends_on:
|
|
347
|
+
- app
|
|
348
|
+
restart: unless-stopped
|
|
349
|
+
networks:
|
|
350
|
+
- qhttpx-network
|
|
351
|
+
|
|
352
|
+
volumes:
|
|
353
|
+
postgres_data:
|
|
354
|
+
|
|
355
|
+
networks:
|
|
356
|
+
qhttpx-network:
|
|
357
|
+
driver: bridge
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
#### Build and Deploy
|
|
361
|
+
|
|
362
|
+
```bash
|
|
363
|
+
# Build image
|
|
364
|
+
docker build -t qhttpx:latest .
|
|
365
|
+
|
|
366
|
+
# Run with compose
|
|
367
|
+
docker-compose up -d
|
|
368
|
+
|
|
369
|
+
# Monitor
|
|
370
|
+
docker-compose logs -f app
|
|
371
|
+
|
|
372
|
+
# Scale
|
|
373
|
+
docker-compose up -d --scale app=4
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
### 3. Kubernetes Deployment
|
|
379
|
+
|
|
380
|
+
#### Deployment
|
|
381
|
+
|
|
382
|
+
```yaml
|
|
383
|
+
apiVersion: apps/v1
|
|
384
|
+
kind: Deployment
|
|
385
|
+
metadata:
|
|
386
|
+
name: qhttpx-app
|
|
387
|
+
labels:
|
|
388
|
+
app: qhttpx
|
|
389
|
+
spec:
|
|
390
|
+
replicas: 3
|
|
391
|
+
selector:
|
|
392
|
+
matchLabels:
|
|
393
|
+
app: qhttpx
|
|
394
|
+
template:
|
|
395
|
+
metadata:
|
|
396
|
+
labels:
|
|
397
|
+
app: qhttpx
|
|
398
|
+
spec:
|
|
399
|
+
containers:
|
|
400
|
+
- name: qhttpx
|
|
401
|
+
image: qhttpx:latest
|
|
402
|
+
imagePullPolicy: Always
|
|
403
|
+
ports:
|
|
404
|
+
- name: http
|
|
405
|
+
containerPort: 3000
|
|
406
|
+
env:
|
|
407
|
+
- name: NODE_ENV
|
|
408
|
+
value: production
|
|
409
|
+
- name: DATABASE_URL
|
|
410
|
+
valueFrom:
|
|
411
|
+
secretKeyRef:
|
|
412
|
+
name: qhttpx-secrets
|
|
413
|
+
key: database-url
|
|
414
|
+
- name: JWT_SECRET
|
|
415
|
+
valueFrom:
|
|
416
|
+
secretKeyRef:
|
|
417
|
+
name: qhttpx-secrets
|
|
418
|
+
key: jwt-secret
|
|
419
|
+
livenessProbe:
|
|
420
|
+
httpGet:
|
|
421
|
+
path: /health
|
|
422
|
+
port: 3000
|
|
423
|
+
initialDelaySeconds: 10
|
|
424
|
+
periodSeconds: 30
|
|
425
|
+
timeoutSeconds: 5
|
|
426
|
+
failureThreshold: 3
|
|
427
|
+
readinessProbe:
|
|
428
|
+
httpGet:
|
|
429
|
+
path: /health
|
|
430
|
+
port: 3000
|
|
431
|
+
initialDelaySeconds: 5
|
|
432
|
+
periodSeconds: 10
|
|
433
|
+
timeoutSeconds: 5
|
|
434
|
+
resources:
|
|
435
|
+
requests:
|
|
436
|
+
memory: "256Mi"
|
|
437
|
+
cpu: "250m"
|
|
438
|
+
limits:
|
|
439
|
+
memory: "512Mi"
|
|
440
|
+
cpu: "500m"
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
#### Service
|
|
444
|
+
|
|
445
|
+
```yaml
|
|
446
|
+
apiVersion: v1
|
|
447
|
+
kind: Service
|
|
448
|
+
metadata:
|
|
449
|
+
name: qhttpx-service
|
|
450
|
+
spec:
|
|
451
|
+
type: LoadBalancer
|
|
452
|
+
selector:
|
|
453
|
+
app: qhttpx
|
|
454
|
+
ports:
|
|
455
|
+
- name: http
|
|
456
|
+
port: 80
|
|
457
|
+
targetPort: 3000
|
|
458
|
+
- name: https
|
|
459
|
+
port: 443
|
|
460
|
+
targetPort: 3000
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
#### Deploy
|
|
464
|
+
|
|
465
|
+
```bash
|
|
466
|
+
# Create secrets
|
|
467
|
+
kubectl create secret generic qhttpx-secrets \
|
|
468
|
+
--from-literal=database-url=$DATABASE_URL \
|
|
469
|
+
--from-literal=jwt-secret=$JWT_SECRET
|
|
470
|
+
|
|
471
|
+
# Deploy
|
|
472
|
+
kubectl apply -f deployment.yaml
|
|
473
|
+
kubectl apply -f service.yaml
|
|
474
|
+
|
|
475
|
+
# Monitor
|
|
476
|
+
kubectl get pods
|
|
477
|
+
kubectl logs -f deployment/qhttpx-app
|
|
478
|
+
kubectl describe service qhttpx-service
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
---
|
|
482
|
+
|
|
483
|
+
## Database Migrations
|
|
484
|
+
|
|
485
|
+
### Migration Script
|
|
486
|
+
|
|
487
|
+
```typescript
|
|
488
|
+
// scripts/migrate.ts
|
|
489
|
+
import { Pool } from 'pg';
|
|
490
|
+
|
|
491
|
+
const pool = new Pool({
|
|
492
|
+
connectionString: process.env.DATABASE_URL,
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
const migrations = [
|
|
496
|
+
{
|
|
497
|
+
name: '001_create_users_table',
|
|
498
|
+
up: async (client) => {
|
|
499
|
+
await client.query(`
|
|
500
|
+
CREATE TABLE users (
|
|
501
|
+
id SERIAL PRIMARY KEY,
|
|
502
|
+
email VARCHAR(255) UNIQUE NOT NULL,
|
|
503
|
+
password VARCHAR(255) NOT NULL,
|
|
504
|
+
name VARCHAR(255),
|
|
505
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
506
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
507
|
+
)
|
|
508
|
+
`);
|
|
509
|
+
},
|
|
510
|
+
down: async (client) => {
|
|
511
|
+
await client.query('DROP TABLE users');
|
|
512
|
+
},
|
|
513
|
+
},
|
|
514
|
+
];
|
|
515
|
+
|
|
516
|
+
async function migrate(direction: 'up' | 'down') {
|
|
517
|
+
const client = await pool.connect();
|
|
518
|
+
|
|
519
|
+
try {
|
|
520
|
+
// Create migrations table
|
|
521
|
+
await client.query(`
|
|
522
|
+
CREATE TABLE IF NOT EXISTS migrations (
|
|
523
|
+
id SERIAL PRIMARY KEY,
|
|
524
|
+
name VARCHAR(255) UNIQUE NOT NULL,
|
|
525
|
+
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
526
|
+
)
|
|
527
|
+
`);
|
|
528
|
+
|
|
529
|
+
for (const migration of migrations) {
|
|
530
|
+
const executed = await client.query(
|
|
531
|
+
'SELECT * FROM migrations WHERE name = $1',
|
|
532
|
+
[migration.name]
|
|
533
|
+
);
|
|
534
|
+
|
|
535
|
+
if (direction === 'up' && executed.rows.length === 0) {
|
|
536
|
+
console.log(`Running: ${migration.name}`);
|
|
537
|
+
await migration.up(client);
|
|
538
|
+
await client.query(
|
|
539
|
+
'INSERT INTO migrations (name) VALUES ($1)',
|
|
540
|
+
[migration.name]
|
|
541
|
+
);
|
|
542
|
+
} else if (direction === 'down' && executed.rows.length > 0) {
|
|
543
|
+
console.log(`Rolling back: ${migration.name}`);
|
|
544
|
+
await migration.down(client);
|
|
545
|
+
await client.query(
|
|
546
|
+
'DELETE FROM migrations WHERE name = $1',
|
|
547
|
+
[migration.name]
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
console.log('Done!');
|
|
553
|
+
} finally {
|
|
554
|
+
await client.end();
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const direction = process.argv[2] as 'up' | 'down' || 'up';
|
|
559
|
+
migrate(direction).catch(console.error);
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
#### Run Migrations
|
|
563
|
+
|
|
564
|
+
```bash
|
|
565
|
+
# Run migrations before deployment
|
|
566
|
+
npm run migrate:up
|
|
567
|
+
|
|
568
|
+
# Test rollback
|
|
569
|
+
npm run migrate:down
|
|
570
|
+
|
|
571
|
+
# Run migrations again
|
|
572
|
+
npm run migrate:up
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
---
|
|
576
|
+
|
|
577
|
+
## Monitoring & Observability
|
|
578
|
+
|
|
579
|
+
### Health Check Endpoint
|
|
580
|
+
|
|
581
|
+
```typescript
|
|
582
|
+
app.get('/health', ({ json }) => {
|
|
583
|
+
json({
|
|
584
|
+
status: 'healthy',
|
|
585
|
+
uptime: process.uptime(),
|
|
586
|
+
timestamp: new Date().toISOString(),
|
|
587
|
+
}, 200);
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
app.get('/readiness', ({ json, db }) => {
|
|
591
|
+
try {
|
|
592
|
+
// Test database
|
|
593
|
+
await db?.query('SELECT 1');
|
|
594
|
+
json({ ready: true }, 200);
|
|
595
|
+
} catch {
|
|
596
|
+
json({ ready: false }, 503);
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
### Metrics Endpoint
|
|
602
|
+
|
|
603
|
+
```typescript
|
|
604
|
+
import client from 'prom-client';
|
|
605
|
+
|
|
606
|
+
app.get('/metrics', ({ send }) => {
|
|
607
|
+
const metrics = client.register.metrics();
|
|
608
|
+
send(metrics, 200);
|
|
609
|
+
});
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
### Logging Aggregation
|
|
613
|
+
|
|
614
|
+
```typescript
|
|
615
|
+
import pino from 'pino';
|
|
616
|
+
|
|
617
|
+
const logger = pino({
|
|
618
|
+
transport: {
|
|
619
|
+
target: 'pino-loki',
|
|
620
|
+
options: {
|
|
621
|
+
host: 'logs-server.example.com',
|
|
622
|
+
basicAuth: process.env.LOKI_AUTH,
|
|
623
|
+
},
|
|
624
|
+
},
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
app.use(async ({ req, next }) => {
|
|
628
|
+
logger.info({ method: req.method, path: req.url });
|
|
629
|
+
if (next) await next();
|
|
630
|
+
});
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
### Error Tracking
|
|
634
|
+
|
|
635
|
+
```typescript
|
|
636
|
+
import * as Sentry from "@sentry/node";
|
|
637
|
+
|
|
638
|
+
Sentry.init({
|
|
639
|
+
dsn: process.env.SENTRY_DSN,
|
|
640
|
+
environment: process.env.NODE_ENV,
|
|
641
|
+
tracesSampleRate: 0.1,
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
app.onError(({ error, json }) => {
|
|
645
|
+
Sentry.captureException(error);
|
|
646
|
+
json({ error: 'Internal server error' }, 500);
|
|
647
|
+
});
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
---
|
|
651
|
+
|
|
652
|
+
## Performance Tuning
|
|
653
|
+
|
|
654
|
+
### Node.js Flags
|
|
655
|
+
|
|
656
|
+
```bash
|
|
657
|
+
# Increase memory limit
|
|
658
|
+
node --max-old-space-size=4096 dist/index.js
|
|
659
|
+
|
|
660
|
+
# Enable profiling
|
|
661
|
+
node --prof dist/index.js
|
|
662
|
+
node --prof-process isolate-*.log > profile.txt
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
### PM2 Cluster Mode
|
|
666
|
+
|
|
667
|
+
```javascript
|
|
668
|
+
module.exports = {
|
|
669
|
+
apps: [
|
|
670
|
+
{
|
|
671
|
+
name: 'qhttpx',
|
|
672
|
+
script: 'dist/index.js',
|
|
673
|
+
instances: 'max', // Use all CPU cores
|
|
674
|
+
exec_mode: 'cluster',
|
|
675
|
+
merge_logs: true,
|
|
676
|
+
},
|
|
677
|
+
],
|
|
678
|
+
};
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
---
|
|
682
|
+
|
|
683
|
+
## Backup & Disaster Recovery
|
|
684
|
+
|
|
685
|
+
### Database Backups
|
|
686
|
+
|
|
687
|
+
```bash
|
|
688
|
+
# Daily backup
|
|
689
|
+
0 2 * * * pg_dump $DATABASE_URL > /backups/db-$(date +%Y%m%d).sql
|
|
690
|
+
|
|
691
|
+
# Weekly full backup to S3
|
|
692
|
+
0 3 * * 0 pg_dump $DATABASE_URL | gzip | aws s3 cp - s3://backups/db-$(date +%Y%m%d).sql.gz
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
### Restore from Backup
|
|
696
|
+
|
|
697
|
+
```bash
|
|
698
|
+
# From local backup
|
|
699
|
+
psql $DATABASE_URL < /backups/db-20240121.sql
|
|
700
|
+
|
|
701
|
+
# From S3 backup
|
|
702
|
+
aws s3 cp s3://backups/db-20240121.sql.gz - | gunzip | psql $DATABASE_URL
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
---
|
|
706
|
+
|
|
707
|
+
## Rollback Procedure
|
|
708
|
+
|
|
709
|
+
```bash
|
|
710
|
+
# Tag release
|
|
711
|
+
git tag v1.0.0
|
|
712
|
+
|
|
713
|
+
# If something goes wrong
|
|
714
|
+
git checkout v0.9.9
|
|
715
|
+
npm install
|
|
716
|
+
npm run build
|
|
717
|
+
|
|
718
|
+
# Restart with PM2
|
|
719
|
+
pm2 restart qhttpx
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
---
|
|
723
|
+
|
|
724
|
+
## Post-Deployment Checklist
|
|
725
|
+
|
|
726
|
+
- [ ] Server responding to health checks
|
|
727
|
+
- [ ] Database connections working
|
|
728
|
+
- [ ] Logging aggregation receiving logs
|
|
729
|
+
- [ ] Monitoring collecting metrics
|
|
730
|
+
- [ ] Alerting configured and tested
|
|
731
|
+
- [ ] SSL certificate valid
|
|
732
|
+
- [ ] All endpoints tested
|
|
733
|
+
- [ ] Performance acceptable
|
|
734
|
+
- [ ] Load testing completed
|
|
735
|
+
- [ ] Security scan passed
|
|
736
|
+
- [ ] Team notified of deployment
|
|
737
|
+
- [ ] Documentation updated
|
|
738
|
+
- [ ] Rollback plan confirmed
|
|
739
|
+
|
|
740
|
+
---
|
|
741
|
+
|
|
742
|
+
## Troubleshooting
|
|
743
|
+
|
|
744
|
+
### High Memory Usage
|
|
745
|
+
|
|
746
|
+
```bash
|
|
747
|
+
# Check memory
|
|
748
|
+
ps aux | grep node
|
|
749
|
+
|
|
750
|
+
# Profile memory
|
|
751
|
+
node --inspect dist/index.js
|
|
752
|
+
# Then open chrome://inspect in Chrome DevTools
|
|
753
|
+
|
|
754
|
+
# Check for memory leaks
|
|
755
|
+
npm install clinic
|
|
756
|
+
clinic doctor -- node dist/index.js
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
### Database Connection Issues
|
|
760
|
+
|
|
761
|
+
```bash
|
|
762
|
+
# Test connection
|
|
763
|
+
psql $DATABASE_URL
|
|
764
|
+
|
|
765
|
+
# Check pool status
|
|
766
|
+
app.get('/debug/db', ({ json }) => {
|
|
767
|
+
json({
|
|
768
|
+
poolSize: pool.totalCount,
|
|
769
|
+
idle: pool.idleCount,
|
|
770
|
+
waiting: pool.waitingCount,
|
|
771
|
+
});
|
|
772
|
+
});
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
### High Latency
|
|
776
|
+
|
|
777
|
+
```bash
|
|
778
|
+
# Check slow endpoints
|
|
779
|
+
app.use(async ({ path, next }) => {
|
|
780
|
+
const start = Date.now();
|
|
781
|
+
if (next) await next();
|
|
782
|
+
const duration = Date.now() - start;
|
|
783
|
+
if (duration > 1000) {
|
|
784
|
+
console.warn(`Slow: ${path} (${duration}ms)`);
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
---
|
|
790
|
+
|
|
791
|
+
## Support
|
|
792
|
+
|
|
793
|
+
For deployment issues:
|
|
794
|
+
1. Check logs: `pm2 logs` or `kubectl logs`
|
|
795
|
+
2. Check metrics: Visit `/metrics` endpoint
|
|
796
|
+
3. Check health: Visit `/health` endpoint
|
|
797
|
+
4. Read security guide: `docs/SECURITY.md`
|
|
798
|
+
5. Consult API reference: `docs/API_REFERENCE.md`
|