Skip to content
getgeolens.com

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.

VariableDefaultRequiredDescription
POSTGRES_DBgeolensYesPostgreSQL database name
POSTGRES_USERgeolensYesPostgreSQL superuser username
POSTGRES_PASSWORDNone (required)YesPostgreSQL superuser password. Generate with openssl rand -base64 24.
POSTGRES_HOSTlocalhostNoDatabase hostname (application default localhost). Docker Compose sets this to db, the database service name.
POSTGRES_PORT5432NoDatabase port (internal). The host-mapped port is configured separately.
VariableDefaultRequiredDescription
JWT_SECRET_KEYNone (required)YesSecret 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_KEYderived from JWT_SECRET_KEYNoFernet 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_PREVIOUSNoneNoThe 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_ALGORITHMHS256NoJWT signing algorithm
ACCESS_TOKEN_EXPIRE_MINUTES15NoJWT access-token lifetime in minutes
REFRESH_TOKEN_EXPIRE_DAYS7NoJWT refresh-token lifetime in days
REFRESH_ROTATION_GRACE_SECONDS30NoHow 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_USERNAMENone (required)YesUsername for the automatically created admin account
GEOLENS_ADMIN_PASSWORDNone (required)YesPassword 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_ENABLEDfalseNoWhether self-registration is enabled. When false, only admins can create users.
EMAIL_VERIFICATION_REQUIREDtrueNoAdded 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_ENABLEDtrueNoAdded in 1.24.0. Whether non-admin users can sign in with a password. Admins always can. Admin-overridable.
LOGIN_RATE_LIMIT5NoAdded 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_DOMAINSemptyNoAdded 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_FIRSTfalseNoRedirect 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_LENGTH12NoMinimum password length, enforced at every password entry point (register, change-password, admin create, SAML->local conversion).
PASSWORD_REQUIRE_CLASSES3NoNumber of character classes required out of 4 (lowercase, uppercase, digit, symbol). Accepts 1 to 4.
VariableDefaultRequiredDescription
UPLOAD_MAX_SIZE_MB500NoMaximum upload file size in megabytes
UPLOAD_STAGING_DIR/app/stagingNo (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,.lazNoComma-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_MB20480NoLargest 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_ENTRIES100000NoMost files an uploaded 3D Tiles archive may contain
PRESIGNED_MULTIPART_THRESHOLD_MB100NoFiles larger than this (MB) use multipart presigned S3 URLs. Only applies when STORAGE_PROVIDER=s3.
INGEST_HTTP_TIMEOUT_SECONDS300NoGDAL_HTTP_TIMEOUT passed to ogr2ogr during remote-fetched ingest. Raise for very large datasets; lower for fail-fast ingest debugging.
URL_IMPORT_FETCH_MAX_SECONDS1800NoHow 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_USER0Admin UI onlyManaged 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_USER0Admin UI onlyManaged 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.

Terminal window
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).

VariableDefaultRequiredDescription
PROCRASTINATE_SCHEMAcatalogNoPostgreSQL schema for the Procrastinate job queue tables
INGEST_JOBS_RETENTION_DAYS30NoDays to retain finished ingest job records before pruning; 0 disables pruning.
PENDING_JOB_TIMEOUT_SECONDS3600NoHow 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_SECONDS86400NoHow 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_SECONDS300NoStatement 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_SECONDS600NoStatement 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_MB64Nowork_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.

