@donkeylabs/server 2.0.19 → 2.0.21

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.
@@ -0,0 +1,1229 @@
1
+ # Production Deployment Guide
2
+
3
+ Deploying DonkeyLabs applications to production environments with Docker, monitoring, and best practices.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Docker Deployment](#docker-deployment)
8
+ - [Environment Configuration](#environment-configuration)
9
+ - [Database Migrations](#database-migrations)
10
+ - [Health Checks](#health-checks)
11
+ - [Logging & Monitoring](#logging--monitoring)
12
+ - [Performance Optimization](#performance-optimization)
13
+ - [Security Hardening](#security-hardening)
14
+ - [CI/CD Pipelines](#cicd-pipelines)
15
+ - [Scaling Strategies](#scaling-strategies)
16
+ - [Troubleshooting Production](#troubleshooting-production)
17
+
18
+ ---
19
+
20
+ ## Docker Deployment
21
+
22
+ ### Basic Dockerfile
23
+
24
+ ```dockerfile
25
+ # Dockerfile
26
+ FROM oven/bun:1.0-alpine
27
+
28
+ WORKDIR /app
29
+
30
+ # Copy package files
31
+ COPY package.json bun.lockb ./
32
+ COPY packages/server/package.json ./packages/server/
33
+ COPY packages/cli/package.json ./packages/cli/
34
+
35
+ # Install dependencies
36
+ RUN bun install --frozen-lockfile
37
+
38
+ # Copy source code
39
+ COPY . .
40
+
41
+ # Generate types (if needed for build)
42
+ RUN bun run gen:types || true
43
+
44
+ # Build for production (if using SvelteKit)
45
+ RUN bun run build || true
46
+
47
+ # Expose port
48
+ EXPOSE 3000
49
+
50
+ # Health check
51
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
52
+ CMD curl -f http://localhost:3000/health || exit 1
53
+
54
+ # Start server
55
+ CMD ["bun", "run", "src/server/index.ts"]
56
+ ```
57
+
58
+ ### Multi-Stage Build (Optimized)
59
+
60
+ ```dockerfile
61
+ # Dockerfile.production
62
+ # Stage 1: Dependencies
63
+ FROM oven/bun:1.0-alpine AS deps
64
+ WORKDIR /app
65
+ COPY package.json bun.lockb ./
66
+ RUN bun install --frozen-lockfile --production
67
+
68
+ # Stage 2: Builder
69
+ FROM oven/bun:1.0-alpine AS builder
70
+ WORKDIR /app
71
+ COPY --from=deps /app/node_modules ./node_modules
72
+ COPY . .
73
+ RUN bun run gen:types
74
+ RUN bun run build
75
+
76
+ # Stage 3: Production
77
+ FROM oven/bun:1.0-alpine AS runner
78
+ WORKDIR /app
79
+
80
+ # Create non-root user
81
+ RUN addgroup -g 1001 -S nodejs
82
+ RUN adduser -S bunuser -u 1001
83
+
84
+ # Copy only necessary files
85
+ COPY --from=builder --chown=bunuser:bunuser /app/dist ./dist
86
+ COPY --from=builder --chown=bunuser:bunuser /app/node_modules ./node_modules
87
+ COPY --from=builder --chown=bunuser:bunuser /app/package.json ./package.json
88
+
89
+ # Switch to non-root user
90
+ USER bunuser
91
+
92
+ EXPOSE 3000
93
+
94
+ ENV NODE_ENV=production
95
+ ENV PORT=3000
96
+
97
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
98
+ CMD curl -f http://localhost:3000/health || exit 1
99
+
100
+ CMD ["bun", "run", "dist/server/index.js"]
101
+ ```
102
+
103
+ ### Docker Compose
104
+
105
+ ```yaml
106
+ # docker-compose.yml
107
+ version: '3.8'
108
+
109
+ services:
110
+ app:
111
+ build:
112
+ context: .
113
+ dockerfile: Dockerfile
114
+ ports:
115
+ - "3000:3000"
116
+ environment:
117
+ - NODE_ENV=production
118
+ - DATABASE_URL=postgresql://user:pass@db:5432/app
119
+ - REDIS_URL=redis://redis:6379
120
+ - JWT_SECRET=${JWT_SECRET}
121
+ depends_on:
122
+ db:
123
+ condition: service_healthy
124
+ redis:
125
+ condition: service_healthy
126
+ restart: unless-stopped
127
+ healthcheck:
128
+ test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
129
+ interval: 30s
130
+ timeout: 3s
131
+ retries: 3
132
+
133
+ db:
134
+ image: postgres:15-alpine
135
+ environment:
136
+ - POSTGRES_USER=user
137
+ - POSTGRES_PASSWORD=${DB_PASSWORD}
138
+ - POSTGRES_DB=app
139
+ volumes:
140
+ - postgres_data:/var/lib/postgresql/data
141
+ healthcheck:
142
+ test: ["CMD-SHELL", "pg_isready -U user -d app"]
143
+ interval: 5s
144
+ timeout: 5s
145
+ retries: 5
146
+ restart: unless-stopped
147
+
148
+ redis:
149
+ image: redis:7-alpine
150
+ volumes:
151
+ - redis_data:/data
152
+ healthcheck:
153
+ test: ["CMD", "redis-cli", "ping"]
154
+ interval: 5s
155
+ timeout: 3s
156
+ retries: 5
157
+ restart: unless-stopped
158
+
159
+ nginx:
160
+ image: nginx:alpine
161
+ ports:
162
+ - "80:80"
163
+ - "443:443"
164
+ volumes:
165
+ - ./nginx.conf:/etc/nginx/nginx.conf:ro
166
+ - ./ssl:/etc/nginx/ssl:ro
167
+ depends_on:
168
+ - app
169
+ restart: unless-stopped
170
+
171
+ volumes:
172
+ postgres_data:
173
+ redis_data:
174
+ ```
175
+
176
+ ### Nginx Configuration
177
+
178
+ ```nginx
179
+ # nginx.conf
180
+ upstream app {
181
+ server app:3000;
182
+ keepalive 32;
183
+ }
184
+
185
+ server {
186
+ listen 80;
187
+ server_name _;
188
+
189
+ # Redirect to HTTPS in production
190
+ return 301 https://$server_name$request_uri;
191
+ }
192
+
193
+ server {
194
+ listen 443 ssl http2;
195
+ server_name your-domain.com;
196
+
197
+ ssl_certificate /etc/nginx/ssl/cert.pem;
198
+ ssl_certificate_key /etc/nginx/ssl/key.pem;
199
+ ssl_protocols TLSv1.2 TLSv1.3;
200
+ ssl_ciphers HIGH:!aNULL:!MD5;
201
+
202
+ # Security headers
203
+ add_header X-Frame-Options "SAMEORIGIN" always;
204
+ add_header X-Content-Type-Options "nosniff" always;
205
+ add_header X-XSS-Protection "1; mode=block" always;
206
+
207
+ # Gzip compression
208
+ gzip on;
209
+ gzip_vary on;
210
+ gzip_proxied any;
211
+ gzip_comp_level 6;
212
+ gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml;
213
+
214
+ # API routes
215
+ location ~ ^/[a-zA-Z][a-zA-Z0-9_.]*$ {
216
+ proxy_pass http://app;
217
+ proxy_http_version 1.1;
218
+ proxy_set_header Connection "";
219
+ proxy_set_header Host $host;
220
+ proxy_set_header X-Real-IP $remote_addr;
221
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
222
+ proxy_set_header X-Forwarded-Proto $scheme;
223
+
224
+ # Timeouts
225
+ proxy_connect_timeout 60s;
226
+ proxy_send_timeout 60s;
227
+ proxy_read_timeout 60s;
228
+
229
+ # Buffering
230
+ proxy_buffering on;
231
+ proxy_buffer_size 4k;
232
+ proxy_buffers 8 4k;
233
+ }
234
+
235
+ # SSE endpoints (no buffering)
236
+ location /sse {
237
+ proxy_pass http://app;
238
+ proxy_http_version 1.1;
239
+ proxy_set_header Connection "";
240
+ proxy_set_header Host $host;
241
+ proxy_buffering off;
242
+ proxy_cache off;
243
+ proxy_read_timeout 86400s;
244
+ }
245
+
246
+ # Static files (SvelteKit)
247
+ location / {
248
+ proxy_pass http://app;
249
+ proxy_http_version 1.1;
250
+ proxy_set_header Connection "";
251
+ proxy_set_header Host $host;
252
+ proxy_cache_bypass $http_upgrade;
253
+ }
254
+
255
+ # Health check endpoint (exposed)
256
+ location /health {
257
+ proxy_pass http://app;
258
+ access_log off;
259
+ }
260
+ }
261
+ ```
262
+
263
+ ---
264
+
265
+ ## Environment Configuration
266
+
267
+ ### Environment Variables
268
+
269
+ ```bash
270
+ # .env.production
271
+ NODE_ENV=production
272
+ PORT=3000
273
+
274
+ # Database
275
+ DATABASE_URL=postgresql://user:password@host:5432/dbname
276
+ DATABASE_POOL_SIZE=20
277
+ DATABASE_TIMEOUT=30000
278
+
279
+ # Redis (optional, for distributed cache)
280
+ REDIS_URL=redis://localhost:6379
281
+ REDIS_POOL_SIZE=10
282
+
283
+ # Security
284
+ JWT_SECRET=your-super-secret-jwt-key-min-32-chars
285
+ ENCRYPTION_KEY=your-encryption-key-32-chars-long
286
+
287
+ # Logging
288
+ LOG_LEVEL=info
289
+ LOG_FORMAT=json
290
+
291
+ # Monitoring
292
+ METRICS_ENABLED=true
293
+ METRICS_PORT=9090
294
+ TRACING_ENABLED=true
295
+
296
+ # Rate Limiting
297
+ RATE_LIMIT_ENABLED=true
298
+ RATE_LIMIT_WINDOW_MS=60000
299
+ RATE_LIMIT_MAX_REQUESTS=100
300
+
301
+ # Cache
302
+ CACHE_TTL_MS=300000
303
+ CACHE_MAX_SIZE=10000
304
+
305
+ # Email (if using email plugin)
306
+ SMTP_HOST=smtp.example.com
307
+ SMTP_PORT=587
308
+ SMTP_USER=notifications@example.com
309
+ SMTP_PASS=your-smtp-password
310
+
311
+ # External APIs
312
+ STRIPE_SECRET_KEY=sk_live_...
313
+ STRIPE_WEBHOOK_SECRET=whsec_...
314
+ ```
315
+
316
+ ### Configuration Validation
317
+
318
+ ```typescript
319
+ // src/server/config.ts
320
+ import { z } from "zod";
321
+
322
+ const configSchema = z.object({
323
+ NODE_ENV: z.enum(["development", "production", "test"]),
324
+ PORT: z.string().transform(Number).default("3000"),
325
+ DATABASE_URL: z.string().url(),
326
+ DATABASE_POOL_SIZE: z.string().transform(Number).default("20"),
327
+ JWT_SECRET: z.string().min(32),
328
+ LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
329
+ RATE_LIMIT_ENABLED: z.string().transform(Boolean).default("true"),
330
+ CACHE_TTL_MS: z.string().transform(Number).default("300000"),
331
+ });
332
+
333
+ export const config = configSchema.parse(process.env);
334
+ ```
335
+
336
+ ### Production Server Setup
337
+
338
+ ```typescript
339
+ // src/server/index.ts
340
+ import { AppServer } from "@donkeylabs/server";
341
+ import { Kysely } from "kysely";
342
+ import { PostgresDialect } from "kysely";
343
+ import { Pool } from "pg";
344
+ import { config } from "./config";
345
+
346
+ const db = new Kysely<Database>({
347
+ dialect: new PostgresDialect({
348
+ pool: new Pool({
349
+ connectionString: config.DATABASE_URL,
350
+ max: config.DATABASE_POOL_SIZE,
351
+ }),
352
+ }),
353
+ });
354
+
355
+ const server = new AppServer({
356
+ port: config.PORT,
357
+ db,
358
+
359
+ // Production logging
360
+ logger: {
361
+ level: config.LOG_LEVEL,
362
+ format: "json", // Structured logging for log aggregation
363
+ redact: ["password", "token", "secret", "authorization"],
364
+ },
365
+
366
+ // Rate limiting
367
+ rateLimiter: config.RATE_LIMIT_ENABLED ? {
368
+ windowMs: 60000,
369
+ maxRequests: 100,
370
+ keyGenerator: (req) => req.headers.get("x-forwarded-for") || "unknown",
371
+ } : undefined,
372
+
373
+ // Production cache
374
+ cache: {
375
+ defaultTtlMs: config.CACHE_TTL_MS,
376
+ maxSize: 10000,
377
+ },
378
+
379
+ // Admin dashboard (disabled in production or protected)
380
+ admin: config.NODE_ENV === "production" ? false : {
381
+ enabled: true,
382
+ auth: {
383
+ username: process.env.ADMIN_USER,
384
+ password: process.env.ADMIN_PASS,
385
+ },
386
+ },
387
+ });
388
+
389
+ // Graceful shutdown
390
+ process.on("SIGTERM", async () => {
391
+ console.log("SIGTERM received, shutting down gracefully");
392
+ await server.shutdown();
393
+ process.exit(0);
394
+ });
395
+
396
+ process.on("SIGINT", async () => {
397
+ console.log("SIGINT received, shutting down gracefully");
398
+ await server.shutdown();
399
+ process.exit(0);
400
+ });
401
+
402
+ await server.start();
403
+ ```
404
+
405
+ ---
406
+
407
+ ## Database Migrations
408
+
409
+ ### Migration Strategy
410
+
411
+ **Option 1: Run Migrations on Startup**
412
+
413
+ ```typescript
414
+ // src/server/index.ts
415
+ const server = new AppServer({ ... });
416
+
417
+ // Run migrations before starting
418
+ await server.migrate();
419
+ await server.start();
420
+ ```
421
+
422
+ **Option 2: Separate Migration Job (Recommended)**
423
+
424
+ ```typescript
425
+ // scripts/migrate.ts
426
+ import { Kysely } from "kysely";
427
+ import { runMigrations } from "@donkeylabs/server";
428
+ import { db } from "./src/server/db";
429
+
430
+ async function main() {
431
+ console.log("Running migrations...");
432
+ await runMigrations(db, {
433
+ migrationsDir: "./src/server/plugins/**/migrations",
434
+ dryRun: process.env.DRY_RUN === "true",
435
+ });
436
+ console.log("Migrations complete");
437
+ await db.destroy();
438
+ }
439
+
440
+ main().catch(console.error);
441
+ ```
442
+
443
+ ```bash
444
+ # Run migrations
445
+ bun scripts/migrate.ts
446
+
447
+ # Dry run (preview)
448
+ DRY_RUN=true bun scripts/migrate.ts
449
+ ```
450
+
451
+ ### Kubernetes Job
452
+
453
+ ```yaml
454
+ # k8s/migration-job.yaml
455
+ apiVersion: batch/v1
456
+ kind: Job
457
+ metadata:
458
+ name: db-migration
459
+ spec:
460
+ template:
461
+ spec:
462
+ containers:
463
+ - name: migrate
464
+ image: your-app:latest
465
+ command: ["bun", "scripts/migrate.ts"]
466
+ env:
467
+ - name: DATABASE_URL
468
+ valueFrom:
469
+ secretKeyRef:
470
+ name: app-secrets
471
+ key: database-url
472
+ restartPolicy: Never
473
+ backoffLimit: 3
474
+ ```
475
+
476
+ ### Migration Rollback
477
+
478
+ ```typescript
479
+ // scripts/rollback.ts
480
+ import { db } from "./src/server/db";
481
+
482
+ async function rollback(steps: number = 1) {
483
+ console.log(`Rolling back ${steps} migration(s)...`);
484
+ await db.migration.rollback({ steps });
485
+ console.log("Rollback complete");
486
+ await db.destroy();
487
+ }
488
+
489
+ rollback(parseInt(process.argv[2] || "1"));
490
+ ```
491
+
492
+ ---
493
+
494
+ ## Health Checks
495
+
496
+ ### Basic Health Check
497
+
498
+ ```typescript
499
+ // src/server/routes/health/index.ts
500
+ import { createRouter, defineRoute } from "@donkeylabs/server";
501
+ import { z } from "zod";
502
+
503
+ export const healthRouter = createRouter("health");
504
+
505
+ healthRouter.route("check").typed(defineRoute({
506
+ output: z.object({
507
+ status: z.enum(["healthy", "unhealthy"]),
508
+ timestamp: z.string(),
509
+ version: z.string(),
510
+ checks: z.object({
511
+ database: z.enum(["ok", "error"]),
512
+ cache: z.enum(["ok", "error"]),
513
+ }),
514
+ }),
515
+ handle: async (_, ctx) => {
516
+ const checks = {
517
+ database: "ok" as const,
518
+ cache: "ok" as const,
519
+ };
520
+
521
+ // Check database
522
+ try {
523
+ await ctx.db.selectFrom("users").select("id").limit(1).execute();
524
+ } catch {
525
+ checks.database = "error";
526
+ }
527
+
528
+ // Check cache
529
+ try {
530
+ await ctx.core.cache.set("health:check", "ok", 1000);
531
+ await ctx.core.cache.get("health:check");
532
+ } catch {
533
+ checks.cache = "error";
534
+ }
535
+
536
+ const isHealthy = checks.database === "ok" && checks.cache === "ok";
537
+
538
+ return {
539
+ status: isHealthy ? "healthy" : "unhealthy",
540
+ timestamp: new Date().toISOString(),
541
+ version: process.env.npm_package_version || "unknown",
542
+ checks,
543
+ };
544
+ },
545
+ }));
546
+ ```
547
+
548
+ ### Readiness Probe
549
+
550
+ ```typescript
551
+ healthRouter.route("ready").typed(defineRoute({
552
+ output: z.object({ ready: z.boolean() }),
553
+ handle: async (_, ctx) => {
554
+ // Check if server is ready to accept traffic
555
+ const isReady = ctx.core.jobs.isInitialized &&
556
+ ctx.core.events.isConnected;
557
+
558
+ return { ready: isReady };
559
+ },
560
+ }));
561
+ ```
562
+
563
+ ### Liveness Probe
564
+
565
+ ```typescript
566
+ healthRouter.route("live").typed(defineRoute({
567
+ output: z.object({ alive: z.boolean() }),
568
+ handle: async () => {
569
+ // Simple check - if we can respond, we're alive
570
+ return { alive: true };
571
+ },
572
+ }));
573
+ ```
574
+
575
+ ---
576
+
577
+ ## Logging & Monitoring
578
+
579
+ ### Structured Logging
580
+
581
+ ```typescript
582
+ // Production logging configuration
583
+ const server = new AppServer({
584
+ logger: {
585
+ level: "info",
586
+ format: "json",
587
+ redact: ["password", "token", "secret", "authorization", "cookie"],
588
+ serializers: {
589
+ req: (req) => ({
590
+ method: req.method,
591
+ url: req.url,
592
+ headers: {
593
+ "user-agent": req.headers.get("user-agent"),
594
+ "x-request-id": req.headers.get("x-request-id"),
595
+ },
596
+ }),
597
+ },
598
+ },
599
+ });
600
+ ```
601
+
602
+ ### Request Logging
603
+
604
+ ```typescript
605
+ // Middleware for request logging
606
+ router.middleware.use(createMiddleware(
607
+ async (req, ctx, next) => {
608
+ const start = Date.now();
609
+ const requestId = crypto.randomUUID();
610
+
611
+ ctx.core.logger.info({
612
+ msg: "Request started",
613
+ requestId,
614
+ method: req.method,
615
+ url: req.url,
616
+ });
617
+
618
+ const response = await next();
619
+
620
+ const duration = Date.now() - start;
621
+ ctx.core.logger.info({
622
+ msg: "Request completed",
623
+ requestId,
624
+ status: response.status,
625
+ duration,
626
+ });
627
+
628
+ return response;
629
+ }
630
+ ));
631
+ ```
632
+
633
+ ### Metrics Collection
634
+
635
+ ```typescript
636
+ // src/server/metrics.ts
637
+ import { createService } from "@donkeylabs/server";
638
+
639
+ export const metricsService = createService("metrics", async (ctx) => {
640
+ const counters = new Map<string, number>();
641
+ const histograms = new Map<string, number[]>();
642
+
643
+ return {
644
+ increment(name: string, tags?: Record<string, string>) {
645
+ const key = this.formatKey(name, tags);
646
+ counters.set(key, (counters.get(key) || 0) + 1);
647
+ },
648
+
649
+ timing(name: string, value: number, tags?: Record<string, string>) {
650
+ const key = this.formatKey(name, tags);
651
+ const values = histograms.get(key) || [];
652
+ values.push(value);
653
+ histograms.set(key, values);
654
+ },
655
+
656
+ formatKey(name: string, tags?: Record<string, string>): string {
657
+ if (!tags) return name;
658
+ const tagStr = Object.entries(tags)
659
+ .map(([k, v]) => `${k}:${v}`)
660
+ .join(",");
661
+ return `${name}{${tagStr}}`;
662
+ },
663
+
664
+ getReport() {
665
+ return {
666
+ counters: Object.fromEntries(counters),
667
+ histograms: Object.fromEntries(
668
+ Array.from(histograms.entries()).map(([k, v]) => [
669
+ k,
670
+ {
671
+ count: v.length,
672
+ min: Math.min(...v),
673
+ max: Math.max(...v),
674
+ avg: v.reduce((a, b) => a + b, 0) / v.length,
675
+ p95: v.sort((a, b) => a - b)[Math.floor(v.length * 0.95)],
676
+ },
677
+ ])
678
+ ),
679
+ };
680
+ },
681
+ };
682
+ });
683
+ ```
684
+
685
+ ### Prometheus Endpoint
686
+
687
+ ```typescript
688
+ // src/server/routes/metrics/index.ts
689
+ import { createRouter, defineRoute } from "@donkeylabs/server";
690
+
691
+ export const metricsRouter = createRouter("metrics");
692
+
693
+ metricsRouter.route("prometheus").typed(defineRoute({
694
+ output: z.string(),
695
+ handle: async (_, ctx) => {
696
+ const report = ctx.services.metrics.getReport();
697
+
698
+ let output = "";
699
+
700
+ // Counters
701
+ for (const [key, value] of Object.entries(report.counters)) {
702
+ output += `# TYPE ${key.split("{")[0]} counter\n`;
703
+ output += `${key} ${value}\n`;
704
+ }
705
+
706
+ // Histograms
707
+ for (const [key, stats] of Object.entries(report.histograms)) {
708
+ const baseName = key.split("{")[0];
709
+ output += `# TYPE ${baseName} histogram\n`;
710
+ output += `${key}_count ${stats.count}\n`;
711
+ output += `${key}_sum ${stats.avg * stats.count}\n`;
712
+ }
713
+
714
+ return output;
715
+ },
716
+ }));
717
+ ```
718
+
719
+ ### Error Tracking (Sentry)
720
+
721
+ ```typescript
722
+ // src/server/errors.ts
723
+ import * as Sentry from "@sentry/node";
724
+
725
+ Sentry.init({
726
+ dsn: process.env.SENTRY_DSN,
727
+ environment: process.env.NODE_ENV,
728
+ release: process.env.npm_package_version,
729
+ tracesSampleRate: 0.1,
730
+ });
731
+
732
+ const server = new AppServer({
733
+ onError: (error, ctx) => {
734
+ Sentry.captureException(error, {
735
+ extra: {
736
+ user: ctx?.user,
737
+ requestId: ctx?.requestId,
738
+ },
739
+ });
740
+ },
741
+ });
742
+ ```
743
+
744
+ ---
745
+
746
+ ## Performance Optimization
747
+
748
+ ### Database Connection Pooling
749
+
750
+ ```typescript
751
+ import { Pool } from "pg";
752
+
753
+ const pool = new Pool({
754
+ connectionString: process.env.DATABASE_URL,
755
+ max: 20, // Maximum connections
756
+ min: 5, // Minimum connections
757
+ acquireTimeoutMillis: 3000, // Timeout for acquiring connection
758
+ idleTimeoutMillis: 30000, // Close idle connections
759
+ connectionTimeoutMillis: 2000,
760
+ });
761
+ ```
762
+
763
+ ### Response Compression
764
+
765
+ ```typescript
766
+ // Enable in nginx (see nginx.conf above)
767
+ // Or use middleware
768
+ import { createMiddleware } from "@donkeylabs/server";
769
+ import { gzip } from "node:zlib";
770
+ import { promisify } from "node:util";
771
+
772
+ const gzipAsync = promisify(gzip);
773
+
774
+ const compressionMiddleware = createMiddleware(
775
+ async (req, ctx, next) => {
776
+ const response = await next();
777
+
778
+ // Only compress JSON responses > 1KB
779
+ const contentType = response.headers.get("content-type");
780
+ if (contentType?.includes("application/json")) {
781
+ const body = await response.text();
782
+ if (body.length > 1024) {
783
+ const compressed = await gzipAsync(Buffer.from(body));
784
+ return new Response(compressed, {
785
+ status: response.status,
786
+ headers: {
787
+ ...Object.fromEntries(response.headers),
788
+ "content-encoding": "gzip",
789
+ "content-length": compressed.length.toString(),
790
+ },
791
+ });
792
+ }
793
+ }
794
+
795
+ return response;
796
+ }
797
+ );
798
+ ```
799
+
800
+ ### Caching Strategy
801
+
802
+ ```typescript
803
+ // API response caching
804
+ router.route("users.list").typed(defineRoute({
805
+ input: z.object({ page: z.number().optional() }),
806
+ output: z.array(userSchema),
807
+ handle: async (input, ctx) => {
808
+ const cacheKey = `users:list:page:${input.page || 1}`;
809
+
810
+ return ctx.core.cache.getOrSet(
811
+ cacheKey,
812
+ async () => {
813
+ return ctx.db
814
+ .selectFrom("users")
815
+ .selectAll()
816
+ .limit(50)
817
+ .offset((input.page || 0) * 50)
818
+ .execute();
819
+ },
820
+ 60000 // 1 minute
821
+ );
822
+ },
823
+ }));
824
+ ```
825
+
826
+ ---
827
+
828
+ ## Security Hardening
829
+
830
+ ### Security Headers
831
+
832
+ ```typescript
833
+ // Security headers middleware
834
+ import { createMiddleware } from "@donkeylabs/server";
835
+
836
+ const securityHeadersMiddleware = createMiddleware(
837
+ async (req, ctx, next) => {
838
+ const response = await next();
839
+
840
+ response.headers.set("X-Content-Type-Options", "nosniff");
841
+ response.headers.set("X-Frame-Options", "DENY");
842
+ response.headers.set("X-XSS-Protection", "1; mode=block");
843
+ response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
844
+ response.headers.set("Permissions-Policy", "geolocation=(), microphone=(), camera=()");
845
+
846
+ // CSP (adjust for your needs)
847
+ response.headers.set(
848
+ "Content-Security-Policy",
849
+ "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';"
850
+ );
851
+
852
+ return response;
853
+ }
854
+ );
855
+ ```
856
+
857
+ ### Rate Limiting
858
+
859
+ ```typescript
860
+ const server = new AppServer({
861
+ rateLimiter: {
862
+ windowMs: 60000, // 1 minute
863
+ maxRequests: 100, // 100 requests per window
864
+ keyGenerator: (req) => {
865
+ // Use forwarded IP if behind proxy
866
+ return req.headers.get("x-forwarded-for")?.split(",")[0] ||
867
+ req.headers.get("x-real-ip") ||
868
+ "unknown";
869
+ },
870
+ skip: (req) => {
871
+ // Skip rate limiting for health checks
872
+ return req.url?.includes("/health");
873
+ },
874
+ },
875
+ });
876
+ ```
877
+
878
+ ### Input Validation
879
+
880
+ ```typescript
881
+ import { z } from "zod";
882
+
883
+ // Strict validation schemas
884
+ const createUserSchema = z.object({
885
+ email: z.string().email().max(255),
886
+ name: z.string().min(1).max(100).regex(/^[\w\s-]+$/),
887
+ password: z.string().min(8).max(100),
888
+ role: z.enum(["user", "admin"]).default("user"),
889
+ });
890
+
891
+ router.route("users.create").typed(defineRoute({
892
+ input: createUserSchema,
893
+ handle: async (input, ctx) => {
894
+ // Input is already validated
895
+ return ctx.plugins.users.create(input);
896
+ },
897
+ }));
898
+ ```
899
+
900
+ ### SQL Injection Prevention
901
+
902
+ ```typescript
903
+ // Always use parameterized queries (Kysely does this automatically)
904
+ // NEVER concatenate user input into SQL
905
+
906
+ // ✅ Good - parameterized
907
+ await ctx.db
908
+ .selectFrom("users")
909
+ .where("email", "=", input.email) // Safe
910
+ .execute();
911
+
912
+ // ❌ Bad - SQL injection risk
913
+ await ctx.db.executeQuery(
914
+ sql`SELECT * FROM users WHERE email = '${input.email}'` // Dangerous!
915
+ );
916
+ ```
917
+
918
+ ---
919
+
920
+ ## CI/CD Pipelines
921
+
922
+ ### GitHub Actions
923
+
924
+ ```yaml
925
+ # .github/workflows/deploy.yml
926
+ name: Deploy
927
+
928
+ on:
929
+ push:
930
+ branches: [main]
931
+ pull_request:
932
+ branches: [main]
933
+
934
+ jobs:
935
+ test:
936
+ runs-on: ubuntu-latest
937
+
938
+ services:
939
+ postgres:
940
+ image: postgres:15
941
+ env:
942
+ POSTGRES_PASSWORD: postgres
943
+ options: >-
944
+ --health-cmd pg_isready
945
+ --health-interval 10s
946
+ --health-timeout 5s
947
+ --health-retries 5
948
+ ports:
949
+ - 5432:5432
950
+
951
+ steps:
952
+ - uses: actions/checkout@v3
953
+
954
+ - name: Setup Bun
955
+ uses: oven-sh/setup-bun@v1
956
+ with:
957
+ bun-version: latest
958
+
959
+ - name: Install dependencies
960
+ run: bun install
961
+
962
+ - name: Type check
963
+ run: bun --bun tsc --noEmit
964
+
965
+ - name: Run migrations
966
+ env:
967
+ DATABASE_URL: postgres://postgres:postgres@localhost:5432/test
968
+ run: bun scripts/migrate.ts
969
+
970
+ - name: Run tests
971
+ env:
972
+ DATABASE_URL: postgres://postgres:postgres@localhost:5432/test
973
+ run: bun test
974
+
975
+ build:
976
+ needs: test
977
+ runs-on: ubuntu-latest
978
+ if: github.ref == 'refs/heads/main'
979
+
980
+ steps:
981
+ - uses: actions/checkout@v3
982
+
983
+ - name: Set up Docker Buildx
984
+ uses: docker/setup-buildx-action@v2
985
+
986
+ - name: Login to Container Registry
987
+ uses: docker/login-action@v2
988
+ with:
989
+ registry: ghcr.io
990
+ username: ${{ github.actor }}
991
+ password: ${{ secrets.GITHUB_TOKEN }}
992
+
993
+ - name: Build and push
994
+ uses: docker/build-push-action@v4
995
+ with:
996
+ context: .
997
+ push: true
998
+ tags: |
999
+ ghcr.io/${{ github.repository }}:latest
1000
+ ghcr.io/${{ github.repository }}:${{ github.sha }}
1001
+ cache-from: type=gha
1002
+ cache-to: type=gha,mode=max
1003
+
1004
+ deploy:
1005
+ needs: build
1006
+ runs-on: ubuntu-latest
1007
+ if: github.ref == 'refs/heads/main'
1008
+
1009
+ steps:
1010
+ - name: Deploy to production
1011
+ uses: appleboy/ssh-action@master
1012
+ with:
1013
+ host: ${{ secrets.SSH_HOST }}
1014
+ username: ${{ secrets.SSH_USER }}
1015
+ key: ${{ secrets.SSH_KEY }}
1016
+ script: |
1017
+ cd /opt/app
1018
+ docker-compose pull
1019
+ docker-compose up -d
1020
+ docker system prune -f
1021
+ ```
1022
+
1023
+ ---
1024
+
1025
+ ## Scaling Strategies
1026
+
1027
+ ### Horizontal Scaling
1028
+
1029
+ ```yaml
1030
+ # docker-compose.scale.yml
1031
+ version: '3.8'
1032
+
1033
+ services:
1034
+ app:
1035
+ build: .
1036
+ deploy:
1037
+ replicas: 3
1038
+ environment:
1039
+ - NODE_ENV=production
1040
+ - DATABASE_URL=postgresql://...
1041
+ depends_on:
1042
+ - db
1043
+ - redis
1044
+
1045
+ nginx:
1046
+ image: nginx:alpine
1047
+ ports:
1048
+ - "80:80"
1049
+ volumes:
1050
+ - ./nginx.conf:/etc/nginx/nginx.conf:ro
1051
+ depends_on:
1052
+ - app
1053
+
1054
+ db:
1055
+ image: postgres:15-alpine
1056
+ volumes:
1057
+ - postgres_data:/var/lib/postgresql/data
1058
+
1059
+ redis:
1060
+ image: redis:7-alpine
1061
+ volumes:
1062
+ - redis_data:/data
1063
+
1064
+ volumes:
1065
+ postgres_data:
1066
+ redis_data:
1067
+ ```
1068
+
1069
+ ### Load Balancing with Nginx
1070
+
1071
+ ```nginx
1072
+ upstream app {
1073
+ least_conn; # Least connections load balancing
1074
+ server app_1:3000 max_fails=3 fail_timeout=30s;
1075
+ server app_2:3000 max_fails=3 fail_timeout=30s;
1076
+ server app_3:3000 max_fails=3 fail_timeout=30s;
1077
+ keepalive 32;
1078
+ }
1079
+ ```
1080
+
1081
+ ### Database Read Replicas
1082
+
1083
+ ```typescript
1084
+ // Connection to read replicas
1085
+ const readDb = new Kysely<Database>({
1086
+ dialect: new PostgresDialect({
1087
+ pool: new Pool({
1088
+ connectionString: process.env.DATABASE_READ_URL,
1089
+ max: 20,
1090
+ }),
1091
+ }),
1092
+ });
1093
+
1094
+ // Use read replica for read operations
1095
+ router.route("users.list").typed(defineRoute({
1096
+ handle: async (_, ctx) => {
1097
+ // Read from replica
1098
+ return readDb.selectFrom("users").selectAll().execute();
1099
+ },
1100
+ }));
1101
+ ```
1102
+
1103
+ ---
1104
+
1105
+ ## Troubleshooting Production
1106
+
1107
+ ### Debugging Checklist
1108
+
1109
+ ```bash
1110
+ # 1. Check container status
1111
+ docker-compose ps
1112
+ docker-compose logs --tail=100 app
1113
+
1114
+ # 2. Check health endpoint
1115
+ curl http://localhost:3000/health
1116
+
1117
+ # 3. Check database connectivity
1118
+ docker-compose exec app bun -e "
1119
+ import { db } from './src/server/db';
1120
+ await db.selectFrom('users').limit(1).execute();
1121
+ console.log('DB OK');
1122
+ "
1123
+
1124
+ # 4. Check resource usage
1125
+ docker stats
1126
+
1127
+ # 5. Check nginx logs
1128
+ docker-compose logs nginx
1129
+ ```
1130
+
1131
+ ### Common Issues
1132
+
1133
+ **Database connection pool exhausted:**
1134
+ ```typescript
1135
+ // Increase pool size
1136
+ const pool = new Pool({
1137
+ max: 50, // Increase from default
1138
+ // ...
1139
+ });
1140
+ ```
1141
+
1142
+ **Memory leaks:**
1143
+ ```typescript
1144
+ // Add heap dump endpoint for debugging
1145
+ if (process.env.NODE_ENV === "production") {
1146
+ router.route("debug.heap").typed(defineRoute({
1147
+ handle: async () => {
1148
+ const heap = require("v8").getHeapStatistics();
1149
+ return {
1150
+ used: heap.used_heap_size,
1151
+ total: heap.total_heap_size,
1152
+ limit: heap.heap_size_limit,
1153
+ };
1154
+ },
1155
+ }));
1156
+ }
1157
+ ```
1158
+
1159
+ **Slow queries:**
1160
+ ```typescript
1161
+ // Enable query logging
1162
+ const db = new Kysely<Database>({
1163
+ dialect: new PostgresDialect({ pool }),
1164
+ log: (event) => {
1165
+ if (event.level === "query" && event.queryDurationMillis > 100) {
1166
+ console.warn("Slow query:", event.query.sql, `${event.queryDurationMillis}ms`);
1167
+ }
1168
+ },
1169
+ });
1170
+ ```
1171
+
1172
+ ### Graceful Degradation
1173
+
1174
+ ```typescript
1175
+ // Fallback when services are down
1176
+ router.route("dashboard").typed(defineRoute({
1177
+ handle: async (_, ctx) => {
1178
+ try {
1179
+ const stats = await ctx.core.cache.getOrSet(
1180
+ "dashboard:stats",
1181
+ () => computeStats(ctx),
1182
+ 60000
1183
+ );
1184
+ return { stats };
1185
+ } catch (error) {
1186
+ // Return stale data or fallback
1187
+ const staleStats = await ctx.core.cache.get("dashboard:stats");
1188
+ if (staleStats) {
1189
+ ctx.core.logger.warn("Using stale dashboard stats");
1190
+ return { stats: staleStats, stale: true };
1191
+ }
1192
+
1193
+ // Ultimate fallback
1194
+ return { stats: null, error: "Service temporarily unavailable" };
1195
+ }
1196
+ },
1197
+ }));
1198
+ ```
1199
+
1200
+ ---
1201
+
1202
+ ## Production Checklist
1203
+
1204
+ Before deploying to production:
1205
+
1206
+ - [ ] Environment variables configured
1207
+ - [ ] Database migrations tested
1208
+ - [ ] Health checks implemented
1209
+ - [ ] Logging structured (JSON)
1210
+ - [ ] Error tracking configured (Sentry)
1211
+ - [ ] Metrics collection enabled
1212
+ - [ ] Rate limiting enabled
1213
+ - [ ] Security headers configured
1214
+ - [ ] SSL/TLS certificates
1215
+ - [ ] Docker multi-stage build
1216
+ - [ ] Graceful shutdown handlers
1217
+ - [ ] Backup strategy
1218
+ - [ ] Monitoring dashboards
1219
+ - [ ] Runbook created
1220
+ - [ ] Load testing completed
1221
+
1222
+ ---
1223
+
1224
+ ## Additional Resources
1225
+
1226
+ - [Docker Best Practices](https://docs.docker.com/develop/dev-best-practices/)
1227
+ - [PostgreSQL Tuning](https://wiki.postgresql.org/wiki/Tuning_Your_PostgreSQL_Server)
1228
+ - [Nginx Load Balancing](https://nginx.org/en/docs/http/load_balancing.html)
1229
+ - [Kubernetes Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/)