Install Guide
Step-by-step reference for deploying GeoLens from scratch using Docker Compose.
Prerequisites
Section titled “Prerequisites”| Requirement | Minimum | Notes |
|---|---|---|
| Docker Engine | 24.0+ | Install Docker |
| Docker Compose | v2.20+ | Ships with Docker Desktop, or as the Docker Engine compose plugin; check yours with docker compose version |
| Memory | 8 GB | Available to Docker. The installer warns below 7 GB; with less, services can be OOM-killed under load |
| Ports | 3 ports | Default: 8080 (Frontend), 5434 (PostgreSQL), 8001 (API), all bound to 127.0.0.1 — see Published ports listen on localhost only |
First install
Section titled “First install”Use the public installer for a first run:
curl -fsSL https://getgeolens.com/install.sh | shOpen http://localhost:8080 when the installer finishes. OGC API clients should connect through the reverse-proxy path at http://localhost:8080/api/.
First login: the admin username defaults to admin. A piped curl … | sh launched from your terminal still prompts for the admin password — the installer reads /dev/tty, not stdin. The prompt reads Admin password (blank = generate a strong one): press Enter and the installer generates a strong random password rather than falling back to any default. It also generates one, with no prompt at all, when there is no controlling terminal (CI, nohup, systemd) and GEOLENS_ADMIN_PASSWORD is unset. Either way the value is stored in the install directory’s .env and never printed to your terminal. The installer clones into geolens/ under the directory where you ran it (override with GEOLENS_INSTALL_DIR), so retrieve the password with:
grep '^GEOLENS_ADMIN_PASSWORD=' geolens/.envFrom inside a source checkout (bash scripts/install.sh) the file is just .env rather than geolens/.env.
If you are installing from a source checkout instead:
git clone https://github.com/geolens-io/geolens.gitcd geolensbash scripts/install.shDefault services
Section titled “Default services”docker compose up -d starts the frontend, API, worker, TiTiler, PostgreSQL, and the backup service, after a one-shot migrate job applies database migrations and exits. Automated backups run by default and write to the backup_data volume. Set BACKUP_S3_ENABLED=true and configure the standard S3_* variables to also push backups to off-site storage. See Configuration Reference: Backup for the full variable list.
Published ports listen on localhost only
Section titled “Published ports listen on localhost only”Every host port in docker-compose.yml and docker-compose.prod.yml is published as 127.0.0.1:<host-port>:<container-port>. That is deliberate. A fresh install is reachable from the machine it runs on and from nowhere else.
Of the seven services that start by default (db, migrate, api, worker, titiler, frontend, backup), only three publish a host port at all: the frontend on 8080, the API on 8001, and PostgreSQL on 5434 (the values the installer writes into .env). migrate, worker, titiler, and backup publish none and are reachable only from inside the Compose network. The cloud-dev services follow the same rule: MinIO on 9000/9001, Valkey on 6379, and, in a source checkout, Azurite on 10000.
What this means for a remote install. Install on a VM or a bare-metal server, then open http://<server-ip>:8080 from your laptop, and you get a refused connection or a hang rather than a GeoLens error page. Nothing is broken — the stack is up, but the published port is not listening on the server’s external interface, so the request never arrives.
Put a reverse proxy on the host. This is the supported way to serve a remote install. Nginx, Caddy, or Traefik running on the server is a local client as far as Docker is concerned, so it reaches 127.0.0.1:8080 normally and forwards traffic from the outside. It is also where you terminate TLS and where you set the public URL the app advertises. See Self-host on managed cloud services for proxy configuration, TLS, and the matching PUBLIC_APP_URL / PUBLIC_API_URL values.
The port variables cannot move the bind address. FRONTEND_PORT, API_PORT, and DB_PORT change the host port number only; the 127.0.0.1 prefix is written into the compose file ahead of the variable. Putting an address in the value, as in FRONTEND_PORT=0.0.0.0:8080, makes Compose refuse to start with invalid IP address: 127.0.0.1:0.0.0.0. No environment variable publishes these ports on another interface, so a reverse proxy on the host is the only supported route in, including on a trusted private network.
Optional services (Docker Compose profiles)
Section titled “Optional services (Docker Compose profiles)”GeoLens ships with a local development profile for S3-compatible storage and shared cache testing. These services are not started by docker compose up -d unless you opt in.
| Profile | Services | When to use |
|---|---|---|
cloud-dev | minio, minio-setup, valkey, azurite (source checkouts; the release compose file omits azurite) | Local S3-compatible object storage, Valkey cache, and an Azure Blob emulator for testing provider-style setups without provisioning real infrastructure |
Run a local cloud-equivalent stack
Section titled “Run a local cloud-equivalent stack”docker compose --profile cloud-dev up -dThis starts MinIO (S3-compatible storage — the S3 API is on http://localhost:9000 and the web console on http://localhost:9001) and Valkey (Redis-compatible cache). In a source checkout it also starts Azurite, the Azure Blob Storage emulator, on http://localhost:10000, for exercising the Azure storage provider locally; the release compose file (docker-compose.prod.yml, which the one-liner installer pins into .env) has no azurite service. To switch GeoLens to use them, uncomment the MinIO block in your .env (the settings are documented under “Local S3 Testing” in .env.example) and set REDIS_URL=redis://valkey:6379/0. MinIO has no default credentials and refuses to start on blank ones: the minio container exits with FATAL: cloud-dev MinIO requires MINIO_ROOT_USER and MINIO_ROOT_PASSWORD, and will not fall back to any default account. scripts/install.sh generates both values into .env for you; if you wrote .env by hand, set them yourself (generate each with openssl rand -base64 24). Use this profile to develop or debug code paths that depend on S3 or shared cache without provisioning real cloud resources.
Stopping and starting
Section titled “Stopping and starting”Stop all services (preserves data)
Section titled “Stop all services (preserves data)”docker compose downStart services
Section titled “Start services”docker compose up -dStop and remove all data
Section titled “Stop and remove all data”docker compose down -vThe -v flag removes all project volumes — including pgdata, upload_staging, and backup_data — deleting all database data, uploaded files, and any local backups.
View logs
Section titled “View logs”# All servicesdocker compose logs -f
# Specific servicedocker compose logs -f apidocker compose logs -f dbUpgrading
Section titled “Upgrading”For version upgrade procedures, rollback steps, and version-specific notes, see the Upgrade Guide.
Data persistence
Section titled “Data persistence”Data is stored in Docker volumes. Which ones exist depends on the compose file and the profiles you enable:
| Volume | Purpose | Path inside container | Profile |
|---|---|---|---|
pgdata | PostgreSQL data directory. PostgreSQL 18+ takes a single mount here — PGDATA lives at 18/docker inside it, and mounting the legacy /var/lib/postgresql/data path makes the entrypoint refuse to boot | /var/lib/postgresql | default |
upload_staging | Uploaded files awaiting ingestion | /app/staging | default |
backup_data | Automated database dumps from the backup service | /backups | default |
frontend_cache | Nginx cache for the frontend container (release compose file only) | /var/cache/nginx | default |
minio_data | Local S3 object store for cloud-dev profile | /data | cloud-dev |
valkey_data | Local Valkey cache state for cloud-dev profile | /data | cloud-dev |
azurite_data | Local Azure Blob emulator state for cloud-dev (source checkouts only) | /data | cloud-dev |
These volumes persist across docker compose down (without -v). See Admin Guide for backup procedures.
Troubleshooting
Section titled “Troubleshooting”Missing or incomplete .env file
Section titled “Missing or incomplete .env file”If you see a ValidationError with “Field required” errors on startup:
pydantic_core._pydantic_core.ValidationError: 3 validation errors for Settings…or JWT_SECRET_KEY must be at least 32 characters, you either skipped
creating .env or copied .env.example without filling in the required
values (JWT_SECRET_KEY, GEOLENS_ADMIN_USERNAME, GEOLENS_ADMIN_PASSWORD
all ship empty). The fastest fix is to run the installer, which generates
a JWT secret and prompts for admin credentials (or auto-generates them when
run non-interactively — see “First login” above):
bash scripts/install.shThe script is idempotent; it only fills in missing values, so it’s safe
to re-run on a partially-configured .env.
Migrations at startup
Section titled “Migrations at startup”The dedicated migrate service applies Alembic migrations before the API starts, and the API and worker entrypoints run alembic upgrade heads again as a safety net. That re-run is idempotent: when the migrate service has already applied everything, it succeeds as a no-op and the API starts normally.
If a migration fails, the API refuses to start and logs a line beginning with:
FATAL: database migrations failedThat exit is a real error, not a startup-ordering artifact. Check the migration logs with docker compose logs migrate, confirm the database is reachable, fix the cause, and start the stack again.
Port conflicts
Section titled “Port conflicts”If a port is already in use, change it in .env:
FRONTEND_PORT=8081API_PORT=8002DB_PORT=5435Then restart:
docker compose down && docker compose up -dOut of memory
Section titled “Out of memory”If the database or API crashes with OOM errors:
- Increase Docker memory allocation (Docker Desktop: Settings > Resources > Memory)
- Reduce concurrent workloads (pause large ingests, narrow query bbox)
Services not starting
Section titled “Services not starting”Check the startup order and health:
# View startup logsdocker compose logs --tail=50 dbdocker compose logs --tail=50 api
# Check health statusdocker compose psCommon issues:
- db not healthy: Check
POSTGRES_USERandPOSTGRES_PASSWORDmatch in.env - api not starting: Verify database is healthy first; check migration errors in API logs
- frontend 502 errors: Upstream API not ready yet; wait 30 seconds and retry
Export 500 errors (staging permission denied)
Section titled “Export 500 errors (staging permission denied)”If /api/datasets/{id}/export returns HTTP 500, verify staging writability inside the API
container. Run it as uid 1001 — docker compose exec defaults to root, which can write a
directory the application user cannot, so a root probe passes on exactly the misconfiguration
you are looking for:
docker compose exec -u 1001:1001 api sh -lc '\ 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"'From a source checkout with dev dependencies installed (npm install), run the runtime export verification spec after permissions are corrected. The spec talks to the API over HTTP and never opens a browser, but it does log in as the admin, so export GEOLENS_ADMIN_USERNAME and GEOLENS_ADMIN_PASSWORD from your .env first — otherwise the login step fails with a 401. An installer-created directory is a shallow tag checkout with no node modules, so the command will not run there.
npm run e2e:exportExpected success signals:
- Exports pass for
gpkg,geojson,shp, andcsvwith attachment payload integrity (SQLite header, FeatureCollection JSON, zip members, CSV header row). target_crs=EPSG:3857export confirms projected coordinate semantics as well as HTTP 200.bboxandwhereexports are true subsets (feature/property assertions pass).- Audit logs include
dataset.exportentries with export parameters.
If the writability check fails:
- Fix ownership/permissions on the mounted staging path so uid:gid
1001:1001can write. - Or repair it in place:
docker compose exec -u 0 api chown -R 1001:1001 /app/staging. SettingUPLOAD_STAGING_DIRin.envwill not help — both Compose files pin it to the literal/app/stagingonapiandworker, so the value is never read from.env. - Re-run
npm run e2e:export(source checkout only) to confirm full runtime behavior.
GDAL/OGR errors during ingestion
Section titled “GDAL/OGR errors during ingestion”The API container includes GDAL. If file ingestion fails:
# Check API logs for OGR errorsdocker compose logs api | grep -i "ogr\|gdal\|error"Supported upload formats (the UPLOAD_ALLOWED_EXTENSIONS default, which admins can narrow in Admin → Storage): .zip (Shapefile, or a zipped File Geodatabase), .gpkg (GeoPackage), .geojson, .json, .csv, .tif/.tiff (GeoTIFF/COG), .parquet (GeoParquet), .xlsx/.xls (Excel), .fgb (FlatGeobuf), and .kml/.kmz (KML/KMZ). See Configuration Reference → Upload settings.
Database connection errors
Section titled “Database connection errors”Verify the database is accessible:
docker compose exec db pg_isready -U geolens -d geolensIf the database is unreachable from the API, ensure the POSTGRES_HOST is set to db (the Docker service name).
Reset to clean state
Section titled “Reset to clean state”To completely reset the installation:
docker compose down -vdocker compose up -dThis removes all data and rebuilds from scratch. The admin account is re-seeded from the GEOLENS_ADMIN_USERNAME / GEOLENS_ADMIN_PASSWORD values still in your .env — including a password the installer generated — so any credential you rotated inside the app is lost and the .env value applies again.