14
14
# - Optional TLS/SSL support for secure connections
15
15
# - Database initialization before server start
16
16
# - Comprehensive error handling and user feedback
17
+ # - Process lock to prevent duplicate instances
18
+ # - Auto-detection of optimal worker count based on CPU cores
19
+ # - Support for preloading application code (memory optimization)
17
20
#
18
21
# Environment Variables:
19
22
# PYTHON : Path to Python interpreter (optional)
20
23
# VIRTUAL_ENV : Path to active virtual environment (auto-detected)
21
- # GUNICORN_WORKERS : Number of worker processes (default: 2 × CPU cores + 1 )
24
+ # GUNICORN_WORKERS : Number of worker processes (default: 2, or "auto" )
22
25
# GUNICORN_TIMEOUT : Worker timeout in seconds (default: 600)
23
26
# GUNICORN_MAX_REQUESTS : Max requests per worker before restart (default: 1000)
24
27
# GUNICORN_MAX_REQUESTS_JITTER : Random jitter for max requests (default: 100)
28
+ # GUNICORN_PRELOAD_APP : Preload app before forking workers (default: false)
29
+ # GUNICORN_DEV_MODE : Enable developer mode with hot reload (default: false)
25
30
# SSL : Enable TLS/SSL (true/false, default: false)
26
31
# CERT_FILE : Path to SSL certificate (default: certs/cert.pem)
27
32
# KEY_FILE : Path to SSL private key (default: certs/key.pem)
33
+ # SKIP_DB_INIT : Skip database initialization (default: false)
34
+ # FORCE_START : Force start even if another instance is running (default: false)
28
35
#
29
36
# Usage:
30
37
# ./run-gunicorn.sh # Run with defaults
31
38
# SSL=true ./run-gunicorn.sh # Run with TLS enabled
32
39
# GUNICORN_WORKERS=16 ./run-gunicorn.sh # Run with 16 workers
40
+ # GUNICORN_PRELOAD_APP=true ./run-gunicorn.sh # Preload app for memory optimization
41
+ # GUNICORN_DEV_MODE=true ./run-gunicorn.sh # Run in developer mode with hot reload
42
+ # FORCE_START=true ./run-gunicorn.sh # Force start (bypass lock check)
33
43
# ───────────────────────────────────────────────────────────────────────────────
34
44
35
45
# Exit immediately on error, undefined variable, or pipe failure
@@ -49,7 +59,62 @@ cd "${SCRIPT_DIR}" || {
49
59
}
50
60
51
61
# ────────────────────────────────────────────────────────────────────────────────
52
- # SECTION 2: Virtual Environment Activation
62
+ # SECTION 2: Process Lock Check
63
+ # Prevent multiple instances from running simultaneously unless forced
64
+ # ────────────────────────────────────────────────────────────────────────────────
65
+ LOCK_FILE=" /tmp/mcpgateway-gunicorn.lock"
66
+ FORCE_START=${FORCE_START:- false}
67
+
68
+ check_existing_process () {
69
+ if [[ -f " ${LOCK_FILE} " ]]; then
70
+ local pid
71
+ pid=$( < " ${LOCK_FILE} " )
72
+
73
+ # Check if the process is actually running
74
+ if kill -0 " ${pid} " 2> /dev/null; then
75
+ echo " ⚠️ WARNING: Another instance of MCP Gateway appears to be running (PID: ${pid} )"
76
+
77
+ # Check if it's actually gunicorn
78
+ if ps -p " ${pid} " -o comm= | grep -q gunicorn; then
79
+ if [[ " ${FORCE_START} " != " true" ]]; then
80
+ echo " ❌ FATAL: MCP Gateway is already running!"
81
+ echo " To stop it: kill ${pid} "
82
+ echo " To force start anyway: FORCE_START=true $0 "
83
+ exit 1
84
+ else
85
+ echo " ⚠️ Force starting despite existing process..."
86
+ fi
87
+ else
88
+ echo " 🔧 Lock file exists but process ${pid} is not gunicorn. Cleaning up..."
89
+ rm -f " ${LOCK_FILE} "
90
+ fi
91
+ else
92
+ echo " 🔧 Stale lock file found. Cleaning up..."
93
+ rm -f " ${LOCK_FILE} "
94
+ fi
95
+ fi
96
+ }
97
+
98
+ # Create cleanup function
99
+ cleanup () {
100
+ # Only clean up if we're the process that created the lock
101
+ if [[ -f " ${LOCK_FILE} " ]] && [[ " $( < " ${LOCK_FILE} " ) " == " $" ]]; then
102
+ rm -f " ${LOCK_FILE} "
103
+ echo " 🔧 Cleaned up lock file"
104
+ fi
105
+ }
106
+
107
+ # Set up signal handlers for cleanup (but not EXIT - let gunicorn manage that)
108
+ trap cleanup INT TERM
109
+
110
+ # Check for existing process
111
+ check_existing_process
112
+
113
+ # Create lock file with current PID (will be updated with gunicorn PID later)
114
+ echo $$ > " ${LOCK_FILE} "
115
+
116
+ # ────────────────────────────────────────────────────────────────────────────────
117
+ # SECTION 3: Virtual Environment Activation
53
118
# Check if a virtual environment is already active. If not, try to activate one
54
119
# from known locations. This ensures dependencies are properly isolated.
55
120
# ────────────────────────────────────────────────────────────────────────────────
83
148
fi
84
149
85
150
# ────────────────────────────────────────────────────────────────────────────────
86
- # SECTION 3 : Python Interpreter Detection
151
+ # SECTION 4 : Python Interpreter Detection
87
152
# Locate a suitable Python interpreter with the following precedence:
88
153
# 1. User-provided PYTHON environment variable
89
154
# 2. 'python' binary in active virtual environment
@@ -136,7 +201,7 @@ if ! "${PYTHON}" -c "import sys; sys.exit(0 if sys.version_info[0] >= 3 else 1)"
136
201
fi
137
202
138
203
# ────────────────────────────────────────────────────────────────────────────────
139
- # SECTION 4 : Display Application Banner
204
+ # SECTION 5 : Display Application Banner
140
205
# Show a fancy ASCII art banner for the MCP Gateway
141
206
# ────────────────────────────────────────────────────────────────────────────────
142
207
cat << 'EOF '
@@ -149,14 +214,18 @@ cat <<'EOF'
149
214
EOF
150
215
151
216
# ────────────────────────────────────────────────────────────────────────────────
152
- # SECTION 5 : Configure Gunicorn Settings
217
+ # SECTION 6 : Configure Gunicorn Settings
153
218
# Set up Gunicorn parameters with sensible defaults that can be overridden
154
219
# via environment variables for different deployment scenarios
155
220
# ────────────────────────────────────────────────────────────────────────────────
156
221
157
222
# Number of worker processes (adjust based on CPU cores and expected load)
158
- # Default: 2 × CPU cores + 1 (automatically detected)
223
+ # Default: 2 (safe default for most systems)
224
+ # Set to "auto" for automatic detection based on CPU cores
159
225
if [[ -z " ${GUNICORN_WORKERS:- } " ]]; then
226
+ # Default to 2 workers if not specified
227
+ GUNICORN_WORKERS=2
228
+ elif [[ " ${GUNICORN_WORKERS} " == " auto" ]]; then
160
229
# Try to detect CPU count
161
230
if command -v nproc & > /dev/null; then
162
231
CPU_COUNT=$( nproc)
@@ -165,7 +234,13 @@ if [[ -z "${GUNICORN_WORKERS:-}" ]]; then
165
234
else
166
235
CPU_COUNT=4 # Fallback to reasonable default
167
236
fi
168
- GUNICORN_WORKERS=$(( CPU_COUNT * 2 + 1 ))
237
+
238
+ # Use a more conservative formula: min(2*CPU+1, 16) to avoid too many workers
239
+ CALCULATED_WORKERS=$(( CPU_COUNT * 2 + 1 ))
240
+ GUNICORN_WORKERS=$(( CALCULATED_WORKERS > 16 ? 16 : CALCULATED_WORKERS))
241
+
242
+ echo " 🔧 Auto-detected CPU cores: ${CPU_COUNT} "
243
+ echo " Calculated workers: ${CALCULATED_WORKERS} → Capped at: ${GUNICORN_WORKERS} "
169
244
fi
170
245
171
246
# Worker timeout in seconds (increase for long-running requests)
@@ -177,13 +252,27 @@ GUNICORN_MAX_REQUESTS=${GUNICORN_MAX_REQUESTS:-1000}
177
252
# Random jitter for max requests (prevents all workers restarting simultaneously)
178
253
GUNICORN_MAX_REQUESTS_JITTER=${GUNICORN_MAX_REQUESTS_JITTER:- 100}
179
254
255
+ # Preload application before forking workers (saves memory but slower reload)
256
+ GUNICORN_PRELOAD_APP=${GUNICORN_PRELOAD_APP:- false}
257
+
258
+ # Developer mode with hot reload (disables preload, enables file watching)
259
+ GUNICORN_DEV_MODE=${GUNICORN_DEV_MODE:- false}
260
+
261
+ # Check for conflicting options
262
+ if [[ " ${GUNICORN_DEV_MODE} " == " true" && " ${GUNICORN_PRELOAD_APP} " == " true" ]]; then
263
+ echo " ⚠️ WARNING: Developer mode disables application preloading"
264
+ GUNICORN_PRELOAD_APP=" false"
265
+ fi
266
+
180
267
echo " 📊 Gunicorn Configuration:"
181
268
echo " Workers: ${GUNICORN_WORKERS} "
182
269
echo " Timeout: ${GUNICORN_TIMEOUT} s"
183
270
echo " Max Requests: ${GUNICORN_MAX_REQUESTS} (±${GUNICORN_MAX_REQUESTS_JITTER} )"
271
+ echo " Preload App: ${GUNICORN_PRELOAD_APP} "
272
+ echo " Developer Mode: ${GUNICORN_DEV_MODE} "
184
273
185
274
# ────────────────────────────────────────────────────────────────────────────────
186
- # SECTION 6 : Configure TLS/SSL Settings
275
+ # SECTION 7 : Configure TLS/SSL Settings
187
276
# Handle optional TLS configuration for secure HTTPS connections
188
277
# ────────────────────────────────────────────────────────────────────────────────
189
278
@@ -226,32 +315,44 @@ else
226
315
fi
227
316
228
317
# ────────────────────────────────────────────────────────────────────────────────
229
- # SECTION 7 : Database Initialization
318
+ # SECTION 8 : Database Initialization
230
319
# Run database setup/migrations before starting the server
231
320
# ────────────────────────────────────────────────────────────────────────────────
232
- echo " 🗄️ Initializing database..."
233
- if ! " ${PYTHON} " -m mcpgateway.db; then
234
- echo " ❌ FATAL: Database initialization failed!"
235
- echo " Please check your database configuration and connection."
236
- exit 1
321
+ SKIP_DB_INIT=${SKIP_DB_INIT:- false}
322
+
323
+ if [[ " ${SKIP_DB_INIT} " != " true" ]]; then
324
+ echo " 🗄️ Initializing database..."
325
+ if ! " ${PYTHON} " -m mcpgateway.db; then
326
+ echo " ❌ FATAL: Database initialization failed!"
327
+ echo " Please check your database configuration and connection."
328
+ echo " To skip DB initialization: SKIP_DB_INIT=true $0 "
329
+ exit 1
330
+ fi
331
+ echo " ✓ Database initialized successfully"
332
+ else
333
+ echo " ⚠️ Skipping database initialization (SKIP_DB_INIT=true)"
237
334
fi
238
- echo " ✓ Database initialized successfully"
239
335
240
336
# ────────────────────────────────────────────────────────────────────────────────
241
- # SECTION 8: Launch Gunicorn Server
242
- # Start the Gunicorn server with all configured options
243
- # Using 'exec' replaces this shell process with Gunicorn for cleaner process management
337
+ # SECTION 9: Verify Gunicorn Installation
338
+ # Check that gunicorn is available before attempting to start
244
339
# ────────────────────────────────────────────────────────────────────────────────
245
- echo " 🚀 Starting Gunicorn server..."
246
- echo " ─────────────────────────────────────────────────────────────────────"
247
-
248
- # Check if gunicorn is available
249
340
if ! command -v gunicorn & > /dev/null; then
250
341
echo " ❌ FATAL: gunicorn command not found!"
251
342
echo " Please install it with: pip install gunicorn"
252
343
exit 1
253
344
fi
254
345
346
+ echo " ✓ Gunicorn found: $( command -v gunicorn) "
347
+
348
+ # ────────────────────────────────────────────────────────────────────────────────
349
+ # SECTION 10: Launch Gunicorn Server
350
+ # Start the Gunicorn server with all configured options
351
+ # Using 'exec' replaces this shell process with Gunicorn for cleaner process management
352
+ # ────────────────────────────────────────────────────────────────────────────────
353
+ echo " 🚀 Starting Gunicorn server..."
354
+ echo " ─────────────────────────────────────────────────────────────────────"
355
+
255
356
# Build command array to handle spaces in paths properly
256
357
cmd=(
257
358
gunicorn
@@ -263,8 +364,29 @@ cmd=(
263
364
--max-requests-jitter " ${GUNICORN_MAX_REQUESTS_JITTER} "
264
365
--access-logfile -
265
366
--error-logfile -
367
+ --forwarded-allow-ips=" *"
368
+ --pid " ${LOCK_FILE} " # Use lock file as PID file
266
369
)
267
370
371
+ # Add developer mode flags if enabled
372
+ if [[ " ${GUNICORN_DEV_MODE} " == " true" ]]; then
373
+ cmd+=( --reload --reload-extra-file gunicorn.config.py )
374
+ echo " 🔧 Developer mode enabled - hot reload active"
375
+ echo " Watching for changes in Python files and gunicorn.config.py"
376
+
377
+ # In dev mode, reduce workers to 1 for better debugging
378
+ if [[ " ${GUNICORN_WORKERS} " -gt 2 ]]; then
379
+ echo " Reducing workers to 2 for developer mode (was ${GUNICORN_WORKERS} )"
380
+ cmd[5]=2 # Update the workers argument
381
+ fi
382
+ fi
383
+
384
+ # Add preload flag if enabled (and not in dev mode)
385
+ if [[ " ${GUNICORN_PRELOAD_APP} " == " true" && " ${GUNICORN_DEV_MODE} " != " true" ]]; then
386
+ cmd+=( --preload )
387
+ echo " ✓ Application preloading enabled"
388
+ fi
389
+
268
390
# Add SSL arguments if enabled
269
391
if [[ " ${SSL} " == " true" ]]; then
270
392
cmd+=( --certfile " ${CERT_FILE} " --keyfile " ${KEY_FILE} " )
273
395
# Add the application module
274
396
cmd+=( " mcpgateway.main:app" )
275
397
398
+ # Display final command for debugging
399
+ echo " 📋 Command: ${cmd[*]} "
400
+ echo " ─────────────────────────────────────────────────────────────────────"
401
+
276
402
# Launch Gunicorn with all configured options
403
+ # Remove EXIT trap before exec - let gunicorn handle its own cleanup
404
+ trap - EXIT
405
+ # exec replaces this shell with gunicorn, so cleanup trap won't fire on normal exit
406
+ # The PID file will be managed by gunicorn itself
277
407
exec " ${cmd[@]} "
0 commit comments