Deploying a Geospatial Model with FastAPI and Docker
Moving spatial analysis from interactive Jupyter notebooks to a production environment requires more than wrapping a script in a web server. Geospatial machine learning pipelines introduce unique deployment challenges: coordinate reference system (CRS) misalignment, heavy C-based binary dependencies (GDAL, GEOS, PROJ), and strict schema validation for spatial payloads. FastAPI addresses the routing and validation layer with asynchronous performance and automatic OpenAPI documentation, while Docker guarantees environment reproducibility across development, staging, and cloud infrastructure.
This guide provides a complete, production-ready workflow for containerizing a Python-based spatial predictor. You will learn how to standardize spatial inputs, build a validated inference endpoint, and package the service into a lightweight, scalable container.
Prerequisites and Environment Preparation
Before constructing the deployment pipeline, verify the following:
- Python 3.10 or newer
- Docker Engine or Docker Desktop
- A trained model artifact (e.g., scikit-learn, XGBoost, or PyTorch) saved as a serialized file
- A serialized preprocessing pipeline to guarantee identical feature transformations during training and inference
The architecture demonstrated below assumes a vector-based classification or regression workflow that ingests GeoJSON and returns predictions. However, the same containerization strategy scales seamlessly to raster inference, point-cloud processing, or Deep Learning for Object Detection pipelines.
Step 1: Standardizing Spatial Inputs and Feature Engineering
Geospatial datasets rarely arrive as clean, model-ready matrices. Raw geometries must be transformed into predictive vectors through spatial operations such as joins, zonal statistics, or topological calculations. Proper Feature Engineering for Spatial Models ensures your API receives consistent numerical inputs regardless of the source geometry format.
Common spatial transformations include:
- Calculating Euclidean or network distances to infrastructure
- Extracting elevation or land-cover values from raster surfaces
- Computing neighborhood density or spatial lag metrics
- Quantifying Spatial Autocorrelation and Statistics to capture regional dependencies
To prevent training-serving skew, serialize your preprocessing steps alongside your trained estimator using joblib. This guarantees that CRS transformations, feature scaling, and categorical encodings remain perfectly synchronized when the API processes new payloads.
# save_pipeline.py
import joblib
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.ensemble import RandomForestClassifier
# Example: A simple pipeline that scales features and runs inference
pipeline = Pipeline([
("scaler", StandardScaler()),
("model", RandomForestClassifier(n_estimators=100, random_state=42))
])
# Train pipeline (placeholder)
# pipeline.fit(X_train, y_train)
# Serialize both the pipeline and the expected input CRS
joblib.dump(pipeline, "model_pipeline.joblib")
print("Pipeline serialized successfully.")
Step 2: Constructing the FastAPI Endpoint
FastAPI excels at validating JSON payloads and routing requests asynchronously. For geospatial services, Pydantic models enforce structural compliance before data reaches the inference engine. The endpoint below accepts a GeoJSON FeatureCollection, standardizes the coordinate reference system, extracts engineered attributes, runs the model, and appends predictions to the original features.
The request/response interaction handled by this endpoint is shown below.
sequenceDiagram
participant C as Client
participant API as FastAPI /predict
participant V as Pydantic validator
participant M as Model pipeline
C->>API: POST /predict (GeoJSON FeatureCollection)
API->>V: Validate schema
V-->>API: Validated features
API->>API: GeoDataFrame + CRS align
API->>M: predict(X)
M-->>API: predictions + confidence
API-->>C: GeoJSON with predictions
# main.py
from contextlib import asynccontextmanager
from typing import Any, Dict, List
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
import geopandas as gpd
import joblib
import numpy as np
# Define a lightweight Pydantic schema for GeoJSON validation
class GeoJSONFeature(BaseModel):
type: str = Field(..., pattern="^Feature$")
properties: Dict[str, Any]
geometry: Dict[str, Any]
class GeoJSONFeatureCollection(BaseModel):
type: str = Field(..., pattern="^FeatureCollection$")
features: List[GeoJSONFeature]
# Global variables for model and pipeline
model_pipeline = None
EXPECTED_CRS = "EPSG:4326" # WGS84
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Load model and pipeline into memory on startup."""
global model_pipeline
try:
model_pipeline = joblib.load("model_pipeline.joblib")
print("✅ Geospatial model and pipeline loaded successfully.")
except Exception as e:
raise RuntimeError(f"Failed to load model pipeline: {e}")
yield
# Cleanup (if needed)
model_pipeline = None
app = FastAPI(title="Geospatial Inference API", lifespan=lifespan)
@app.post("/predict", response_model=Dict[str, Any])
async def predict_spatial(payload: GeoJSONFeatureCollection):
if model_pipeline is None:
raise HTTPException(status_code=503, detail="Model not loaded")
try:
# Convert GeoJSON payload to GeoDataFrame
gdf = gpd.GeoDataFrame.from_features(payload.model_dump()["features"])
# Standardize CRS to match training data
if gdf.crs is None:
gdf.set_crs(EXPECTED_CRS, inplace=True)
elif gdf.crs != EXPECTED_CRS:
gdf = gdf.to_crs(EXPECTED_CRS)
# Extract numeric features (assumes properties contain engineered columns)
feature_cols = [col for col in gdf.columns if col not in ["geometry", "type"]]
X = gdf[feature_cols].values
# Run inference
predictions = model_pipeline.predict(X)
probabilities = model_pipeline.predict_proba(X).max(axis=1)
# Append predictions to GeoJSON structure
for i, feature in enumerate(payload.features):
feature.properties["prediction"] = int(predictions[i])
feature.properties["confidence"] = round(float(probabilities[i]), 4)
return {
"type": "FeatureCollection",
"features": [f.model_dump() for f in payload.features]
}
except Exception as e:
raise HTTPException(status_code=400, detail=f"Geospatial validation or inference failed: {str(e)}")
Key Implementation Details:
asynccontextmanagerhandles model loading efficiently, avoiding repeated disk I/O during requests.geopandas.GeoDataFrame.from_features()safely parses GeoJSON while preserving topology.- CRS alignment prevents silent spatial misregistration, a common source of degraded Evaluating Geospatial AI Performance metrics.
- Pydantic v2 enforces strict GeoJSON structure compliance before any spatial computation occurs.
Step 3: Containerizing with Docker
Geospatial Python packages depend on compiled C libraries. A production Dockerfile must explicitly install system dependencies, pin package versions, and optimize layer caching.
# Dockerfile
FROM python:3.10-slim
# Set environment variables for non-interactive installs and Python optimization
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
DEBIAN_FRONTEND=noninteractive
# Install system dependencies required for GDAL/GEOS/PROJ
RUN apt-get update && apt-get install -y --no-install-recommends \
gdal-bin \
libgdal-dev \
libgeos-dev \
libproj-dev \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Set GDAL version environment variable for pip compatibility
ENV GDAL_VERSION=3.6.2
WORKDIR /app
# Copy requirements first to leverage Docker layer caching
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code and model artifacts
COPY main.py .
COPY model_pipeline.joblib .
# Expose port and define healthcheck
EXPOSE 8000
HEALTHCHECK \
CMD python -c "import requests; requests.get('http://localhost:8000/docs')" || exit 1
# Run with Uvicorn for async performance
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
requirements.txt
fastapi==0.109.0
uvicorn[standard]==0.27.1
pydantic==2.6.1
geopandas==0.14.3
shapely==2.0.2
joblib==1.3.2
numpy==1.26.4
Step 4: Running, Testing, and Scaling the Service
With the container defined, build and run the service locally to validate the deployment pipeline.
# Build the image
docker build -t geospatial-inference-api:latest .
# Run the container
docker run -d -p 8000:8000 --name geo-api geospatial-inference-api:latest
Test the endpoint using curl or Python’s requests library:
curl -X POST "http://localhost:8000/predict" \
-H "Content-Type: application/json" \
-d '{
"type": "FeatureCollection",
"features": [{
"type": "Feature",
"properties": {"elevation": 150.2, "distance_to_road": 0.45},
"geometry": {"type": "Point", "coordinates": [-122.4194, 37.7749]}
}]
}'
For production workloads, orchestrate the container using Docker Compose or Kubernetes. Implement connection pooling, enable response caching for identical spatial queries, and monitor memory consumption during heavy vector operations. These practices form the foundation of Advanced Geospatial AI Optimization, ensuring your service remains responsive under concurrent GIS workloads.
When scaling horizontally, ensure stateless design: the container should not rely on local filesystem writes during inference. Instead, offload logging, metrics, and payload storage to external services. This architecture aligns with modern Model Deployment for GIS Applications, where reproducibility, observability, and low-latency routing dictate infrastructure choices.
Conclusion
Deploying a geospatial model requires careful coordination between spatial data validation, dependency management, and asynchronous routing. By standardizing CRS transformations, serializing preprocessing pipelines, and leveraging FastAPI’s schema validation alongside Docker’s environment isolation, you can transition experimental spatial analysis into a reliable, production-grade API. As your models grow in complexity, prioritize stateless design, automated health checks, and continuous performance benchmarking to maintain accuracy and throughput at scale.