VariableDefaultRequiredDescription
PUBLIC_APP_URLhttp://localhost:8080NoBrowser-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_URLhttp://localhost:8080/apiNoExternally-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_URLNoneNoDeprecated. 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_URLNoneNoPrivacy-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_EMAILNoneNoOrganization 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.
VariableDefaultRequiredDescription
BANNER_ENABLEDfalseNoShow 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_TEXTemptyNoBanner text. Admin-overridable.
BANNER_COLORwarningNoBanner color, one of warning, info, success, destructive. Admin-overridable.
VariableDefaultRequiredDescription
CORS_ALLOWED_ORIGINS""NoComma-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.
VariableDefaultRequiredDescription
TILE_CACHE_TTL300NoTile cache TTL in seconds
TILE_SIGNING_SECRETNone (falls back to JWT_SECRET_KEY)NoSecret for signing tile request URLs. Set separately when you want to rotate tile secrets without invalidating JWT tokens.
CDN_BASE_URLNoneNoCDN origin URL for tile delivery. When set, the frontend requests tiles from this URL instead of the API.
TITILER_BASE_URLhttp://titiler:8000NoInternal Titiler origin the API’s raster proxy calls. Override it outside Compose or when the raster service has another name.
REMOTE_RASTER_RELAY_BASE_URLhttp://api:8000NoAPI 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_WORKERS1NoTitiler 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_MAX2Noglibc 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.
VariableDefaultRequiredDescription
LOG_JSONfalseNoOutput 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_LEVELINFONoLog level. Options: DEBUG, INFO, WARNING, ERROR, CRITICAL.
VariableDefaultRequiredDescription
STORAGE_PROVIDERlocalNoStorage backend for uploaded files. Options: local, s3, azure.
S3_ENDPOINTNoneNoS3-compatible endpoint URL. Leave unset for AWS S3. For MinIO: http://minio:9000.
S3_BUCKETNoneWhen s3S3 bucket name.
S3_ACCESS_KEY_IDNoneWhen s3, without ambient credentialsS3 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_KEYNoneWhen s3, without ambient credentialsS3 secret access key.
S3_REGIONus-east-1NoS3 region.
S3_PUBLIC_ENDPOINTNoneNoHost 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_DOWNLOADSfalseNoRedirect 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_KEYS3_ACCESS_KEY_ID / S3_SECRET_ACCESS_KEYNoA 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_HTTPfalseNoAllow HTTP (non-TLS) connections to S3 endpoint. Enable for local MinIO.
S3_ADDRESSING_STYLEautoNoS3 addressing style. Options: auto, path, virtual. Use path for MinIO.
MINIO_ROOT_USER / MINIO_ROOT_PASSWORDNoneWith the cloud-dev profileRoot 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.

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).

VariableDefaultRequiredDescription
AZURE_STORAGE_CONTAINERNoneWhen azureBlob container name (e.g. geolens-prod).
AZURE_STORAGE_CONNECTION_STRINGNoneOne auth pathFull connection string (also used for Azurite). Provide this or AZURE_STORAGE_ACCOUNT_URL.
AZURE_STORAGE_ACCOUNT_URLNoneOne auth pathAccount blob endpoint, https://<account>.blob.core.windows.net.
AZURE_STORAGE_ACCOUNT_KEYNoneWith account URL + key authStorage account access key. Leave unset (with AZURE_STORAGE_ACCOUNT_URL set) to authenticate via managed identity / Entra ID.
VariableDefaultRequiredDescription
DATABASE_URL_OVERRIDENoneNoFull PostgreSQL connection URL for managed databases (RDS, Cloud SQL). Overrides individual POSTGRES_* variables.
DATABASE_SSL_MODEpreferNoDatabase SSL mode. Options: disable, prefer, require, verify-full.
DATABASE_SSL_CA_CERTNoneWhen verify-fullPath to CA certificate file for database SSL verification.
DATABASE_POOL_PRE_PINGtrueNoEnable 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_POOLERfalseNoEnable external connection pooler mode (PgBouncer, RDS Proxy). Disables prepared statements.
SANDBOX_QUERY_SLOTS(empty)NoSince 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.

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.

VariableDefaultRequiredDescription
GEOLENS_RUNTIME_DB_ROLENoneNoLogin 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_PASSWORDNoneWith the rolePassword for that role: at least 32 characters, and different from the privileged password. openssl rand -hex 32 needs no URL encoding.
GEOLENS_MIGRATION_DB_ROLEPOSTGRES_USERNoThe 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_LOCALfalseNoSet 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_OVERRIDEDATABASE_URL_OVERRIDENoPrivileged connection URL used only by the migrate service. Keep it distinct from DATABASE_URL_OVERRIDE in least-privilege deployments.
GEOLENS_API_RUN_MIGRATIONStrueNoWhether 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_OVERRIDEDATABASE_URL_OVERRIDENoSeparate connection URL for the vector tile pool, with its own login. Optional on a single-organization install.

These variables control the SQLAlchemy connection pool. Ignored when DB_USE_EXTERNAL_POOLER is true.

VariableDefaultRequiredDescription
DB_POOL_SIZE10NoMaximum number of persistent connections in the pool.
DB_MAX_OVERFLOW3NoMaximum number of additional connections beyond DB_POOL_SIZE. Tuned down from SQLAlchemy’s default to fit the connection budget below.
DB_POOL_TIMEOUT30NoSeconds to wait for a connection from the pool before raising an error.
DB_STATEMENT_TIMEOUT_SECONDS300NoQuery 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_RECYCLE1800NoSeconds after which a connection is recycled (replaced). Prevents stale connections with managed databases.
TILE_POOL_MIN_SIZE2NoMinimum connections in the dedicated asyncpg tile query pool.
TILE_POOL_MAX_SIZE10NoMaximum connections in the dedicated asyncpg tile query pool.
UVICORN_WORKERS2 (release stack); 1 (source-build stack)NoUvicorn 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_REQUESTS10000NoRecycle 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_ALIVE5NoKeep-alive timeout in seconds; matches the uvicorn upstream default. Tune up for clients with chatty long-poll patterns.
UVICORN_TIMEOUT_GRACEFUL_SHUTDOWN30NoMax seconds uvicorn waits for in-flight requests to finish on shutdown before SIGKILL. The default covers slow GDAL ingest finalization.
VariableDefaultRequiredDescription
REDIS_URLNoneNoRedis/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_PASSWORDNoneNoPassword 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.

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.

