GeoServer Infrastructure & Development Guide

This guide provides comprehensive information for developers and operators working with GeoServer in the SDL platform, covering infrastructure architecture, configuration management, backup strategies, and development workflows.

Overview

GeoServer in SDL is deployed as a containerized service with extensive customizations for security, persistence, and integration with other platform components. The deployment leverages:

  • JDBC Configuration Storage - PostgreSQL-based configuration management

  • Automated Security Backups - Continuous backup of critical security files

  • Persistent Volume Management - Selective persistence for non-database data

  • PostGIS Integration - Native spatial database support

  • MinIO S3 Storage - Distributed tile caching

  • Keycloak OIDC - Enterprise authentication and authorization

Architecture

Component Overview

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│   Keycloak      │────▶│    GeoServer    │────▶│   PostgreSQL    │
│  (Auth/OIDC)    │     │                 │     │  (JDBC Config)  │
└─────────────────┘     └─────────────────┘     └─────────────────┘
                               │                         │
                               ▼                         ▼
                        ┌─────────────────┐     ┌─────────────────┐
                        │     MinIO       │     │    PostGIS      │
                        │ (Tile Cache)    │     │ (Spatial Data)  │
                        └─────────────────┘     └─────────────────┘

Key Components

  1. GeoServer Container (df-geoserver)

    • Based on Kartoza GeoServer image with custom extensions

    • ABAC (Attribute-Based Access Control) support

    • OIDC/Keycloak integration

    • JDBC Configuration module

  2. Security Backup Service

    • Runs as separate deployment

    • Backs up security files every 10 seconds

    • Stores in PostgreSQL tables

  3. Init Containers

    • Database state checker

    • Configuration injector

    • Security restoration

Configuration Management

JDBC Configuration

Most GeoServer configurations are stored in PostgreSQL using the JDBC Config community module. This provides:

  • Centralized configuration storage

  • Easy backup and restore

  • Support for multiple GeoServer instances

  • Configuration versioning

Database Schema

-- Main configuration database
Database: geoserver_configs

-- Key tables created by JDBC Config:
- object           -- Main configuration objects
- property         -- Object properties
- property_type    -- Property type definitions
- default_object   -- Default configurations

Dynamic Initialization

The system automatically detects database state on startup:

# Check if JDBC config tables exist
TABLE_EXISTS=$(psql ... -c "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'object');")

if [ "$TABLE_EXISTS" = "t" ]; then
    # Tables exist - check for data
    CONFIG_COUNT=$(psql ... -c "SELECT COUNT(*) FROM object;")

    if [ "$CONFIG_COUNT" -gt "0" ]; then
        # Existing config - don't reinitialize
        initdb=false
        import=false
    else
        # Empty tables - import from filesystem
        initdb=false
        import=true
    fi
else
    # No tables - full initialization
    initdb=true
    import=true
fi

Security Backup System

Critical security files are continuously backed up to prevent data loss during pod restarts or failures.

Backed Up Directories

/opt/geoserver/data_dir/security/
├── usergroup/default/     # User and group definitions
├── role/default/          # Default role mappings
└── role/keycloak_service/ # Keycloak role mappings

Backup Tables

