#!/usr/bin/env bash set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" cd "$ROOT" GEOINTEL_CONTAINER_LOCK_FILE="${GEOINTEL_CONTAINER_LOCK_FILE:-/tmp/geointel-container-replacement.lock}" if ! command -v flock >/dev/null 2>&1; then echo "GeoIntel container replacement requires flock to prevent concurrent Docker mutations." >&2 exit 2 fi exec 8>"$GEOINTEL_CONTAINER_LOCK_FILE" if ! flock -w 300 8; then echo "Timed out waiting for another GeoIntel container replacement to finish." >&2 exit 3 fi if [ -f .env ]; then set -a # shellcheck disable=SC1091 . ./.env set +a fi GEOINTEL_FRONTEND_PORT="${GEOINTEL_FRONTEND_PORT:-1202}" GEOINTEL_IMAGE="${GEOINTEL_IMAGE:-geointel-all-in-one:latest}" GEOINTEL_STORAGE_PATH="${GEOINTEL_STORAGE_PATH:-/mnt/user/appdata/geointel/storage}" GEOINTEL_MODELS_PATH="${GEOINTEL_MODELS_PATH:-/mnt/user/appdata/geointel/models}" GEOINTEL_BACKUPS_PATH="${GEOINTEL_BACKUPS_PATH:-/mnt/user/appdata/geointel/backups}" GEOINTEL_POSTGIS_DATA_PATH="${GEOINTEL_POSTGIS_DATA_PATH:-/mnt/user/appdata/geointel/postgres-data}" GEOINTEL_POSTGRES_DB="${GEOINTEL_POSTGRES_DB:-geointel}" GEOINTEL_POSTGRES_USER="${GEOINTEL_POSTGRES_USER:-geointel}" GEOINTEL_POSTGRES_PASSWORD="${GEOINTEL_POSTGRES_PASSWORD:-}" GEOINTEL_CORS_ORIGINS="${GEOINTEL_CORS_ORIGINS:-http://localhost:${GEOINTEL_FRONTEND_PORT},http://127.0.0.1:${GEOINTEL_FRONTEND_PORT}}" GEOINTEL_MAX_UPLOAD_MB="${GEOINTEL_MAX_UPLOAD_MB:-500}" GEOINTEL_MAX_IN_MEMORY_VECTOR_MB="${GEOINTEL_MAX_IN_MEMORY_VECTOR_MB:-64}" GEOINTEL_AOI_WORKER_ENABLED="${GEOINTEL_AOI_WORKER_ENABLED:-true}" GEOINTEL_AOI_WORKER_POLL_SECONDS="${GEOINTEL_AOI_WORKER_POLL_SECONDS:-2}" GEOINTEL_AUTH_ENABLED="${GEOINTEL_AUTH_ENABLED:-true}" GEOINTEL_AUTH_REQUIRE_HTTPS="${GEOINTEL_AUTH_REQUIRE_HTTPS:-true}" GEOINTEL_AUTH_USERNAME="${GEOINTEL_AUTH_USERNAME:-}" GEOINTEL_AUTH_PASSWORD_HASH="${GEOINTEL_AUTH_PASSWORD_HASH:-}" GEOINTEL_AUTH_SESSION_SECRET="${GEOINTEL_AUTH_SESSION_SECRET:-}" GEOINTEL_AUTH_SESSION_TTL_SECONDS="${GEOINTEL_AUTH_SESSION_TTL_SECONDS:-43200}" GEOINTEL_PUBLIC_BASE_URL="${GEOINTEL_PUBLIC_BASE_URL:-https://geointel.example.com}" GEOINTEL_AUTHENTIK_ISSUER="${GEOINTEL_AUTHENTIK_ISSUER:-}" GEOINTEL_AUTHENTIK_CLIENT_ID="${GEOINTEL_AUTHENTIK_CLIENT_ID:-}" GEOINTEL_AUTHENTIK_CLIENT_SECRET="${GEOINTEL_AUTHENTIK_CLIENT_SECRET:-}" GEOINTEL_AUTHENTIK_ALLOWED_EMAIL="${GEOINTEL_AUTHENTIK_ALLOWED_EMAIL:-}" GEOINTEL_GUEST_ACCESS_ENABLED="${GEOINTEL_GUEST_ACCESS_ENABLED:-false}" GEOINTEL_GUEST_DISPLAY_NAME="${GEOINTEL_GUEST_DISPLAY_NAME:-Gast}" GEOINTEL_GUEST_SESSION_TTL_SECONDS="${GEOINTEL_GUEST_SESSION_TTL_SECONDS:-7200}" ORTHOPHOTO_ENABLED="${ORTHOPHOTO_ENABLED:-true}" ORTHOPHOTO_WMS_URL="${ORTHOPHOTO_WMS_URL:-https://geo.api.vlaanderen.be/OMWRGBMRVL/wms}" SPW_ORTHOPHOTO_WMS_URL="${SPW_ORTHOPHOTO_WMS_URL:-https://geoservices.wallonie.be/arcgis/services/IMAGERIE/ORTHO_LAST/MapServer/WMSServer}" BRUSSELS_ORTHOPHOTO_WMS_URL="${BRUSSELS_ORTHOPHOTO_WMS_URL:-https://geoservices-grid.irisnet.be/geoserver/urbisgrid/ows}" ORTHOPHOTO_WMS_LAYER="${ORTHOPHOTO_WMS_LAYER:-Ortho}" ORTHOPHOTO_RESOLUTION_M="${ORTHOPHOTO_RESOLUTION_M:-1.0}" ORTHOPHOTO_MIN_SIDE_M="${ORTHOPHOTO_MIN_SIDE_M:-128}" ORTHOPHOTO_MAX_SIDE_M="${ORTHOPHOTO_MAX_SIDE_M:-1024}" ORTHOPHOTO_CACHE_TTL_HOURS="${ORTHOPHOTO_CACHE_TTL_HOURS:-24}" SOURCE_CATALOG_PROBE_ENABLED="${SOURCE_CATALOG_PROBE_ENABLED:-true}" SOURCE_CATALOG_GRB_WFS_URL="${SOURCE_CATALOG_GRB_WFS_URL:-https://geo.api.vlaanderen.be/GRB/wfs}" SOURCE_CATALOG_STATBEL_DCAT_URL="${SOURCE_CATALOG_STATBEL_DCAT_URL:-https://doc.statbel.be/publications/DCAT/DCAT_opendata_datasets.ttl}" SOURCE_CATALOG_STATBEL_MAX_RESPONSE_MB="${SOURCE_CATALOG_STATBEL_MAX_RESPONSE_MB:-5}" SOURCE_CATALOG_ALZ_RELEASE_URL="${SOURCE_CATALOG_ALZ_RELEASE_URL:-https://landbouwcijfers.vlaanderen.be/open-geodata-landbouwgebruikspercelen}" SOURCE_CATALOG_PROBE_TIMEOUT_SECONDS="${SOURCE_CATALOG_PROBE_TIMEOUT_SECONDS:-10}" SOURCE_CATALOG_PROBE_MAX_RESPONSE_MB="${SOURCE_CATALOG_PROBE_MAX_RESPONSE_MB:-2}" SOURCE_CATALOG_PROBE_CACHE_TTL_SECONDS="${SOURCE_CATALOG_PROBE_CACHE_TTL_SECONDS:-900}" OFFICIAL_VECTOR_ENABLED="${OFFICIAL_VECTOR_ENABLED:-true}" BWK_WFS_URL="${BWK_WFS_URL:-https://geo.api.vlaanderen.be/BWK/wfs}" DOV_SOIL_WFS_URL="${DOV_SOIL_WFS_URL:-https://www.dov.vlaanderen.be/geoserver/wfs}" SPW_PICC_ENABLED="${SPW_PICC_ENABLED:-true}" SPW_PICC_MAPSERVER_URL="${SPW_PICC_MAPSERVER_URL:-https://geoservices.wallonie.be/arcgis/rest/services/TOPOGRAPHIE/PICC_VDIFF/MapServer}" SPW_FLOOD_HAZARD_ENABLED="${SPW_FLOOD_HAZARD_ENABLED:-true}" SPW_FLOOD_HAZARD_MAPSERVER_URL="${SPW_FLOOD_HAZARD_MAPSERVER_URL:-https://geoservices.wallonie.be/arcgis/rest/services/EAU/ALEA_INOND/MapServer}" URBIS_ENABLED="${URBIS_ENABLED:-true}" URBIS_WFS_URL="${URBIS_WFS_URL:-https://geoservices-vector.irisnet.be/geoserver/urbisvector/ows}" OFFICIAL_VECTOR_MIN_SIDE_M="${OFFICIAL_VECTOR_MIN_SIDE_M:-10}" OFFICIAL_VECTOR_MAX_SIDE_M="${OFFICIAL_VECTOR_MAX_SIDE_M:-20000}" OFFICIAL_VECTOR_PAGE_SIZE="${OFFICIAL_VECTOR_PAGE_SIZE:-1000}" OFFICIAL_VECTOR_MAX_PAGES="${OFFICIAL_VECTOR_MAX_PAGES:-200}" OFFICIAL_VECTOR_MAX_FEATURES="${OFFICIAL_VECTOR_MAX_FEATURES:-100000}" OFFICIAL_VECTOR_TIMEOUT_SECONDS="${OFFICIAL_VECTOR_TIMEOUT_SECONDS:-180}" OFFICIAL_VECTOR_MAX_RESPONSE_MB="${OFFICIAL_VECTOR_MAX_RESPONSE_MB:-20}" OFFICIAL_VECTOR_MAX_TOTAL_RESPONSE_MB="${OFFICIAL_VECTOR_MAX_TOTAL_RESPONSE_MB:-256}" OFFICIAL_VECTOR_CACHE_TTL_HOURS="${OFFICIAL_VECTOR_CACHE_TTL_HOURS:-24}" DHMV_ENABLED="${DHMV_ENABLED:-true}" DHMV_WCS_URL="${DHMV_WCS_URL:-https://geo.api.vlaanderen.be/DHMV/wcs}" DHMV_RESOLUTION_M="${DHMV_RESOLUTION_M:-5.0}" DHMV_MIN_SIDE_M="${DHMV_MIN_SIDE_M:-10}" DHMV_MAX_SIDE_M="${DHMV_MAX_SIDE_M:-20000}" DHMV_MAX_PIXELS="${DHMV_MAX_PIXELS:-12000000}" DHMV_TIMEOUT_SECONDS="${DHMV_TIMEOUT_SECONDS:-300}" DHMV_MAX_RESPONSE_MB="${DHMV_MAX_RESPONSE_MB:-160}" FLOOD_HAZARD_ENABLED="${FLOOD_HAZARD_ENABLED:-true}" FLOOD_HAZARD_WCS_URL="${FLOOD_HAZARD_WCS_URL:-https://geoservice.waterinfo.be/OGRK/wcs}" FLOOD_HAZARD_RESOLUTION_M="${FLOOD_HAZARD_RESOLUTION_M:-5.0}" FLOOD_HAZARD_MIN_SIDE_M="${FLOOD_HAZARD_MIN_SIDE_M:-10}" FLOOD_HAZARD_MAX_SIDE_M="${FLOOD_HAZARD_MAX_SIDE_M:-20000}" FLOOD_HAZARD_MAX_PIXELS="${FLOOD_HAZARD_MAX_PIXELS:-12000000}" FLOOD_HAZARD_TIMEOUT_SECONDS="${FLOOD_HAZARD_TIMEOUT_SECONDS:-300}" FLOOD_HAZARD_MAX_RESPONSE_MB="${FLOOD_HAZARD_MAX_RESPONSE_MB:-160}" BATHYMETRY_PROFILES_ENABLED="${BATHYMETRY_PROFILES_ENABLED:-true}" BATHYMETRY_PROFILES_LAYER_URL="${BATHYMETRY_PROFILES_LAYER_URL:-https://vha.waterinfo.be/arcgis/rest/services/digitale_atlas/MapServer/0}" BATHYMETRY_WATERCOURSE_LAYER_URL="${BATHYMETRY_WATERCOURSE_LAYER_URL:-https://vha.waterinfo.be/arcgis/rest/services/digitale_atlas/MapServer/1}" BATHYMETRY_PROFILES_PAGE_SIZE="${BATHYMETRY_PROFILES_PAGE_SIZE:-1000}" BATHYMETRY_PROFILES_MAX_FEATURES="${BATHYMETRY_PROFILES_MAX_FEATURES:-50000}" BATHYMETRY_PROFILES_TIMEOUT_SECONDS="${BATHYMETRY_PROFILES_TIMEOUT_SECONDS:-120}" BATHYMETRY_PROFILES_MAX_RESPONSE_MB="${BATHYMETRY_PROFILES_MAX_RESPONSE_MB:-32}" MDK_BATHYMETRY_PROBE_ENABLED="${MDK_BATHYMETRY_PROBE_ENABLED:-true}" MDK_BATHYMETRY_WCS_URL="${MDK_BATHYMETRY_WCS_URL:-https://bathy.agentschapmdk.be/spatialfusionserver/services/ows/wcs/EL_wcs}" MDK_BATHYMETRY_PROBE_TIMEOUT_SECONDS="${MDK_BATHYMETRY_PROBE_TIMEOUT_SECONDS:-20}" MDK_BATHYMETRY_PROBE_MAX_RESPONSE_MB="${MDK_BATHYMETRY_PROBE_MAX_RESPONSE_MB:-4}" MDK_BATHYMETRY_ACQUISITION_ENABLED="${MDK_BATHYMETRY_ACQUISITION_ENABLED:-false}" MDK_BATHYMETRY_COVERAGE_ID="${MDK_BATHYMETRY_COVERAGE_ID:-}" MDK_BATHYMETRY_REQUEST_CRS="${MDK_BATHYMETRY_REQUEST_CRS:-EPSG:4326}" MDK_BATHYMETRY_MAX_BBOX_DEG2="${MDK_BATHYMETRY_MAX_BBOX_DEG2:-0.25}" MDK_BATHYMETRY_ACQUISITION_TIMEOUT_SECONDS="${MDK_BATHYMETRY_ACQUISITION_TIMEOUT_SECONDS:-120}" MDK_BATHYMETRY_ACQUISITION_MAX_RESPONSE_MB="${MDK_BATHYMETRY_ACQUISITION_MAX_RESPONSE_MB:-160}" THEMATIC_RASTER_ENABLED="${THEMATIC_RASTER_ENABLED:-true}" THEMATIC_RASTER_WCS_URL="${THEMATIC_RASTER_WCS_URL:-https://www.mercator.vlaanderen.be/raadpleegdienstenmercatorpubliek/wcs}" THEMATIC_RASTER_MIN_SIDE_M="${THEMATIC_RASTER_MIN_SIDE_M:-100}" THEMATIC_RASTER_MAX_SIDE_M="${THEMATIC_RASTER_MAX_SIDE_M:-60000}" THEMATIC_RASTER_MAX_PIXELS="${THEMATIC_RASTER_MAX_PIXELS:-30000000}" THEMATIC_RASTER_TIMEOUT_SECONDS="${THEMATIC_RASTER_TIMEOUT_SECONDS:-300}" THEMATIC_RASTER_MAX_RESPONSE_MB="${THEMATIC_RASTER_MAX_RESPONSE_MB:-160}" WALOUS_ENABLED="${WALOUS_ENABLED:-true}" WALOUS_SOURCE_DIR="${WALOUS_SOURCE_DIR:-/app/storage/source-cache/walous}" WALOUS_ANALYSIS_RESOLUTION_M="${WALOUS_ANALYSIS_RESOLUTION_M:-10}" WALOUS_MAX_SIDE_M="${WALOUS_MAX_SIDE_M:-60000}" WALOUS_MAX_PIXELS="${WALOUS_MAX_PIXELS:-36000000}" SPW_TERRAIN_ENABLED="${SPW_TERRAIN_ENABLED:-true}" SPW_TERRAIN_SOURCE_DIR="${SPW_TERRAIN_SOURCE_DIR:-/app/storage/source-cache/spw-terrain}" SPW_TERRAIN_ANALYSIS_RESOLUTION_M="${SPW_TERRAIN_ANALYSIS_RESOLUTION_M:-5}" SPW_TERRAIN_MAX_SIDE_M="${SPW_TERRAIN_MAX_SIDE_M:-20000}" SPW_TERRAIN_MAX_PIXELS="${SPW_TERRAIN_MAX_PIXELS:-12000000}" YOLO_ENABLED="${YOLO_ENABLED:-false}" YOLO_MODELS_DIR="${YOLO_MODELS_DIR:-/app/models}" YOLO_MODEL_PATH="${YOLO_MODEL_PATH:-}" YOLO_MODEL_ID="${YOLO_MODEL_ID:-yolo-configured}" YOLO_MODEL_DISPLAY_NAME="${YOLO_MODEL_DISPLAY_NAME:-Configured YOLO detector}" YOLO_MODEL_VERSION="${YOLO_MODEL_VERSION:-}" YOLO_MODEL_CLASSES="${YOLO_MODEL_CLASSES:-building}" YOLO_ENFORCE_VALIDATION_SCOPE="${YOLO_ENFORCE_VALIDATION_SCOPE:-true}" YOLO_VALIDATION_SCOPE_MANIFEST_PATH="${YOLO_VALIDATION_SCOPE_MANIFEST_PATH:-}" YOLO_VALIDATION_SCOPE_MANIFEST_SHA256="${YOLO_VALIDATION_SCOPE_MANIFEST_SHA256:-}" YOLO_VALIDATED_AREA_NAMES="${YOLO_VALIDATED_AREA_NAMES:-Mol,Kempen}" YOLO_CONFIG_DIR="${YOLO_CONFIG_DIR:-/app/storage/ultralytics}" YOLO_DEVICE="${YOLO_DEVICE:-cuda:0}" YOLO_REQUIRE_CUDA="${YOLO_REQUIRE_CUDA:-true}" YOLO_IMAGE_SIZE="${YOLO_IMAGE_SIZE:-640}" YOLO_MAX_TILES="${YOLO_MAX_TILES:-100}" YOLO_MAX_DETECTIONS="${YOLO_MAX_DETECTIONS:-1000}" YOLO_DUPLICATE_IOU_THRESHOLD="${YOLO_DUPLICATE_IOU_THRESHOLD:-0.5}" YOLO_BATCH_SIZE="${YOLO_BATCH_SIZE:-1}" YOLO_SEG_ENABLED="${YOLO_SEG_ENABLED:-false}" YOLO_SEG_MODEL_PATH="${YOLO_SEG_MODEL_PATH:-}" YOLO_SEG_MODEL_ID="${YOLO_SEG_MODEL_ID:-yolo-seg-configured}" YOLO_SEG_MODEL_DISPLAY_NAME="${YOLO_SEG_MODEL_DISPLAY_NAME:-Configured YOLO segmentation}" YOLO_SEG_MODEL_VERSION="${YOLO_SEG_MODEL_VERSION:-}" SAM_ENABLED="${SAM_ENABLED:-false}" SAM_MODEL_PATH="${SAM_MODEL_PATH:-}" SAM_MODEL_ID="${SAM_MODEL_ID:-sam-configured}" SAM_MODEL_DISPLAY_NAME="${SAM_MODEL_DISPLAY_NAME:-Configured SAM segmentation}" SAM_MODEL_VERSION="${SAM_MODEL_VERSION:-}" SEGMENTATION_MAX_MASKS_PER_TILE="${SEGMENTATION_MAX_MASKS_PER_TILE:-300}" SEGMENTATION_DUPLICATE_IOU_THRESHOLD="${SEGMENTATION_DUPLICATE_IOU_THRESHOLD:-0.5}" OLLAMA_ENABLED="${OLLAMA_ENABLED:-true}" OLLAMA_BASE_URL="${OLLAMA_BASE_URL:-http://host.docker.internal:11434}" OLLAMA_DEFAULT_MODEL="${OLLAMA_DEFAULT_MODEL:-qwen3.5:9b}" OLLAMA_TIMEOUT_SECONDS="${OLLAMA_TIMEOUT_SECONDS:-120}" OLLAMA_MAX_OUTPUT_TOKENS="${OLLAMA_MAX_OUTPUT_TOKENS:-1200}" OLLAMA_CONTEXT_TOKENS="${OLLAMA_CONTEXT_TOKENS:-16384}" validate_runtime_config() { case "$GEOINTEL_FRONTEND_PORT" in ''|*[!0-9]*) echo "GEOINTEL_FRONTEND_PORT must be a whole number." >&2 return 2 ;; esac if [ "$GEOINTEL_FRONTEND_PORT" -lt 1 ] || [ "$GEOINTEL_FRONTEND_PORT" -gt 65535 ]; then echo "GEOINTEL_FRONTEND_PORT must be between 1 and 65535." >&2 return 2 fi case "$GEOINTEL_MAX_UPLOAD_MB" in ''|*[!0-9]*) echo "GEOINTEL_MAX_UPLOAD_MB must be a whole number." >&2 return 2 ;; esac if [ "$GEOINTEL_MAX_UPLOAD_MB" -lt 1 ] || [ "$GEOINTEL_MAX_UPLOAD_MB" -gt 2048 ]; then echo "GEOINTEL_MAX_UPLOAD_MB must be between 1 and 2048." >&2 return 2 fi case "$GEOINTEL_MAX_IN_MEMORY_VECTOR_MB" in ''|*[!0-9]*) echo "GEOINTEL_MAX_IN_MEMORY_VECTOR_MB must be a whole number." >&2 return 2 ;; esac if [ "$GEOINTEL_MAX_IN_MEMORY_VECTOR_MB" -lt 1 ] || [ "$GEOINTEL_MAX_IN_MEMORY_VECTOR_MB" -gt 256 ]; then echo "GEOINTEL_MAX_IN_MEMORY_VECTOR_MB must be between 1 and 256." >&2 return 2 fi case "$GEOINTEL_AUTH_ENABLED" in true|false) ;; *) echo "GEOINTEL_AUTH_ENABLED must be true or false." >&2 return 2 ;; esac case "$GEOINTEL_AUTH_REQUIRE_HTTPS" in true|false) ;; *) echo "GEOINTEL_AUTH_REQUIRE_HTTPS must be true or false." >&2 return 2 ;; esac if [ "$GEOINTEL_AUTH_ENABLED" = "true" ]; then if [ -z "$GEOINTEL_AUTH_USERNAME" ] \ || [ -z "$GEOINTEL_AUTH_PASSWORD_HASH" ] \ || [ "${#GEOINTEL_AUTH_SESSION_SECRET}" -lt 32 ]; then echo "Enabled operator authentication requires username, password hash and a 32+ character session secret." >&2 return 2 fi case "$GEOINTEL_AUTH_PASSWORD_HASH" in pbkdf2_sha256\$*) ;; *) echo "GEOINTEL_AUTH_PASSWORD_HASH must use the pbkdf2_sha256 format." >&2 return 2 ;; esac if [ "$GEOINTEL_AUTH_REQUIRE_HTTPS" = "true" ]; then case "$GEOINTEL_PUBLIC_BASE_URL" in https://*) ;; *) echo "GEOINTEL_PUBLIC_BASE_URL must use HTTPS when operator login requires HTTPS." >&2 return 2 ;; esac fi fi local authentik_count=0 local authentik_value for authentik_value in \ "$GEOINTEL_AUTHENTIK_ISSUER" \ "$GEOINTEL_AUTHENTIK_CLIENT_ID" \ "$GEOINTEL_AUTHENTIK_CLIENT_SECRET" \ "$GEOINTEL_AUTHENTIK_ALLOWED_EMAIL"; do if [ -n "$authentik_value" ]; then authentik_count=$((authentik_count + 1)) fi done if [ "$authentik_count" -ne 0 ] && [ "$authentik_count" -ne 4 ]; then echo "All GEOINTEL_AUTHENTIK_* values must be configured together." >&2 return 2 fi if [ "$authentik_count" -eq 4 ]; then if [ "$GEOINTEL_AUTH_ENABLED" != "true" ]; then echo "GEOINTEL_AUTH_ENABLED must be true when Authentik is configured." >&2 return 2 fi case "$GEOINTEL_AUTHENTIK_ISSUER" in https://*) ;; *) echo "GEOINTEL_AUTHENTIK_ISSUER must use HTTPS." >&2; return 2 ;; esac case "$GEOINTEL_PUBLIC_BASE_URL" in https://*) ;; *) echo "GEOINTEL_PUBLIC_BASE_URL must use HTTPS for Authentik." >&2; return 2 ;; esac fi case "$GEOINTEL_GUEST_ACCESS_ENABLED" in true|false) ;; *) echo "GEOINTEL_GUEST_ACCESS_ENABLED must be true or false." >&2 return 2 ;; esac if [ -z "${GEOINTEL_GUEST_DISPLAY_NAME// }" ]; then echo "GEOINTEL_GUEST_DISPLAY_NAME must not be blank." >&2 return 2 fi case "$GEOINTEL_GUEST_SESSION_TTL_SECONDS" in ''|*[!0-9]*) echo "GEOINTEL_GUEST_SESSION_TTL_SECONDS must be an integer." >&2 return 2 ;; esac if [ "$GEOINTEL_GUEST_SESSION_TTL_SECONDS" -lt 900 ] || [ "$GEOINTEL_GUEST_SESSION_TTL_SECONDS" -gt 86400 ]; then echo "GEOINTEL_GUEST_SESSION_TTL_SECONDS must be between 900 and 86400." >&2 return 2 fi case "$GEOINTEL_POSTGRES_PASSWORD" in ''|geointel|postgres|password|changeme|change-me-before-shared-use) echo "Refusing deployment with an empty or known-default PostGIS password." >&2 return 2 ;; esac docker image inspect "$GEOINTEL_IMAGE" >/dev/null } install_dockerman_metadata() { if [ -d /boot/config/plugins/dockerMan ]; then mkdir -p /boot/config/plugins/dockerMan/templates-user /boot/config/plugins/dockerMan/images cp deploy/unraid/geointel-unraid-template.xml /boot/config/plugins/dockerMan/templates-user/my-geointel.xml cp deploy/unraid/geointel-icon.png /boot/config/plugins/dockerMan/images/geointel-icon.png fi } migrate_compose_volume_if_needed() { if [ -f "${GEOINTEL_POSTGIS_DATA_PATH}/PG_VERSION" ]; then return 0 fi local compose_volume_path compose_volume_path="$(docker volume inspect geointel_geointel_postgis --format '{{ .Mountpoint }}' 2>/dev/null || true)" if [ -z "$compose_volume_path" ] || [ ! -f "${compose_volume_path}/PG_VERSION" ]; then return 0 fi echo "Migrating existing Compose PostGIS volume to ${GEOINTEL_POSTGIS_DATA_PATH}..." mkdir -p "$GEOINTEL_POSTGIS_DATA_PATH" cp -a "${compose_volume_path}/." "$GEOINTEL_POSTGIS_DATA_PATH/" } validate_runtime_config install_dockerman_metadata # GeoIntel runs as ONE container started by docker run below. # Tear down any leftover Compose stack first, including the multi-container # development stack (db + backend + frontend) that also claims port 1202. for compose_file in docker-compose.yml docker-compose.unraid.yml; do if [ -f "$compose_file" ]; then docker compose -f "$compose_file" down --remove-orphans || true fi done if docker ps -a --format '{{.Names}}' | grep -qx geointel; then docker rm -f geointel >/dev/null 2>&1 || true for attempt in $(seq 1 60); do if ! docker ps -a --format '{{.Names}}' | grep -qx geointel; then break fi if [ "$attempt" -eq 60 ]; then echo "GeoIntel container removal did not complete within 60 seconds." >&2 exit 1 fi sleep 1 done fi mkdir -p "$GEOINTEL_STORAGE_PATH" "$GEOINTEL_MODELS_PATH" "$GEOINTEL_BACKUPS_PATH" "$GEOINTEL_POSTGIS_DATA_PATH" migrate_compose_volume_if_needed docker run -d \ --name geointel \ --gpus all \ --restart unless-stopped \ --label "com.docker.compose.project=${COMPOSE_PROJECT_NAME:-geointel}" \ --label "tech.itworx.commit=${GITEA_COMMIT_SHA:-unknown}" \ --label net.unraid.docker.managed=dockerman \ --label 'net.unraid.docker.webui=http://[IP]:[PORT:80]/' \ --label net.unraid.docker.icon=/boot/config/plugins/dockerMan/images/geointel-icon.png \ --add-host host.docker.internal:host-gateway \ -p "${GEOINTEL_FRONTEND_PORT}:80" \ -e GEOINTEL_POSTGRES_DB="$GEOINTEL_POSTGRES_DB" \ -e GEOINTEL_POSTGRES_USER="$GEOINTEL_POSTGRES_USER" \ -e GEOINTEL_POSTGRES_PASSWORD="$GEOINTEL_POSTGRES_PASSWORD" \ -e GEOINTEL_STORAGE_ROOT=/app/storage \ -e GEOINTEL_CORS_ORIGINS="$GEOINTEL_CORS_ORIGINS" \ -e GEOINTEL_MAX_UPLOAD_MB="$GEOINTEL_MAX_UPLOAD_MB" \ -e GEOINTEL_MAX_IN_MEMORY_VECTOR_MB="$GEOINTEL_MAX_IN_MEMORY_VECTOR_MB" \ -e GEOINTEL_AOI_WORKER_ENABLED="$GEOINTEL_AOI_WORKER_ENABLED" \ -e GEOINTEL_AOI_WORKER_POLL_SECONDS="$GEOINTEL_AOI_WORKER_POLL_SECONDS" \ -e GEOINTEL_AUTH_ENABLED="$GEOINTEL_AUTH_ENABLED" \ -e GEOINTEL_AUTH_REQUIRE_HTTPS="$GEOINTEL_AUTH_REQUIRE_HTTPS" \ -e GEOINTEL_AUTH_USERNAME="$GEOINTEL_AUTH_USERNAME" \ -e GEOINTEL_AUTH_PASSWORD_HASH="$GEOINTEL_AUTH_PASSWORD_HASH" \ -e GEOINTEL_AUTH_SESSION_SECRET="$GEOINTEL_AUTH_SESSION_SECRET" \ -e GEOINTEL_AUTH_SESSION_TTL_SECONDS="$GEOINTEL_AUTH_SESSION_TTL_SECONDS" \ -e GEOINTEL_PUBLIC_BASE_URL="$GEOINTEL_PUBLIC_BASE_URL" \ -e GEOINTEL_AUTHENTIK_ISSUER="$GEOINTEL_AUTHENTIK_ISSUER" \ -e GEOINTEL_AUTHENTIK_CLIENT_ID="$GEOINTEL_AUTHENTIK_CLIENT_ID" \ -e GEOINTEL_AUTHENTIK_CLIENT_SECRET="$GEOINTEL_AUTHENTIK_CLIENT_SECRET" \ -e GEOINTEL_AUTHENTIK_ALLOWED_EMAIL="$GEOINTEL_AUTHENTIK_ALLOWED_EMAIL" \ -e GEOINTEL_GUEST_ACCESS_ENABLED="$GEOINTEL_GUEST_ACCESS_ENABLED" \ -e GEOINTEL_GUEST_DISPLAY_NAME="$GEOINTEL_GUEST_DISPLAY_NAME" \ -e GEOINTEL_GUEST_SESSION_TTL_SECONDS="$GEOINTEL_GUEST_SESSION_TTL_SECONDS" \ -e ORTHOPHOTO_ENABLED="$ORTHOPHOTO_ENABLED" \ -e ORTHOPHOTO_WMS_URL="$ORTHOPHOTO_WMS_URL" \ -e SPW_ORTHOPHOTO_WMS_URL="$SPW_ORTHOPHOTO_WMS_URL" \ -e BRUSSELS_ORTHOPHOTO_WMS_URL="$BRUSSELS_ORTHOPHOTO_WMS_URL" \ -e ORTHOPHOTO_WMS_LAYER="$ORTHOPHOTO_WMS_LAYER" \ -e ORTHOPHOTO_RESOLUTION_M="$ORTHOPHOTO_RESOLUTION_M" \ -e ORTHOPHOTO_MIN_SIDE_M="$ORTHOPHOTO_MIN_SIDE_M" \ -e ORTHOPHOTO_MAX_SIDE_M="$ORTHOPHOTO_MAX_SIDE_M" \ -e ORTHOPHOTO_CACHE_TTL_HOURS="$ORTHOPHOTO_CACHE_TTL_HOURS" \ -e SOURCE_CATALOG_PROBE_ENABLED="$SOURCE_CATALOG_PROBE_ENABLED" \ -e SOURCE_CATALOG_GRB_WFS_URL="$SOURCE_CATALOG_GRB_WFS_URL" \ -e SOURCE_CATALOG_STATBEL_DCAT_URL="$SOURCE_CATALOG_STATBEL_DCAT_URL" \ -e SOURCE_CATALOG_STATBEL_MAX_RESPONSE_MB="$SOURCE_CATALOG_STATBEL_MAX_RESPONSE_MB" \ -e SOURCE_CATALOG_ALZ_RELEASE_URL="$SOURCE_CATALOG_ALZ_RELEASE_URL" \ -e SOURCE_CATALOG_PROBE_TIMEOUT_SECONDS="$SOURCE_CATALOG_PROBE_TIMEOUT_SECONDS" \ -e SOURCE_CATALOG_PROBE_MAX_RESPONSE_MB="$SOURCE_CATALOG_PROBE_MAX_RESPONSE_MB" \ -e SOURCE_CATALOG_PROBE_CACHE_TTL_SECONDS="$SOURCE_CATALOG_PROBE_CACHE_TTL_SECONDS" \ -e OFFICIAL_VECTOR_ENABLED="$OFFICIAL_VECTOR_ENABLED" \ -e BWK_WFS_URL="$BWK_WFS_URL" \ -e DOV_SOIL_WFS_URL="$DOV_SOIL_WFS_URL" \ -e SPW_PICC_ENABLED="$SPW_PICC_ENABLED" \ -e SPW_PICC_MAPSERVER_URL="$SPW_PICC_MAPSERVER_URL" \ -e SPW_FLOOD_HAZARD_ENABLED="$SPW_FLOOD_HAZARD_ENABLED" \ -e SPW_FLOOD_HAZARD_MAPSERVER_URL="$SPW_FLOOD_HAZARD_MAPSERVER_URL" \ -e URBIS_ENABLED="$URBIS_ENABLED" \ -e URBIS_WFS_URL="$URBIS_WFS_URL" \ -e OFFICIAL_VECTOR_MIN_SIDE_M="$OFFICIAL_VECTOR_MIN_SIDE_M" \ -e OFFICIAL_VECTOR_MAX_SIDE_M="$OFFICIAL_VECTOR_MAX_SIDE_M" \ -e OFFICIAL_VECTOR_PAGE_SIZE="$OFFICIAL_VECTOR_PAGE_SIZE" \ -e OFFICIAL_VECTOR_MAX_PAGES="$OFFICIAL_VECTOR_MAX_PAGES" \ -e OFFICIAL_VECTOR_MAX_FEATURES="$OFFICIAL_VECTOR_MAX_FEATURES" \ -e OFFICIAL_VECTOR_TIMEOUT_SECONDS="$OFFICIAL_VECTOR_TIMEOUT_SECONDS" \ -e OFFICIAL_VECTOR_MAX_RESPONSE_MB="$OFFICIAL_VECTOR_MAX_RESPONSE_MB" \ -e OFFICIAL_VECTOR_MAX_TOTAL_RESPONSE_MB="$OFFICIAL_VECTOR_MAX_TOTAL_RESPONSE_MB" \ -e OFFICIAL_VECTOR_CACHE_TTL_HOURS="$OFFICIAL_VECTOR_CACHE_TTL_HOURS" \ -e DHMV_ENABLED="$DHMV_ENABLED" \ -e DHMV_WCS_URL="$DHMV_WCS_URL" \ -e DHMV_RESOLUTION_M="$DHMV_RESOLUTION_M" \ -e DHMV_MIN_SIDE_M="$DHMV_MIN_SIDE_M" \ -e DHMV_MAX_SIDE_M="$DHMV_MAX_SIDE_M" \ -e DHMV_MAX_PIXELS="$DHMV_MAX_PIXELS" \ -e DHMV_TIMEOUT_SECONDS="$DHMV_TIMEOUT_SECONDS" \ -e DHMV_MAX_RESPONSE_MB="$DHMV_MAX_RESPONSE_MB" \ -e FLOOD_HAZARD_ENABLED="$FLOOD_HAZARD_ENABLED" \ -e FLOOD_HAZARD_WCS_URL="$FLOOD_HAZARD_WCS_URL" \ -e FLOOD_HAZARD_RESOLUTION_M="$FLOOD_HAZARD_RESOLUTION_M" \ -e FLOOD_HAZARD_MIN_SIDE_M="$FLOOD_HAZARD_MIN_SIDE_M" \ -e FLOOD_HAZARD_MAX_SIDE_M="$FLOOD_HAZARD_MAX_SIDE_M" \ -e FLOOD_HAZARD_MAX_PIXELS="$FLOOD_HAZARD_MAX_PIXELS" \ -e FLOOD_HAZARD_TIMEOUT_SECONDS="$FLOOD_HAZARD_TIMEOUT_SECONDS" \ -e FLOOD_HAZARD_MAX_RESPONSE_MB="$FLOOD_HAZARD_MAX_RESPONSE_MB" \ -e BATHYMETRY_PROFILES_ENABLED="$BATHYMETRY_PROFILES_ENABLED" \ -e BATHYMETRY_PROFILES_LAYER_URL="$BATHYMETRY_PROFILES_LAYER_URL" \ -e BATHYMETRY_WATERCOURSE_LAYER_URL="$BATHYMETRY_WATERCOURSE_LAYER_URL" \ -e BATHYMETRY_PROFILES_PAGE_SIZE="$BATHYMETRY_PROFILES_PAGE_SIZE" \ -e BATHYMETRY_PROFILES_MAX_FEATURES="$BATHYMETRY_PROFILES_MAX_FEATURES" \ -e BATHYMETRY_PROFILES_TIMEOUT_SECONDS="$BATHYMETRY_PROFILES_TIMEOUT_SECONDS" \ -e BATHYMETRY_PROFILES_MAX_RESPONSE_MB="$BATHYMETRY_PROFILES_MAX_RESPONSE_MB" \ -e MDK_BATHYMETRY_PROBE_ENABLED="$MDK_BATHYMETRY_PROBE_ENABLED" \ -e MDK_BATHYMETRY_WCS_URL="$MDK_BATHYMETRY_WCS_URL" \ -e MDK_BATHYMETRY_PROBE_TIMEOUT_SECONDS="$MDK_BATHYMETRY_PROBE_TIMEOUT_SECONDS" \ -e MDK_BATHYMETRY_PROBE_MAX_RESPONSE_MB="$MDK_BATHYMETRY_PROBE_MAX_RESPONSE_MB" \ -e MDK_BATHYMETRY_ACQUISITION_ENABLED="$MDK_BATHYMETRY_ACQUISITION_ENABLED" \ -e MDK_BATHYMETRY_COVERAGE_ID="$MDK_BATHYMETRY_COVERAGE_ID" \ -e MDK_BATHYMETRY_REQUEST_CRS="$MDK_BATHYMETRY_REQUEST_CRS" \ -e MDK_BATHYMETRY_MAX_BBOX_DEG2="$MDK_BATHYMETRY_MAX_BBOX_DEG2" \ -e MDK_BATHYMETRY_ACQUISITION_TIMEOUT_SECONDS="$MDK_BATHYMETRY_ACQUISITION_TIMEOUT_SECONDS" \ -e MDK_BATHYMETRY_ACQUISITION_MAX_RESPONSE_MB="$MDK_BATHYMETRY_ACQUISITION_MAX_RESPONSE_MB" \ -e THEMATIC_RASTER_ENABLED="$THEMATIC_RASTER_ENABLED" \ -e THEMATIC_RASTER_WCS_URL="$THEMATIC_RASTER_WCS_URL" \ -e THEMATIC_RASTER_MIN_SIDE_M="$THEMATIC_RASTER_MIN_SIDE_M" \ -e THEMATIC_RASTER_MAX_SIDE_M="$THEMATIC_RASTER_MAX_SIDE_M" \ -e THEMATIC_RASTER_MAX_PIXELS="$THEMATIC_RASTER_MAX_PIXELS" \ -e THEMATIC_RASTER_TIMEOUT_SECONDS="$THEMATIC_RASTER_TIMEOUT_SECONDS" \ -e THEMATIC_RASTER_MAX_RESPONSE_MB="$THEMATIC_RASTER_MAX_RESPONSE_MB" \ -e WALOUS_ENABLED="$WALOUS_ENABLED" \ -e WALOUS_SOURCE_DIR="$WALOUS_SOURCE_DIR" \ -e WALOUS_ANALYSIS_RESOLUTION_M="$WALOUS_ANALYSIS_RESOLUTION_M" \ -e WALOUS_MAX_SIDE_M="$WALOUS_MAX_SIDE_M" \ -e WALOUS_MAX_PIXELS="$WALOUS_MAX_PIXELS" \ -e SPW_TERRAIN_ENABLED="$SPW_TERRAIN_ENABLED" \ -e SPW_TERRAIN_SOURCE_DIR="$SPW_TERRAIN_SOURCE_DIR" \ -e SPW_TERRAIN_ANALYSIS_RESOLUTION_M="$SPW_TERRAIN_ANALYSIS_RESOLUTION_M" \ -e SPW_TERRAIN_MAX_SIDE_M="$SPW_TERRAIN_MAX_SIDE_M" \ -e SPW_TERRAIN_MAX_PIXELS="$SPW_TERRAIN_MAX_PIXELS" \ -e YOLO_ENABLED="$YOLO_ENABLED" \ -e YOLO_MODELS_DIR="$YOLO_MODELS_DIR" \ -e YOLO_MODEL_PATH="$YOLO_MODEL_PATH" \ -e YOLO_MODEL_ID="$YOLO_MODEL_ID" \ -e YOLO_MODEL_DISPLAY_NAME="$YOLO_MODEL_DISPLAY_NAME" \ -e YOLO_MODEL_VERSION="$YOLO_MODEL_VERSION" \ -e YOLO_MODEL_CLASSES="$YOLO_MODEL_CLASSES" \ -e YOLO_ENFORCE_VALIDATION_SCOPE="$YOLO_ENFORCE_VALIDATION_SCOPE" \ -e YOLO_VALIDATION_SCOPE_MANIFEST_PATH="$YOLO_VALIDATION_SCOPE_MANIFEST_PATH" \ -e YOLO_VALIDATION_SCOPE_MANIFEST_SHA256="$YOLO_VALIDATION_SCOPE_MANIFEST_SHA256" \ -e YOLO_VALIDATED_AREA_NAMES="$YOLO_VALIDATED_AREA_NAMES" \ -e YOLO_CONFIG_DIR="$YOLO_CONFIG_DIR" \ -e YOLO_DEVICE="$YOLO_DEVICE" \ -e YOLO_REQUIRE_CUDA="$YOLO_REQUIRE_CUDA" \ -e YOLO_IMAGE_SIZE="$YOLO_IMAGE_SIZE" \ -e YOLO_MAX_TILES="$YOLO_MAX_TILES" \ -e YOLO_MAX_DETECTIONS="$YOLO_MAX_DETECTIONS" \ -e YOLO_DUPLICATE_IOU_THRESHOLD="$YOLO_DUPLICATE_IOU_THRESHOLD" \ -e YOLO_BATCH_SIZE="$YOLO_BATCH_SIZE" \ -e YOLO_SEG_ENABLED="$YOLO_SEG_ENABLED" \ -e YOLO_SEG_MODEL_PATH="$YOLO_SEG_MODEL_PATH" \ -e YOLO_SEG_MODEL_ID="$YOLO_SEG_MODEL_ID" \ -e YOLO_SEG_MODEL_DISPLAY_NAME="$YOLO_SEG_MODEL_DISPLAY_NAME" \ -e YOLO_SEG_MODEL_VERSION="$YOLO_SEG_MODEL_VERSION" \ -e SAM_ENABLED="$SAM_ENABLED" \ -e SAM_MODEL_PATH="$SAM_MODEL_PATH" \ -e SAM_MODEL_ID="$SAM_MODEL_ID" \ -e SAM_MODEL_DISPLAY_NAME="$SAM_MODEL_DISPLAY_NAME" \ -e SAM_MODEL_VERSION="$SAM_MODEL_VERSION" \ -e SEGMENTATION_MAX_MASKS_PER_TILE="$SEGMENTATION_MAX_MASKS_PER_TILE" \ -e SEGMENTATION_DUPLICATE_IOU_THRESHOLD="$SEGMENTATION_DUPLICATE_IOU_THRESHOLD" \ -e OLLAMA_ENABLED="$OLLAMA_ENABLED" \ -e OLLAMA_BASE_URL="$OLLAMA_BASE_URL" \ -e OLLAMA_DEFAULT_MODEL="$OLLAMA_DEFAULT_MODEL" \ -e OLLAMA_TIMEOUT_SECONDS="$OLLAMA_TIMEOUT_SECONDS" \ -e OLLAMA_MAX_OUTPUT_TOKENS="$OLLAMA_MAX_OUTPUT_TOKENS" \ -e OLLAMA_CONTEXT_TOKENS="$OLLAMA_CONTEXT_TOKENS" \ -v "${GEOINTEL_POSTGIS_DATA_PATH}:/var/lib/postgresql/data" \ -v "${GEOINTEL_STORAGE_PATH}:/app/storage" \ -v "${GEOINTEL_MODELS_PATH}:/app/models" \ -v "${GEOINTEL_BACKUPS_PATH}:/app/backups:ro" \ "$GEOINTEL_IMAGE" docker ps --filter name=geointel