VariableDefaultDescription
DB_MEM_LIMIT2gPostgreSQL
API_MEM_LIMIT2gFastAPI
WORKER_MEM_LIMIT4gIngest and background worker
TITILER_MEM_LIMIT1gTitiler raster renderer
FRONTEND_MEM_LIMIT128mnginx frontend (release stack)
VALKEY_MEM_LIMIT256mBundled Valkey (release stack, cache profile)
VALKEY_MAXMEMORY192mbValkey’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.

These variables configure the default-on backup service. Off-site upload stays disabled until BACKUP_S3_ENABLED=true.

VariableDefaultRequiredDescription
BACKUP_SCHEDULE0 2 * * *NoCron expression for automated database backups. Default: daily at 2:00 AM UTC.
BACKUP_RETENTION_DAILY7NoDays 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_WEEKLY4NoISO 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_ENABLEDfalseNoEnable off-site backup upload to S3-compatible storage. Uses S3_* credentials.
BACKUP_MAX_AGE_MINUTES1560NoFreshness 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_LIMIT512mNoMemory 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.

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.

VariableDefaultRequiredDescription
NOTIFICATIONS_ENABLEDfalseNoMaster toggle. When false, all notification sends are a no-op regardless of channel config.
SMTP_HOSTNoneNoSMTP server hostname. Configure together with SMTP_USERNAME, SMTP_PASSWORD, and SMTP_FROM_ADDRESS to enable the email channel.
SMTP_PORT587NoSMTP server port.
SMTP_USERNAME / SMTP_PASSWORDNoneNoSMTP credentials (secret; never rendered in logs).
SMTP_FROM_ADDRESSNoneNoFrom address for outbound email.
SMTP_USE_TLStrueNoUse STARTTLS for the SMTP connection.
NOTIFICATION_WEBHOOK_URLNoneNoIncoming-webhook endpoint (Slack, Teams, or custom) that receives JSON notifications.
NOTIFICATION_WEBHOOK_SECRETNoneNoOptional HMAC signing secret for webhook payloads.
NOTIFICATION_ADMIN_EMAILNoneNoRecipient for event alerts. Falls back to SMTP_FROM_ADDRESS when unset.
NOTIFY_ON_SIGNUPfalseNoSend an alert when a new account registers.
NOTIFY_ON_INGEST_COMPLETEfalseNoSend an alert when an ingest finishes successfully.
NOTIFY_ON_INGEST_FAILEDfalseNoSend an alert when an ingest fails.
NOTIFY_ON_HEALTH_ALERTfalseNoSend 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.

VariableDefaultRequiredDescription
WORKER_CONCURRENCY1NoNumber of jobs the background worker processes concurrently.
WORKER_QUEUESpriority,ingest,raster,download,ingest-auth-v2NoComma-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_TIMEOUT30NoGraceful shutdown timeout for the background worker in seconds
VariableDefaultRequiredDescription
ENV_ONLY_CONFIGfalseNoWhen 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).
VariableDefaultRequiredDescription
ENVIRONMENT(unset)NoDeployment 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.

The installer writes both variables. scripts/upgrade.sh reads and updates them.

VariableDefaultRequiredDescription
GEOLENS_VERSIONNone (latest in the release stack)NoPublished 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_FILEdocker-compose.ymlNoWhich 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.

VariableDefaultRequiredDescription
API_UPSTREAMhttp://api:8000NoWhere 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_RESOLVERfirst nameserver in /etc/resolv.conf, else 127.0.0.11NoDNS resolver nginx uses to resolve API_UPSTREAM. Bare IPv6 resolvers are bracketed automatically.
CSP_SCRIPT_SRC_EXTRA(empty)NoExtra 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_SIZE500mNoUpload 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)NoComma/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.

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.

