Skip to content

Commit 38cc71e

Browse files
Add gunicorn with TLS as web server for mapping app (#529)
1 parent 0375c71 commit 38cc71e

File tree

6 files changed

+159
-57
lines changed

6 files changed

+159
-57
lines changed

mapping/Dockerfile

Lines changed: 15 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -142,49 +142,24 @@ if [ "$SKIP_MODEL_DOWNLOAD" != "1" ]; then
142142
python3 /usr/local/bin/ondemand_model_loader.py
143143
fi
144144

145-
# Execute the provided command or default to API service
146-
exec /home/scenescape/SceneScape/api_service_cmd
145+
# Check if development mode is enabled via environment variable
146+
if [ "$DEV_MODE" = "true" ] || [ "$DEV_MODE" = "1" ] || [ "$DEVELOPMENT" = "true" ]; then
147+
echo "Starting in DEVELOPMENT mode with ${MODEL_TYPE} model..."
148+
exec python ${MODEL_TYPE}_service.py --dev-mode
149+
else
150+
echo "Starting in PRODUCTION mode with TLS and ${MODEL_TYPE} model..."
151+
# Ensure certificate directory exists and has proper permissions
152+
if [ -d "/run/secrets/certs" ]; then
153+
echo "TLS certificates directory found"
154+
else
155+
echo "WARNING: TLS certificates directory not found at /run/secrets/certs"
156+
echo "Make sure to mount certificates for production use"
157+
fi
158+
exec python ${MODEL_TYPE}_service.py
159+
fi
147160
EOF
148161
RUN chmod +x /usr/local/bin/startup.sh
149162

150-
# Create API service script based on model type
151-
ARG MODEL_TYPE
152-
RUN cat <<EOF > $SCENESCAPE_HOME/api_service_cmd
153-
#!/bin/bash
154-
155-
# Enable job control and set up signal handling
156-
set -e
157-
set -m
158-
159-
# Function to handle shutdown
160-
shutdown() {
161-
echo "Received shutdown signal, stopping API service..."
162-
# Kill any background processes
163-
jobs -p | xargs -r kill
164-
exit 0
165-
}
166-
167-
# Set up signal handlers
168-
trap shutdown SIGINT SIGTERM
169-
170-
echo "Starting 3D Mapping ${MODEL_TYPE} API Service..."
171-
172-
# Change to workspace directory
173-
cd $SCENESCAPE_HOME
174-
175-
# Set Python to unbuffered mode for immediate log output
176-
export PYTHONUNBUFFERED=1
177-
178-
# Start Flask service with model-specific service file
179-
echo "Starting Flask service with ${MODEL_TYPE} model on port 8000..."
180-
echo "Press Ctrl+C to stop the service"
181-
182-
# Run Python in foreground with signal handling
183-
exec python ${MODEL_TYPE}_service.py
184-
EOF
185-
186-
RUN chmod +x $SCENESCAPE_HOME/api_service_cmd
187-
188163
# Set working directory
189164
WORKDIR $SCENESCAPE_HOME
190165

mapping/requirements_api.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ flask-cors==4.0.0
88
gradio
99
open3d-cpu[headless]==0.19.0
1010
requests
11+
gunicorn==21.2.0

mapping/src/api_service_base.py

Lines changed: 109 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88
Flask service with build-time model selection (no runtime model parameter needed).
99
"""
1010

11+
import argparse
1112
import base64
1213
import os
1314
import signal
15+
import subprocess
1416
import sys
1517
import tempfile
1618
import time
@@ -257,9 +259,105 @@ def signalHandler(sig, frame):
257259
log.info("Received SIGINT (Ctrl+C), shutting down gracefully...")
258260
sys.exit(0)
259261

262+
def runDevelopmentServer():
263+
"""Run Flask development server"""
264+
log.info("Starting in DEVELOPMENT mode...")
265+
log.info("Flask development server starting on http://0.0.0.0:8000")
266+
log.info("Press Ctrl+C to stop the server")
267+
268+
try:
269+
# Run Flask development server
270+
app.run(
271+
host="0.0.0.0",
272+
port=8000,
273+
debug=True,
274+
threaded=True
275+
)
276+
except KeyboardInterrupt:
277+
log.info("Server interrupted by user")
278+
except Exception as e:
279+
log.error(f"Server error: {e}")
280+
finally:
281+
log.info("Server shutdown complete")
282+
283+
def runProductionServer(cert_file=None, key_file=None):
284+
"""Run Gunicorn production server with TLS"""
285+
log.info("Starting in PRODUCTION mode with TLS...")
286+
287+
# Check if certificates exist
288+
if not os.path.exists(cert_file):
289+
log.error(f"TLS certificate file not found: {cert_file}")
290+
sys.exit(1)
291+
292+
if not os.path.exists(key_file):
293+
log.error(f"TLS key file not found: {key_file}")
294+
sys.exit(1)
295+
296+
log.info(f"Using TLS certificate: {cert_file}")
297+
log.info(f"Using TLS key: {key_file}")
298+
log.info("Gunicorn HTTPS server starting on https://0.0.0.0:8000")
299+
300+
# Determine the service module based on model type
301+
model_type = os.getenv("MODEL_TYPE", "mapanything")
302+
service_module = f"{model_type}_service:app"
303+
304+
# Gunicorn command arguments
305+
gunicorn_cmd = [
306+
"gunicorn",
307+
"--bind", "0.0.0.0:8000",
308+
"--workers", "1",
309+
"--worker-class", "sync",
310+
"--timeout", "300",
311+
"--keep-alive", "5",
312+
"--max-requests", "1000",
313+
"--max-requests-jitter", "100",
314+
"--access-logfile", "-",
315+
"--error-logfile", "-",
316+
"--log-level", "info",
317+
"--certfile", cert_file,
318+
"--keyfile", key_file,
319+
service_module
320+
]
321+
322+
log.info(f"Starting Gunicorn with service module: {service_module}")
323+
324+
try:
325+
# Run Gunicorn with TLS
326+
subprocess.run(gunicorn_cmd, check=True)
327+
except subprocess.CalledProcessError as e:
328+
log.error(f"Gunicorn failed to start: {e}")
329+
sys.exit(1)
330+
except KeyboardInterrupt:
331+
log.info("Server interrupted by user")
332+
except Exception as e:
333+
log.error(f"Server error: {e}")
334+
sys.exit(1)
335+
260336
def startApp():
261-
"""Start the application with model initialization"""
262-
global device, loaded_model, model_name
337+
"""Start the application with command line argument parsing"""
338+
parser = argparse.ArgumentParser(description="3D Mapping Models API Server")
339+
parser.add_argument(
340+
"--dev-mode",
341+
action="store_true",
342+
help="Run in development mode with Flask development server (default: production mode with Gunicorn + TLS)"
343+
)
344+
parser.add_argument(
345+
"--development",
346+
action="store_true",
347+
help="Alias for --dev-mode"
348+
)
349+
parser.add_argument(
350+
"--cert-file",
351+
default="/run/secrets/certs/scenescape-mapping.crt",
352+
help="Path to TLS certificate file (default: /run/secrets/certs/scenescape-mapping.crt)"
353+
)
354+
parser.add_argument(
355+
"--key-file",
356+
default="/run/secrets/certs/scenescape-mapping.key",
357+
help="Path to TLS private key file (default: /run/secrets/certs/scenescape-mapping.key)"
358+
)
359+
360+
args = parser.parse_args()
263361

264362
# Set up signal handler for graceful shutdown
265363
signal.signal(signal.SIGINT, signalHandler)
@@ -268,23 +366,23 @@ def startApp():
268366
log.info("Starting 3D Mapping API server...")
269367

270368
# Initialize model before starting server
369+
global device, loaded_model, model_name
271370
device = "cpu"
272371
log.info(f"Using device: {device}")
273372

274373
try:
374+
# Only initialize model if not already loaded
275375
loaded_model, model_name = initializeModel()
276376
log.info("API Service startup completed successfully")
277377

278-
log.info("Flask server starting on http://0.0.0.0:8000")
279-
log.info("Press Ctrl+C to stop the server")
378+
# Determine which server to run
379+
dev_mode = args.dev_mode or args.development or os.getenv("DEV_MODE", "").lower() in ("true", "1", "yes")
380+
381+
if dev_mode:
382+
runDevelopmentServer()
383+
else:
384+
runProductionServer(cert_file=args.cert_file, key_file=args.key_file)
280385

281-
# Run Flask development server
282-
app.run(
283-
host="0.0.0.0",
284-
port=8000,
285-
debug=False,
286-
threaded=True
287-
)
288386
except KeyboardInterrupt:
289387
log.info("Server interrupted by user")
290388
except Exception as e:

mapping/src/vggt_service.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@
1010
from scene_common import log
1111

1212
# Import the base API service
13+
<<<<<<< HEAD
14+
from api_service_base import start_app, app
15+
=======
1316
from api_service_base import startApp
17+
>>>>>>> feature/mapping-service
1418

1519
def initializeModel():
1620
"""Initialize VGGT model"""

sample_data/docker-compose-dl-streamer-example.yml

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ secrets:
3434
file: ${SECRETSDIR}/certs/scenescape-camcalibration.crt
3535
camcalibration-key:
3636
file: ${SECRETSDIR}/certs/scenescape-camcalibration.key
37+
mapping-cert:
38+
file: ${SECRETSDIR}/certs/scenescape-mapping.crt
39+
mapping-key:
40+
file: ${SECRETSDIR}/certs/scenescape-mapping.key
3741

3842
services:
3943
ntpserv:
@@ -258,7 +262,8 @@ services:
258262
retail-cams:
259263
condition: service_started
260264
healthcheck:
261-
test: ["CMD", "curl", "-I", "-s", "http://localhost:8080/pipelines"]
265+
test:
266+
["CMD", "curl", "-k", "-I", "-s", "https://localhost:8080/pipelines"]
262267
interval: 10s
263268
timeout: 5s
264269
retries: 5
@@ -422,8 +427,15 @@ services:
422427
- NO_PROXY=${no_proxy},.scenescape.intel.com,influxdb2
423428
- HTTP_PROXY=${http_proxy}
424429
- HTTPS_PROXY=${https_proxy}
430+
secrets:
431+
- source: root-cert
432+
target: certs/scenescape-ca.pem
433+
- source: mapping-cert
434+
target: certs/scenescape-mapping.crt
435+
- source: mapping-key
436+
target: certs/scenescape-mapping.key
425437
healthcheck:
426-
test: ["CMD", "curl", "-I", "-s", "http://localhost:8000/health"]
438+
test: ["CMD", "curl", "-k", "-I", "-s", "https://localhost:8000/health"]
427439
interval: 10s
428440
timeout: 60s
429441
retries: 5

tools/certificates/Makefile

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@ default: deploy-certificates
1212

1313
#.PHONY targets won't be skipped if files with these names happen to exist
1414
.PHONY: default ca deploy-certificates deploy-csr
15-
.PHONY: broker-cert web-cert vdms-c-cert vdms-s-cert camcalibration-cert
16-
.PHONY: broker-csr web-csr vdms-c-csr vdms-s-csr camcalibration-csr
15+
.PHONY: broker-cert web-cert vdms-c-cert vdms-s-cert camcalibration-cert mapping-cert
16+
.PHONY: broker-csr web-csr vdms-c-csr vdms-s-csr camcalibration-csr mapping-csr
1717

1818
#.SECONDARY prevents make from deleting files created by intermediate targets
1919
.SECONDARY:
2020

21-
deploy-certificates: ca broker-cert web-cert vdms-c-cert vdms-s-cert camcalibration-cert
21+
deploy-certificates: ca broker-cert web-cert vdms-c-cert vdms-s-cert camcalibration-cert mapping-cert
2222

23-
deploy-csr: broker-csr web-csr vdms-c-csr vdms-s-csr camcalibration-csr
23+
deploy-csr: broker-csr web-csr vdms-c-csr vdms-s-csr camcalibration-csr mapping-csr
2424

2525
broker-cert: HOST=broker
2626
broker-cert: KEY_USAGE=serverAuth
@@ -52,6 +52,12 @@ camcalibration-cert: \
5252
$(SECRETSDIR)/certs/scenescape-camcalibration.key \
5353
$(SECRETSDIR)/certs/scenescape-camcalibration.crt \
5454

55+
mapping-cert: HOST=mapping
56+
mapping-cert: KEY_USAGE=serverAuth
57+
mapping-cert: \
58+
$(SECRETSDIR)/certs/scenescape-mapping.key \
59+
$(SECRETSDIR)/certs/scenescape-mapping.crt \
60+
5561
broker-csr: HOST=broker
5662
broker-csr: KEY_USAGE=serverAuth
5763
broker-csr: \
@@ -82,6 +88,12 @@ camcalibration-csr: \
8288
$(SECRETSDIR)/certs/scenescape-camcalibration.key \
8389
$(SECRETSDIR)/certs/scenescape-camcalibration.csr \
8490

91+
mapping-csr: HOST=mapping
92+
mapping-csr: KEY_USAGE=serverAuth
93+
mapping-csr: \
94+
$(SECRETSDIR)/certs/scenescape-mapping.key \
95+
$(SECRETSDIR)/certs/scenescape-mapping.csr \
96+
8597
ca: $(CASECRETSDIR)/certs/scenescape-ca.pem
8698

8799
$(CASECRETSDIR)/ca/scenescape-ca.key:

0 commit comments

Comments
 (0)