Configuration Reference
The operator-facing environment variables for GeoLens, their defaults, and descriptions. Unless noted otherwise, set these in the .env file at the project root. A few quota and policy settings (flagged below) are managed only from the admin UI and are not read from .env.
Many of the remaining settings are admin-overridable: the .env value is only the default, and a value saved from the admin UI takes precedence for as long as it is stored. That applies to REGISTRATION_ENABLED, EMAIL_VERIFICATION_REQUIRED, PASSWORD_LOGIN_ENABLED, LOGIN_RATE_LIMIT, ALLOWED_EMAIL_DOMAINS (the last four from 1.24), ACCESS_TOKEN_EXPIRE_MINUTES, REFRESH_TOKEN_EXPIRE_DAYS, LOG_LEVEL, LOG_JSON, CORS_ALLOWED_ORIGINS, UPLOAD_MAX_SIZE_MB, UPLOAD_ALLOWED_EXTENSIONS, TILE_CACHE_TTL, PUBLIC_APP_URL, PUBLIC_API_URL, LANDING_FIRST, PRIVACY_URL, BANNER_ENABLED, BANNER_TEXT, BANNER_COLOR, and the AI rows marked below. If an .env edit appears to have no effect, check the matching admin tab — or set ENV_ONLY_CONFIG=true to lock every admin-overridable setting to its environment value. LOG_JSON is an exception: both the log format and the security posture below are read from the environment value when the process starts, so saving it in the admin UI does not change either one.
Database
Section titled “Database”| Variable | Default | Required | Description |
|---|---|---|---|
POSTGRES_DB | geolens | Yes | PostgreSQL database name |
POSTGRES_USER | geolens | Yes | PostgreSQL superuser username |
POSTGRES_PASSWORD | None (required) | Yes | PostgreSQL superuser password. Generate with openssl rand -base64 24. |
POSTGRES_HOST | localhost | No | Database hostname (application default localhost). Docker Compose sets this to db, the database service name. |
POSTGRES_PORT | 5432 | No | Database port (internal). The host-mapped port is configured separately. |
Authentication
Section titled “Authentication”| Variable | Default | Required | Description |
|---|---|---|---|
JWT_SECRET_KEY | None (required) | Yes | Secret key for signing JWT tokens. Generate with openssl rand -hex 32. Must be at least 32 chars; the boot validator also rejects six well-known public placeholders (change-me, secret, etc.). |
SECRET_ENCRYPTION_KEY | derived from JWT_SECRET_KEY | No | Fernet key that encrypts the secrets GeoLens stores in the database: SSO client secrets. The installer generates one for a new .env, so back it up with .env: without it those stored secrets cannot be read and each provider’s credentials must be entered again. When unset, the key is derived from JWT_SECRET_KEY, and rotating JWT_SECRET_KEY then makes every stored SSO secret unreadable. Must be url-safe base64 of 32 random bytes, which openssl rand -hex 32 does not produce; generate one with openssl rand -base64 32 | tr '+/' '-_'. The API refuses to start on a malformed value, a known public value, or the same value as JWT_SECRET_KEY. |
SECRET_ENCRYPTION_KEY_PREVIOUS | None | No | The old SECRET_ENCRYPTION_KEY, kept readable while you rotate. Set it only next to a new SECRET_ENCRYPTION_KEY, run backend/scripts/rotate_secrets.py, then remove it and restart. The steps are in section 11 of the RUNBOOK. |
JWT_ALGORITHM | HS256 | No | JWT signing algorithm |
ACCESS_TOKEN_EXPIRE_MINUTES | 15 | No | JWT access-token lifetime in minutes |
REFRESH_TOKEN_EXPIRE_DAYS | 7 | No | JWT refresh-token lifetime in days |
REFRESH_ROTATION_GRACE_SECONDS | 30 | No | How long a just-rotated refresh token stays usable, so concurrent refreshes from several tabs do not sign out the loser of the rotation race. 0 makes every refresh token strictly single-use. |
GEOLENS_ADMIN_USERNAME | None (required) | Yes | Username for the automatically created admin account |
GEOLENS_ADMIN_PASSWORD | None (required) | Yes | Password for the initial admin account. At most 72 bytes when encoded as UTF-8, because the password hash accepts no more; a longer value stops the API at boot. openssl rand -base64 16 makes a suitable one. |
REGISTRATION_ENABLED | false | No | Whether self-registration is enabled. When false, only admins can create users. |
EMAIL_VERIFICATION_REQUIRED | true | No | Added in 1.24.0. Whether a self-registered account activates by verifying its email address. Needs a configured SMTP channel; when false, or with no SMTP host, an admin approves each sign-up. Only relevant when REGISTRATION_ENABLED=true. Admin-overridable. |
PASSWORD_LOGIN_ENABLED | true | No | Added in 1.24.0. Whether non-admin users can sign in with a password. Admins always can. Admin-overridable. |
LOGIN_RATE_LIMIT | 5 | No | Added in 1.24.0. Password sign-in attempts allowed per minute from one IP address. An integer from 1 to 1000. Since 1.25.0 sign-in is also limited per account: 10 failures in an hour answer 429 login_throttled to browsers the account does not know. See Sign-in throttling. Admin-overridable. |
ALLOWED_EMAIL_DOMAINS | empty | No | Added in 1.24.0. Comma-separated email domains allowed to sign up and sign in, such as example.com,*.example.org. A *. prefix matches subdomains only. Empty allows every domain. Admin-overridable. |
LANDING_FIRST | false | No | Redirect unauthenticated visits to / to the login page instead of the catalog (admin-overridable, “Login-as-Landing Page”). Useful for demo or lead-capture front doors. |
PASSWORD_MIN_LENGTH | 12 | No | Minimum password length, enforced at every password entry point (register, change-password, admin create, SAML->local conversion). |
PASSWORD_REQUIRE_CLASSES | 3 | No | Number of character classes required out of 4 (lowercase, uppercase, digit, symbol). Accepts 1 to 4. |
Upload settings
Section titled “Upload settings”| Variable | Default | Required | Description |
|---|---|---|---|
UPLOAD_MAX_SIZE_MB | 500 | No | Maximum upload file size in megabytes |
UPLOAD_STAGING_DIR | /app/staging | No (Compose-pinned) | Directory for temporary file storage during ingestion/export. Both Compose stacks pin this to the literal /app/staging on the api and worker services, and titiler resolves the same path from the built-in default with no env passthrough at all. All three share the upload_staging volume at that fixed mount target, so setting it in .env has no effect. Override it only for a bare-metal/non-Compose run. Must be writable by the API runtime user (uid:gid 1001:1001). |
UPLOAD_ALLOWED_EXTENSIONS | .zip,.gpkg,.geojson,.json,.csv,.tif,.tiff,.xlsx,.xls,.parquet,.fgb,.kml,.kmz,.3tz,.laz | No | Comma-separated list of allowed file extensions for upload. Note: an admin-saved override in Admin → Storage takes precedence over this default — existing deployments that customized the list must add newer extensions (such as .parquet, .fgb, .kml, .kmz, .3tz, .laz) there |
MAX_TILESET_UNPACKED_MB | 20480 | No | Largest unpacked size, in megabytes, of an uploaded 3D Tiles tileset. Checked against the archive’s directory before anything is unpacked. See 3D Tiles and point clouds |
MAX_TILESET_ENTRIES | 100000 | No | Most files an uploaded 3D Tiles archive may contain |
PRESIGNED_MULTIPART_THRESHOLD_MB | 100 | No | Files larger than this (MB) use multipart presigned S3 URLs. Only applies when STORAGE_PROVIDER=s3. |
INGEST_HTTP_TIMEOUT_SECONDS | 300 | No | GDAL_HTTP_TIMEOUT passed to ogr2ogr during remote-fetched ingest. Raise for very large datasets; lower for fail-fast ingest debugging. |
URL_IMPORT_FETCH_MAX_SECONDS | 1800 | No | How long one File URL import may spend downloading, in seconds. Accepts 30 to 86400; a value outside that range is refused at startup. The download runs on the worker with no proxy or client deadline over it, so this is the only ceiling. |
MAX_STORAGE_BYTES_PER_USER | 0 | Admin UI only | Managed on the admin Storage tab — not read from .env. Per-user storage quota in bytes; 0 = unlimited. The value shown is the built-in default. |
MAX_DATASETS_PER_USER | 0 | Admin UI only | Managed on the admin Storage tab — not read from .env. Per-user dataset-count quota; 0 = unlimited. The value shown is the built-in default. |
UPLOAD_STAGING_DIR writability requirement
Section titled “UPLOAD_STAGING_DIR writability requirement”GeoLens validates staging writability at startup and before export execution. Both UPLOAD_STAGING_DIR and ${UPLOAD_STAGING_DIR}/exports must allow write access for the API runtime user.
Quick validation command. It runs as uid 1001 deliberately: the api container starts as root and drops to 1001:1001 inside its own entrypoint, so an unqualified docker compose exec would test root’s access rather than the runtime user’s and pass on the very misconfiguration you are looking for.
docker compose exec -u 1001:1001 api sh -lc '\ dir=${UPLOAD_STAGING_DIR:-/app/staging}; \ mkdir -p "$dir/exports" && \ touch "$dir/.geolens-write-test" "$dir/exports/.geolens-write-test" && \ rm -f "$dir/.geolens-write-test" "$dir/exports/.geolens-write-test"'The api entrypoint already chowns the staging directory to 1001:1001 on every boot while it is still root, and ignores any error from that chown; its own boot probe then runs as root, which passes on exactly the root-writable, appuser-unwritable state the command above is designed to catch. So a failing probe on a running container means the boot-time chown did not take effect — on an NFS root-squash mount, for instance — and restarting will not change that. Fix ownership on the mounted path directly with docker compose exec -u 0 api chown -R 1001:1001 /app/staging, then restart the API container. Under Compose, pointing UPLOAD_STAGING_DIR somewhere else is not an option: the path is pinned (see the table above).
Job queue
Section titled “Job queue”| Variable | Default | Required | Description |
|---|---|---|---|
PROCRASTINATE_SCHEMA | catalog | No | PostgreSQL schema for the Procrastinate job queue tables |
INGEST_JOBS_RETENTION_DAYS | 30 | No | Days to retain finished ingest job records before pruning; 0 disables pruning. |
PENDING_JOB_TIMEOUT_SECONDS | 3600 | No | How long an unfinished upload job stays alive. A job left pending this long is abandoned, and every presigned upload URL issued for it expires at the same deadline. Accepts 61 to 604800. Past 86400 (24 hours), raise the bucket’s abandoned multipart-part cleanup to match, or parts of uploads still in progress get aborted; the RUNBOOK covers this under “Abandoned multipart uploads”. |
STAGING_ORPHAN_MIN_AGE_SECONDS | 86400 | No | How old a staged upload must be before the reconciliation sweep deletes it for having no ingest job that still needs it. A local file also waits this long after every job naming it has ended. Minimum 3600. |
ANALYSIS_MATERIALIZE_TIMEOUT_SECONDS | 300 | No | Statement timeout, in seconds, for the query that builds a buffer, dissolve, clip or centroid output. Raise it when a large dataset fails with “The analysis exceeded its 300s processing time limit”, and consider raising WORKER_CONCURRENCY with it, because a longer budget holds a worker slot longer. 0 is refused at startup. |
ANALYSIS_REGISTRATION_TIMEOUT_SECONDS | 600 | No | Statement timeout, in seconds, for the registration step that follows, which scans the output for its count, extent and sample values. Raise it together with the materialize timeout. |
ANALYSIS_MATERIALIZE_WORK_MEM_MB | 64 | No | work_mem budget, in MB, for the materialize query, divided across WORKER_CONCURRENCY slots. PostgreSQL allocates it, so size it against DB_MEM_LIMIT and lower it if you shrink that or run more than one worker service. 0 issues no override and analysis runs on your cluster’s own work_mem. The application refuses to start if a slot’s share falls under PostgreSQL’s 64 kB minimum. |
The bundled Compose files don’t pass PENDING_JOB_TIMEOUT_SECONDS,
STAGING_ORPHAN_MIN_AGE_SECONDS or the three ANALYSIS_* variables through to
the containers, so under Compose they keep these defaults and a value in .env
has no effect. On Kubernetes or another runtime, set them in the api and
worker container environment.
Public URLs
Section titled “Public URLs”| Variable | Default | Required | Description |
|---|---|---|---|
PUBLIC_APP_URL | http://localhost:8080 | No | Browser-facing app URL. Used for share links, OAuth redirect URIs and the absolute URLs in per-map link previews (set it so a public map shared in chat tools shows its own title and image). |
PUBLIC_API_URL | http://localhost:8080/api | No | Externally-reachable API base URL. Used in OGC self/collection/next link hrefs. That default is the Docker Compose one; outside Compose the application falls back to the request origin, then to http://localhost:8000. Set it explicitly on any deployment behind a proxy or a real hostname — OAuth flows refuse the request-derived fallback outright. |
PUBLIC_BASE_URL | None | No | Deprecated. Will be removed in a future release. Legacy alias for PUBLIC_API_URL. Use PUBLIC_API_URL instead. The application logs a deprecation warning at startup when this is set. |
PRIVACY_URL | None | No | Privacy-policy link shown on the login and register pages. Unset, no link is shown, so an instance never points at another operator’s policy. An absolute http(s) URL without embedded credentials; an invalid value stops the API at boot. Admin-overridable. |
DCAT_CONTACT_EMAIL | None | No | Organization mailbox used as the DCAT-US contact point when a published dataset has no usable record-level contact. Use a role mailbox, not a person’s address. DCAT-US exports with no resolvable contact answer 503. |
Site banner
Section titled “Site banner”| Variable | Default | Required | Description |
|---|---|---|---|
BANNER_ENABLED | false | No | Show the site-wide announcement banner. It appears when this is true and BANNER_TEXT is not empty. Admins can also manage it in Settings > General; with ENV_ONLY_CONFIG=true these variables are the only way to turn it on. Admin-overridable. |
BANNER_TEXT | empty | No | Banner text. Admin-overridable. |
BANNER_COLOR | warning | No | Banner color, one of warning, info, success, destructive. Admin-overridable. |
| Variable | Default | Required | Description |
|---|---|---|---|
CORS_ALLOWED_ORIGINS | "" | No | Comma-separated list of exact origins allowed to make credentialed cross-origin API requests (a literal * is rejected). Required when the frontend is served from a different domain than the API. Anonymous reads of the standards and tile routes are unaffected; see Settings -> Network. |
Tile serving & CDN
Section titled “Tile serving & CDN”| Variable | Default | Required | Description |
|---|---|---|---|
TILE_CACHE_TTL | 300 | No | Tile cache TTL in seconds |
TILE_SIGNING_SECRET | None (falls back to JWT_SECRET_KEY) | No | Secret for signing tile request URLs. Set separately when you want to rotate tile secrets without invalidating JWT tokens. |
CDN_BASE_URL | None | No | CDN origin URL for tile delivery. When set, the frontend requests tiles from this URL instead of the API. |
TITILER_BASE_URL | http://titiler:8000 | No | Internal Titiler origin the API’s raster proxy calls. Override it outside Compose or when the raster service has another name. |
REMOTE_RASTER_RELAY_BASE_URL | http://api:8000 | No | API origin Titiler uses to read remote (STAC by-reference) rasters through the API relay. Titiler never fetches a remote raster directly, so it must be able to reach this address. The default is right under either Compose file. |
TITILER_WORKERS | 1 | No | Titiler uvicorn worker processes. Tile rendering is CPU-bound, so one worker renders one tile at a time and concurrent cache misses queue; this is the first setting to raise when rasters feel slow. Raise TITILER_MEM_LIMIT with it, because the GDAL block cache is per process. |
TITILER_MALLOC_ARENA_MAX | 2 | No | glibc malloc arena cap inside the Titiler container. It slows Titiler’s memory growth under concurrent reads. Raise it only if malloc contention shows up on a many-core host. |
Logging
Section titled “Logging”| Variable | Default | Required | Description |
|---|---|---|---|
LOG_JSON | false | No | Output logs in structured JSON format. Recommended for production. Also acts as the legacy security-posture switch: when ENVIRONMENT is unset (the default), LOG_JSON=true additionally hides /api/docs and /api/redoc and sets the Secure flag on the OAuth session cookie. Set ENVIRONMENT explicitly to decouple the two — see Deployment / security posture. |
LOG_LEVEL | INFO | No | Log level. Options: DEBUG, INFO, WARNING, ERROR, CRITICAL. |
Storage provider
Section titled “Storage provider”| Variable | Default | Required | Description |
|---|---|---|---|
STORAGE_PROVIDER | local | No | Storage backend for uploaded files. Options: local, s3, azure. |
S3_ENDPOINT | None | No | S3-compatible endpoint URL. Leave unset for AWS S3. For MinIO: http://minio:9000. |
S3_BUCKET | None | When s3 | S3 bucket name. |
S3_ACCESS_KEY_ID | None | When s3, without ambient credentials | S3 access key ID. Leave both keys unset when the pod or task supplies ambient AWS credentials (IRSA, EKS Pod Identity, or ECS container credentials) — see Deploy on EKS. A half-configured pair is always rejected at boot. |
S3_SECRET_ACCESS_KEY | None | When s3, without ambient credentials | S3 secret access key. |
S3_REGION | us-east-1 | No | S3 region. |
S3_PUBLIC_ENDPOINT | None | No | Host that presigned URLs (uploads, STAC asset links, COG download redirects) are signed against, when S3_ENDPOINT is a container hostname or private address. The signature covers the Host header, so the host must reach your storage unchanged; a reverse proxy in front of it has to forward the Host it received. The API still reads and writes through S3_ENDPOINT. A value without a scheme takes https:// unless S3_ALLOW_HTTP is true. |
S3_PRESIGNED_DOWNLOADS | false | No | Redirect COG downloads to a presigned S3 URL instead of streaming them. Leave it off unless every client can reach the storage endpoint (S3_PUBLIC_ENDPOINT when set, otherwise S3_ENDPOINT) and follows redirects. |
TITILER_S3_ACCESS_KEY_ID / TITILER_S3_SECRET_ACCESS_KEY | S3_ACCESS_KEY_ID / S3_SECRET_ACCESS_KEY | No | A dedicated read-only principal for Titiler, limited to rasters/* and tenants/*/rasters/* in the bucket. Set both or neither. When unset, Compose gives Titiler the application’s S3_* keys. |
S3_ALLOW_HTTP | false | No | Allow HTTP (non-TLS) connections to S3 endpoint. Enable for local MinIO. |
S3_ADDRESSING_STYLE | auto | No | S3 addressing style. Options: auto, path, virtual. Use path for MinIO. |
MINIO_ROOT_USER / MINIO_ROOT_PASSWORD | None | With the cloud-dev profile | Root credentials for the bundled MinIO. Only the optional cloud-dev profile reads them, and MinIO refuses to start without both, so there is no default. The installer generates them. Generate your own with openssl rand -base64 24, and keep them in step with S3_ACCESS_KEY_ID / S3_SECRET_ACCESS_KEY when you use the bundled MinIO as storage. |
Azure Blob Storage
Section titled “Azure Blob Storage”Set STORAGE_PROVIDER=azure and configure a container plus one authentication path: either a connection string, or an account URL with an access key (leave the key unset to use managed identity / Entra ID).
| Variable | Default | Required | Description |
|---|---|---|---|
AZURE_STORAGE_CONTAINER | None | When azure | Blob container name (e.g. geolens-prod). |
AZURE_STORAGE_CONNECTION_STRING | None | One auth path | Full connection string (also used for Azurite). Provide this or AZURE_STORAGE_ACCOUNT_URL. |
AZURE_STORAGE_ACCOUNT_URL | None | One auth path | Account blob endpoint, https://<account>.blob.core.windows.net. |
AZURE_STORAGE_ACCOUNT_KEY | None | With account URL + key auth | Storage account access key. Leave unset (with AZURE_STORAGE_ACCOUNT_URL set) to authenticate via managed identity / Entra ID. |
Managed database / SSL
Section titled “Managed database / SSL”| Variable | Default | Required | Description |
|---|---|---|---|
DATABASE_URL_OVERRIDE | None | No | Full PostgreSQL connection URL for managed databases (RDS, Cloud SQL). Overrides individual POSTGRES_* variables. |
DATABASE_SSL_MODE | prefer | No | Database SSL mode. Options: disable, prefer, require, verify-full. |
DATABASE_SSL_CA_CERT | None | When verify-full | Path to CA certificate file for database SSL verification. |
DATABASE_POOL_PRE_PING | true | No | Enable connection pool pre-ping to detect broken connections before use. Adds slight latency per checkout. Set to false only if you need to disable this for a specific environment. |
DB_USE_EXTERNAL_POOLER | false | No | Enable external connection pooler mode (PgBouncer, RDS Proxy). Disables prepared statements. |
SANDBOX_QUERY_SLOTS | (empty) | No | Since 1.19.1. Concurrent SQL sandbox queries (POST /api/query/ and the AI chat’s data queries) each API worker admits when DB_USE_EXTERNAL_POOLER is true; ignored otherwise. Empty means 4 divided by UVICORN_WORKERS, never below 1, because every worker draws on the one pooler. Set it when more than one API replica shares the pooler: a third of the pooler’s connections, divided by replicas times workers, so ordinary requests keep the rest; the AI chat can hold two connections per slot. Must be at least 1. |
Least-privilege runtime login
Section titled “Least-privilege runtime login”By default the API and worker connect as POSTGRES_USER. To run them as a dedicated login without superuser rights, set the role, its password, DATABASE_URL_OVERRIDE with that login, the migration role and URL, and GEOLENS_API_RUN_MIGRATIONS=false before the first docker compose up, or follow the RUNBOOK’s section on adopting the single-tenant runtime role on an existing install. Compose creates and reconciles the role on a fresh volume.
| Variable | Default | Required | Description |
|---|---|---|---|
GEOLENS_RUNTIME_DB_ROLE | None | No | Login name for the dedicated runtime role. It must differ from POSTGRES_USER. When set, the username in DATABASE_URL_OVERRIDE must match it. |
GEOLENS_RUNTIME_DB_PASSWORD | None | With the role | Password for that role: at least 32 characters, and different from the privileged password. openssl rand -hex 32 needs no URL encoding. |
GEOLENS_MIGRATION_DB_ROLE | POSTGRES_USER | No | The login that owns the objects migrations create, so reconciliation attaches future grants to the right owner. Set it when your migration URL uses a different user from the provider admin. |
GEOLENS_MIGRATION_DB_LOCAL | false | No | Set to true when a custom migration URL points at the bundled db service, so the role above reaches the local reconciler. Leave it false for managed or external URLs. |
MIGRATION_DATABASE_URL_OVERRIDE | DATABASE_URL_OVERRIDE | No | Privileged connection URL used only by the migrate service. Keep it distinct from DATABASE_URL_OVERRIDE in least-privilege deployments. |
GEOLENS_API_RUN_MIGRATIONS | true | No | Whether the API image runs Alembic at startup as a safety net. Set it to false when the ordered migrate service runs with a separate privileged credential, so the runtime login never attempts schema changes. The official Compose files forward it and refuse a mismatch with the runtime role. |
TILE_DATABASE_URL_OVERRIDE | DATABASE_URL_OVERRIDE | No | Separate connection URL for the vector tile pool, with its own login. Optional on a single-organization install. |
Connection pool tuning
Section titled “Connection pool tuning”These variables control the SQLAlchemy connection pool. Ignored when DB_USE_EXTERNAL_POOLER is true.
| Variable | Default | Required | Description |
|---|---|---|---|
DB_POOL_SIZE | 10 | No | Maximum number of persistent connections in the pool. |
DB_MAX_OVERFLOW | 3 | No | Maximum number of additional connections beyond DB_POOL_SIZE. Tuned down from SQLAlchemy’s default to fit the connection budget below. |
DB_POOL_TIMEOUT | 30 | No | Seconds to wait for a connection from the pool before raising an error. |
DB_STATEMENT_TIMEOUT_SECONDS | 300 | No | Query deadline for the API process, in seconds; 0 disables it. Distinct from DB_POOL_TIMEOUT: that one bounds how long a request waits for a connection, this one bounds how long a statement runs once it has one, so one pathological plan can’t pin a pool slot indefinitely. Applies to every API transaction; the worker is deliberately excluded, since it legitimately runs a single statement for minutes while indexing or reprojecting a freshly ingested table. Raise it (or set it to 0) if a legitimate API workload needs longer than five minutes. |
DB_POOL_RECYCLE | 1800 | No | Seconds after which a connection is recycled (replaced). Prevents stale connections with managed databases. |
TILE_POOL_MIN_SIZE | 2 | No | Minimum connections in the dedicated asyncpg tile query pool. |
TILE_POOL_MAX_SIZE | 10 | No | Maximum connections in the dedicated asyncpg tile query pool. |
UVICORN_WORKERS | 2 (release stack); 1 (source-build stack) | No | Uvicorn worker processes for the API. The bundled production compose file sets 2. Each one opens its own SQLAlchemy, asyncpg tile, and Procrastinate pool — check the connection budget below before raising it. With two or more workers, every rate limit is counted per worker unless REDIS_URL is set (see Cache). Under DB_USE_EXTERNAL_POOLER the SQL sandbox slots each worker admits are 4 divided by this count unless SANDBOX_QUERY_SLOTS sets them. |
UVICORN_MAX_REQUESTS | 10000 | No | Recycle each API worker after this many requests (graceful exit + supervisor respawn), so slow memory growth can’t ride a worker into the container’s limit. Only applies with UVICORN_WORKERS ≥ 2 — with a single worker there is no supervisor to respawn it, so the prod stack omits the limit entirely rather than exiting the whole process. |
UVICORN_TIMEOUT_KEEP_ALIVE | 5 | No | Keep-alive timeout in seconds; matches the uvicorn upstream default. Tune up for clients with chatty long-poll patterns. |
UVICORN_TIMEOUT_GRACEFUL_SHUTDOWN | 30 | No | Max seconds uvicorn waits for in-flight requests to finish on shutdown before SIGKILL. The default covers slow GDAL ingest finalization. |
| Variable | Default | Required | Description |
|---|---|---|---|
REDIS_URL | None | No | Redis/Valkey connection URL for cross-instance caching and shared rate-limit counts. Importing, re-uploading or refreshing a token-protected service requires this credential store whatever the number of instances. Without one the API answers 503 credential_store_unavailable, and a run the API admitted fails if only the worker cannot reach it. Imports and refreshes that carry no token do not need it. Leave unset for in-memory caching and per-worker rate-limit counting, which suits one API worker that never handles a token-protected service. Rate limits use the store only for a redis:// or rediss:// URL; any other scheme logs rate_limit_storage_scheme_unsupported and counting stays per worker. Example: redis://valkey:6379/0. |
VALKEY_PASSWORD | None | No | Password for the bundled Valkey in docker-compose.prod.yml. When set, Valkey requires it and the healthcheck authenticates. Put the same value in REDIS_URL (redis://:<password>@valkey:6379/0) and recreate valkey, api and worker together; until REDIS_URL matches, the cache falls back to memory and token-protected imports answer 503. Letters, digits and . _ ~ - only; Valkey refuses to start on anything else. Generate one with openssl rand -hex 32. Unset, any container on the compose network can use the cache. |
Since 1.20.0, configuration validation reports cache and
credential_store separately. A healthy cache does not prove that the refresh credential store is
available; check both entries before running protected refreshes.
Container memory limits
Section titled “Container memory limits”Memory caps for the Compose services. Each takes a number and a unit, such as 512m or 2g. Size them together so their sum fits the host with room for the operating system.
| Variable | Default | Description |
|---|---|---|
DB_MEM_LIMIT | 2g | PostgreSQL |
API_MEM_LIMIT | 2g | FastAPI |
WORKER_MEM_LIMIT | 4g | Ingest and background worker |
TITILER_MEM_LIMIT | 1g | Titiler raster renderer |
FRONTEND_MEM_LIMIT | 128m | nginx frontend (release stack) |
VALKEY_MEM_LIMIT | 256m | Bundled Valkey (release stack, cache profile) |
VALKEY_MAXMEMORY | 192mb | Valkey’s own ceiling. Keep it under VALKEY_MEM_LIMIT so Valkey evicts the soonest-expiring keys (tiles, rate-limit windows) before credential handoffs, instead of being killed for memory. |
BACKUP_MEM_LIMIT is listed under Backup.
Backup
Section titled “Backup”These variables configure the default-on backup service. Off-site upload stays disabled until BACKUP_S3_ENABLED=true.
| Variable | Default | Required | Description |
|---|---|---|---|
BACKUP_SCHEDULE | 0 2 * * * | No | Cron expression for automated database backups. Default: daily at 2:00 AM UTC. |
BACKUP_RETENTION_DAILY | 7 | No | Days of daily backups to retain. Keeps the newest dump of each of this many most recent days that have a backup, so extra cycles on one day do not use up the window. Before 1.25.0 this counted dumps. Must be an integer of at least 1; the container refuses to start otherwise. |
BACKUP_RETENTION_WEEKLY | 4 | No | ISO weeks of weekly (Sunday) backups to retain, one per week. A weekly entry is a hard link to the daily dump (a copy where links fail). Before 1.25.0 this counted dumps. Must be an integer of at least 1; the container refuses to start otherwise. |
BACKUP_S3_ENABLED | false | No | Enable off-site backup upload to S3-compatible storage. Uses S3_* credentials. |
BACKUP_MAX_AGE_MINUTES | 1560 | No | Freshness threshold for the backup container’s healthcheck, in minutes. The service reports unhealthy when the last fully successful backup cycle is older than this. Once a cycle has uploaded offsite, /backups/.last-offsite-success must be as fresh, even if BACKUP_S3_ENABLED is later turned off; see Backups & Restore. Raise it to about 1.5x the interval for less-frequent BACKUP_SCHEDULE values. |
BACKUP_MEM_LIMIT | 512m | No | Memory cap for the backup container (the nightly pg_dump + tar peaks around 300 MB). Honoured by the release stack (docker-compose.prod.yml) only; the source-build stack sets no memory cap on the backup container. |
Outbound notifications
Section titled “Outbound notifications”Optional email (SMTP) and webhook alerts for signup, ingest, and health events. Everything defaults to off, so existing deployments are unaffected until NOTIFICATIONS_ENABLED=true and at least one channel is configured. See Backups & Restore and Infrastructure & Monitoring for the operational context.
| Variable | Default | Required | Description |
|---|---|---|---|
NOTIFICATIONS_ENABLED | false | No | Master toggle. When false, all notification sends are a no-op regardless of channel config. |
SMTP_HOST | None | No | SMTP server hostname. Configure together with SMTP_USERNAME, SMTP_PASSWORD, and SMTP_FROM_ADDRESS to enable the email channel. |
SMTP_PORT | 587 | No | SMTP server port. |
SMTP_USERNAME / SMTP_PASSWORD | None | No | SMTP credentials (secret; never rendered in logs). |
SMTP_FROM_ADDRESS | None | No | From address for outbound email. |
SMTP_USE_TLS | true | No | Use STARTTLS for the SMTP connection. |
NOTIFICATION_WEBHOOK_URL | None | No | Incoming-webhook endpoint (Slack, Teams, or custom) that receives JSON notifications. |
NOTIFICATION_WEBHOOK_SECRET | None | No | Optional HMAC signing secret for webhook payloads. |
NOTIFICATION_ADMIN_EMAIL | None | No | Recipient for event alerts. Falls back to SMTP_FROM_ADDRESS when unset. |
NOTIFY_ON_SIGNUP | false | No | Send an alert when a new account registers. |
NOTIFY_ON_INGEST_COMPLETE | false | No | Send an alert when an ingest finishes successfully. |
NOTIFY_ON_INGEST_FAILED | false | No | Send an alert when an ingest fails. |
NOTIFY_ON_HEALTH_ALERT | false | No | Send an alert when the health check reports a degraded status (cooldown-deduplicated). |
These are environment-only settings (not stored in the admin settings database) because the channel credentials are secrets.
Worker
Section titled “Worker”| Variable | Default | Required | Description |
|---|---|---|---|
WORKER_CONCURRENCY | 1 | No | Number of jobs the background worker processes concurrently. |
WORKER_QUEUES | priority,ingest,raster,download,ingest-auth-v2 | No | Comma-separated list of job queues the worker consumes, in priority order. Set it only to move work between workers, and start from the full default when you do. Dropping download leaves File URL imports enqueued with no consumer. ingest-auth-v2 is a drain queue with no producer in this release: it exists so a credentialed import queued by a 1.18.0 or 1.18.1 API still runs, and a worker that omits it logs worker_queue_missing_drain_queue at startup. |
WORKER_SHUTDOWN_TIMEOUT | 30 | No | Graceful shutdown timeout for the background worker in seconds |
Configuration lockdown
Section titled “Configuration lockdown”| Variable | Default | Required | Description |
|---|---|---|---|
ENV_ONLY_CONFIG | false | No | When true, all admin-overridable settings are locked to their environment values. The PersistentConfig DB layer is bypassed for reads and returns 403 on writes. Use for hardened production deployments where operators want to prevent runtime configuration changes via the admin UI. SSO providers are records rather than settings, so they stay manageable through the provider API in this mode, and in the admin UI from 1.24 (see OAuth/OIDC setup). |
Deployment / security posture
Section titled “Deployment / security posture”| Variable | Default | Required | Description |
|---|---|---|---|
ENVIRONMENT | (unset) | No | Deployment environment: development or production. When production, the API hides its docs (/api/docs and /api/redoc return 404) and sets the Secure flag on the OAuth session cookie. When unset, the posture falls back to LOG_JSON for backward compatibility. Set ENVIRONMENT=production on any public, TLS-terminated deployment. |
Release pinning
Section titled “Release pinning”The installer writes both variables. scripts/upgrade.sh reads and updates them.
| Variable | Default | Required | Description |
|---|---|---|---|
GEOLENS_VERSION | None (latest in the release stack) | No | Published image tag to run, as bare semver such as 1.2.3 with no leading v; Compose uses it verbatim as the tag. The installer sets it to the release it installed. |
COMPOSE_FILE | docker-compose.yml | No | Which Compose file a bare docker compose command uses. The installer writes docker-compose.prod.yml for a release install and docker-compose.yml for a source build. |
Deployment knobs (running outside Docker Compose)
Section titled “Deployment knobs (running outside Docker Compose)”These knobs matter when the frontend image fronts the API outside the bundled Compose stack (bare containers, Kubernetes, the community Helm chart). Under stock Docker Compose the defaults are correct and you can leave them unset.
| Variable | Default | Required | Description |
|---|---|---|---|
API_UPSTREAM | http://api:8000 | No | Where the frontend’s nginx proxies /api, raster tiles, and embeds. Set to the API Service’s fully qualified name on Kubernetes. Trailing slashes are stripped. |
NGINX_RESOLVER | first nameserver in /etc/resolv.conf, else 127.0.0.11 | No | DNS resolver nginx uses to resolve API_UPSTREAM. Bare IPv6 resolvers are bracketed automatically. |
CSP_SCRIPT_SRC_EXTRA | (empty) | No | Extra space-separated script-src sources for the frontend’s Content-Security-Policy. The default script-src is 'self' 'wasm-unsafe-eval'. Needed only when something injects a script at the edge; Cloudflare Web Analytics on a Cloudflare-proxied site needs the host https://static.cloudflareinsights.com, since the beacon loads from a versioned path (1.25.0 stopped allowing that host by default). Letters, digits and : / . _ * ' - only; any other character stops the frontend container at start. |
CLIENT_MAX_BODY_SIZE | 500m | No | Upload ceiling enforced by the frontend nginx. Invalid values fail fast at boot. Under Compose it is derived from UPLOAD_MAX_SIZE_MB in .env (${UPLOAD_MAX_SIZE_MB}m), and a CLIENT_MAX_BODY_SIZE line in .env is ignored. Set it directly only when you run the bare frontend image, and keep it at or above UPLOAD_MAX_SIZE_MB. A larger limit saved in the admin UI is still capped by this value, so raise UPLOAD_MAX_SIZE_MB in .env as well. |
TRUSTED_PROXY_CIDRS | (empty) | No | Comma/space-separated CIDRs of proxies trusted to set X-Forwarded-For/-Proto (e.g. Cloudflare’s published ranges). Required for correct client IPs and anonymous raster rate limiting behind a load balancer or CDN. Empty treats the direct peer as the client. |
For a non-AWS S3 backend, the API and worker derive AWS_S3_ENDPOINT, AWS_HTTPS, and AWS_VIRTUAL_HOSTING from the S3_* settings above automatically; you do not normally set them by hand.
AI & LLM
Section titled “AI & LLM”GeoLens supports two AI subsystems: inference (chat, map generation, metadata drafts) and embeddings (semantic search). They can use different providers.
API keys are set exclusively via environment variables. Provider and model can also be overridden at runtime from the admin Settings > AI tab. Base URL cannot: once OPENAI_API_KEY is set, the API refuses any saved base URL that does not resolve to the environment-approved endpoint for that purpose — OPENAI_BASE_URL for inference, and EMBEDDING_BASE_URL (falling back to OPENAI_BASE_URL) for embeddings, with OpenAI’s own endpoint standing in when neither is set. A database write therefore cannot redirect the environment-provided credential. Leaving the admin field blank is still accepted and means “inherit the environment endpoint”.
Outbound TLS from the AI providers and the OAuth login client checks the operating system trust store, not the certifi bundle (since 1.25.0). A private CA for a self-hosted OpenAI-compatible endpoint or an identity provider must be in the system store, or named by SSL_CERT_FILE.
Inference (chat / map generation)
Section titled “Inference (chat / map generation)”| Variable | Default | Required | Description |
|---|---|---|---|
ANTHROPIC_API_KEY | None | No | Anthropic API key. When set, Anthropic is the default inference provider. |
LLM_MODEL | claude-sonnet-5-5 | No | Default Anthropic model name (admin-overridable). On a cloud platform it must be that platform’s own model ID: the deployment name on Foundry, a model or inference-profile ID on Bedrock, the Vertex model ID on Vertex. Bedrock and Vertex have no default. An install that sets it keeps its value. |
LLM_MODEL_LIGHT | claude-haiku-5-5 on Anthropic’s API; LLM_MODEL on Foundry, Bedrock and Vertex | No | Lighter Claude model for SQL generation and AI metadata. Overrides the default on any platform. |
MAX_AI_TOKENS_PER_USER_PER_DAY | 0 | Admin UI only | Managed on the admin AI tab — not read from .env. Per-user daily AI token budget; 0 = unlimited. The value shown is the built-in default. |
OPENAI_API_KEY | None | No | OpenAI-compatible API key. Used for inference when Claude is not configured (no ANTHROPIC_API_KEY and no cloud platform), and always used for embeddings. |
OPENAI_MODEL | gpt-4o | No | Default OpenAI-compatible model name (admin-overridable). |
OPENAI_BASE_URL | None | No | Custom endpoint for OpenAI-compatible providers (Ollama, Groq, Together). Leave unset for default OpenAI. |
OPENAI_MODEL_LIGHT | OPENAI_MODEL | No | Lighter model for SQL generation and AI metadata on the OpenAI-compatible provider. Must be a model your provider serves; on Azure OpenAI it must match a deployment name. |
OPENAI_API_STYLE | chat_completions | No | Which API the OpenAI-compatible provider calls: chat_completions (most compatible servers and gateways) or responses. Some OpenAI models, such as gpt-6.1-sol on Azure OpenAI, call function tools only through the Responses API. With responses, tool calls, streamed chat and AI metadata drafts run on /v1/responses with nothing stored server side. A model that needs the Responses API but is set to Chat Completions fails with a message that names this setting. |
OPENAI_REASONING_EFFORT | unset (the model’s default) | No | reasoning_effort sent with OpenAI chat requests (none, low, medium, high, and so on). A model that does not recognise it gets the request without it. |
Claude on a cloud platform
Section titled “Claude on a cloud platform”By default Claude requests go to Anthropic’s API. ANTHROPIC_PLATFORM sends them to Microsoft Foundry, Amazon Bedrock or Google Vertex AI instead. These settings are environment-only: the admin UI cannot change where Claude credentials go. A cloud platform with its settings counts as configured without ANTHROPIC_API_KEY, and Claude clients do not follow redirects, so a gateway cannot bounce the key to another origin. The destinations are listed in EGRESS.md.
| Variable | Default | Required | Description |
|---|---|---|---|
ANTHROPIC_PLATFORM | anthropic | No | anthropic, foundry, bedrock or vertex. |
ANTHROPIC_BASE_URL | https://api.anthropic.com | No | With the anthropic platform, the base URL of a gateway or proxy that speaks the Messages API. Uses ANTHROPIC_API_KEY. |
ANTHROPIC_FOUNDRY_API_KEY | None | With foundry | Foundry API key. |
ANTHROPIC_FOUNDRY_RESOURCE | None | With foundry | The <resource> in https://<resource>.services.ai.azure.com/anthropic/. |
ANTHROPIC_BEDROCK_AWS_REGION | None | With bedrock | Bedrock region. |
ANTHROPIC_BEDROCK_API_KEY | None | One auth path | Bedrock API key. Alternatives: the ANTHROPIC_BEDROCK_AWS_ACCESS_KEY_ID and ANTHROPIC_BEDROCK_AWS_SECRET_ACCESS_KEY pair, or neither to use the AWS default credential chain (instance, task or IRSA role). |
ANTHROPIC_BEDROCK_AWS_ACCESS_KEY_ID / ANTHROPIC_BEDROCK_AWS_SECRET_ACCESS_KEY | None | One auth path | IAM key pair for Bedrock. These are separate from the S3_* storage credentials, because GeoLens exports those as AWS_* for GDAL. The default credential chain is refused while storage uses a static S3_ACCESS_KEY_ID. |
CLOUD_ML_REGION | None | With vertex | Vertex region, such as us-east5, or global. |
ANTHROPIC_VERTEX_PROJECT_ID | None | With vertex | Google Cloud project. |
GOOGLE_APPLICATION_CREDENTIALS | None | No | Service-account key file for Application Default Credentials, mounted into the api and worker containers. Not needed when the attached service account supplies them. |
Embeddings (semantic search)
Section titled “Embeddings (semantic search)”Embeddings always use the OpenAI-compatible API. Anthropic does not provide an embedding endpoint.
| Variable | Default | Required | Description |
|---|---|---|---|
EMBEDDING_MODEL | text-embedding-3-small | No | Embedding model name (admin-overridable). |
EMBEDDING_DIMS | 1536 | No | Expected vector dimensions (admin-overridable, auto-detectable from admin UI). |
EMBEDDING_BASE_URL | None | No | Separate endpoint for embeddings. Falls back to OPENAI_BASE_URL if unset. |
Common provider configurations
Section titled “Common provider configurations”Anthropic inference + OpenAI embeddings (recommended):
ANTHROPIC_API_KEY=sk-ant-...OPENAI_API_KEY=sk-...OpenAI for everything:
OPENAI_API_KEY=sk-...Anthropic inference + Ollama embeddings:
ANTHROPIC_API_KEY=sk-ant-...OPENAI_API_KEY=ollama # any non-empty valueEMBEDDING_BASE_URL=http://ollama:11434/v1EMBEDDING_MODEL=nomic-embed-textOllama for everything:
OPENAI_API_KEY=ollama # any non-empty valueOPENAI_BASE_URL=http://ollama:11434/v1OPENAI_MODEL=llama3EMBEDDING_MODEL=nomic-embed-textHost port mapping
Section titled “Host port mapping”These variables control which ports are exposed on the Docker host. They do not affect internal container communication.
| Variable | Default | Description |
|---|---|---|
DB_PORT | 5432 | Host port for PostgreSQL. Set to 5434 in .env.example to avoid conflicts. |
API_PORT | 8000 | Host port for the FastAPI backend. Set to 8001 in .env.example. |
FRONTEND_PORT | 8080 | Host port for the frontend. |
Internal service ports
Section titled “Internal service ports”These are the in-container ports; they are not operator knobs. The release stack (docker-compose.prod.yml) and the source-build stack (docker-compose.yml) differ only for the frontend:
| Service | Port | Protocol |
|---|---|---|
PostgreSQL (db) | 5432 | TCP |
FastAPI (api) | 8000 | HTTP |
Worker (worker) | 8001 | HTTP (health only) |
Titiler (titiler) | 8000 | HTTP |
Frontend (frontend), release stack | 8080 | HTTP (nginx) |
Frontend (frontend), source-build stack | 5173 | HTTP (Vite dev server) |
Docker volumes
Section titled “Docker volumes”| Volume | Purpose | Mount Point |
|---|---|---|
pgdata | PostgreSQL data persistence | /var/lib/postgresql on db (PostgreSQL 18+ takes a single mount here and keeps PGDATA at <major>/docker inside it; mounting the legacy /var/lib/postgresql/data path makes the entrypoint refuse to boot) |
upload_staging | Uploaded file staging area | /app/staging on api, worker, and titiler |
backup_data | Nightly pg_dump output plus a tar of upload_staging | /backups on backup |
frontend_cache | nginx raster cache (release stack only) | /var/cache/nginx on frontend |
minio_data / valkey_data | MinIO and Valkey data for the optional cloud-dev profile | /data on minio / valkey |
azurite_data | Azurite emulator data (source-build stack only) | /data on azurite |
Example .env file
Section titled “Example .env file”# DatabasePOSTGRES_DB=geolensPOSTGRES_USER=geolensPOSTGRES_PASSWORD=secure-db-password
# AuthJWT_SECRET_KEY=a1b2c3d4e5f6... # Use: openssl rand -hex 32GEOLENS_ADMIN_USERNAME=adminGEOLENS_ADMIN_PASSWORD=secure-admin-password
# AI (optional; omit to disable AI features)ANTHROPIC_API_KEY=sk-ant-... # Inference (chat, map generation)OPENAI_API_KEY=sk-... # Embeddings (semantic search)
# Ports (non-default to avoid conflicts)DB_PORT=5434API_PORT=8001FRONTEND_PORT=8080