A spatial dashboard that runs perfectly on a laptop can fail in three different ways the moment it meets real traffic: the container image is too large to deploy quickly, the coordinate libraries take so long to import that serverless cold starts time out, and long-lived map sessions accumulate GeoDataFrame copies until the pod is killed for exceeding its memory limit. None of these failures show up in a notebook. They only appear once the application is packaged, replicated, and put behind a load balancer with dozens of analysts panning and zooming at once. For data scientists, GIS teams, and internal tooling groups, closing the gap between a working prototype and a resilient production service is an operations problem with a distinctly geospatial shape.

This section covers the full deployment surface for Streamlit and Panel dashboards that carry heavy geospatial dependencies. You will see how to containerise the GDAL and PROJ native stack without shipping a bloated image, how to survive the cold-start cost of importing geopandas and rasterio on serverless runtimes, how to autoscale a Kubernetes cluster while keeping stateful map sessions pinned to a single replica, and how to instrument the metrics that expose degradation before an analyst files a ticket.


Jump to heading Architecture overview

The deployment pipeline moves a spatial dashboard from source to a scaled, observable service. A container image is built with the native geospatial libraries baked in, pushed to a registry, pulled by either a serverless runtime or an orchestrator, and multiplied by an autoscaler in response to session load. Observability taps every stage so that a slow image pull, a cold-start spike, or a memory climb is visible as a metric rather than as a user complaint.

Spatial dashboard deployment pipelineA left-to-right pipeline: a container image carrying GDAL and PROJ is built, pushed to a registry, pulled by a serverless runtime or Kubernetes orchestrator, replicated by an autoscaler in response to session load, and served to users over WebSocket sessions. A wide observability layer below collects logs, metrics, traces, and health-probe results from the build, registry, runtime, and autoscaler stages.DEPLOY PIPELINEBuild imageGDAL · PROJ · GEOSmulti-stageRegistrytagged + signedServerless / OrchestratorCloud Run · KubernetesWebSocket sessionsAutoscalerreplicas ∝ sessionsUsersanalysts · mapsObservability layerlogs · metrics · traces · health probes — taps every stage

Jump to heading Foundational design constraints for spatial workloads

Five constraints separate a spatial dashboard deployment from an ordinary Python web service. Each one changes a decision that would otherwise be routine, and ignoring any of them produces a failure that generic deployment tutorials never warn you about.

1. Native dependency weight and image size. The geospatial stack is not pure Python. geopandas, rasterio, fiona, and pyproj all bind to compiled C and C++ libraries — GDAL for format translation, PROJ for coordinate transformations, and GEOS for geometry predicates. Those libraries and their format drivers, plus the PROJ data grids that make reprojection accurate, add hundreds of megabytes before your first line of application code. A naive pip install geopandas on a full Python base image routinely yields a 2 GB or larger container, which slows every registry push, every autoscaler pull, and every cold start. Shrinking that image without breaking the native links is the first job of Docker containerization for spatial workloads.

2. GeoDataFrame memory footprint. Geometry is expensive to hold in memory. A GeoDataFrame of 500,000 polygons can occupy 2 GB before rendering, and every user who filters or reprojects it materialises a second copy. Because Streamlit runs each session as a thread in one process, ten analysts each holding a viewport-clipped copy of the same layer multiply the resident set of a single replica far beyond the on-disk dataset size. Right-sizing the container limit depends on measuring real resident set size, which is the discipline covered in memory limit management.

3. Long-lived WebSocket sessions versus stateless scaling. Autoscalers and serverless platforms are designed for short, stateless HTTP requests. Streamlit and Panel break that model: each browser tab opens a persistent WebSocket that stays connected for the whole working session and carries every widget interaction. A load balancer that round-robins messages across replicas will scatter one user’s clicks over several containers, none of which hold that user’s session state. Every scaling decision on this page has to preserve the affinity between a session and the replica that owns it.

4. Cold-start cost of importing the geospatial stack. Importing geopandas pulls in pyproj, shapely, fiona, and the GDAL bindings, and reading the PROJ database on first use adds more. On a cold container this import graph alone can take several seconds before the app even binds its port. On a scale-to-zero serverless platform that cost is paid on the first request after every idle period, and if it exceeds the startup probe deadline the platform declares the container unhealthy and retries — a loop that looks like an outage. Warming and trimming this import path is central to Cloud Run and serverless deployment.