-- Security file backups
CREATE TABLE geoserver_security_backup (
    id SERIAL PRIMARY KEY,
    file_path VARCHAR(255) NOT NULL UNIQUE,
    file_content TEXT NOT NULL,      -- Base64 encoded
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Keystore backup
CREATE TABLE geoserver_keystore (
    id SERIAL PRIMARY KEY,
    jceks BYTEA,                     -- Binary keystore data
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Restoration Process

During startup, the bootstrap script:

  1. Checks for backup existence in PostgreSQL

  2. Restores files if backups exist

  3. Creates marker file (.security_restored)

  4. Prevents backing up vanilla configurations on first startup

Persistent Volumes

For data that cannot be stored in JDBC Config, selective PVC mounting is used:

persistence:
  enabled: true
  size: 1Gi
  directories:
    - gwc                # GeoWebCache configuration
    - gwc-layers         # Layer-specific cache settings
    - user_projections   # Custom CRS definitions
    - styles            # SLD/CSS style files
    - logs              # Application logs
    - monitoring        # Performance data

Development Workflows

Local Development Setup

Prerequisites

  1. Docker and Docker Compose

  2. Access to SDL container registry

  3. PostgreSQL client tools

  4. kubectl and Helm

Running Locally

# Clone repositories
git clone https://github.com/your-org/df-geoserver.git
git clone https://github.com/your-org/sdl.git

# Build custom GeoServer image
cd df-geoserver
docker build -t df-geoserver:local .

# Deploy with Helm (from SDL repo)
cd ../sdl
helm install geoserver kubernetes/helm/charts/geoserver \
  --set image.geoserver.tag=local \
  --set global.namespace=default

Adding Extensions

The Kartoza GeoServer image provides multiple ways to add extensions:

Stable Extensions

Enable stable extensions using the STABLE_EXTENSIONS environment variable:

env:
  - name: STABLE_EXTENSIONS
    value: "charts-plugin,db2-plugin,mongodb-plugin"
Use the plugin name without the full filename. For example, use wps-plugin instead of geoserver-2.27.1-wps-plugin.zip.

Community Extensions

Enable community extensions that are still in development:

env:
  - name: COMMUNITY_EXTENSIONS
    value: "gwc-sqlite-plugin,ogr-datastore-plugin,sec-keycloak-plugin"
  - name: FORCE_DOWNLOAD_COMMUNITY_EXTENSIONS
    value: "true"  # Force download of latest versions

Default Extensions

By default, these extensions are activated: - vectortiles-plugin - Vector tiles support - wps-plugin - Web Processing Service - libjpeg-turbo-plugin - Optimized JPEG encoding - control-flow-plugin - Request flow control - pyramid-plugin - Image pyramid support - gdal-plugin - GDAL raster formats - monitor-plugin - Request monitoring - inspire-plugin - INSPIRE compliance - csw-plugin - Catalog Service for Web

To customize which default extensions are active:

env:
  - name: ACTIVE_EXTENSIONS
    value: "control-flow-plugin,csw-plugin,wps-plugin"
    # This will ONLY activate these three, skipping other defaults

Custom JAR Extensions

For custom extensions not available in the repositories:

  1. Build-time approach - Add to Dockerfile:

    # In df-geoserver Dockerfile
    FROM kartoza/geoserver:${GS_VERSION}
    
    # Add custom extension JARs
    COPY --chown=geoserveruser:geoserverusers \
      ./extensions/custom-extension-*.jar \
      /usr/local/tomcat/webapps/geoserver/WEB-INF/lib/
    
    # For {sdl-product-acronym} ABAC extension
    COPY --chown=geoserveruser:geoserverusers \
      ./df-geoserver-abac/target/df-geoserver-abac.jar \
      /tmp/df-geoserver-abac.jar
  2. Runtime approach - Configure in bootstrap.sh:

    # Enable custom extensions conditionally
    if [[ "$GEOSERVER_ABAC" =~ ^[Tt][Rr][Uu][Ee]$ ]]; then
      echo "Support for GeoServer ABAC has been enabled."
      cp /tmp/df-geoserver-abac.jar \
         /usr/local/tomcat/webapps/geoserver/WEB-INF/lib/df-geoserver-abac.jar
    
      # Set proper ownership
      chown geoserveruser:geoserverusers \
         /usr/local/tomcat/webapps/geoserver/WEB-INF/lib/df-geoserver-abac.jar
    fi
  3. Volume mount approach - For development:

    volumes:
      - name: custom-extensions
        hostPath:
          path: /path/to/extensions
          type: Directory
    
    volumeMounts:
      - name: custom-extensions
        mountPath: /opt/geoserver/data_dir/extensions
    
    env:
      - name: GEOSERVER_DATA_DIR_EXTENSIONS
        value: "/opt/geoserver/data_dir/extensions"

Extension Configuration Example

Complete example combining multiple extension types:

# In values.yaml or deployment.yaml
env:
  # Stable extensions
  - name: STABLE_EXTENSIONS
    value: "monitor-plugin,importer-plugin,web-resource-plugin"

  # Community extensions
  - name: COMMUNITY_EXTENSIONS
    value: "sec-keycloak-plugin,jdbcconfig-plugin"

  # Force latest community versions
  - name: FORCE_DOWNLOAD_COMMUNITY_EXTENSIONS
    value: "true"

  # Custom extensions
  - name: GEOSERVER_ABAC
    value: "true"

  # Override default active extensions
  - name: ACTIVE_EXTENSIONS
    value: "control-flow-plugin,monitor-plugin,wps-plugin,csw-plugin"

Creating Custom Datastores

PostGIS Datastore Example

<dataStore>
  <name>df-postgis</name>
  <type>PostGIS</type>
  <enabled>true</enabled>
  <connectionParameters>
    <host>df-postgres</host>
    <port>5432</port>
    <database>spatial_data</database>
    <user>datahub</user>
    <passwd>${POSTGRES_PASSWORD}</passwd>
    <dbtype>postgis</dbtype>
    <schema>public</schema>
    <Loose bbox="">true</Loose>
    <Estimated extends="">true</Estimated>
    <validate connections="">true</validate>
    <Connection timeout="">20</Connection>
    <min connections="">1</min>
    <max connections="">10</max>
  </connectionParameters>
</dataStore>

Implementing Custom Security

ABAC Extension

The custom df-geoserver-abac extension provides attribute-based access control:

// Example ABAC rule evaluation
public boolean evaluate(Authentication auth, Resource resource) {
    // Get user attributes from JWT
    Map<String, Object> attributes = extractAttributes(auth);

    // Check against resource requirements
    return checkAccess(attributes, resource.getRequiredAttributes());
}

Keycloak Integration

Token validation and role extraction:

# OIDC configuration
oidc:
  clientId: df-geoserver
  clientSecret: ${GEOSERVER_CLIENT_SECRET}
  discoveryUrl: http://df-keycloak/auth/realms/data-fabric/.well-known/openid-configuration
  rolesClaim: resource_access.df-geoserver.roles

Integration Examples

Accessing GeoServer from Applications

Python Example

import requests
from requests.auth import HTTPBasicAuth

class GeoServerClient:
    def __init__(self, base_url, token):
        self.base_url = base_url
        self.headers = {'Authorization': f'Bearer {token}'}

    def get_layers(self):
        response = requests.get(
            f"{self.base_url}/rest/layers",
            headers=self.headers
        )
        return response.json()

    def get_wms_image(self, layers, bbox, width=800, height=600):
        params = {
            'service': 'WMS',
            'version': '1.1.0',
            'request': 'GetMap',
            'layers': layers,
            'bbox': bbox,
            'width': width,
            'height': height,
            'srs': 'EPSG:4326',
            'format': 'image/png'
        }
        response = requests.get(
            f"{self.base_url}/wms",
            params=params,
            headers=self.headers
        )
        return response.content

JavaScript/OpenLayers Example

// Initialize map with GeoServer WMS layer
const wmsLayer = new ol.layer.Tile({
  source: new ol.source.TileWMS({
    url: 'https://map.example.com/geoserver/wms',
    params: {
      'LAYERS': 'nasa:blue_marble',
      'TILED': true
    },
    serverType: 'geoserver',
    crossOrigin: 'anonymous'
  })
});

// Add authentication header
wmsLayer.getSource().setTileLoadFunction((tile, src) => {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', src);
  xhr.setRequestHeader('Authorization', `Bearer ${authToken}`);
  xhr.responseType = 'blob';
  xhr.onload = () => {
    tile.getImage().src = URL.createObjectURL(xhr.response);
  };
  xhr.send();
});

Federation

Multi-Instance Deployment

For high availability, deploy multiple GeoServer instances sharing the same JDBC config:

# In values.yaml
replicaCount: 3

# Ensure all instances use same database
env:
  - name: POSTGRES_HOST
    value: df-postgres-primary  # Use primary for writes

Federation with Other SDL Nodes

Configure cascading WMS/WFS to federate layers from other nodes:

<remoteStore>
  <name>remote-sdl-node</name>
  <type>WMS</type>
  <capabilitiesURL>
    https://remote.sdl.example.com/geoserver/wms?request=GetCapabilities
  </capabilitiesURL>
  <username>federation-user</username>
  <password>${FEDERATION_PASSWORD}</password>
</remoteStore>