/** * Custom server entry point. * * Boots Next.js, attaches Socket.io to the same HTTP server, and in * development mode also registers BullMQ recurring jobs and starts the * workers inline (so you don't need the separate crm-worker container). * * In production the workers run in the crm-worker container (Dockerfile.worker * → dist/worker.js) and this file only handles Next.js + Socket.io. */ import { createServer, type Server as HttpServer } from 'node:http'; import next from 'next'; import { initSocketServer, closeSocketServer } from '@/lib/socket/server'; import { logger } from '@/lib/logger'; import { env } from '@/lib/env'; import { redis } from '@/lib/redis'; const dev = env.NODE_ENV !== 'production'; async function gracefulShutdown(signal: string, httpServer: HttpServer): Promise { logger.info({ signal }, 'Shutdown signal received; closing connections'); // Stop accepting new HTTP connections, then drain in-flight ones. await new Promise((resolve) => { httpServer.close((err) => { if (err) logger.warn({ err }, 'httpServer.close emitted error'); resolve(); }); // Hard timeout — `httpServer.close` waits for ALL keep-alive sockets // to drain on their own, which can stretch much longer than the // compose stop_grace_period. 25s leaves headroom under a 30s grace. setTimeout(() => resolve(), 25_000).unref(); }); await closeSocketServer().catch((err) => logger.warn({ err }, 'closeSocketServer error')); try { redis.disconnect(); } catch (err) { logger.warn({ err }, 'redis.disconnect error'); } logger.info({ signal }, 'Shutdown complete'); } async function main(): Promise { const app = next({ dev, port: env.PORT }); const handle = app.getRequestHandler(); await app.prepare(); const httpServer = createServer((req, res) => { handle(req, res); }); // Attach Socket.io to the HTTP server (uses Redis adapter for pub/sub) initSocketServer(httpServer); logger.info('Socket.io initialized'); // In development, run BullMQ workers inline so a single `pnpm dev` is enough if (dev) { const { registerRecurringJobs } = await import('@/lib/queue/scheduler'); const { emailWorker } = await import('@/lib/queue/workers/email'); const { documentsWorker } = await import('@/lib/queue/workers/documents'); const { notificationsWorker } = await import('@/lib/queue/workers/notifications'); const { importWorker } = await import('@/lib/queue/workers/import'); const { exportWorker } = await import('@/lib/queue/workers/export'); await registerRecurringJobs(); logger.info('BullMQ recurring jobs registered (dev mode)'); // Keep a reference so workers aren't GC'd void [emailWorker, documentsWorker, notificationsWorker, importWorker, exportWorker]; } httpServer.listen(env.PORT, () => { logger.info({ port: env.PORT, env: env.NODE_ENV }, 'Port Nimara CRM server listening'); }); // Graceful stop on container restart / deploy. Without this, every // `docker compose up -d` rolling restart drops in-flight uploads, EOI // generation, Documenso requests, and Socket.io frames mid-statement. // Match docker-compose `stop_grace_period: 30s` (or longer) so the // 25s drain inside gracefulShutdown can complete before SIGKILL. for (const sig of ['SIGTERM', 'SIGINT'] as const) { process.once(sig, () => { void gracefulShutdown(sig, httpServer).finally(() => process.exit(0)); }); } } main().catch((err) => { logger.error(err, 'Failed to start server'); process.exit(1); });