VariableDefaultRequiredDescription
ANTHROPIC_API_KEYNoneNoAnthropic API key. When set, Anthropic is the default inference provider.
LLM_MODELclaude-sonnet-5-5NoDefault 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_LIGHTclaude-haiku-5-5 on Anthropic’s API; LLM_MODEL on Foundry, Bedrock and VertexNoLighter Claude model for SQL generation and AI metadata. Overrides the default on any platform.
MAX_AI_TOKENS_PER_USER_PER_DAY0Admin UI onlyManaged 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_KEYNoneNoOpenAI-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_MODELgpt-4oNoDefault OpenAI-compatible model name (admin-overridable).
OPENAI_BASE_URLNoneNoCustom endpoint for OpenAI-compatible providers (Ollama, Groq, Together). Leave unset for default OpenAI.
OPENAI_MODEL_LIGHTOPENAI_MODELNoLighter 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_STYLEchat_completionsNoWhich 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_EFFORTunset (the model’s default)Noreasoning_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.

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.

VariableDefaultRequiredDescription
ANTHROPIC_PLATFORManthropicNoanthropic, foundry, bedrock or vertex.
ANTHROPIC_BASE_URLhttps://api.anthropic.comNoWith the anthropic platform, the base URL of a gateway or proxy that speaks the Messages API. Uses ANTHROPIC_API_KEY.
ANTHROPIC_FOUNDRY_API_KEYNoneWith foundryFoundry API key.
ANTHROPIC_FOUNDRY_RESOURCENoneWith foundryThe <resource> in https://<resource>.services.ai.azure.com/anthropic/.
ANTHROPIC_BEDROCK_AWS_REGIONNoneWith bedrockBedrock region.
ANTHROPIC_BEDROCK_API_KEYNoneOne auth pathBedrock 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_KEYNoneOne auth pathIAM 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_REGIONNoneWith vertexVertex region, such as us-east5, or global.
ANTHROPIC_VERTEX_PROJECT_IDNoneWith vertexGoogle Cloud project.
GOOGLE_APPLICATION_CREDENTIALSNoneNoService-account key file for Application Default Credentials, mounted into the api and worker containers. Not needed when the attached service account supplies them.

Embeddings always use the OpenAI-compatible API. Anthropic does not provide an embedding endpoint.

VariableDefaultRequiredDescription
EMBEDDING_MODELtext-embedding-3-smallNoEmbedding model name (admin-overridable).
EMBEDDING_DIMS1536NoExpected vector dimensions (admin-overridable, auto-detectable from admin UI).
EMBEDDING_BASE_URLNoneNoSeparate endpoint for embeddings. Falls back to OPENAI_BASE_URL if unset.

Anthropic inference + OpenAI embeddings (recommended):

Terminal window
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...

OpenAI for everything:

Terminal window
OPENAI_API_KEY=sk-...

Anthropic inference + Ollama embeddings:

Terminal window
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=ollama # any non-empty value
EMBEDDING_BASE_URL=http://ollama:11434/v1
EMBEDDING_MODEL=nomic-embed-text

Ollama for everything:

Terminal window
OPENAI_API_KEY=ollama # any non-empty value
OPENAI_BASE_URL=http://ollama:11434/v1
OPENAI_MODEL=llama3
EMBEDDING_MODEL=nomic-embed-text

These variables control which ports are exposed on the Docker host. They do not affect internal container communication.

VariableDefaultDescription
DB_PORT5432Host port for PostgreSQL. Set to 5434 in .env.example to avoid conflicts.
API_PORT8000Host port for the FastAPI backend. Set to 8001 in .env.example.
FRONTEND_PORT8080Host port for the frontend.

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:

ServicePortProtocol
PostgreSQL (db)5432TCP
FastAPI (api)8000HTTP
Worker (worker)8001HTTP (health only)
Titiler (titiler)8000HTTP
Frontend (frontend), release stack8080HTTP (nginx)
Frontend (frontend), source-build stack5173HTTP (Vite dev server)
VolumePurposeMount Point
pgdataPostgreSQL 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_stagingUploaded file staging area/app/staging on api, worker, and titiler
backup_dataNightly pg_dump output plus a tar of upload_staging/backups on backup
frontend_cachenginx raster cache (release stack only)/var/cache/nginx on frontend
minio_data / valkey_dataMinIO and Valkey data for the optional cloud-dev profile/data on minio / valkey
azurite_dataAzurite emulator data (source-build stack only)/data on azurite
Terminal window
# Database
POSTGRES_DB=geolens
POSTGRES_USER=geolens
POSTGRES_PASSWORD=secure-db-password
# Auth
JWT_SECRET_KEY=a1b2c3d4e5f6... # Use: openssl rand -hex 32
GEOLENS_ADMIN_USERNAME=admin
GEOLENS_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=5434
API_PORT=8001
FRONTEND_PORT=8080