|
FROM ghcr.io/developmentseed/titiler:latest |
|
|
|
|
|
ENV PYTHONUNBUFFERED=1 \ |
|
PYTHONDONTWRITEBYTECODE=1 \ |
|
PYTHONWARNINGS=ignore |
|
|
|
|
|
ENV TITILER_API_CORS_ORIGINS=* \ |
|
TITILER_API_CORS_ALLOW_METHODS=GET,POST,OPTIONS |
|
|
|
|
|
ENV FORWARDED_ALLOW_IPS=* \ |
|
TITILER_API_ROOT_PATH="" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ENV CPL_TMPDIR=/tmp \ |
|
GDAL_CACHEMAX=75% \ |
|
GDAL_INGESTED_BYTES_AT_OPEN=32768 \ |
|
GDAL_DISABLE_READDIR_ON_OPEN=EMPTY_DIR \ |
|
GDAL_HTTP_MERGE_CONSECUTIVE_RANGES=YES \ |
|
GDAL_HTTP_MULTIPLEX=YES \ |
|
GDAL_HTTP_VERSION=2 \ |
|
VSI_CACHE=TRUE \ |
|
VSI_CACHE_SIZE=1073741824 \ |
|
CPL_VSIL_CURL_ALLOWED_EXTENSIONS=.tif,.tiff,.vrt,.jp2,.png,.jpg \ |
|
CPL_VSIL_CURL_USE_HEAD=NO \ |
|
CPL_VSIL_CURL_CACHE_SIZE=2147483648 \ |
|
CPL_VSIL_CURL_CHUNK_SIZE=10485760 \ |
|
GDAL_HTTP_TIMEOUT=30 \ |
|
GDAL_HTTP_CONNECTTIMEOUT=10 \ |
|
GDAL_HTTP_MAX_RETRY=3 \ |
|
GDAL_HTTP_RETRY_DELAY=1 \ |
|
GDAL_NUM_THREADS=6 \ |
|
GDAL_MAX_DATASET_POOL_SIZE=300 \ |
|
PROJ_NETWORK=ON \ |
|
GDAL_ENABLE_WMS_CACHE=YES |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ENV TITILER_API_DISABLE_MOSAIC=FALSE \ |
|
TITILER_API_ENABLE_TILES_CACHE=TRUE \ |
|
MOSAIC_CONCURRENCY=3 \ |
|
RIO_TILER_MAX_THREADS=4 |
|
|
|
|
|
RUN pip install gunicorn uvloop slowapi |
|
|
|
|
|
WORKDIR /app |
|
|
|
|
|
RUN printf '%s\n' \ |
|
'import multiprocessing' \ |
|
'import os' \ |
|
'' \ |
|
'# WORKER CONFIGURATION' \ |
|
'workers = 6' \ |
|
'worker_class = "uvicorn.workers.UvicornWorker"' \ |
|
'worker_connections = 500' \ |
|
'' \ |
|
'# REQUEST RECYCLING' \ |
|
'max_requests = 3000' \ |
|
'max_requests_jitter = 300' \ |
|
'' \ |
|
'# TIMEOUTS' \ |
|
'timeout = 60' \ |
|
'graceful_timeout = 30' \ |
|
'keepalive = 5' \ |
|
'' \ |
|
'# PERFORMANCE TUNING' \ |
|
'backlog = 1024' \ |
|
'limit_request_line = 4094' \ |
|
'limit_request_fields = 100' \ |
|
'limit_request_field_size = 8190' \ |
|
'' \ |
|
'# SERVER MECHANICS' \ |
|
'bind = "0.0.0.0:7860"' \ |
|
'daemon = False' \ |
|
'reuse_port = True' \ |
|
'preload_app = True' \ |
|
'' \ |
|
'# LOGGING' \ |
|
'accesslog = "-"' \ |
|
'errorlog = "-"' \ |
|
'loglevel = "info"' \ |
|
'' \ |
|
'def when_ready(server):' \ |
|
' server.log.info("Server ready. Spawning workers")' \ |
|
'' \ |
|
'def pre_fork(server, worker):' \ |
|
' server.log.info(f"Worker spawned (pid: {worker.pid})")' \ |
|
'' \ |
|
'def post_fork(server, worker):' \ |
|
' server.log.info(f"Worker initialized (pid: {worker.pid})")' \ |
|
'' \ |
|
'def worker_exit(server, worker):' \ |
|
' server.log.info(f"Worker exited (pid: {worker.pid})")' \ |
|
> /app/gunicorn_config.py |
|
|
|
|
|
RUN printf '%s\n' \ |
|
'from titiler.application.main import app' \ |
|
'from slowapi import Limiter, _rate_limit_exceeded_handler' \ |
|
'from slowapi.util import get_remote_address' \ |
|
'from slowapi.errors import RateLimitExceeded' \ |
|
'from slowapi.middleware import SlowAPIMiddleware' \ |
|
'from starlette.middleware.base import BaseHTTPMiddleware' \ |
|
'from starlette.responses import Response' \ |
|
'import time' \ |
|
'import asyncio' \ |
|
'from collections import defaultdict, deque' \ |
|
'import logging' \ |
|
'' \ |
|
'logging.basicConfig(level=logging.INFO)' \ |
|
'logger = logging.getLogger(__name__)' \ |
|
'' \ |
|
'# RATE LIMITING CONFIGURATION' \ |
|
'limiter = Limiter(' \ |
|
' key_func=get_remote_address,' \ |
|
' default_limits=["400 per minute", "5000 per hour"],' \ |
|
' storage_uri="memory://",' \ |
|
' swallow_errors=True' \ |
|
')' \ |
|
'' \ |
|
'app.state.limiter = limiter' \ |
|
'app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)' \ |
|
'app.add_middleware(SlowAPIMiddleware)' \ |
|
'' \ |
|
'# ADAPTIVE BURST PROTECTION' \ |
|
'class AdaptiveBurstProtection(BaseHTTPMiddleware):' \ |
|
' def __init__(self, app, burst_size=50, window=1.0, decay_rate=0.9):' \ |
|
' super().__init__(app)' \ |
|
' self.burst_size = burst_size' \ |
|
' self.window = window' \ |
|
' self.decay_rate = decay_rate' \ |
|
' self.requests = defaultdict(lambda: deque(maxlen=burst_size))' \ |
|
' self.delays = defaultdict(float)' \ |
|
' ' \ |
|
' async def dispatch(self, request, call_next):' \ |
|
' client_ip = request.client.host' \ |
|
' now = time.time()' \ |
|
' ' \ |
|
' request_times = self.requests[client_ip]' \ |
|
' request_times.append(now)' \ |
|
' ' \ |
|
' if len(request_times) >= 2:' \ |
|
' time_span = now - request_times[0]' \ |
|
' if time_span > 0:' \ |
|
' current_rate = len(request_times) / time_span' \ |
|
' ' \ |
|
' if current_rate > self.burst_size:' \ |
|
' self.delays[client_ip] = min(0.5, self.delays[client_ip] + 0.05)' \ |
|
' await asyncio.sleep(self.delays[client_ip])' \ |
|
' ' \ |
|
' if self.delays[client_ip] > 0.1:' \ |
|
' logger.warning(f"Rate limiting {client_ip}: {current_rate:.1f} req/s")' \ |
|
' ' \ |
|
' elif self.delays[client_ip] > 0:' \ |
|
' self.delays[client_ip] *= self.decay_rate' \ |
|
' ' \ |
|
' try:' \ |
|
' response = await call_next(request)' \ |
|
' return response' \ |
|
' except Exception as e:' \ |
|
' logger.error(f"Request failed: {e}")' \ |
|
' return Response(content="Internal error", status_code=500)' \ |
|
'' \ |
|
'app.add_middleware(AdaptiveBurstProtection, burst_size=75, window=1.0)' \ |
|
'' \ |
|
'# CACHE HEADERS' \ |
|
'@app.middleware("http")' \ |
|
'async def add_cache_headers(request, call_next):' \ |
|
' response = await call_next(request)' \ |
|
' path = str(request.url.path)' \ |
|
' ' \ |
|
' if "/tiles/" in path:' \ |
|
' response.headers["Cache-Control"] = "public, max-age=3600, stale-while-revalidate=7200"' \ |
|
' elif "/cog/statistics" in path or "/cog/info" in path:' \ |
|
' response.headers["Cache-Control"] = "public, max-age=600"' \ |
|
' elif "/cog/bounds" in path:' \ |
|
' response.headers["Cache-Control"] = "public, max-age=86400"' \ |
|
' ' \ |
|
' return response' \ |
|
'' \ |
|
'logger.info("Custom app with rate limiting initialized")' \ |
|
> /app/custom_app.py |
|
|
|
|
|
RUN printf '%s\n' \ |
|
'#!/bin/bash' \ |
|
'echo "==============================================="' \ |
|
|
|
'cd /app' \ |
|
'exec gunicorn custom_app:app -c gunicorn_config.py' \ |
|
> /app/start.sh |
|
|
|
RUN chmod +x /app/start.sh |
|
|
|
EXPOSE 7860 |
|
|
|
CMD ["/app/start.sh"] |