bunker-admin abdfd50cb8 Make embed proxy ports configurable via env vars for multi-instance deployments
All 13 nginx embed proxy ports (8881-8895) are now driven by environment
variables instead of being hardcoded. This prevents port conflicts when
running multiple Changemaker instances on the same host.

Chain: .env → docker-compose port mappings → nginx container env →
entrypoint.sh envsubst → services.conf.template listen directives →
API /services/config endpoint → frontend buildServiceUrl().

Existing deployments are unaffected (all vars default to current values).

Bunker Admin
2026-03-25 15:25:00 -06:00

80 lines
2.7 KiB
TypeScript

import 'express-async-errors';
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import compression from 'compression';
import rateLimit from 'express-rate-limit';
import { env } from './config/env';
import { logger } from './utils/logger';
import { errorHandler } from './middleware/error-handler';
// Route imports
import authRoutes from './modules/auth/auth.routes';
import instanceRoutes from './modules/instances/instances.routes';
import settingsRoutes from './modules/settings/settings.routes';
import healthRoutes from './modules/health/health.routes';
import auditRoutes from './modules/audit/audit.routes';
import backupRoutes from './modules/backups/backup.routes';
import eventsRoutes, { instanceEventsRouter } from './modules/events/events.routes';
import { startHealthScheduler } from './services/health.service';
import { autoDiscoverOnStartup } from './services/discovery.service';
const app = express();
// Global middleware
app.use(helmet());
app.use(compression());
app.use(express.json({ limit: '10mb' }));
app.use(
cors({
origin: env.CORS_ORIGINS.split(',').map((s) => s.trim()),
credentials: true,
})
);
// Rate limiters
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 15, // 15 attempts per window
standardHeaders: true,
legacyHeaders: false,
message: { error: { message: 'Too many attempts, please try again later', code: 'RATE_LIMITED' } },
});
// Global API rate limiter — safety net against resource exhaustion
const apiLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 300, // 300 req/min per IP (generous for a control panel)
standardHeaders: true,
legacyHeaders: false,
message: { error: { message: 'Too many requests, please try again later', code: 'RATE_LIMITED' } },
});
// Routes
app.use('/api', apiLimiter);
app.use('/api/auth', authLimiter, authRoutes);
app.use('/api/instances', instanceRoutes);
app.use('/api/settings', settingsRoutes);
app.use('/api/health', healthRoutes);
app.use('/api/audit', auditRoutes);
app.use('/api/backups', backupRoutes);
app.use('/api/events', eventsRoutes);
app.use('/api/instances/:id/events', instanceEventsRouter);
// Error handler (must be last)
app.use(errorHandler);
app.listen(env.PORT, () => {
logger.info(`CCP API listening on port ${env.PORT} (${env.NODE_ENV})`);
startHealthScheduler(env.HEALTH_CHECK_INTERVAL_MS);
// Auto-discover parent CML instance on first boot (5s delay for DB readiness)
setTimeout(() => {
autoDiscoverOnStartup().catch((err) =>
logger.error(`[discovery] Auto-discovery failed: ${(err as Error).message}`)
);
}, 5_000);
});
export default app;