5. Payload size on the wire. Spatial responses are large. A GeoJSON feature collection for a dense administrative layer can reach tens of megabytes, and streaming that to a browser over the session WebSocket blocks other messages and inflates memory on both ends. Simplifying geometry per zoom level, encoding as compact binary, and streaming rather than buffering the whole response are the levers that keep the transport tier healthy under real map interaction.


Jump to heading Containerising the geospatial stack

The first production step is a container image that carries the native libraries reliably and stays small enough to pull quickly. A single-stage Dockerfile that installs build tools, compiles wheels, and runs the app in the same layer traps the compiler toolchain, header packages, and apt caches in the final image. A multi-stage build compiles in a fat builder stage and copies only the finished artefacts — the installed Python packages and the shared libraries they link against — into a slim runtime stage.

dockerfile
# ---- builder stage: has compilers and headers ----
FROM python:3.12-slim AS builder
RUN apt-get update && apt-get install -y --no-install-recommends \
        build-essential gdal-bin libgdal-dev proj-bin libproj-dev \
    && rm -rf /var/lib/apt/lists/*
ENV GDAL_CONFIG=/usr/bin/gdal-config
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

# ---- runtime stage: slim, no toolchain ----
FROM python:3.12-slim AS runtime
RUN apt-get update && apt-get install -y --no-install-recommends \
        gdal-bin proj-bin proj-data \
    && rm -rf /var/lib/apt/lists/*
COPY --from=builder /install /usr/local
COPY app/ /app/
WORKDIR /app
EXPOSE 8501
CMD ["streamlit", "run", "dashboard.py", "--server.port=8501", "--server.address=0.0.0.0"]

The runtime stage still needs the GDAL and PROJ runtime packages — copying Python wheels alone leaves dangling links to libgdal.so and a missing proj.db. Getting the split right, choosing between python-slim with apt packages and a conda base, caching the heavy geo wheels, and running the server as a non-root user are all worked through in Docker containerization for spatial workloads. Two focused tasks branch from it: setting memory limits for GeoPandas containers to stop OOM kills, and pinning GDAL and PROJ versions so that reprojection results stay reproducible across rebuilds.


Jump to heading Serverless deployment and the cold-start problem

Serverless runtimes such as Cloud Run are attractive for internal spatial dashboards because they scale to zero when nobody is looking at the map and bill only for active time. The catch is the import graph. A cold instance must load the interpreter, import the full geospatial stack, and open the PROJ database before it can answer the startup probe. If that takes longer than the probe’s timeout, the platform kills and restarts the container in a loop.

Three levers tame cold starts. Keep at least one instance warm so the common path never pays the import cost. Move expensive imports and dataset loads behind the health check so the port binds quickly and the probe passes, then hydrate caches lazily. And configure session affinity plus a long request timeout so the persistent WebSocket is not severed mid-session.

python
import os
import streamlit as st

# Bind the port fast: do NOT import geopandas at module top level on a
# scale-to-zero instance. Defer the heavy stack until first real use.
@st.cache_resource
def get_geostack():
    import geopandas as gpd  # deferred: keeps cold-start import off the probe path
    import pyproj
    pyproj.datadir.get_data_dir()  # touch proj.db once so later calls are warm
    return gpd

st.title("Regional coverage")
if st.session_state.get("ready"):
    gpd = get_geostack()
    gdf = gpd.read_parquet("data/uk_lsoa_2021.parquet").to_crs("EPSG:3857")
    st.map(gdf.to_crs("EPSG:4326"))
else:
    st.button("Load map", on_click=lambda: st.session_state.update(ready=True))

The full treatment — minimum-instance configuration, session affinity, streaming large GeoJSON so a single response does not exceed the platform’s payload ceiling, and warm-up requests — lives in Cloud Run and serverless deployment, with a dedicated walkthrough for mitigating cold starts for Streamlit spatial dashboards.


Jump to heading Kubernetes autoscaling and session orchestration

When a spatial dashboard outgrows a single serverless service — because it needs GPU-backed raster processing, a private VPC, or predictable capacity for a large internal audience — a Kubernetes cluster gives finer control over how replicas scale and how sessions are routed. The Horizontal Pod Autoscaler (HPA) adds and removes pods based on a metric, but the default CPU metric is a poor proxy for a dashboard whose load is measured in concurrent WebSocket sessions rather than request rate.

The orchestration challenge is twofold. First, scaling has to respond to session count and memory pressure, not just CPU, because a map session can hold a large GeoDataFrame while using almost no CPU. Second, because each session is stateful, the ingress must pin a browser to the pod that owns its state using sticky sessions, or a scale-up event will strand users on pods that never saw their earlier interactions.

yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: spatial-dashboard
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: spatial-dashboard
  minReplicas: 2
  maxReplicas: 12
  metrics:
    - type: Resource
      resource:
        name: memory          # scale on RSS, not just CPU, for GeoDataFrame load
        target:
          type: Utilization
          averageUtilization: 70
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300   # avoid evicting active map sessions early

Tuning the HPA for the bursty arrival pattern of dashboard users and configuring cookie-based affinity so a session never migrates mid-analysis are covered in Kubernetes autoscaling and orchestration, with task-level detail in tuning HPA for bursty spatial dashboard sessions and sticky sessions for stateful spatial dashboards on Kubernetes.


Jump to heading Monitoring, health, and observability

A spatial dashboard fails quietly. Memory climbs across a long session until a pod is OOM-killed; a spatial join that was fast on test data crawls on production volumes; a reprojection silently returns wrong coordinates after a library upgrade. None of these throw an obvious error at deploy time, so the deployment has to expose them as signals.

The minimum instrumentation is a health endpoint the orchestrator can probe, structured logs that record the shape and latency of each spatial query, and metrics for resident set size and session count. Streamlit exposes an internal health path, and you can add an application-level check that confirms the geospatial stack actually loaded:

python
import logging
import time
import geopandas as gpd

logger = logging.getLogger("spatial.ops")

def health() -> dict:
    """Readiness check that proves GDAL/PROJ are usable, not just that Python is up."""
    t0 = time.perf_counter()
    ok = gpd.GeoSeries.from_wkt(["POINT (-0.1276 51.5072)"]).set_crs(
        "EPSG:4326"
    ).to_crs("EPSG:27700").notna().all()
    logger.info("healthcheck reproject_ok=%s ms=%.1f", ok, (time.perf_counter() - t0) * 1000)
    return {"status": "ok" if ok else "degraded"}

Health endpoints, structured spatial-query logging, and the alert thresholds that catch degradation early are the subject of Monitoring and observability in production, including adding health-check endpoints to Streamlit dashboards and logging spatial query performance with structured logs.


Jump to heading Production configuration

Beyond the individual runtimes, a handful of settings apply to every spatial dashboard deployment.

Resource requests and limits. Set the memory request to the steady-state resident set of an idle replica and the limit to the measured peak under concurrent sessions plus headroom. A limit that is too tight triggers OOM kills mid-session; one that is too loose lets the autoscaler pack too many replicas onto a node and cascade failures under load.

yaml
resources:
  requests:
    memory: "1500Mi"
    cpu: "500m"
  limits:
    memory: "3Gi"
    cpu: "2000m"

Graceful shutdown. When the autoscaler removes a replica it sends SIGTERM. A dashboard with active sessions should stop accepting new connections, let in-flight renders finish, and flush logs before exiting. Set terminationGracePeriodSeconds generously — 60 seconds or more — so a mid-render spatial join is not truncated.

Environment pinning. Fix PROJ_DATA (or the legacy PROJ_LIB) and GDAL_DATA explicitly in the image so reprojection finds its grids regardless of how the base image lays out its filesystem. An unset PROJ_DATA is the most common cause of a container that reprojects correctly in one environment and silently wrongly in another.


Jump to heading Observability and failure modes

Spatial deployments have a recognisable catalogue of production failures. Learning their signatures shortens diagnosis from hours to minutes.

OOMKilled, exit code 137. The container exceeded its memory limit. On a spatial dashboard the usual cause is accumulated per-session GeoDataFrame copies or an unbounded cache. Confirm with kubectl describe pod and raise the limit only after measuring the true peak — see setting memory limits for GeoPandas containers.

ImportError: libgdal.so: cannot open shared object file. The runtime image kept the Python wheels but dropped the GDAL runtime package during the multi-stage copy. The application will not even start.

PROJ: proj_create: Cannot find proj.db. The PROJ data directory is missing or PROJ_DATA points at nothing. Reprojection either fails loudly or, worse, returns subtly wrong coordinates. Version pinning prevents the matching-version variant of this bug.

Startup probe failures on serverless. The cold-start import graph exceeded the probe deadline. Defer heavy imports and keep an instance warm.

Scattered session state after scale-up. Widget clicks land on a pod that does not hold the session, so the map resets. The ingress is missing sticky sessions.

MetricSourceAlert threshold
container_memory_working_set_bytescAdvisor / kubelet> 85% of limit for 2 min
spatial_query_duration_msapplication structured logp95 > 1500 ms
active_websocket_sessionsreverse-proxy metric> 0.8 × per-replica capacity
cold_start_secondsserverless platform metric> 0.7 × probe deadline

Jump to heading Compliance and security notes for spatial data

Location data is frequently sensitive. Precise geometry can reveal a person’s home, a facility’s security perimeter, or the movement of a monitored asset, so a production spatial deployment inherits the same obligations as any system handling personal data.

Least-privilege containers. Run the dashboard as a non-root user and mount the filesystem read-only where possible. A compromised map server should not be able to write to the image or reach beyond its session scope.

Signed images and pinned bases. Sign container images and pin base image digests so a supply-chain substitution cannot swap the GDAL build under you. Pinning also keeps the reprojection stack reproducible, which matters when a coordinate result feeds a regulated decision.

Encrypted transport and short-lived data URLs. Terminate TLS at the ingress and serve tiles or raster exports from short-lived signed URLs rather than embedding long-lived credentials in the client. Do not cache signed URLs beyond their expiry.

Row-level authorisation after the shared cache. When a shared cache holds a full dataset for performance, apply per-user geographic filters downstream in the session, never before the shared layer, so one analyst’s authorised extent is not served to another. This boundary is the same one described for session state patterns.


Jump to heading Conclusion

Deploying a spatial dashboard is not the same problem as deploying a generic web app, because the geospatial stack changes the economics at every layer. The native GDAL and PROJ libraries dominate image size, so a multi-stage build is not an optimisation but a requirement. Importing that stack is slow, so serverless cold starts need warm instances and deferred imports. Map sessions are long-lived and stateful, so autoscaling has to preserve session affinity and scale on memory rather than CPU. And geometry is heavy, so per-replica memory limits must be set from measured resident set size, not from the size of a file on disk.

Instrument the deployment from the first release. Health endpoints that actually exercise a reprojection, structured logs that record spatial query latency, and memory metrics that alert before an OOM kill turn silent geospatial failures into visible signals. The task pages below carry each layer to implementation depth: Docker containerization for spatial workloads for the image, Cloud Run and serverless deployment for scale-to-zero economics, Kubernetes autoscaling and orchestration for stateful scaling, and Monitoring and observability in production for the signals that keep it all honest.


Jump to heading Frequently asked questions

Why is my spatial dashboard container image several gigabytes?

The size is dominated by the native geospatial stack — GDAL, PROJ, GEOS, and their format drivers plus the PROJ data grids — not by your Python code. A single-stage build also traps compilers and header packages in the final layer. Use a multi-stage build so the compiled wheels and shared libraries are copied into a slim runtime image while the build toolchain is discarded, which typically cuts a 2.5 GB image to well under 900 MB. The full recipe is in Docker containerization for spatial workloads.

Can I run a Streamlit spatial dashboard on a stateless serverless platform?

Yes, but Streamlit and Panel both hold a long-lived WebSocket per browser tab, so the platform must keep the instance alive for the whole session and route every message from that tab back to the same instance. On Cloud Run, set session affinity, raise the request timeout to the maximum, and keep a minimum instance warm so the geopandas and rasterio import cost is not paid on every cold start. See Cloud Run and serverless deployment.

How much memory should I allocate per spatial dashboard replica?

Budget for the peak resident set of a single loaded GeoDataFrame multiplied by the number of concurrent sessions that hold their own filtered copy, plus roughly 300 MB of headroom for the interpreter, Arrow buffers, and CRS transformation intermediates. A 200 MB dataset that each of ten sessions clips to a viewport can easily need a 3 Gi limit. Measure resident set size directly rather than trusting on-disk file size — the method is in memory limit management.


Back to Spatial Dashboard Home

Related