Initial public release
GeoIntel release gates / Compile, test, contracts and builds (push) Successful in 1m49s
GeoIntel release gates / Python and npm vulnerability policy (push) Successful in 21s
GeoIntel release gates / Production AI image, SBOM and container scan (push) Successful in 5m39s
GeoIntel release gates / Deploy exact gated revision to Unraid (push) Failing after 58m43s

This commit is contained in:
Jens
2026-08-31 21:56:53 +02:00
commit faeb58ef6d
1386 changed files with 263203 additions and 0 deletions
+199
View File
@@ -0,0 +1,199 @@
FROM node:20-alpine@sha256:fb4cd12c85ee03686f6af5362a0b0d56d50c58a04632e6c0fb8363f609372293 AS frontend-build
WORKDIR /frontend
COPY frontend/package.json frontend/package-lock.json ./
RUN npm ci
COPY frontend/ ./
RUN npm run build
FROM postgres:16-bookworm@sha256:bb3e1a57e5407e0a5280b4211980a5e537f4abd234a87014ac979849a78dd825 AS runtime
ARG GEOINTEL_INSTALL_AI=false
ARG GEOINTEL_TORCH_INDEX_URL=https://download.pytorch.org/whl/cu128
ENV GEOINTEL_ENV=production \
GEOINTEL_API_PREFIX=/api/v1 \
GEOINTEL_STORAGE_ROOT=/app/storage \
STORAGE_ROOT=/app/storage \
GEOINTEL_ALL_IN_ONE=1 \
PATH="/opt/geointel/venv/bin:${PATH}"
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
curl \
gdal-bin \
libgl1 \
libglib2.0-0 \
libgdal-dev \
libgeos-dev \
libpq-dev \
libproj-dev \
libsm6 \
libx11-6 \
libxcb1 \
libxext6 \
libxrender1 \
nginx \
postgresql-16-postgis-3 \
postgresql-16-postgis-3-scripts \
proj-bin \
python3-dev \
python3-venv \
&& ln -sf /usr/bin/python3.11 /usr/local/bin/python3 \
&& ln -sf /usr/bin/python3.11 /usr/bin/python3 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY deploy/unraid/gosu-setpriv /usr/local/bin/gosu
COPY backend/pyproject.toml /app/
COPY backend/requirements-runtime.lock /app/
COPY backend/requirements-ai-linux.lock /app/
COPY backend/requirements-build-tools.lock /app/
COPY backend/app/__init__.py /app/app/__init__.py
RUN printf '# GeoIntel backend package metadata\n' > /app/README.md \
&& /usr/bin/python3.11 -m venv /opt/geointel/venv \
&& pip install --no-cache-dir --require-hashes -r requirements-runtime.lock \
&& if [ "$GEOINTEL_INSTALL_AI" = "true" ]; then \
pip install --no-cache-dir --require-hashes \
--index-url https://pypi.org/simple \
--extra-index-url "$GEOINTEL_TORCH_INDEX_URL" \
-r requirements-ai-linux.lock; \
fi \
&& pip install --no-cache-dir --require-hashes -r requirements-build-tools.lock \
&& pip check
COPY backend/ /app/
COPY fixtures/ /app/fixtures/
RUN python scripts/gis_import_smoke.py \
&& python scripts/yolo_preflight.py --json >/tmp/geointel-yolo-preflight.json \
&& rm -f /etc/nginx/sites-enabled/default \
&& mkdir -p /app/storage /run/nginx /var/log/nginx
COPY scripts/prepare_operator_real_data_samples.py /app/scripts/prepare_operator_real_data_samples.py
COPY scripts/provision_mol_municipality_workspace.py /app/scripts/provision_mol_municipality_workspace.py
COPY scripts/provision_mol_context_layers.py /app/scripts/provision_mol_context_layers.py
COPY scripts/provision_mol_dhmv.py /app/scripts/provision_mol_dhmv.py
COPY scripts/provision_regional_dhmv.py /app/scripts/provision_regional_dhmv.py
COPY scripts/provision_mol_flood_hazards.py /app/scripts/provision_mol_flood_hazards.py
COPY scripts/provision_regional_flood_hazards.py /app/scripts/provision_regional_flood_hazards.py
COPY scripts/provision_thematic_rasters.py /app/scripts/provision_thematic_rasters.py
COPY scripts/provision_mol_soil_map.py /app/scripts/provision_mol_soil_map.py
COPY scripts/provision_regional_soil_map.py /app/scripts/provision_regional_soil_map.py
COPY scripts/provision_mol_population_history.py /app/scripts/provision_mol_population_history.py
COPY scripts/statbel_population_preflight.py /app/scripts/statbel_population_preflight.py
COPY scripts/manage_statbel_population_release.py /app/scripts/manage_statbel_population_release.py
COPY scripts/provision_mol_historical_landuse.py /app/scripts/provision_mol_historical_landuse.py
COPY scripts/provision_regional_historical_landuse.py /app/scripts/provision_regional_historical_landuse.py
COPY scripts/provision_official_landuse_timeseries.py /app/scripts/provision_official_landuse_timeseries.py
COPY scripts/provision_waterinfo_station_history.py /app/scripts/provision_waterinfo_station_history.py
COPY scripts/provision_mol_bathymetry_profiles.py /app/scripts/provision_mol_bathymetry_profiles.py
COPY scripts/provision_flanders_geographic_scope.py /app/scripts/provision_flanders_geographic_scope.py
COPY scripts/provision_flanders_bathymetry_profiles.py /app/scripts/provision_flanders_bathymetry_profiles.py
COPY scripts/probe_mdk_bathymetry.py /app/scripts/probe_mdk_bathymetry.py
COPY scripts/import_spw_bathymetry.py /app/scripts/import_spw_bathymetry.py
COPY scripts/provision_walous_sources.py /app/scripts/provision_walous_sources.py
COPY scripts/provision_spw_terrain_source.py /app/scripts/provision_spw_terrain_source.py
COPY scripts/provision_mol_bwk_natura2000.py /app/scripts/provision_mol_bwk_natura2000.py
COPY scripts/provision_regional_bwk_natura2000.py /app/scripts/provision_regional_bwk_natura2000.py
COPY scripts/provision_agricultural_parcel_history.py /app/scripts/provision_agricultural_parcel_history.py
COPY scripts/manage_alz_agriculture_release.py /app/scripts/manage_alz_agriculture_release.py
COPY scripts/orthophoto_release_preflight.py /app/scripts/orthophoto_release_preflight.py
COPY scripts/manage_orthophoto_release.py /app/scripts/manage_orthophoto_release.py
COPY scripts/provision_buildings_addresses_register.py /app/scripts/provision_buildings_addresses_register.py
COPY scripts/provision_regional_timeseries.py /app/scripts/provision_regional_timeseries.py
COPY scripts/geographic_scopes.py /app/scripts/geographic_scopes.py
COPY scripts/provision_geographic_scope.py /app/scripts/provision_geographic_scope.py
COPY scripts/build_model_validation_scope_manifest.py /app/scripts/build_model_validation_scope_manifest.py
COPY scripts/provision_belgium_north_sea_scope.py /app/scripts/provision_belgium_north_sea_scope.py
COPY scripts/provision_release_golden_areas.py /app/scripts/provision_release_golden_areas.py
COPY scripts/provision_regional_grb_buildings.py /app/scripts/provision_regional_grb_buildings.py
COPY scripts/provision_regional_grb_context.py /app/scripts/provision_regional_grb_context.py
COPY scripts/audit_source_freshness.py /app/scripts/audit_source_freshness.py
COPY scripts/manage_grb_refresh.py /app/scripts/manage_grb_refresh.py
COPY scripts/export_operator_yolo_tile_dataset.py /app/scripts/export_operator_yolo_tile_dataset.py
COPY scripts/normalize_belgium_building_labels.py /app/scripts/normalize_belgium_building_labels.py
COPY scripts/assemble_belgium_building_corpus.py /app/scripts/assemble_belgium_building_corpus.py
COPY scripts/provision_belgium_building_training_portfolio.py /app/scripts/provision_belgium_building_training_portfolio.py
COPY scripts/audit_belgium_building_corpus.py /app/scripts/audit_belgium_building_corpus.py
COPY scripts/evaluate_belgium_building_candidate.py /app/scripts/evaluate_belgium_building_candidate.py
COPY scripts/assess_belgium_building_training_iteration.py /app/scripts/assess_belgium_building_training_iteration.py
COPY scripts/run_belgium_building_training_loop.py /app/scripts/run_belgium_building_training_loop.py
COPY scripts/build_failure_driven_yolo_sampling.py /app/scripts/build_failure_driven_yolo_sampling.py
COPY scripts/training_dataset_eligibility.py /app/scripts/training_dataset_eligibility.py
COPY scripts/training_release_manifest.py /app/scripts/training_release_manifest.py
COPY scripts/build_grayscale_yolo_dataset.py /app/scripts/build_grayscale_yolo_dataset.py
COPY scripts/audit_operator_yolo_dataset_quality.py /app/scripts/audit_operator_yolo_dataset_quality.py
COPY scripts/render_operator_yolo_label_qa_contact_sheets.py /app/scripts/render_operator_yolo_label_qa_contact_sheets.py
COPY scripts/render_operator_polygon_label_qa.py /app/scripts/render_operator_polygon_label_qa.py
COPY scripts/train_operator_yolo_detector.sh /app/scripts/train_operator_yolo_detector.sh
COPY scripts/verify_real_data_detection_qa_workflow.sh /app/scripts/verify_real_data_detection_qa_workflow.sh
COPY scripts/run_detection_quality_matrix.sh /app/scripts/run_detection_quality_matrix.sh
COPY scripts/run_multi_sample_detection_quality_matrix.sh /app/scripts/run_multi_sample_detection_quality_matrix.sh
COPY scripts/run_mol_operational_validation.sh /app/scripts/run_mol_operational_validation.sh
COPY scripts/export_detection_calibration_evidence.sh /app/scripts/export_detection_calibration_evidence.sh
COPY scripts/assemble_detection_calibration_evidence_portfolio.sh /app/scripts/assemble_detection_calibration_evidence_portfolio.sh
COPY scripts/build_fixed_threshold_evidence_portfolio_inputs.py /app/scripts/build_fixed_threshold_evidence_portfolio_inputs.py
COPY scripts/audit_detection_false_negative_evidence.py /app/scripts/audit_detection_false_negative_evidence.py
COPY scripts/audit_detection_false_positive_evidence.py /app/scripts/audit_detection_false_positive_evidence.py
COPY scripts/render_detection_false_positive_review_contact_sheets.py /app/scripts/render_detection_false_positive_review_contact_sheets.py
COPY scripts/render_detection_false_negative_review_contact_sheets.py /app/scripts/render_detection_false_negative_review_contact_sheets.py
COPY scripts/validate_detection_false_positive_review_decisions.py /app/scripts/validate_detection_false_positive_review_decisions.py
COPY scripts/validate_detection_false_negative_review_decisions.py /app/scripts/validate_detection_false_negative_review_decisions.py
COPY scripts/run_operator_hard_negative_detection_matrix.sh /app/scripts/run_operator_hard_negative_detection_matrix.sh
COPY scripts/run_background_corpus_split_matrix.sh /app/scripts/run_background_corpus_split_matrix.sh
COPY scripts/build_background_corpus_split_report.py /app/scripts/build_background_corpus_split_report.py
COPY scripts/build_detection_model_promotion_report.py /app/scripts/build_detection_model_promotion_report.py
COPY scripts/build_mol_operational_benchmark_report.py /app/scripts/build_mol_operational_benchmark_report.py
COPY scripts/run_split_background_promotion_workflow.sh /app/scripts/run_split_background_promotion_workflow.sh
COPY scripts/activate_promoted_yolo_candidate.py /app/scripts/activate_promoted_yolo_candidate.py
COPY scripts/migrate_runtime_model_provenance.py /app/scripts/migrate_runtime_model_provenance.py
COPY scripts/archive_technical_projects.py /app/scripts/archive_technical_projects.py
COPY scripts/runtime_state_report.py /app/scripts/runtime_state_report.py
COPY scripts/release_backup_guard.py /app/scripts/release_backup_guard.py
COPY scripts/release_backup_snapshot.py /app/scripts/release_backup_snapshot.py
COPY scripts/audit_data_operations.py /app/scripts/audit_data_operations.py
COPY scripts/cleanup_storage_artifacts.py /app/scripts/cleanup_storage_artifacts.py
COPY scripts/restore_storage_quarantine.py /app/scripts/restore_storage_quarantine.py
COPY deploy/unraid/nginx-all-in-one.conf /etc/nginx/conf.d/default.conf
COPY deploy/unraid/all-in-one-start.sh /usr/local/bin/geointel-all-in-one-start
COPY --from=frontend-build /frontend/dist/ /usr/share/nginx/html/
RUN chmod +x /usr/local/bin/geointel-all-in-one-start /usr/local/bin/gosu \
&& chmod +x \
/app/scripts/train_operator_yolo_detector.sh \
/app/scripts/verify_real_data_detection_qa_workflow.sh \
/app/scripts/run_detection_quality_matrix.sh \
/app/scripts/run_multi_sample_detection_quality_matrix.sh \
/app/scripts/run_mol_operational_validation.sh \
/app/scripts/export_detection_calibration_evidence.sh \
/app/scripts/assemble_detection_calibration_evidence_portfolio.sh \
/app/scripts/run_operator_hard_negative_detection_matrix.sh \
/app/scripts/run_background_corpus_split_matrix.sh \
/app/scripts/run_split_background_promotion_workflow.sh
ARG GEOINTEL_BUILD_SHA=unknown
ARG GEOINTEL_BUILD_TIME=unknown
ARG GEOINTEL_APP_VERSION=1.0.0
ENV GEOINTEL_BUILD_SHA="${GEOINTEL_BUILD_SHA}" \
GEOINTEL_BUILD_TIME="${GEOINTEL_BUILD_TIME}" \
GEOINTEL_APP_VERSION="${GEOINTEL_APP_VERSION}"
LABEL org.opencontainers.image.title="GeoIntel" \
org.opencontainers.image.description="GeoIntel workbench for Belgium and the Belgian North Sea" \
org.opencontainers.image.version="${GEOINTEL_APP_VERSION}" \
org.opencontainers.image.revision="${GEOINTEL_BUILD_SHA}" \
org.opencontainers.image.created="${GEOINTEL_BUILD_TIME}" \
io.geointel.ai.enabled="${GEOINTEL_INSTALL_AI}"
VOLUME ["/var/lib/postgresql/data", "/app/storage", "/app/backups"]
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=5s --retries=10 --start-period=60s \
CMD curl -fsS http://127.0.0.1/health/ready >/dev/null || exit 1
CMD ["/usr/local/bin/geointel-all-in-one-start"]
+212
View File
@@ -0,0 +1,212 @@
# Handmatige deploy naar Unraid (1 container, poort 1202)
Voor als de bestaande deploy stuk is en je de code met de hand kopieert
(zip/scp/SMB) in plaats van via `git clone`. Eindresultaat: **één** container
met de naam `geointel`, bereikbaar op `http://192.0.2.10:1202`.
---
## 1. Kopiëren
Kopieer de **volledige** map `C:\Projects\geointel` naar
`/mnt/user/appdata/geointel` op de server. Verder niets uitzoeken.
`.dockerignore` regelt de rest: `node_modules/`, `.git/`, `docs/`, `artifacts/`
en testoutput gaan niet mee de build-context in, ook al staan ze in de map.
Eén waarschuwing bij het overschrijven: laat `storage/`, `postgres-data/`,
`backups/` en `models/` op de server **staan**. Dat is je bestaande data, en
die staat niet in je Windows-map. Kopieer dus overschrijvend, niet met een
"mirror"/"purge"-optie die bestanden aan de doelzijde weggooit.
```powershell
# Vanaf Windows, veilig: kopieert en overschrijft, verwijdert niets aan de doelkant.
robocopy C:\Projects\geointel \\192.0.2.10\appdata\geointel /E
```
---
## 2. Rechten en regeleindes goedzetten
Na een kopie via Windows zijn de execute-bits weg. Op de Unraid-shell:
```bash
cd /mnt/user/appdata/geointel
find . -name '*.sh' -exec sed -i 's/\r$//' {} +
sed -i 's/\r$//' deploy/unraid/gosu-setpriv
chmod +x deploy/unraid/*.sh scripts/*.sh deploy/unraid/gosu-setpriv
```
---
## 3. `.env` aanmaken
```bash
cd /mnt/user/appdata/geointel
cp deploy/unraid/geointel.env.example .env
nano .env
```
Verplicht aan te passen:
- `GEOINTEL_POSTGRES_PASSWORD` — de deploy **weigert** te starten met een lege
of standaardwaarde (`geointel`, `postgres`, `changeme`,
`change-me-before-shared-use`). Gebruik hier dezelfde waarde als je vorige
installatie, anders kan de bestaande PostGIS-data niet worden geopend.
Controleer dat deze op de bestaande data wijzen:
```env
GEOINTEL_FRONTEND_PORT=1202
GEOINTEL_STORAGE_PATH=/mnt/user/appdata/geointel/storage
GEOINTEL_POSTGIS_DATA_PATH=/mnt/user/appdata/geointel/postgres-data
GEOINTEL_MODELS_PATH=/mnt/user/appdata/geointel/models
GEOINTEL_BACKUPS_PATH=/mnt/user/appdata/geointel/backups
GEOINTEL_CORS_ORIGINS=https://geointel.example.com,http://geointel.example.com,http://localhost:1202,http://127.0.0.1:1202,http://192.0.2.10:1202
```
Staat Nginx Proxy Manager voor poort 1202 op `geointel.example.com`, dan moet
die hostname in `GEOINTEL_CORS_ORIGINS` staan — anders blokkeert de browser de
API-calls vanaf het publieke adres. De backend doet geen host-validatie, dus
verder is er niets nodig aan applicatiekant.
De productie-image bevat altijd de gepinde AI-runtime. Inferentie zelf kan
uitblijven totdat een lokaal, gevalideerd model beschikbaar is:
```env
GEOINTEL_INSTALL_AI=true
YOLO_ENABLED=true
YOLO_DEVICE=cuda:0
YOLO_REQUIRE_CUDA=true
YOLO_MODEL_PATH=/app/models/<jouw-model>.pt
```
Laat `YOLO_ENABLED=false` zolang er geen geschikt modelbestand is. Het
release-deployscript weigert bewust een GIS-only productie-image.
---
## 4. Login instellen
De app slaat geen plaintext wachtwoord op: `.env` bevat alleen een
pbkdf2_sha256-hash. Dit script maakt die hash plus een sessiegeheim en zet ze
in `.env`:
```bash
cd /mnt/user/appdata/geointel
bash scripts/configure_operator_login.sh --username ITWorx --guest-access true
```
`--guest-access true` is nodig om de demo-ingang te behouden; zonder die vlag
zet het script `GEOINTEL_GUEST_ACCESS_ENABLED=false`.
Het script vraagt het wachtwoord tweemaal (je typt het niet zichtbaar en het
komt niet in je shell-history), en zet:
```env
GEOINTEL_AUTH_ENABLED=true
GEOINTEL_AUTH_USERNAME=ITWorx
GEOINTEL_AUTH_PASSWORD_HASH=pbkdf2_sha256$600000$...
GEOINTEL_AUTH_SESSION_SECRET=<48 willekeurige tekens>
GEOINTEL_GUEST_ACCESS_ENABLED=false
```
Gastentoegang wordt bewust uitgezet — met auth aan zou anders de publieke
demo-ingang "Als gast verkennen" op de loginpagina blijven staan. Wil je die
wel: `--guest-access true`.
Wachtwoord later wijzigen: draai hetzelfde commando opnieuw en deploy. Er
wordt dan ook een nieuw sessiegeheim gemaakt, dus openstaande browsersessies
worden ongeldig.
> Het wachtwoord dat je me in de chat gaf staat nu in die chatgeschiedenis.
> Draai het script met een nieuw wachtwoord zodra de deploy staat.
---
## 5. Deployen
```bash
cd /mnt/user/appdata/geointel
docker compose -f docker-compose.unraid.yml config >/dev/null # syntaxcheck
bash deploy/unraid/deploy-release.sh
```
Het script:
1. bindt het exacte huidige image-ID aan een unieke backup-specifieke
`rollback-predeploy-*`-tag;
2. bouwt of hergebruikt de AI-candidate terwijl de huidige release beschikbaar
blijft, legt het exacte lokale image-ID vast en maakt daarop SBOM- en
Trivy-evidence;
3. controleert vóór het pauzeren de vrije ruimte en maakt vervolgens een
byte-complete, SHA-256-geverifieerde database-, storage- en modelsnapshot in
`/mnt/user/appdata/geointel/backups`; ongewijzigde bestanden mogen alleen
vanuit een oudere geverifieerde backup worden gehardlinkt;
4. ruimt een eventueel achtergebleven Compose-stack op (ook de oude
3-container dev-stack die óók poort 1202 pakt);
5. start één container `geointel` met `-p 1202:80` en `--gpus all`;
6. bewijst een rollbackdump eerst in een geïsoleerde tijdelijke database,
bewaart de oude productiedatabase als herstelpad en start pas daarna
automatisch het image-ID dat cryptografisch in die pre-deploybackup staat.
De eerste build duurt lang (PostGIS + GDAL + npm build). Volgende deploys
hergebruiken de Docker-layercache.
---
## 6. Controleren
```bash
docker ps --filter name=geointel # exact 1 regel verwacht
curl -fsS http://127.0.0.1:1202/health/live
curl -fsS http://127.0.0.1:1202/health/ready
curl -fsS http://127.0.0.1:1202/api/v1/system/capabilities
```
Browser: `http://192.0.2.10:1202`
Bij problemen:
```bash
docker logs --tail 200 geointel
```
---
## 7. Geretireerde dubbele projectmap
De vroegere geneste mirror `C:\Projects\geointel\geointel` is geretireerd. De
immutable Git-herkomst, niet-getrackte recoverybestanden en verificatiegrens
staan in de private release-evidence buiten Git. Behandel die
evidence als herstelreferentie; deze handleiding vraagt geen extra kopieer- of
opruimactie.
---
## 8. Terugrollen
```bash
cd /mnt/user/appdata/geointel
bash deploy/unraid/rollback-dockerman-container.sh \
--backup-dir /mnt/user/appdata/geointel/backups/<predeploy-release-id> \
--confirm-production-database-restore
```
Rollback hergebruikt dezelfde storage-paden, bewijst de geverifieerde dump
eerst geïsoleerd, wisselt daarna databases via no-clobber namen en draait nooit
een Alembic-downgrade. De oude productiedatabase blijft staan totdat een
operator haar na controle expliciet opruimt.
---
## Wat er is aangepast om dit mogelijk te maken
| Bestand | Aanpassing |
|---|---|
| `.dockerignore` | Sluit root-`node_modules`, `.git`, `docs/`, `artifacts/` en testoutput uit de build-context |
| `deploy/unraid/deploy-release.sh` | `git rev-parse HEAD` crashte op een kopie zonder `.git`. Valt nu terug op `GEOINTEL_BUILD_SHA`, een `RELEASE_SHA`-bestand of een content-hash van de broncode |
| `deploy/unraid/deploy-release.sh` | Smoke-scripts worden op bestaan getest in plaats van op de execute-bit, die bij een Windows-kopie verloren gaat |
| `deploy/unraid/run-dockerman-container.sh` | Ruimt expliciet zowel `docker-compose.yml` (3 containers) als `docker-compose.unraid.yml` op, zodat poort 1202 gegarandeerd vrij is |
| `deploy/unraid/geointel.env.example` | `COMPOSE_FILE=docker-compose.unraid.yml`, zodat een kaal `docker compose up -d` nooit per ongeluk de 3-container dev-stack start |
| `scripts/configure_operator_login.sh` | Nieuw. Zet de operator-login in `.env` met een correcte pbkdf2_sha256-hash (600.000 iteraties, identiek aan `AuthService.hash_password`) en een vers sessiegeheim |
+368
View File
@@ -0,0 +1,368 @@
# GeoIntel Unraid all-in-one container
GeoIntel can run on Unraid as one DockerMan-native container.
Inside that single container:
- embedded PostGIS stores the application database
- Alembic migrations run at startup
- FastAPI runs on internal `127.0.0.1:8000`
- nginx serves the React/MapLibre frontend on container port `80`
- nginx proxies `/api` and `/health` to the internal backend
The browser entrypoint is:
```text
http://<unraid-ip>:${GEOINTEL_FRONTEND_PORT}
```
The app icons are served from the same container:
```text
http://<unraid-ip>:${GEOINTEL_FRONTEND_PORT}/geointel-icon.svg
http://<unraid-ip>:${GEOINTEL_FRONTEND_PORT}/geointel-icon.png
```
## Files
- `docker-compose.unraid.yml`: config validation reference for the all-in-one image.
- `deploy/unraid/Dockerfile.all-in-one`: builds the single container.
- `deploy/unraid/all-in-one-start.sh`: starts embedded PostGIS, backend and nginx.
- `deploy/unraid/run-dockerman-container.sh`: starts/replaces the running container with DockerMan labels and editable Unraid metadata.
- `deploy/unraid/nginx-all-in-one.conf`: frontend and API proxy config for one container.
- `deploy/unraid/geointel.env.example`: copy to `.env` and edit ports/paths.
- `deploy/unraid/geointel-unraid-template.xml`: Unraid/DockerMan metadata for editable fields.
- `deploy/unraid/geointel-icon.svg`: frontend favicon source.
- `deploy/unraid/geointel-icon.png`: DockerMan/Unraid icon source.
- `frontend/public/geointel-icon.svg`: frontend-served SVG icon.
- `frontend/public/geointel-icon-180.png`: frontend PNG icon, also served at
the stable `/geointel-icon.png` compatibility URL by nginx.
The Tower deploy scripts also copy the editable DockerMan template to:
```text
/boot/config/plugins/dockerMan/templates-user/my-geointel.xml
```
and copy the PNG icon to:
```text
/boot/config/plugins/dockerMan/images/geointel-icon.png
```
The template name is `geointel` so it matches the running all-in-one container name. If the Unraid Docker page was already open, refresh it after deploy so DockerMan reloads the template/icon metadata.
`docker-compose.unraid.yml` also applies DockerMan labels to the running container:
```text
net.unraid.docker.managed=dockerman
net.unraid.docker.webui=http://[IP]:[PORT:80]/
net.unraid.docker.icon=/boot/config/plugins/dockerMan/images/geointel-icon.png
```
These labels are required because a plain Compose container can run correctly while still missing the normal Unraid edit/icon controls.
## First setup from the repo
From the Unraid shell:
```bash
cd /mnt/user/appdata
git clone gitea-widefrog:NuklearRabbit/geointel.git geointel
cd /mnt/user/appdata/geointel
cp deploy/unraid/geointel.env.example .env
nano .env
docker compose -f docker-compose.unraid.yml config
bash deploy/unraid/deploy-release.sh
```
Set `GEOINTEL_POSTGRES_PASSWORD` to a unique value before that first start.
Browser login is enabled by default for the packaged runtime. Configure one exact
`GEOINTEL_AUTH_USERNAME`, a `pbkdf2_sha256` password hash and an independent
random `GEOINTEL_AUTH_SESSION_SECRET` of at least 32 characters. The plaintext
password is never stored in the repository or container configuration. Browser
API calls require the signed HttpOnly session cookie; direct loopback calls to
the backend remain available to trusted in-container operator scripts.
Production startup fails before replacing the active container when the
password is empty or one of the documented defaults.
`GEOINTEL_AUTH_REQUIRE_HTTPS=true` is also the packaged default. Configure
`GEOINTEL_PUBLIC_BASE_URL` with the external HTTPS origin and terminate TLS at
the trusted reverse proxy; plaintext operator login is rejected.
Guest access is an explicit opt-in for dedicated public demo installations:
```env
GEOINTEL_GUEST_ACCESS_ENABLED=true
GEOINTEL_GUEST_DISPLAY_NAME=Gast
GEOINTEL_GUEST_SESSION_TTL_SECONDS=7200
```
The packaged default is `false`. Setting it to `true` adds **Als gast
verkennen** to the landing page. The generated guest cookie
is short-lived, project-scoped and limited to the canonical demo workflow.
Operator mutations and access to another project are rejected by the backend,
and the frontend hides management and task-starting controls. The mechanism is
not tenant isolation: never enable it on an instance that contains private,
customer or operational data. Deploy a separate demo container and storage
root for public or recruiter-facing access.
The repository deploy scripts run the same flow automatically. They validate
the Compose reference, preserve the current image under a unique
backup-specific `rollback-predeploy-*` tag, build the immutable production
`<commit-sha>-ai` tag plus `latest`, attest its exact local image ID, generate
an SBOM and enforce the Trivy policy before starting that same ID. An existing
matching tag is reused, never rebuilt. A failed start, live migration smoke or
browser/API smoke automatically attempts the previous image without changing
the configured PostGIS or storage paths.
`scripts/deploy_tower.sh` and `scripts/deploy_tower.ps1` source the remote
`.env` before building the image. Production deployment requires
`GEOINTEL_INSTALL_AI=true`; an explicit false value fails closed before the
image or running container is replaced.
Database credentials are runtime configuration, not image metadata. The
all-in-one image does not bake `GEOINTEL_POSTGRES_PASSWORD` into the Dockerfile;
set it through `.env`, the Unraid template or `docker run -e`.
Production images always include the pinned AI dependencies. Mount models
through `GEOINTEL_MODELS_PATH` and configure `YOLO_ENABLED=true` plus
`YOLO_MODELS_DIR=/app/models` and `YOLO_MODEL_PATH=/app/models/<model>.pt` only
when you have a local model file.
The AI-enabled image installs PyTorch/Ultralytics plus the native OpenCV runtime
libraries needed for Ultralytics imports; it still never downloads model weights.
The documented CPU runtime installs pinned `torch==2.13.0` and
`torchvision==0.28.0` from `https://download.pytorch.org/whl/cpu`, avoiding the
unused CUDA runtime wheels included by the general Linux package index. The
Dockerfile copies dependency metadata before backend source and applies
commit/build metadata only after the heavy file layers. Normal code-only
redeploys therefore reuse the apt, GIS and optional PyTorch dependency layers.
`YOLO_CONFIG_DIR` defaults to `/app/storage/ultralytics`, a writable persistent
path, so Ultralytics settings do not fall back to root user config directories.
To safely configure an existing local YOLO model on Tower, place one supported
model file (`.pt`, `.onnx` or `.engine`) under:
```text
/mnt/user/appdata/geointel/models
```
Then run a dry-run first:
```bash
cd /mnt/user/appdata/geointel
python scripts/configure_yolo_model.py \
--models-dir /mnt/user/appdata/geointel/models \
--env-file .env
```
Apply only after the selected host and container paths are correct:
```bash
python scripts/configure_yolo_model.py \
--models-dir /mnt/user/appdata/geointel/models \
--env-file .env \
--apply
bash deploy/unraid/run-dockerman-container.sh
```
If multiple model files are present, add `--model-file /mnt/user/appdata/geointel/models/<name>.pt`.
The helper does not download weights, does not load a model and does not run
inference; it only updates the env file for the mounted local model.
Validate liveness, dependency readiness and the canonical API:
```bash
curl -fsS "http://192.0.2.10:${GEOINTEL_FRONTEND_PORT:-1202}/health/live"
curl -fsS "http://192.0.2.10:${GEOINTEL_FRONTEND_PORT:-1202}/health/ready"
curl -fsS "http://192.0.2.10:${GEOINTEL_FRONTEND_PORT:-1202}/api/v1/system/capabilities"
curl -fsS "http://192.0.2.10:${GEOINTEL_FRONTEND_PORT:-1202}/api/v1/projects"
curl -I "http://192.0.2.10:${GEOINTEL_FRONTEND_PORT:-1202}/geointel-icon.svg"
curl -I "http://192.0.2.10:${GEOINTEL_FRONTEND_PORT:-1202}/geointel-icon.png"
```
The live migration smoke also checks PostgreSQL database collation metadata.
When reusing a PostGIS volume created by an older Debian/glibc runtime, it may
print `COLLATION_VERSION_MISMATCH`. This is a maintenance warning, not an app
startup failure. Review backups first, then acknowledge the new runtime
collation version inside the running container:
```bash
docker exec -it geointel psql -U "${GEOINTEL_POSTGRES_USER:-geointel}" -d "${GEOINTEL_POSTGRES_DB:-geointel}"
ALTER DATABASE "geointel" REFRESH COLLATION VERSION;
```
If you rely on text indexes with locale-specific ordering, plan a maintenance
window and rebuild the affected indexes before acknowledging the version. The
current GeoIntel V1 spatial workflows primarily use UUIDs, JSON metadata and
PostGIS geometry indexes, but the warning should still be tracked explicitly.
## Change the browser port
Edit `.env`:
```env
GEOINTEL_FRONTEND_PORT=1203
GEOINTEL_CORS_ORIGINS=http://localhost:1203,http://127.0.0.1:1203,http://192.0.2.10:1203
```
Apply:
```bash
bash deploy/unraid/deploy-release.sh
```
## Persistent paths
Recommended Unraid paths:
```env
GEOINTEL_STORAGE_PATH=/mnt/user/appdata/geointel/storage
GEOINTEL_POSTGIS_DATA_PATH=/mnt/user/appdata/geointel/postgres-data
```
`GEOINTEL_STORAGE_PATH` contains uploads, tiles, masks, reports and exports.
`GEOINTEL_POSTGIS_DATA_PATH` contains the embedded PostGIS database files.
## Local Ollama assistant
The repository Compose file, DockerMan template and automatic deployment all
map `host.docker.internal` to the Unraid host and enable the source-grounded
assistant by default. Ollama must already listen on host port `11434`;
GeoIntel does not install or expose Ollama itself.
```env
OLLAMA_ENABLED=true
OLLAMA_BASE_URL=http://host.docker.internal:11434
OLLAMA_DEFAULT_MODEL=qwen3.5:9b
OLLAMA_TIMEOUT_SECONDS=120
OLLAMA_MAX_OUTPUT_TOKENS=1200
OLLAMA_CONTEXT_TOKENS=16384
```
`/health/live` proves only that FastAPI is serving. `/health/ready` returns
HTTP 503 when PostgreSQL, PostGIS, the migration head or persistent storage is
not ready, and is the container healthcheck. The all-in-one startup also marks
work left in `running` by a previous process as failed with
`PROCESS_INTERRUPTED`; synchronous work cannot survive a container restart.
The model dropdown comes from Ollama `/api/tags`, so changing the installed
models requires no frontend rebuild. Verify after deployment with:
```bash
curl http://127.0.0.1:1202/api/v1/assistant/status
curl http://127.0.0.1:1202/api/v1/assistant/models
```
## Update from Gitea
```bash
cd /mnt/user/appdata/geointel
git fetch origin main
git reset --hard origin/main
bash deploy/unraid/deploy-release.sh
```
The equivalent low-level GIS-only build remains available only for local
debugging; it is not a production deployment path:
```bash
docker build --build-arg GEOINTEL_INSTALL_AI=${GEOINTEL_INSTALL_AI:-false} -f deploy/unraid/Dockerfile.all-in-one -t geointel-all-in-one:latest .
bash deploy/unraid/run-dockerman-container.sh
```
## Release identity, fresh install and rollback
The complete final-release order, including backup/restore, browser journeys,
SBOM, vulnerability policy, SSH-signed manifest and checksums, is in
`docs/RELEASE_RUNBOOK.md`. `VERSION` is the canonical semantic version and is
also written to the image's `org.opencontainers.image.version` label.
Inspect the running immutable revision and retained images:
```bash
docker inspect --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' geointel
docker image ls geointel-all-in-one
```
Run a fresh install in isolated temporary PostGIS, storage and model paths. The
script binds only a random loopback port and removes its own container and
temporary directory:
```bash
bash scripts/verify_release_fresh_install.sh geointel-all-in-one:latest
```
Verify an upgrade against an isolated restore of a checksum-verified backup.
The generated temporary database is removed even when the check fails:
```bash
bash scripts/verify_release_upgrade_smoke.sh \
--backup-dir /mnt/user/appdata/geointel/backups/<verified-backup> \
--confirm-isolated-upgrade
```
Return to the image that was active immediately before the latest deployment:
```bash
bash deploy/unraid/rollback-dockerman-container.sh \
--backup-dir /mnt/user/appdata/geointel/backups/<predeploy-release-id> \
--confirm-production-database-restore
```
For an older retained commit, select its immutable tag explicitly:
```bash
GEOINTEL_ROLLBACK_IMAGE=geointel-all-in-one:<commit-sha>-ai \
bash deploy/unraid/rollback-dockerman-container.sh \
--backup-dir /mnt/user/appdata/geointel/backups/<matching-predeploy-release-id> \
--confirm-production-database-restore
```
Rollback restores and verifies the selected dump in an isolated proof database
before any production replacement. It then swaps database names, retains the
pre-restore production database for operator recovery, reuses the configured
storage mount and never runs an Alembic downgrade or an older app against an
unknown newer schema. Remove the retained recovery database and old backup
directories only in a separately reviewed operator retention step; deployment
never deletes them automatically.
The configured upload limit is shared by FastAPI and the generated nginx
runtime configuration. Values outside `1..2048` MiB are rejected before the
active application is replaced.
## Data operations
`GEOINTEL_BACKUPS_PATH` defaults to
`/mnt/user/appdata/geointel/backups` and is mounted read-only at
`/app/backups`. Explicit cleanup commands can therefore verify a recent
backup without permission to alter it.
These commands are non-mutating:
```bash
docker exec geointel python /app/scripts/audit_data_operations.py
docker exec geointel python /app/scripts/cleanup_storage_artifacts.py
```
The full backup, confirmation, candidate-limit and apply sequence is in
`docs/DATA_OPERATIONS_RUNBOOK.md`. Apply moves bytes to protected,
checksum-bound quarantine rather than deleting them; a separate confirmed
restore command reverses the move. GeoIntel installs no automatic cleanup
schedule.
## Safe cleanup
Safe cache cleanup if Docker build cache fills the Unraid Docker image:
```bash
docker builder prune -af
```
Avoid broad volume pruning unless you explicitly intend to remove persisted PostGIS data or GeoIntel artifacts.
## Multi-container development stack
The root `docker-compose.yml` remains available for development and CI-like validation with separate `db`, `backend` and `frontend` services. For Unraid app-style operation, prefer `docker-compose.unraid.yml`.
+109
View File
@@ -0,0 +1,109 @@
#!/usr/bin/env bash
set -euo pipefail
export POSTGRES_DB="${GEOINTEL_POSTGRES_DB:-${POSTGRES_DB:-geointel}}"
export POSTGRES_USER="${GEOINTEL_POSTGRES_USER:-${POSTGRES_USER:-geointel}}"
export POSTGRES_PASSWORD="${GEOINTEL_POSTGRES_PASSWORD:-${POSTGRES_PASSWORD:-geointel}}"
export PGDATA="${PGDATA:-/var/lib/postgresql/data}"
export STORAGE_ROOT="${STORAGE_ROOT:-${GEOINTEL_STORAGE_ROOT:-/app/storage}}"
export DATABASE_URL="${DATABASE_URL:-postgresql+psycopg://${POSTGRES_USER}:${POSTGRES_PASSWORD}@127.0.0.1:5432/${POSTGRES_DB}}"
export CORS_ORIGINS="${GEOINTEL_CORS_ORIGINS:-${CORS_ORIGINS:-http://localhost:1202,http://127.0.0.1:1202}}"
export MAX_UPLOAD_MB="${GEOINTEL_MAX_UPLOAD_MB:-${MAX_UPLOAD_MB:-500}}"
export YOLO_MODELS_DIR="${YOLO_MODELS_DIR:-/app/models}"
export YOLO_CONFIG_DIR="${YOLO_CONFIG_DIR:-$STORAGE_ROOT/ultralytics}"
export GEOINTEL_RECONCILE_INTERRUPTED_RUNS_ON_STARTUP="${GEOINTEL_RECONCILE_INTERRUPTED_RUNS_ON_STARTUP:-true}"
case "$MAX_UPLOAD_MB" in
''|*[!0-9]*)
echo "GEOINTEL_MAX_UPLOAD_MB must be a whole number between 1 and 2048." >&2
exit 2
;;
esac
if [ "$MAX_UPLOAD_MB" -lt 1 ] || [ "$MAX_UPLOAD_MB" -gt 2048 ]; then
echo "GEOINTEL_MAX_UPLOAD_MB must be between 1 and 2048." >&2
exit 2
fi
case "${GEOINTEL_ENV:-production}:${POSTGRES_PASSWORD}" in
production:|production:geointel|production:postgres|production:password|production:changeme|production:change-me-before-shared-use)
echo "Refusing to start production with an empty or known-default PostGIS password." >&2
exit 2
;;
esac
sed -i "s/__GEOINTEL_MAX_UPLOAD_MB__/${MAX_UPLOAD_MB}/g" /etc/nginx/conf.d/default.conf
nginx -t
mkdir -p "$PGDATA" "$STORAGE_ROOT" "$YOLO_CONFIG_DIR" /run/nginx /var/log/nginx
# The persisted database can contain millions of relation files. A recursive
# chown on every start rewrites metadata even when ownership is already right,
# delaying startup and creating avoidable recovery I/O.
chown postgres:postgres "$PGDATA"
postgres_pid=""
backend_pid=""
shutdown() {
if [ -n "$backend_pid" ] && kill -0 "$backend_pid" 2>/dev/null; then
kill "$backend_pid" 2>/dev/null || true
fi
if [ -n "$postgres_pid" ] && kill -0 "$postgres_pid" 2>/dev/null; then
kill "$postgres_pid" 2>/dev/null || true
wait "$postgres_pid" 2>/dev/null || true
fi
}
trap shutdown INT TERM EXIT
echo "Starting embedded PostGIS..."
/usr/local/bin/docker-entrypoint.sh postgres &
postgres_pid="$!"
echo "Waiting for embedded PostGIS..."
# Large persistent PostGIS volumes can require several minutes of crash
# recovery after an interrupted container replacement. Keep the wait bounded,
# but do not terminate recovery at the former two-minute ceiling.
for attempt in $(seq 1 450); do
if pg_isready -h 127.0.0.1 -U "$POSTGRES_USER" -d "$POSTGRES_DB" >/dev/null 2>&1; then
echo "PostGIS is ready after attempt ${attempt}."
break
fi
if ! kill -0 "$postgres_pid" 2>/dev/null; then
echo "PostGIS process exited before becoming ready."
wait "$postgres_pid"
fi
sleep 2
done
if ! pg_isready -h 127.0.0.1 -U "$POSTGRES_USER" -d "$POSTGRES_DB" >/dev/null 2>&1; then
echo "PostGIS did not become ready within 15 minutes."
exit 1
fi
echo "Applying Alembic migrations..."
python -m alembic upgrade head
echo "Starting GeoIntel backend..."
uvicorn app.main:app --host 127.0.0.1 --port 8000 &
backend_pid="$!"
echo "Waiting for GeoIntel backend..."
python - <<'PY'
import time
import urllib.request
last_error = None
for attempt in range(1, 61):
try:
urllib.request.urlopen("http://127.0.0.1:8000/health/ready", timeout=3).read()
print(f"Backend is ready after attempt {attempt}.")
break
except Exception as exc:
last_error = exc
print(f"Backend not ready yet ({attempt}/60): {exc}")
time.sleep(1)
else:
raise SystemExit(f"Backend did not become ready: {last_error}")
PY
echo "Starting nginx frontend on container port 80..."
exec nginx -g "daemon off;"
+630
View File
@@ -0,0 +1,630 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$ROOT"
GEOINTEL_DEPLOY_LOCK_FILE="${GEOINTEL_DEPLOY_LOCK_FILE:-/tmp/geointel-release-deploy.lock}"
if ! command -v flock >/dev/null 2>&1; then
echo "GeoIntel release deployment requires flock to prevent concurrent container replacement." >&2
exit 2
fi
exec 9>"$GEOINTEL_DEPLOY_LOCK_FILE"
if ! flock -n 9; then
echo "Another GeoIntel release deployment is already running." >&2
exit 3
fi
if [ -f .env ]; then
set -a
# shellcheck disable=SC1091
. ./.env
set +a
fi
if [ -n "${DEPLOY_GEOINTEL_INSTALL_AI:-}" ]; then
GEOINTEL_INSTALL_AI="$DEPLOY_GEOINTEL_INSTALL_AI"
fi
GEOINTEL_INSTALL_AI="${GEOINTEL_INSTALL_AI:-true}"
if [ "$GEOINTEL_INSTALL_AI" != "true" ]; then
echo "Production release deployment requires the gated AI image (GEOINTEL_INSTALL_AI=true)." >&2
exit 2
fi
GEOINTEL_APP_VERSION="$(tr -d '[:space:]' < VERSION)"
if ! [[ "$GEOINTEL_APP_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
echo "Invalid semantic version in VERSION: ${GEOINTEL_APP_VERSION}" >&2
exit 2
fi
# Hash of everything that actually lands in the image. This is the honest
# answer to "does this image need rebuilding?" — unlike a git SHA, it changes
# when working-tree files change without a commit.
source_tree_hash() {
local hash=""
command -v sha1sum >/dev/null 2>&1 || return 1
hash="$(
find backend frontend deploy scripts fixtures VERSION \
-type f \
! -path '*/node_modules/*' \
! -path '*/dist/*' \
! -path '*/__pycache__/*' \
! -path '*/.pytest_cache/*' \
! -name '*.pyc' \
-print0 2>/dev/null \
| sort -z \
| xargs -0 sha1sum 2>/dev/null \
| sha1sum \
| cut -c1-40
)" || return 1
[ -n "$hash" ] || return 1
printf '%s' "$hash"
}
resolve_build_sha() {
local head="" content="" controller_sha="" controller_source=""
local git_top="" marker_sha="" marker_path="$ROOT/.gitea-deploy/revision"
if [ -n "${GITEA_COMMIT_SHA:-}" ]; then
controller_sha="$GITEA_COMMIT_SHA"
controller_source="GITEA_COMMIT_SHA"
fi
if [ -n "${GITHUB_SHA:-}" ]; then
if ! [[ "$GITHUB_SHA" =~ ^[0-9A-Fa-f]{40}$ ]]; then
echo "GITHUB_SHA must contain one full 40-character Git commit SHA." >&2
return 2
fi
if [ -n "$controller_sha" ] && [ "${controller_sha,,}" != "${GITHUB_SHA,,}" ]; then
echo "Controller commit variables disagree." >&2
return 2
fi
controller_sha="$GITHUB_SHA"
controller_source="${controller_source:-GITHUB_SHA}"
fi
if [ -n "$controller_sha" ]; then
if ! [[ "$controller_sha" =~ ^[0-9A-Fa-f]{40}$ ]]; then
echo "${controller_source} must contain one full 40-character Git commit SHA." >&2
return 2
fi
controller_sha="${controller_sha,,}"
if [ -n "${GEOINTEL_BUILD_SHA:-}" ] && [ "${GEOINTEL_BUILD_SHA,,}" != "$controller_sha" ]; then
echo "Explicit build revision differs from the controller revision." >&2
return 2
fi
if [ -f "$marker_path" ]; then
marker_sha="$(tr -d '[:space:]' < "$marker_path")"
if ! [[ "$marker_sha" =~ ^[0-9A-Fa-f]{40}$ ]]; then
echo "Prepared source revision marker is invalid." >&2
return 2
fi
if [ "${marker_sha,,}" != "$controller_sha" ]; then
echo "Prepared source revision marker does not match the controller revision." >&2
return 2
fi
fi
if command -v git >/dev/null 2>&1 && git rev-parse --git-dir >/dev/null 2>&1; then
git_top="$(git rev-parse --show-toplevel 2>/dev/null || true)"
fi
if [ -n "$git_top" ] && [ "$(cd "$git_top" && pwd -P)" = "$(pwd -P)" ]; then
head="$(git rev-parse HEAD 2>/dev/null || true)"
if [ "${head,,}" != "$controller_sha" ]; then
echo "Prepared Git checkout does not match the controller revision." >&2
return 2
fi
if [ -n "$(git status --porcelain 2>/dev/null)" ]; then
echo "Prepared Git checkout contains changes outside the controller revision." >&2
return 2
fi
elif [ -z "$marker_sha" ]; then
echo "Prepared source is neither an exact Git checkout nor bound by a controller revision marker." >&2
return 2
fi
printf '%s' "$controller_sha"
return 0
fi
if [ -n "${GITEA_REPOSITORY:-}" ] || [ -n "${GITHUB_REPOSITORY:-}" ]; then
echo "Automated deployment context is missing GITEA_COMMIT_SHA/GITHUB_SHA." >&2
return 2
fi
# 1. Explicit override wins.
if [ -n "${GEOINTEL_BUILD_SHA:-}" ]; then
printf '%s' "$GEOINTEL_BUILD_SHA"
return 0
fi
# 2. Git checkout, but only when the working tree matches the commit.
# A manually copied tree often carries .git along while the files on disk
# have moved on. Trusting HEAD there produces an unchanged image tag, and
# the deploy silently reuses the previous image instead of rebuilding.
if command -v git >/dev/null 2>&1 && git rev-parse --git-dir >/dev/null 2>&1; then
head="$(git rev-parse HEAD 2>/dev/null || true)"
if [ -n "$head" ]; then
if [ -z "$(git status --porcelain 2>/dev/null)" ]; then
printf '%s' "$head"
return 0
fi
echo "Working tree differs from HEAD; tagging this build by content." >&2
content="$(source_tree_hash || true)"
if [ -n "$content" ]; then
printf '%s-wip%s' "${head:0:12}" "${content:0:12}"
return 0
fi
fi
fi
# 3. Manually copied tree with a RELEASE_SHA marker file.
if [ -f RELEASE_SHA ]; then
tr -d '[:space:]' < RELEASE_SHA
return 0
fi
# 4. No git: content hash, so an unchanged redeploy still reuses its image.
content="$(source_tree_hash || true)"
if [ -n "$content" ]; then
printf '%s' "$content"
return 0
fi
# 5. Last resort: unique per deploy.
printf 'manual%s' "$(date -u +%Y%m%d%H%M%S)"
}
GEOINTEL_BUILD_SHA="$(resolve_build_sha)"
if [ -z "$GEOINTEL_BUILD_SHA" ]; then
echo "Could not determine a build revision for this deployment." >&2
exit 2
fi
export GEOINTEL_BUILD_SHA
echo "Build revision: ${GEOINTEL_BUILD_SHA}"
GEOINTEL_RELEASE_TOKEN="$(printf '%s' "$GEOINTEL_BUILD_SHA" | tr -c 'A-Za-z0-9._-' '_' | cut -c1-48)"
if [ -z "$GEOINTEL_RELEASE_TOKEN" ]; then
echo "Could not derive a safe release evidence identifier." >&2
exit 2
fi
GEOINTEL_BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
GEOINTEL_IMAGE_REPOSITORY="${GEOINTEL_IMAGE_REPOSITORY:-geointel-all-in-one}"
if [ "$GEOINTEL_INSTALL_AI" = "true" ]; then
GEOINTEL_RELEASE_VARIANT="ai"
else
GEOINTEL_RELEASE_VARIANT="gis"
fi
GEOINTEL_RELEASE_IMAGE="${GEOINTEL_IMAGE_REPOSITORY}:${GEOINTEL_BUILD_SHA}-${GEOINTEL_RELEASE_VARIANT}"
FRONTEND_URL="${FRONTEND_URL:-http://127.0.0.1:${GEOINTEL_FRONTEND_PORT:-1202}}"
GEOINTEL_BACKUPS_PATH="${GEOINTEL_BACKUPS_PATH:-/mnt/user/appdata/geointel/backups}"
GEOINTEL_STORAGE_PATH="${GEOINTEL_STORAGE_PATH:-/mnt/user/appdata/geointel/storage}"
GEOINTEL_MODELS_PATH="${GEOINTEL_MODELS_PATH:-/mnt/user/appdata/geointel/models}"
GEOINTEL_POSTGIS_DATA_PATH="${GEOINTEL_POSTGIS_DATA_PATH:-/mnt/user/appdata/geointel/postgres-data}"
GEOINTEL_DEPLOY_EVIDENCE_DIR="${GEOINTEL_DEPLOY_EVIDENCE_DIR:-artifacts/release-evidence/deploy/${GEOINTEL_RELEASE_TOKEN}-ai}"
GEOINTEL_PREDEPLOY_BACKUP_DIR=""
GEOINTEL_RELEASE_IMAGE_ID=""
GEOINTEL_BACKUP_LINK_DEST=""
GEOINTEL_PREDEPLOY_ROLLBACK_TAG=""
case "$GEOINTEL_DEPLOY_EVIDENCE_DIR" in
/*|*..*)
echo "Deployment evidence directory must be repository-relative and must not contain '..'." >&2
exit 2
;;
esac
wait_for_geointel_health() {
local status=""
for attempt in $(seq 1 480); do
status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' geointel 2>/dev/null || true)"
if [ "$status" = "healthy" ]; then
echo "GeoIntel container is healthy after attempt ${attempt}."
return 0
fi
if [ "$status" = "unhealthy" ] || [ "$status" = "exited" ] || [ "$status" = "dead" ]; then
echo "GeoIntel container entered terminal state: ${status}" >&2
docker logs --tail 120 geointel >&2 || true
return 1
fi
sleep 2
done
echo "GeoIntel container did not become healthy (last state: ${status:-missing})." >&2
docker logs --tail 120 geointel >&2 || true
return 1
}
start_image() {
local image="$1"
local running_image_id=""
local running_revision=""
local running_ai=""
GEOINTEL_IMAGE="$image" bash deploy/unraid/run-dockerman-container.sh
wait_for_geointel_health
running_image_id="$(docker inspect --format '{{.Image}}' geointel)"
if [ "$running_image_id" != "$image" ]; then
echo "Running container image ${running_image_id} differs from attested image ${image}." >&2
return 1
fi
running_revision="$(docker inspect --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' geointel)"
running_ai="$(docker inspect --format '{{index .Config.Labels "io.geointel.ai.enabled"}}' geointel)"
if [ "$running_revision" != "$GEOINTEL_BUILD_SHA" ] || [ "$running_ai" != "true" ]; then
echo "Running container labels do not match the attested AI revision." >&2
return 1
fi
echo "Running container matches attested image: ${running_image_id}"
}
scan_release_image() {
local scanned_image_id=""
local current_image_id=""
local inspect_output="${GEOINTEL_DEPLOY_EVIDENCE_DIR}/image-inspect.json"
local sbom_output="${GEOINTEL_DEPLOY_EVIDENCE_DIR}/geointel-sbom.spdx.json"
local vulnerability_output="${GEOINTEL_DEPLOY_EVIDENCE_DIR}/geointel-container-vulnerabilities.json"
local attestation_output="${GEOINTEL_DEPLOY_EVIDENCE_DIR}/deployment-attestation.json"
scanned_image_id="$(docker image inspect --format '{{.Id}}' "$GEOINTEL_RELEASE_IMAGE")"
test -n "$scanned_image_id"
mkdir -p "$ROOT/$GEOINTEL_DEPLOY_EVIDENCE_DIR"
docker image inspect "$GEOINTEL_RELEASE_IMAGE" > "$ROOT/$inspect_output"
(
export GEOINTEL_IMAGE_ARCHIVE="${GEOINTEL_DEPLOY_EVIDENCE_DIR}/geointel-image.tar"
export GEOINTEL_KEEP_IMAGE_ARCHIVE=true
export SYFT_PARALLELISM=1
trap 'rm -f -- \
"$ROOT/$GEOINTEL_IMAGE_ARCHIVE" \
"$ROOT/$GEOINTEL_IMAGE_ARCHIVE.image-id" \
"$ROOT/$GEOINTEL_IMAGE_ARCHIVE".partial.*' EXIT
bash scripts/generate_container_sbom.sh "$scanned_image_id" "$sbom_output"
bash scripts/scan_container_image.sh "$scanned_image_id" "$vulnerability_output"
)
current_image_id="$(docker image inspect --format '{{.Id}}' "$GEOINTEL_RELEASE_IMAGE")"
if [ "$current_image_id" != "$scanned_image_id" ]; then
echo "Release image tag changed while SBOM/scan evidence was being generated." >&2
return 1
fi
test -s "$ROOT/$inspect_output"
test -s "$ROOT/$sbom_output"
test -s "$ROOT/$vulnerability_output"
GEOINTEL_RELEASE_IMAGE_ID="$scanned_image_id"
python3 - \
"$ROOT/$attestation_output" \
"$GEOINTEL_RELEASE_IMAGE" \
"$GEOINTEL_RELEASE_IMAGE_ID" \
"$GEOINTEL_BUILD_SHA" \
"$inspect_output" \
"$sbom_output" \
"$vulnerability_output" <<'PY'
import datetime
import json
import pathlib
import sys
output, image_tag, image_id, revision, inspect_path, sbom_path, vulnerability_path = sys.argv[1:]
payload = {
"schema_version": 1,
"attested_at": datetime.datetime.now(datetime.timezone.utc).isoformat(),
"image_tag": image_tag,
"image_id": image_id,
"image_config_digest": image_id,
"revision": revision,
"variant": "ai",
"evidence": {
"image_inspect": inspect_path,
"sbom": sbom_path,
"vulnerabilities": vulnerability_path,
},
}
path = pathlib.Path(output)
temporary = path.with_suffix(".json.partial")
temporary.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
temporary.replace(path)
PY
test -s "$ROOT/$attestation_output"
echo "Exact deployment image scanned: ${GEOINTEL_RELEASE_IMAGE_ID}"
}
preflight_backup_capacity() {
local database_name=""
local database_user=""
local database_size_bytes=""
database_name="$(docker exec geointel sh -c 'printf %s "${POSTGRES_DB:-${GEOINTEL_POSTGRES_DB:-geointel}}"')"
database_user="$(docker exec geointel sh -c 'printf %s "${POSTGRES_USER:-${GEOINTEL_POSTGRES_USER:-geointel}}"')"
database_size_bytes="$(docker exec geointel psql -X -v ON_ERROR_STOP=1 \
-U "$database_user" -d "$database_name" -Atqc \
'SELECT pg_database_size(current_database());')"
mkdir -p "$GEOINTEL_BACKUPS_PATH"
python3 - \
"$GEOINTEL_BACKUPS_PATH" \
"$GEOINTEL_STORAGE_PATH" \
"$GEOINTEL_MODELS_PATH" \
"$database_size_bytes" <<'PY'
import os
import pathlib
import shutil
import stat
import sys
backup_root = pathlib.Path(sys.argv[1]).expanduser().resolve()
sources = [pathlib.Path(value).expanduser().resolve() for value in sys.argv[2:4]]
database_bytes = int(sys.argv[4])
def retained_bytes(root: pathlib.Path) -> int:
if not root.is_dir():
raise SystemExit(f"Mandatory snapshot source is not a directory: {root}")
total = 0
for current, directories, files in os.walk(root, topdown=True, followlinks=False):
current_path = pathlib.Path(current)
for name in [*directories, *files]:
path = current_path / name
details = path.lstat()
if stat.S_ISLNK(details.st_mode):
raise SystemExit(f"Mandatory snapshot refuses symlinked content: {path}")
if name in directories and not stat.S_ISDIR(details.st_mode):
raise SystemExit(f"Snapshot directory changed during capacity preflight: {path}")
if name in files:
if not stat.S_ISREG(details.st_mode):
raise SystemExit(f"Mandatory snapshot refuses non-regular content: {path}")
total += details.st_size
return total
source_bytes = sum(retained_bytes(source) for source in sources)
# Reflink clones are used when the backing filesystem supports them. Budget for
# a complete copy plus two uncompressed database sizes (dump and isolated
# restore/cutover recovery) so fallback still fails before the live backend is
# quiesced rather than midway through the snapshot.
required = source_bytes + (2 * database_bytes)
headroom = max(5 * 1024**3, required // 10)
free = shutil.disk_usage(backup_root).free
if free < required + headroom:
raise SystemExit(
"Insufficient free space for a fail-safe predeploy snapshot: "
f"required={required + headroom} free={free} source={source_bytes} database={database_bytes}"
)
print(
"Predeploy snapshot capacity: "
f"source_bytes={source_bytes} database_bytes={database_bytes} free_bytes={free}"
)
PY
}
run_low_impact() {
local priority_command=()
# Backups are mandatory, but their first byte-complete copy and SHA-256
# verification must not starve the live Unraid services. BusyBox hosts do
# not always provide both tools, so use every available scheduler without
# weakening the backup when one is absent.
if command -v ionice >/dev/null 2>&1; then
priority_command+=(ionice -c 2 -n 7)
fi
if command -v nice >/dev/null 2>&1; then
priority_command+=(nice -n 10)
fi
if [ "${#priority_command[@]}" -eq 0 ]; then
"$@"
return
fi
"${priority_command[@]}" "$@"
}
select_verified_link_dest() {
local candidate=""
GEOINTEL_BACKUP_LINK_DEST=""
while IFS= read -r candidate; do
if (
cd "$candidate" \
&& run_low_impact sha256sum -c CHECKSUMS.sha256 >/dev/null \
&& run_low_impact python3 "$ROOT/scripts/release_backup_snapshot.py" verify-backup --backup-dir "$candidate"
); then
GEOINTEL_BACKUP_LINK_DEST="$candidate"
echo "Using verified prior byte snapshot as link-dest: ${candidate}"
return 0
fi
echo "Skipping unusable prior backup link-dest: ${candidate}" >&2
done < <(
python3 - "$GEOINTEL_BACKUPS_PATH" <<'PY'
import pathlib
import sys
root = pathlib.Path(sys.argv[1]).expanduser().resolve()
candidates = sorted(
(
path
for path in root.iterdir()
if path.is_dir() and not path.name.startswith(".") and (path / "manifest.json").is_file()
),
key=lambda path: path.stat().st_mtime_ns,
reverse=True,
)
for candidate in candidates:
print(candidate)
PY
)
echo "No verified prior byte snapshot found; this deployment will create a full first snapshot."
}
create_predeploy_backup() {
local container_exists="false"
local container_running="false"
local release_id=""
local backup_link_args=()
local current_image_id=""
local existing_rollback_id=""
if docker ps -a --format '{{.Names}}' | grep -Fxq geointel; then
container_exists="true"
fi
if [ "$(docker inspect -f '{{.State.Running}}' geointel 2>/dev/null || true)" = "true" ]; then
container_running="true"
fi
if [ "$container_exists" = "false" ]; then
if [ -f "$GEOINTEL_POSTGIS_DATA_PATH/PG_VERSION" ]; then
echo "PostGIS data exists without a running GeoIntel container; refusing an unbacked migration." >&2
return 1
fi
echo "No existing GeoIntel state found; pre-deploy backup is not required for this fresh install."
return 0
fi
if [ "$container_running" != "true" ]; then
echo "Existing GeoIntel container is not running; refusing deployment because a consistent backup cannot be created." >&2
return 1
fi
current_image_id="$(docker inspect --format '{{.Image}}' geointel)"
if ! [[ "$current_image_id" =~ ^sha256:[0-9a-f]{64}$ ]]; then
echo "Running release does not expose one immutable Docker image ID." >&2
return 1
fi
release_id="predeploy-${GEOINTEL_RELEASE_TOKEN:0:24}-$(date -u +%Y%m%dT%H%M%SZ)-$$"
GEOINTEL_PREDEPLOY_ROLLBACK_TAG="${GEOINTEL_IMAGE_REPOSITORY}:rollback-${release_id}"
existing_rollback_id="$(docker image inspect --format '{{.Id}}' "$GEOINTEL_PREDEPLOY_ROLLBACK_TAG" 2>/dev/null || true)"
if [ -n "$existing_rollback_id" ] && [ "$existing_rollback_id" != "$current_image_id" ]; then
echo "Backup-specific rollback tag already identifies different image bytes." >&2
return 1
fi
docker tag "$current_image_id" "$GEOINTEL_PREDEPLOY_ROLLBACK_TAG"
# This conservative full-copy fallback estimate runs while the existing
# release is still healthy. Verified backup-to-backup hardlinks normally
# avoid recopying unchanged bytes, but are never assumed for this fail-closed
# capacity decision.
preflight_backup_capacity
select_verified_link_dest
if [ -n "$GEOINTEL_BACKUP_LINK_DEST" ]; then
backup_link_args=(--link-dest-backup "$GEOINTEL_BACKUP_LINK_DEST")
fi
echo "Quiescing the current backend so the rollback point cannot miss concurrent writes..."
if ! docker exec -i geointel python - <<'PY'
import os
import pathlib
import signal
import time
matches = []
for item in pathlib.Path("/proc").iterdir():
if not item.name.isdigit() or int(item.name) in {os.getpid(), os.getppid()}:
continue
try:
command = (item / "cmdline").read_bytes().replace(b"\0", b" ")
except (OSError, PermissionError):
continue
if b"uvicorn" in command and b"app.main:app" in command:
matches.append(int(item.name))
if not matches:
raise SystemExit("Could not identify the running GeoIntel backend")
for process_id in matches:
os.kill(process_id, signal.SIGTERM)
deadline = time.monotonic() + 60
remaining = matches
while remaining and time.monotonic() < deadline:
time.sleep(0.25)
remaining = [process_id for process_id in remaining if pathlib.Path(f"/proc/{process_id}").exists()]
if remaining:
raise SystemExit(f"Backend did not stop cleanly: {remaining}")
print(f"Stopped {len(matches)} backend process(es)")
PY
then
echo "Could not quiesce the current backend; refusing a potentially inconsistent backup." >&2
docker restart geointel >/dev/null || true
wait_for_geointel_health || true
return 1
fi
GEOINTEL_PREDEPLOY_BACKUP_DIR="${GEOINTEL_BACKUPS_PATH%/}/${release_id}"
echo "Creating mandatory pre-deploy backup ${release_id}..."
if ! run_low_impact bash scripts/backup_release_state.sh \
--container geointel \
--output-root "$GEOINTEL_BACKUPS_PATH" \
--release-id "$release_id" \
--storage-path "$GEOINTEL_STORAGE_PATH" \
--models-path "$GEOINTEL_MODELS_PATH" \
--inventory-mode sha256 \
--rollback-image-tag "$GEOINTEL_PREDEPLOY_ROLLBACK_TAG" \
"${backup_link_args[@]}" \
|| ! run_low_impact bash scripts/verify_release_backup.sh \
--container geointel \
--backup-dir "$GEOINTEL_PREDEPLOY_BACKUP_DIR"; then
echo "Pre-deploy backup failed; restarting the unchanged current release." >&2
docker restart geointel >/dev/null || true
wait_for_geointel_health || true
GEOINTEL_PREDEPLOY_BACKUP_DIR=""
return 1
fi
echo "Pre-deploy backup verified: ${GEOINTEL_PREDEPLOY_BACKUP_DIR}"
}
rollback_previous() {
if [ -z "$GEOINTEL_PREDEPLOY_BACKUP_DIR" ]; then
echo "Automatic rollback unavailable: no verified pre-deploy database backup was created." >&2
return 1
fi
echo "Rolling back database and image to the verified pre-deploy state..."
GEOINTEL_DEPLOY_LOCK_HELD=true \
bash deploy/unraid/rollback-dockerman-container.sh \
--backup-dir "$GEOINTEL_PREDEPLOY_BACKUP_DIR" \
--confirm-production-database-restore
}
docker compose -f docker-compose.unraid.yml config >/dev/null
if docker image inspect "$GEOINTEL_RELEASE_IMAGE" >/dev/null 2>&1; then
stored_revision="$(
docker image inspect \
--format '{{index .Config.Labels "org.opencontainers.image.revision"}}' \
"$GEOINTEL_RELEASE_IMAGE"
)"
stored_ai="$(
docker image inspect \
--format '{{index .Config.Labels "io.geointel.ai.enabled"}}' \
"$GEOINTEL_RELEASE_IMAGE"
)"
stored_version="$(
docker image inspect \
--format '{{index .Config.Labels "org.opencontainers.image.version"}}' \
"$GEOINTEL_RELEASE_IMAGE"
)"
if (
[ "$stored_revision" != "$GEOINTEL_BUILD_SHA" ] ||
[ "$stored_ai" != "$GEOINTEL_INSTALL_AI" ] ||
[ "$stored_version" != "$GEOINTEL_APP_VERSION" ]
); then
echo "Immutable release tag has conflicting metadata: ${GEOINTEL_RELEASE_IMAGE}" >&2
exit 2
fi
echo "Reusing existing immutable image ${GEOINTEL_RELEASE_IMAGE}."
docker tag "$GEOINTEL_RELEASE_IMAGE" "${GEOINTEL_IMAGE_REPOSITORY}:latest"
else
docker build \
--build-arg GEOINTEL_INSTALL_AI="$GEOINTEL_INSTALL_AI" \
--build-arg GEOINTEL_BUILD_SHA="$GEOINTEL_BUILD_SHA" \
--build-arg GEOINTEL_BUILD_TIME="$GEOINTEL_BUILD_TIME" \
--build-arg GEOINTEL_APP_VERSION="$GEOINTEL_APP_VERSION" \
-f deploy/unraid/Dockerfile.all-in-one \
-t "$GEOINTEL_RELEASE_IMAGE" \
-t "${GEOINTEL_IMAGE_REPOSITORY}:latest" \
.
fi
scan_release_image
create_predeploy_backup
if ! start_image "$GEOINTEL_RELEASE_IMAGE_ID"; then
rollback_previous || true
exit 1
fi
if [ -f scripts/live_migration_smoke.sh ]; then
if ! LIVE_SMOKE_CONTAINER=geointel bash scripts/live_migration_smoke.sh; then
rollback_previous || true
exit 1
fi
fi
if [ -f scripts/verify_browser_runtime.sh ]; then
if ! bash scripts/verify_browser_runtime.sh "$FRONTEND_URL"; then
rollback_previous || true
exit 1
fi
fi
echo "Deployed immutable image ${GEOINTEL_RELEASE_IMAGE}."
docker image inspect \
--format 'version={{index .Config.Labels "org.opencontainers.image.version"}} revision={{index .Config.Labels "org.opencontainers.image.revision"}} created={{index .Config.Labels "org.opencontainers.image.created"}}' \
"$GEOINTEL_RELEASE_IMAGE"
Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-labelledby="title desc">
<title id="title">GeoIntel</title>
<desc id="desc">Een geometrische G als geografische lens met een gemarkeerd coördinaatpunt.</desc>
<defs><linearGradient id="surface" x1="28" y1="20" x2="226" y2="236" gradientUnits="userSpaceOnUse"><stop stop-color="#123F3B"/><stop offset="1" stop-color="#061F20"/></linearGradient><linearGradient id="geo" x1="68" y1="54" x2="190" y2="207" gradientUnits="userSpaceOnUse"><stop stop-color="#B9FFF0"/><stop offset="1" stop-color="#48C9B1"/></linearGradient><filter id="lift" x="-30%" y="-30%" width="160%" height="160%"><feDropShadow dx="0" dy="8" stdDeviation="8" flood-color="#001615" flood-opacity=".45"/></filter></defs>
<rect width="256" height="256" rx="54" fill="url(#surface)"/><path d="M28 74c36-17 62-9 91-20 32-12 61-27 108-13M20 117c39-17 69-8 100-21 33-13 65-24 116-7M20 163c42-17 72-7 105-19 31-12 59-20 111-5M31 207c37-14 66-5 96-16 31-12 56-17 96-7" fill="none" stroke="#A6E8DC" stroke-opacity=".10" stroke-width="7" stroke-linecap="round"/><g filter="url(#lift)"><path d="M179 78a72 72 0 1 0 9 83" fill="none" stroke="url(#geo)" stroke-width="27" stroke-linecap="round"/><path d="M137 128h62v50" fill="none" stroke="url(#geo)" stroke-width="27" stroke-linecap="round" stroke-linejoin="round"/><circle cx="128" cy="128" r="23" fill="#082927" stroke="#DFFFF8" stroke-width="7"/><circle cx="128" cy="128" r="10" fill="#F2C864"/></g><path d="M203 52v18M194 61h18" stroke="#DFFFF8" stroke-opacity=".55" stroke-width="4" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+174
View File
@@ -0,0 +1,174 @@
<?xml version="1.0" encoding="utf-8"?>
<Container version="2">
<Name>geointel</Name>
<Repository>geointel-all-in-one:latest</Repository>
<Registry>https://gitea.example.com/owner/geointel</Registry>
<Network>bridge</Network>
<Shell>bash</Shell>
<Privileged>false</Privileged>
<Support>https://gitea.example.com/owner/geointel/issues</Support>
<Project>GeoIntel Belgium and North Sea</Project>
<Overview>GeoIntel all-in-one runs the Belgium and Belgian North Sea workbench in one Docker container: embedded PostGIS, FastAPI backend, nginx frontend and MapLibre UI. Use docker-compose.unraid.yml or this template so the web port, storage path and database path can be edited from Unraid.</Overview>
<Category>Productivity: Tools: GIS:</Category>
<WebUI>http://[IP]:[PORT:80]/</WebUI>
<TemplateURL>deploy/unraid/geointel-unraid-template.xml</TemplateURL>
<Icon>https://gitea.example.com/owner/geointel/raw/branch/main/frontend/public/geointel-icon.png</Icon>
<ExtraParams>--add-host=host.docker.internal:host-gateway</ExtraParams>
<PostArgs/>
<CPUset/>
<DateInstalled/>
<DonateText/>
<DonateLink/>
<Description>
Single-container GeoIntel runtime for Unraid. The container starts embedded PostGIS, runs Alembic migrations, starts the FastAPI backend on localhost and serves the frontend through nginx on container port 80.
</Description>
<Config Name="Web UI Port" Target="80" Default="1202" Mode="tcp" Description="Host port mapped to the GeoIntel all-in-one web UI. Change this to edit the browser port." Type="Port" Display="always" Required="true" Mask="false">1202</Config>
<Config Name="Storage Path" Target="/app/storage" Default="/mnt/user/appdata/geointel/storage" Mode="rw" Description="Persistent GeoIntel artifact storage for uploads, tiles, masks, reports and exports." Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/geointel/storage</Config>
<Config Name="AI Models Path" Target="/app/models" Default="/mnt/user/appdata/geointel/models" Mode="rw" Description="Persistent local model files mounted into the container. GeoIntel never downloads weights automatically." Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/geointel/models</Config>
<Config Name="Release Backups Path" Target="/app/backups" Default="/mnt/user/appdata/geointel/backups" Mode="ro" Description="Read-only release backups used to guard explicitly confirmed cleanup operations." Type="Path" Display="advanced" Required="true" Mask="false">/mnt/user/appdata/geointel/backups</Config>
<Config Name="PostGIS Data Path" Target="/var/lib/postgresql/data" Default="/mnt/user/appdata/geointel/postgres-data" Mode="rw" Description="Persistent embedded PostGIS data directory for the all-in-one container." Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/geointel/postgres-data</Config>
<Config Name="Postgres Database" Target="GEOINTEL_POSTGRES_DB" Default="geointel" Mode="" Description="Embedded PostGIS database name." Type="Variable" Display="advanced" Required="true" Mask="false">geointel</Config>
<Config Name="Postgres User" Target="GEOINTEL_POSTGRES_USER" Default="geointel" Mode="" Description="Embedded PostGIS database user." Type="Variable" Display="advanced" Required="true" Mask="false">geointel</Config>
<Config Name="Postgres Password" Target="GEOINTEL_POSTGRES_PASSWORD" Default="change-me-before-shared-use" Mode="" Description="Embedded PostGIS database password. Change before shared use." Type="Variable" Display="advanced" Required="true" Mask="true">change-me-before-shared-use</Config>
<Config Name="CORS Origins" Target="GEOINTEL_CORS_ORIGINS" Default="https://geointel.example.com,http://localhost:1202,http://127.0.0.1:1202" Mode="" Description="Comma-separated browser origins allowed to call the backend directly; replace the example HTTPS host." Type="Variable" Display="advanced" Required="false" Mask="false">https://geointel.example.com,http://localhost:1202,http://127.0.0.1:1202</Config>
<Config Name="Max Upload MB" Target="GEOINTEL_MAX_UPLOAD_MB" Default="500" Mode="" Description="Maximum upload size in MiB enforced consistently by nginx and the backend (1-2048)." Type="Variable" Display="advanced" Required="true" Mask="false">500</Config>
<Config Name="Max In-memory Vector MB" Target="GEOINTEL_MAX_IN_MEMORY_VECTOR_MB" Default="64" Mode="" Description="Maximum decompressed vector payload processed fully in memory by the backend (1-256 MiB)." Type="Variable" Display="advanced" Required="true" Mask="false">64</Config>
<Config Name="Operator Login Enabled" Target="GEOINTEL_AUTH_ENABLED" Default="true" Mode="" Description="Require the single configured operator login before the browser may access workbench APIs." Type="Variable" Display="advanced" Required="true" Mask="false">true</Config>
<Config Name="Require HTTPS Login" Target="GEOINTEL_AUTH_REQUIRE_HTTPS" Default="true" Mode="" Description="Reject operator password login unless the trusted proxy reports HTTPS." Type="Variable" Display="advanced" Required="true" Mask="false">true</Config>
<Config Name="Operator Username" Target="GEOINTEL_AUTH_USERNAME" Default="" Mode="" Description="Exact username for the single operator account." Type="Variable" Display="advanced" Required="false" Mask="false"></Config>
<Config Name="Operator Password Hash" Target="GEOINTEL_AUTH_PASSWORD_HASH" Default="" Mode="" Description="PBKDF2-SHA256 password hash. Never enter a plaintext password." Type="Variable" Display="advanced" Required="false" Mask="true"></Config>
<Config Name="Operator Session Secret" Target="GEOINTEL_AUTH_SESSION_SECRET" Default="" Mode="" Description="Random secret of at least 32 characters used only to sign browser sessions." Type="Variable" Display="advanced" Required="false" Mask="true"></Config>
<Config Name="Operator Session TTL" Target="GEOINTEL_AUTH_SESSION_TTL_SECONDS" Default="43200" Mode="" Description="Session lifetime in seconds (900-604800)." Type="Variable" Display="advanced" Required="true" Mask="false">43200</Config>
<Config Name="Public Base URL" Target="GEOINTEL_PUBLIC_BASE_URL" Default="https://geointel.example.com" Mode="" Description="Public HTTPS browser origin used for secure login and optional Authentik." Type="Variable" Display="advanced" Required="true" Mask="false">https://geointel.example.com</Config>
<Config Name="Authentik Issuer" Target="GEOINTEL_AUTHENTIK_ISSUER" Default="" Mode="" Description="Optional HTTPS OIDC issuer. Configure all Authentik values together." Type="Variable" Display="advanced" Required="false" Mask="false"></Config>
<Config Name="Authentik Client ID" Target="GEOINTEL_AUTHENTIK_CLIENT_ID" Default="" Mode="" Description="OIDC client identifier for GeoIntel." Type="Variable" Display="advanced" Required="false" Mask="false"></Config>
<Config Name="Authentik Client Secret" Target="GEOINTEL_AUTHENTIK_CLIENT_SECRET" Default="" Mode="" Description="OIDC client secret; never commit this value." Type="Variable" Display="advanced" Required="false" Mask="true"></Config>
<Config Name="Authentik Allowed Email" Target="GEOINTEL_AUTHENTIK_ALLOWED_EMAIL" Default="" Mode="" Description="Exact verified operator e-mail address allowed to create an operator session." Type="Variable" Display="advanced" Required="false" Mask="false"></Config>
<Config Name="Guest Demo Enabled" Target="GEOINTEL_GUEST_ACCESS_ENABLED" Default="false" Mode="" Description="Opt in to a guest button only on a dedicated demo installation with public data." Type="Variable" Display="advanced" Required="true" Mask="false">false</Config>
<Config Name="Guest Display Name" Target="GEOINTEL_GUEST_DISPLAY_NAME" Default="Gast" Mode="" Description="Label shown for the temporary guest session." Type="Variable" Display="advanced" Required="true" Mask="false">Gast</Config>
<Config Name="Guest Session TTL" Target="GEOINTEL_GUEST_SESSION_TTL_SECONDS" Default="7200" Mode="" Description="Temporary guest session lifetime in seconds (900-86400)." Type="Variable" Display="advanced" Required="true" Mask="false">7200</Config>
<Config Name="Official Orthophoto Acquisition" Target="ORTHOPHOTO_ENABLED" Default="true" Mode="" Description="Allow explicit bounded map selections to request the official Digitaal Vlaanderen orthophoto WMS." Type="Variable" Display="advanced" Required="true" Mask="false">true</Config>
<Config Name="Orthophoto WMS URL" Target="ORTHOPHOTO_WMS_URL" Default="https://geo.api.vlaanderen.be/OMWRGBMRVL/wms" Mode="" Description="Official Digitaal Vlaanderen most-recent winter orthophoto WMS endpoint." Type="Variable" Display="advanced" Required="true" Mask="false">https://geo.api.vlaanderen.be/OMWRGBMRVL/wms</Config>
<Config Name="SPW Orthophoto WMS URL" Target="SPW_ORTHOPHOTO_WMS_URL" Default="https://geoservices.wallonie.be/arcgis/services/IMAGERIE/ORTHO_LAST/MapServer/WMSServer" Mode="" Description="Official SPW latest Walloon orthophoto WMS endpoint." Type="Variable" Display="advanced" Required="true" Mask="false">https://geoservices.wallonie.be/arcgis/services/IMAGERIE/ORTHO_LAST/MapServer/WMSServer</Config>
<Config Name="Brussels Orthophoto WMS URL" Target="BRUSSELS_ORTHOPHOTO_WMS_URL" Default="https://geoservices-grid.irisnet.be/geoserver/urbisgrid/ows" Mode="" Description="Official Paradigm UrbIS Brussels orthophoto WMS endpoint." Type="Variable" Display="advanced" Required="true" Mask="false">https://geoservices-grid.irisnet.be/geoserver/urbisgrid/ows</Config>
<Config Name="Orthophoto WMS Layer" Target="ORTHOPHOTO_WMS_LAYER" Default="Ortho" Mode="" Description="Allowlisted official orthophoto WMS layer identifier." Type="Variable" Display="advanced" Required="true" Mask="false">Ortho</Config>
<Config Name="Orthophoto Resolution (m)" Target="ORTHOPHOTO_RESOLUTION_M" Default="1.0" Mode="" Description="Requested analysis sampling in metres per pixel. Keep at 1.0 for the active building model profile." Type="Variable" Display="advanced" Required="true" Mask="false">1.0</Config>
<Config Name="Orthophoto Minimum Side (m)" Target="ORTHOPHOTO_MIN_SIDE_M" Default="128" Mode="" Description="Minimum bounded request side length." Type="Variable" Display="advanced" Required="true" Mask="false">128</Config>
<Config Name="Orthophoto Maximum Side (m)" Target="ORTHOPHOTO_MAX_SIDE_M" Default="1024" Mode="" Description="Safety limit for each selected rectangle side before external acquisition and local inference." Type="Variable" Display="advanced" Required="true" Mask="false">1024</Config>
<Config Name="Orthophoto Cache (hours)" Target="ORTHOPHOTO_CACHE_TTL_HOURS" Default="24" Mode="" Description="Lifetime of reusable bounded orthophoto acquisitions." Type="Variable" Display="advanced" Required="true" Mask="false">24</Config>
<Config Name="Official Catalog Edition Probe" Target="SOURCE_CATALOG_PROBE_ENABLED" Default="true" Mode="" Description="Allow explicit read-only GRB, orthophoto, Statbel and ALZ edition checks. This never imports provider data." Type="Variable" Display="advanced" Required="true" Mask="false">true</Config>
<Config Name="GRB Catalog WFS URL" Target="SOURCE_CATALOG_GRB_WFS_URL" Default="https://geo.api.vlaanderen.be/GRB/wfs" Mode="" Description="Official GRB WFS used only for capabilities and linked ISO metadata checks." Type="Variable" Display="advanced" Required="true" Mask="false">https://geo.api.vlaanderen.be/GRB/wfs</Config>
<Config Name="Statbel DCAT URL" Target="SOURCE_CATALOG_STATBEL_DCAT_URL" Default="https://doc.statbel.be/publications/DCAT/DCAT_opendata_datasets.ttl" Mode="" Description="Exact official Statbel Turtle catalog used only to identify population-by-sector releases and distribution identities." Type="Variable" Display="advanced" Required="true" Mask="false">https://doc.statbel.be/publications/DCAT/DCAT_opendata_datasets.ttl</Config>
<Config Name="Statbel Catalog Maximum Response (MiB)" Target="SOURCE_CATALOG_STATBEL_MAX_RESPONSE_MB" Default="5" Mode="" Description="Separate bounded response limit for the official Statbel DCAT Turtle catalog; linked data files are never downloaded." Type="Variable" Display="advanced" Required="true" Mask="false">5</Config>
<Config Name="ALZ Release Page URL" Target="SOURCE_CATALOG_ALZ_RELEASE_URL" Default="https://landbouwcijfers.vlaanderen.be/open-geodata-landbouwgebruikspercelen" Mode="" Description="Exact official ALZ publication page used only to identify definitive and provisional agricultural parcel editions." Type="Variable" Display="advanced" Required="true" Mask="false">https://landbouwcijfers.vlaanderen.be/open-geodata-landbouwgebruikspercelen</Config>
<Config Name="Catalog Probe Timeout (seconds)" Target="SOURCE_CATALOG_PROBE_TIMEOUT_SECONDS" Default="10" Mode="" Description="Per-request timeout for explicit read-only official catalog checks." Type="Variable" Display="advanced" Required="true" Mask="false">10</Config>
<Config Name="Catalog Probe Maximum Response (MiB)" Target="SOURCE_CATALOG_PROBE_MAX_RESPONSE_MB" Default="2" Mode="" Description="Maximum capabilities or ISO metadata response size accepted by a catalog probe." Type="Variable" Display="advanced" Required="true" Mask="false">2</Config>
<Config Name="Catalog Probe Cache (seconds)" Target="SOURCE_CATALOG_PROBE_CACHE_TTL_SECONDS" Default="900" Mode="" Description="Short in-memory cache for repeated official edition checks; use zero to disable." Type="Variable" Display="advanced" Required="true" Mask="false">900</Config>
<Config Name="Official Regional Vector Acquisition" Target="OFFICIAL_VECTOR_ENABLED" Default="true" Mode="" Description="Allow bounded official Flemish, Walloon and Brussels vector acquisition after a map selection." Type="Variable" Display="advanced" Required="true" Mask="false">true</Config>
<Config Name="BWK WFS URL" Target="BWK_WFS_URL" Default="https://geo.api.vlaanderen.be/BWK/wfs" Mode="" Description="Official allowlisted INBO BWK and Natura 2000 WFS 2.0 endpoint." Type="Variable" Display="advanced" Required="true" Mask="false">https://geo.api.vlaanderen.be/BWK/wfs</Config>
<Config Name="DOV Soil WFS URL" Target="DOV_SOIL_WFS_URL" Default="https://www.dov.vlaanderen.be/geoserver/wfs" Mode="" Description="Official allowlisted DOV WFS endpoint for historical soil type polygons." Type="Variable" Display="advanced" Required="true" Mask="false">https://www.dov.vlaanderen.be/geoserver/wfs</Config>
<Config Name="SPW PICC Acquisition" Target="SPW_PICC_ENABLED" Default="true" Mode="" Description="Enable bounded Walloon PICC building, road and hydrography queries. Requests remain clipped, paged and read-only at source." Type="Variable" Display="advanced" Required="true" Mask="false">true</Config>
<Config Name="SPW PICC MapServer URL" Target="SPW_PICC_MAPSERVER_URL" Default="https://geoservices.wallonie.be/arcgis/rest/services/TOPOGRAPHIE/PICC_VDIFF/MapServer" Mode="" Description="Official allowlisted SPW PICC ArcGIS REST MapServer root." Type="Variable" Display="advanced" Required="true" Mask="false">https://geoservices.wallonie.be/arcgis/rest/services/TOPOGRAPHIE/PICC_VDIFF/MapServer</Config>
<Config Name="SPW Flood Hazard Acquisition" Target="SPW_FLOOD_HAZARD_ENABLED" Default="true" Mode="" Description="Enable bounded authoritative Walloon flood-hazard polygon acquisition." Type="Variable" Display="advanced" Required="true" Mask="false">true</Config>
<Config Name="SPW Flood Hazard MapServer URL" Target="SPW_FLOOD_HAZARD_MAPSERVER_URL" Default="https://geoservices.wallonie.be/arcgis/rest/services/EAU/ALEA_INOND/MapServer" Mode="" Description="Official allowlisted SPW legal flood-hazard ArcGIS REST MapServer root." Type="Variable" Display="advanced" Required="true" Mask="false">https://geoservices.wallonie.be/arcgis/rest/services/EAU/ALEA_INOND/MapServer</Config>
<Config Name="UrbIS Acquisition" Target="URBIS_ENABLED" Default="true" Mode="" Description="Enable bounded Brussels UrbIS building and cadastral parcel queries. Requests remain clipped, paged and read-only at source." Type="Variable" Display="advanced" Required="true" Mask="false">true</Config>
<Config Name="UrbIS WFS URL" Target="URBIS_WFS_URL" Default="https://geoservices-vector.irisnet.be/geoserver/urbisvector/ows" Mode="" Description="Official allowlisted Paradigm Brussels UrbIS WFS 2.0 endpoint." Type="Variable" Display="advanced" Required="true" Mask="false">https://geoservices-vector.irisnet.be/geoserver/urbisvector/ows</Config>
<Config Name="Official Vector Minimum Side (m)" Target="OFFICIAL_VECTOR_MIN_SIDE_M" Default="10" Mode="" Description="Minimum bounded official vector request side length." Type="Variable" Display="advanced" Required="true" Mask="false">10</Config>
<Config Name="Official Vector Maximum Side (m)" Target="OFFICIAL_VECTOR_MAX_SIDE_M" Default="20000" Mode="" Description="Maximum side length for one official regional vector selection before provider access." Type="Variable" Display="advanced" Required="true" Mask="false">20000</Config>
<Config Name="Official Vector Page Size" Target="OFFICIAL_VECTOR_PAGE_SIZE" Default="1000" Mode="" Description="Maximum features requested per provider page." Type="Variable" Display="advanced" Required="true" Mask="false">1000</Config>
<Config Name="Official Vector Maximum Pages" Target="OFFICIAL_VECTOR_MAX_PAGES" Default="200" Mode="" Description="Hard page limit for one bounded provider acquisition." Type="Variable" Display="advanced" Required="true" Mask="false">200</Config>
<Config Name="Official Vector Maximum Features" Target="OFFICIAL_VECTOR_MAX_FEATURES" Default="100000" Mode="" Description="Hard feature limit for one bounded official vector acquisition; larger selections fail without truncated persistence." Type="Variable" Display="advanced" Required="true" Mask="false">100000</Config>
<Config Name="Official Vector Timeout (seconds)" Target="OFFICIAL_VECTOR_TIMEOUT_SECONDS" Default="180" Mode="" Description="Per-request provider timeout." Type="Variable" Display="advanced" Required="true" Mask="false">180</Config>
<Config Name="Official Vector Maximum Response (MiB)" Target="OFFICIAL_VECTOR_MAX_RESPONSE_MB" Default="20" Mode="" Description="Maximum accepted size of one provider page." Type="Variable" Display="advanced" Required="true" Mask="false">20</Config>
<Config Name="Official Vector Total Response (MiB)" Target="OFFICIAL_VECTOR_MAX_TOTAL_RESPONSE_MB" Default="256" Mode="" Description="Hard cumulative response-size limit for one acquisition." Type="Variable" Display="advanced" Required="true" Mask="false">256</Config>
<Config Name="Official Vector Cache (hours)" Target="OFFICIAL_VECTOR_CACHE_TTL_HOURS" Default="24" Mode="" Description="Lifetime of reusable bounded vector acquisitions." Type="Variable" Display="advanced" Required="true" Mask="false">24</Config>
<Config Name="Official DHMV Acquisition" Target="DHMV_ENABLED" Default="true" Mode="" Description="Allow bounded official DHMV II terrain and surface raster acquisition." Type="Variable" Display="advanced" Required="true" Mask="false">true</Config>
<Config Name="DHMV WCS URL" Target="DHMV_WCS_URL" Default="https://geo.api.vlaanderen.be/DHMV/wcs" Mode="" Description="Official Digitaal Vlaanderen DHMV WCS endpoint." Type="Variable" Display="advanced" Required="true" Mask="false">https://geo.api.vlaanderen.be/DHMV/wcs</Config>
<Config Name="DHMV Analysis Resolution (m)" Target="DHMV_RESOLUTION_M" Default="5.0" Mode="" Description="Stored analysis grid resolution. Native source resolution remains recorded as 1 metre." Type="Variable" Display="advanced" Required="true" Mask="false">5.0</Config>
<Config Name="DHMV Minimum Side (m)" Target="DHMV_MIN_SIDE_M" Default="10" Mode="" Description="Minimum bounded terrain request side length." Type="Variable" Display="advanced" Required="true" Mask="false">10</Config>
<Config Name="DHMV Maximum Side (m)" Target="DHMV_MAX_SIDE_M" Default="20000" Mode="" Description="Maximum bounded terrain request side length." Type="Variable" Display="advanced" Required="true" Mask="false">20000</Config>
<Config Name="DHMV Maximum Cells" Target="DHMV_MAX_PIXELS" Default="12000000" Mode="" Description="Maximum raster cells per acquisition or selection analysis." Type="Variable" Display="advanced" Required="true" Mask="false">12000000</Config>
<Config Name="DHMV Timeout (seconds)" Target="DHMV_TIMEOUT_SECONDS" Default="300" Mode="" Description="Maximum wait for one bounded terrain request." Type="Variable" Display="advanced" Required="true" Mask="false">300</Config>
<Config Name="DHMV Maximum Response (MiB)" Target="DHMV_MAX_RESPONSE_MB" Default="160" Mode="" Description="Maximum accepted terrain response size." Type="Variable" Display="advanced" Required="true" Mask="false">160</Config>
<Config Name="Official VMM Flood Hazard Acquisition" Target="FLOOD_HAZARD_ENABLED" Default="true" Mode="" Description="Allow bounded official VMM flood-depth scenario acquisition." Type="Variable" Display="advanced" Required="true" Mask="false">true</Config>
<Config Name="VMM Flood Hazard WCS URL" Target="FLOOD_HAZARD_WCS_URL" Default="https://geoservice.waterinfo.be/OGRK/wcs" Mode="" Description="Official VMM OGRK WCS endpoint for governed flood-depth scenarios." Type="Variable" Display="advanced" Required="true" Mask="false">https://geoservice.waterinfo.be/OGRK/wcs</Config>
<Config Name="Flood Hazard Analysis Resolution (m)" Target="FLOOD_HAZARD_RESOLUTION_M" Default="5.0" Mode="" Description="Stored analysis grid resolution; official source values are converted from centimetres to metres." Type="Variable" Display="advanced" Required="true" Mask="false">5.0</Config>
<Config Name="Flood Hazard Minimum Side (m)" Target="FLOOD_HAZARD_MIN_SIDE_M" Default="10" Mode="" Description="Minimum bounded flood request side length." Type="Variable" Display="advanced" Required="true" Mask="false">10</Config>
<Config Name="Flood Hazard Maximum Side (m)" Target="FLOOD_HAZARD_MAX_SIDE_M" Default="20000" Mode="" Description="Maximum bounded flood request side length." Type="Variable" Display="advanced" Required="true" Mask="false">20000</Config>
<Config Name="Flood Hazard Maximum Cells" Target="FLOOD_HAZARD_MAX_PIXELS" Default="12000000" Mode="" Description="Maximum raster cells per flood-hazard acquisition or selection analysis." Type="Variable" Display="advanced" Required="true" Mask="false">12000000</Config>
<Config Name="Flood Hazard Timeout (seconds)" Target="FLOOD_HAZARD_TIMEOUT_SECONDS" Default="300" Mode="" Description="Maximum wait for one bounded flood request." Type="Variable" Display="advanced" Required="true" Mask="false">300</Config>
<Config Name="Flood Hazard Maximum Response (MiB)" Target="FLOOD_HAZARD_MAX_RESPONSE_MB" Default="160" Mode="" Description="Maximum accepted flood raster response size." Type="Variable" Display="advanced" Required="true" Mask="false">160</Config>
<Config Name="VHA Bathymetry Profiles" Target="BATHYMETRY_PROFILES_ENABLED" Default="true" Mode="" Description="Allow explicit bounded acquisition of official VHA cross-section profile points." Type="Variable" Display="advanced" Required="true" Mask="false">true</Config>
<Config Name="VHA Profile Layer URL" Target="BATHYMETRY_PROFILES_LAYER_URL" Default="https://vha.waterinfo.be/arcgis/rest/services/digitale_atlas/MapServer/0" Mode="" Description="Official VHA cross-section profile endpoint." Type="Variable" Display="advanced" Required="true" Mask="false">https://vha.waterinfo.be/arcgis/rest/services/digitale_atlas/MapServer/0</Config>
<Config Name="VHA Watercourse Layer URL" Target="BATHYMETRY_WATERCOURSE_LAYER_URL" Default="https://vha.waterinfo.be/arcgis/rest/services/digitale_atlas/MapServer/1" Mode="" Description="Official VHA watercourse context endpoint." Type="Variable" Display="advanced" Required="true" Mask="false">https://vha.waterinfo.be/arcgis/rest/services/digitale_atlas/MapServer/1</Config>
<Config Name="VHA Profile Page Size" Target="BATHYMETRY_PROFILES_PAGE_SIZE" Default="1000" Mode="" Description="Maximum features requested per ArcGIS page." Type="Variable" Display="advanced" Required="true" Mask="false">1000</Config>
<Config Name="VHA Profile Maximum Features" Target="BATHYMETRY_PROFILES_MAX_FEATURES" Default="50000" Mode="" Description="Safety limit per municipality or bounded VHA profile request." Type="Variable" Display="advanced" Required="true" Mask="false">50000</Config>
<Config Name="VHA Profile Timeout (seconds)" Target="BATHYMETRY_PROFILES_TIMEOUT_SECONDS" Default="120" Mode="" Description="Maximum wait for one profile request." Type="Variable" Display="advanced" Required="true" Mask="false">120</Config>
<Config Name="VHA Profile Maximum Response (MiB)" Target="BATHYMETRY_PROFILES_MAX_RESPONSE_MB" Default="32" Mode="" Description="Maximum accepted profile response size." Type="Variable" Display="advanced" Required="true" Mask="false">32</Config>
<Config Name="MDK Bathymetry Readiness Probe" Target="MDK_BATHYMETRY_PROBE_ENABLED" Default="true" Mode="" Description="Enable strict-TLS read-only GetCapabilities checks. This never downloads or activates a North Sea raster." Type="Variable" Display="advanced" Required="true" Mask="false">true</Config>
<Config Name="MDK Bathymetry WCS URL" Target="MDK_BATHYMETRY_WCS_URL" Default="https://bathy.agentschapmdk.be/spatialfusionserver/services/ows/wcs/EL_wcs" Mode="" Description="Official metadata WCS endpoint. TLS verification is mandatory and cannot be bypassed." Type="Variable" Display="advanced" Required="true" Mask="false">https://bathy.agentschapmdk.be/spatialfusionserver/services/ows/wcs/EL_wcs</Config>
<Config Name="MDK Probe Timeout Seconds" Target="MDK_BATHYMETRY_PROBE_TIMEOUT_SECONDS" Default="20" Mode="" Description="Maximum wait for one read-only MDK GetCapabilities request." Type="Variable" Display="advanced" Required="true" Mask="false">20</Config>
<Config Name="MDK Probe Maximum Response (MiB)" Target="MDK_BATHYMETRY_PROBE_MAX_RESPONSE_MB" Default="4" Mode="" Description="Maximum accepted MDK capabilities response size." Type="Variable" Display="advanced" Required="true" Mask="false">4</Config>
<Config Name="MDK Bathymetry Acquisition" Target="MDK_BATHYMETRY_ACQUISITION_ENABLED" Default="false" Mode="" Description="Enable bounded strict-TLS North Sea depth raster acquisition. Requires a reachable readiness probe and an advertised coverage id." Type="Variable" Display="advanced" Required="true" Mask="false">false</Config>
<Config Name="MDK Coverage ID" Target="MDK_BATHYMETRY_COVERAGE_ID" Default="" Mode="" Description="WCS coverage identifier as advertised by the live MDK capabilities document. Acquisition fails closed without it." Type="Variable" Display="advanced" Required="false" Mask="false"></Config>
<Config Name="MDK Request CRS" Target="MDK_BATHYMETRY_REQUEST_CRS" Default="EPSG:4326" Mode="" Description="CRS used for bounded MDK GetCoverage requests." Type="Variable" Display="advanced" Required="true" Mask="false">EPSG:4326</Config>
<Config Name="MDK Maximum BBox (deg2)" Target="MDK_BATHYMETRY_MAX_BBOX_DEG2" Default="0.25" Mode="" Description="Hard EPSG:4326 area limit per bounded MDK acquisition." Type="Variable" Display="advanced" Required="true" Mask="false">0.25</Config>
<Config Name="MDK Acquisition Timeout Seconds" Target="MDK_BATHYMETRY_ACQUISITION_TIMEOUT_SECONDS" Default="120" Mode="" Description="Maximum wait for one bounded MDK GetCoverage request." Type="Variable" Display="advanced" Required="true" Mask="false">120</Config>
<Config Name="MDK Acquisition Maximum Response (MiB)" Target="MDK_BATHYMETRY_ACQUISITION_MAX_RESPONSE_MB" Default="160" Mode="" Description="Maximum accepted MDK coverage response size." Type="Variable" Display="advanced" Required="true" Mask="false">160</Config>
<Config Name="Official Thematic Raster Acquisition" Target="THEMATIC_RASTER_ENABLED" Default="true" Mode="" Description="Allow bounded official Departement Omgeving rasters for space, population, accessibility and services." Type="Variable" Display="advanced" Required="true" Mask="false">true</Config>
<Config Name="Thematic Raster WCS URL" Target="THEMATIC_RASTER_WCS_URL" Default="https://www.mercator.vlaanderen.be/raadpleegdienstenmercatorpubliek/wcs" Mode="" Description="Official public MercatorNet WCS endpoint. Product identifiers remain server allowlisted." Type="Variable" Display="advanced" Required="true" Mask="false">https://www.mercator.vlaanderen.be/raadpleegdienstenmercatorpubliek/wcs</Config>
<Config Name="Thematic Raster Minimum Side (m)" Target="THEMATIC_RASTER_MIN_SIDE_M" Default="100" Mode="" Description="Minimum bounded thematic raster request side length." Type="Variable" Display="advanced" Required="true" Mask="false">100</Config>
<Config Name="Thematic Raster Maximum Side (m)" Target="THEMATIC_RASTER_MAX_SIDE_M" Default="60000" Mode="" Description="Maximum side length for one bounded allowlisted thematic raster request; larger national selections must remain partitioned." Type="Variable" Display="advanced" Required="true" Mask="false">60000</Config>
<Config Name="Thematic Raster Maximum Cells" Target="THEMATIC_RASTER_MAX_PIXELS" Default="30000000" Mode="" Description="Maximum raster cells per allowlisted thematic acquisition or selection analysis." Type="Variable" Display="advanced" Required="true" Mask="false">30000000</Config>
<Config Name="Thematic Raster Timeout (seconds)" Target="THEMATIC_RASTER_TIMEOUT_SECONDS" Default="300" Mode="" Description="Maximum wait for one bounded thematic raster request." Type="Variable" Display="advanced" Required="true" Mask="false">300</Config>
<Config Name="Thematic Raster Maximum Response (MiB)" Target="THEMATIC_RASTER_MAX_RESPONSE_MB" Default="160" Mode="" Description="Maximum accepted thematic raster response size." Type="Variable" Display="advanced" Required="true" Mask="false">160</Config>
<Config Name="WALOUS Land Cover" Target="WALOUS_ENABLED" Default="true" Mode="" Description="Enable bounded analysis from operator-provisioned official WALOUS 2018/2020/2023 rasters." Type="Variable" Display="advanced" Required="true" Mask="false">true</Config>
<Config Name="WALOUS Source Directory" Target="WALOUS_SOURCE_DIR" Default="/app/storage/source-cache/walous" Mode="" Description="Persistent directory containing the checksum-validated official WALOUS GeoTIFF sources." Type="Variable" Display="advanced" Required="true" Mask="false">/app/storage/source-cache/walous</Config>
<Config Name="WALOUS Analysis Resolution (m)" Target="WALOUS_ANALYSIS_RESOLUTION_M" Default="10" Mode="" Description="Nearest-neighbour analysis resolution used for bounded WALOUS derivatives; the 1 m source remains unchanged." Type="Variable" Display="advanced" Required="true" Mask="false">10</Config>
<Config Name="WALOUS Maximum Side (m)" Target="WALOUS_MAX_SIDE_M" Default="60000" Mode="" Description="Maximum side length for one bounded WALOUS selection." Type="Variable" Display="advanced" Required="true" Mask="false">60000</Config>
<Config Name="WALOUS Maximum Cells" Target="WALOUS_MAX_PIXELS" Default="36000000" Mode="" Description="Maximum persisted analysis cells per bounded WALOUS acquisition." Type="Variable" Display="advanced" Required="true" Mask="false">36000000</Config>
<Config Name="SPW Wallonia Terrain" Target="SPW_TERRAIN_ENABLED" Default="true" Mode="" Description="Enable bounded terrain analysis from the operator-provisioned official SPW 1 m MNT 2021-2022." Type="Variable" Display="advanced" Required="true" Mask="false">true</Config>
<Config Name="SPW Terrain Source Directory" Target="SPW_TERRAIN_SOURCE_DIR" Default="/app/storage/source-cache/spw-terrain" Mode="" Description="Persistent directory containing the checksum-validated official SPW MNT GeoTIFF." Type="Variable" Display="advanced" Required="true" Mask="false">/app/storage/source-cache/spw-terrain</Config>
<Config Name="SPW Terrain Analysis Resolution (m)" Target="SPW_TERRAIN_ANALYSIS_RESOLUTION_M" Default="5" Mode="" Description="Bilinear analysis resolution for bounded SPW MNT derivatives; the official 1 m source remains unchanged." Type="Variable" Display="advanced" Required="true" Mask="false">5</Config>
<Config Name="SPW Terrain Maximum Side (m)" Target="SPW_TERRAIN_MAX_SIDE_M" Default="20000" Mode="" Description="Maximum side length for one bounded SPW terrain selection." Type="Variable" Display="advanced" Required="true" Mask="false">20000</Config>
<Config Name="SPW Terrain Maximum Cells" Target="SPW_TERRAIN_MAX_PIXELS" Default="12000000" Mode="" Description="Maximum persisted analysis cells per bounded SPW terrain acquisition." Type="Variable" Display="advanced" Required="true" Mask="false">12000000</Config>
<Config Name="AOI Background Worker" Target="GEOINTEL_AOI_WORKER_ENABLED" Default="true" Mode="" Description="Continuously execute persisted, restart-safe regional and national AOI partitions." Type="Variable" Display="advanced" Required="true" Mask="false">true</Config>
<Config Name="AOI Worker Poll Seconds" Target="GEOINTEL_AOI_WORKER_POLL_SECONDS" Default="2" Mode="" Description="Idle polling interval for the persistent AOI partition worker." Type="Variable" Display="advanced" Required="true" Mask="false">2</Config>
<Config Name="Configured YOLO" Target="YOLO_ENABLED" Default="false" Mode="" Description="Enable only a locally mounted and explicitly configured detection model." Type="Variable" Display="advanced" Required="true" Mask="false">false</Config>
<Config Name="YOLO Models Directory" Target="YOLO_MODELS_DIR" Default="/app/models" Mode="" Description="In-container directory containing local model assets." Type="Variable" Display="advanced" Required="true" Mask="false">/app/models</Config>
<Config Name="YOLO Model Path" Target="YOLO_MODEL_PATH" Default="" Mode="" Description="Absolute in-container path to a local model asset; no download occurs." Type="Variable" Display="advanced" Required="false" Mask="false"></Config>
<Config Name="YOLO Model ID" Target="YOLO_MODEL_ID" Default="yolo-configured" Mode="" Description="Stable model identifier shown in GeoIntel." Type="Variable" Display="advanced" Required="true" Mask="false">yolo-configured</Config>
<Config Name="YOLO Display Name" Target="YOLO_MODEL_DISPLAY_NAME" Default="Configured YOLO detector" Mode="" Description="Operator-facing model name." Type="Variable" Display="advanced" Required="true" Mask="false">Configured YOLO detector</Config>
<Config Name="YOLO Model Version" Target="YOLO_MODEL_VERSION" Default="" Mode="" Description="Operator-supplied local model version." Type="Variable" Display="advanced" Required="false" Mask="false"></Config>
<Config Name="YOLO Config Directory" Target="YOLO_CONFIG_DIR" Default="/app/storage/ultralytics" Mode="" Description="Writable persistent Ultralytics settings path." Type="Variable" Display="advanced" Required="true" Mask="false">/app/storage/ultralytics</Config>
<Config Name="YOLO Device" Target="YOLO_DEVICE" Default="cuda:0" Mode="" Description="Required NVIDIA CUDA inference device." Type="Variable" Display="advanced" Required="true" Mask="false">cuda:0</Config>
<Config Name="Require CUDA" Target="YOLO_REQUIRE_CUDA" Default="true" Mode="" Description="Fail closed instead of silently falling back to CPU when NVIDIA CUDA is unavailable." Type="Variable" Display="advanced" Required="true" Mask="false">true</Config>
<Config Name="YOLO Classes" Target="YOLO_MODEL_CLASSES" Default="building" Mode="" Description="Comma-separated classes proven for the active model; the current promoted model is building-only." Type="Variable" Display="advanced" Required="true" Mask="false">building</Config>
<Config Name="Enforce YOLO Scope" Target="YOLO_ENFORCE_VALIDATION_SCOPE" Default="true" Mode="" Description="Reject inference outside the checksum-bound model validation geometry." Type="Variable" Display="advanced" Required="true" Mask="false">true</Config>
<Config Name="YOLO Scope Manifest" Target="YOLO_VALIDATION_SCOPE_MANIFEST_PATH" Default="/app/storage/operator-data/model-validation-scopes/active-building-model.json" Mode="" Description="Immutable model-bound EPSG:4326 validation-scope manifest." Type="Variable" Display="advanced" Required="true" Mask="false">/app/storage/operator-data/model-validation-scopes/active-building-model.json</Config>
<Config Name="YOLO Scope Manifest SHA256" Target="YOLO_VALIDATION_SCOPE_MANIFEST_SHA256" Default="" Mode="" Description="Exact lowercase SHA-256 of the validation-scope manifest." Type="Variable" Display="advanced" Required="true" Mask="false"></Config>
<Config Name="Validated YOLO Areas (Display Only)" Target="YOLO_VALIDATED_AREA_NAMES" Default="Mol,Kempen" Mode="" Description="Deprecated display metadata; never authorizes inference." Type="Variable" Display="advanced" Required="false" Mask="false">Mol,Kempen</Config>
<Config Name="YOLO Image Size" Target="YOLO_IMAGE_SIZE" Default="640" Mode="" Description="Inference image size in pixels." Type="Variable" Display="advanced" Required="true" Mask="false">640</Config>
<Config Name="YOLO Maximum Tiles" Target="YOLO_MAX_TILES" Default="100" Mode="" Description="Hard tile limit per detection run." Type="Variable" Display="advanced" Required="true" Mask="false">100</Config>
<Config Name="YOLO Maximum Detections" Target="YOLO_MAX_DETECTIONS" Default="1000" Mode="" Description="Hard persisted detection limit per run." Type="Variable" Display="advanced" Required="true" Mask="false">1000</Config>
<Config Name="YOLO Duplicate IoU" Target="YOLO_DUPLICATE_IOU_THRESHOLD" Default="0.5" Mode="" Description="Cross-tile duplicate suppression threshold." Type="Variable" Display="advanced" Required="true" Mask="false">0.5</Config>
<Config Name="YOLO Batch Size" Target="YOLO_BATCH_SIZE" Default="1" Mode="" Description="Bounded inference batch size." Type="Variable" Display="advanced" Required="true" Mask="false">1</Config>
<Config Name="Configured YOLO Segmentation" Target="YOLO_SEG_ENABLED" Default="false" Mode="" Description="Enable only a locally mounted and explicitly configured YOLO segmentation model. No weights are downloaded." Type="Variable" Display="advanced" Required="true" Mask="false">false</Config>
<Config Name="YOLO Segmentation Model Path" Target="YOLO_SEG_MODEL_PATH" Default="" Mode="" Description="Absolute in-container path to a local segmentation model asset; no download occurs." Type="Variable" Display="advanced" Required="false" Mask="false"></Config>
<Config Name="YOLO Segmentation Model ID" Target="YOLO_SEG_MODEL_ID" Default="yolo-seg-configured" Mode="" Description="Stable segmentation model identifier shown in GeoIntel." Type="Variable" Display="advanced" Required="true" Mask="false">yolo-seg-configured</Config>
<Config Name="YOLO Segmentation Display Name" Target="YOLO_SEG_MODEL_DISPLAY_NAME" Default="Configured YOLO segmentation" Mode="" Description="Operator-facing segmentation model name." Type="Variable" Display="advanced" Required="true" Mask="false">Configured YOLO segmentation</Config>
<Config Name="YOLO Segmentation Model Version" Target="YOLO_SEG_MODEL_VERSION" Default="" Mode="" Description="Operator-supplied local segmentation model version." Type="Variable" Display="advanced" Required="false" Mask="false"></Config>
<Config Name="Configured SAM" Target="SAM_ENABLED" Default="false" Mode="" Description="Enable only a locally mounted SAM-compatible model through the ultralytics interface. No weights are downloaded." Type="Variable" Display="advanced" Required="true" Mask="false">false</Config>
<Config Name="SAM Model Path" Target="SAM_MODEL_PATH" Default="" Mode="" Description="Absolute in-container path to a local SAM model asset; no download occurs." Type="Variable" Display="advanced" Required="false" Mask="false"></Config>
<Config Name="SAM Model ID" Target="SAM_MODEL_ID" Default="sam-configured" Mode="" Description="Stable SAM model identifier shown in GeoIntel." Type="Variable" Display="advanced" Required="true" Mask="false">sam-configured</Config>
<Config Name="SAM Display Name" Target="SAM_MODEL_DISPLAY_NAME" Default="Configured SAM segmentation" Mode="" Description="Operator-facing SAM model name." Type="Variable" Display="advanced" Required="true" Mask="false">Configured SAM segmentation</Config>
<Config Name="SAM Model Version" Target="SAM_MODEL_VERSION" Default="" Mode="" Description="Operator-supplied local SAM model version." Type="Variable" Display="advanced" Required="false" Mask="false"></Config>
<Config Name="Segmentation Maximum Masks Per Tile" Target="SEGMENTATION_MAX_MASKS_PER_TILE" Default="300" Mode="" Description="Hard per-tile mask limit for segmentation inference." Type="Variable" Display="advanced" Required="true" Mask="false">300</Config>
<Config Name="Segmentation Duplicate IoU" Target="SEGMENTATION_DUPLICATE_IOU_THRESHOLD" Default="0.5" Mode="" Description="Cross-tile duplicate mask suppression threshold." Type="Variable" Display="advanced" Required="true" Mask="false">0.5</Config>
<Config Name="Local Ollama Assistant" Target="OLLAMA_ENABLED" Default="true" Mode="" Description="Enable the source-grounded GeoIntel assistant backed by Ollama on the Unraid host." Type="Variable" Display="always" Required="true" Mask="false">true</Config>
<Config Name="Ollama Base URL" Target="OLLAMA_BASE_URL" Default="http://host.docker.internal:11434" Mode="" Description="Ollama API reachable from the container. The deployment maps host.docker.internal to the Unraid host gateway." Type="Variable" Display="always" Required="true" Mask="false">http://host.docker.internal:11434</Config>
<Config Name="Default Ollama Model" Target="OLLAMA_DEFAULT_MODEL" Default="qwen3.5:9b" Mode="" Description="Preferred locally installed Ollama model. Users can select another installed model in GeoIntel." Type="Variable" Display="always" Required="true" Mask="false">qwen3.5:9b</Config>
<Config Name="Ollama Timeout Seconds" Target="OLLAMA_TIMEOUT_SECONDS" Default="120" Mode="" Description="Maximum wait for one local assistant response." Type="Variable" Display="advanced" Required="true" Mask="false">120</Config>
<Config Name="Ollama Maximum Output Tokens" Target="OLLAMA_MAX_OUTPUT_TOKENS" Default="1200" Mode="" Description="Maximum generated tokens for one grounded answer. Length-truncated responses remain rejected." Type="Variable" Display="advanced" Required="true" Mask="false">1200</Config>
<Config Name="Ollama Context Tokens" Target="OLLAMA_CONTEXT_TOKENS" Default="16384" Mode="" Description="Context window reserved for grounded GIS measurements and the generated answer." Type="Variable" Display="advanced" Required="true" Mask="false">16384</Config>
</Container>
+212
View File
@@ -0,0 +1,212 @@
# GeoIntel Unraid all-in-one environment template.
# Copy this file to /mnt/user/appdata/geointel/.env and edit values there.
# GeoIntel runs as ONE container. This makes a bare `docker compose ...` in this
# directory use the single-container file instead of the multi-container
# development stack in docker-compose.yml.
COMPOSE_FILE=docker-compose.unraid.yml
# Browser URL: configure the HTTPS reverse-proxy origin below.
GEOINTEL_FRONTEND_PORT=1202
# Persisted application artifacts: uploads, tiles, masks, reports and exports.
GEOINTEL_STORAGE_PATH=/mnt/user/appdata/geointel/storage
# Local AI model files mounted into the container as /app/models.
GEOINTEL_MODELS_PATH=/mnt/user/appdata/geointel/models
# Checksum-verified release backups mounted read-only for cleanup guards.
GEOINTEL_BACKUPS_PATH=/mnt/user/appdata/geointel/backups
# Embedded PostGIS data directory for the all-in-one container.
GEOINTEL_POSTGIS_DATA_PATH=/mnt/user/appdata/geointel/postgres-data
# Internal embedded PostGIS settings. The database is not published to the LAN.
# Replace the placeholder with a unique secret before the first start. Production
# startup rejects empty and known-default passwords.
GEOINTEL_POSTGRES_DB=geointel
GEOINTEL_POSTGRES_USER=geointel
GEOINTEL_POSTGRES_PASSWORD=change-me-before-shared-use
# Browser origins allowed when directly calling the backend API.
# Neem hier ook de publieke hostname op zodra Nginx Proxy Manager ervoor staat.
GEOINTEL_CORS_ORIGINS=https://geointel.example.com,http://localhost:1202,http://127.0.0.1:1202
# Upload guard in MiB. The same 1-2048 limit is applied by nginx and FastAPI.
GEOINTEL_MAX_UPLOAD_MB=500
# Maximum decompressed vector payload processed fully in memory (1-256 MiB).
GEOINTEL_MAX_IN_MEMORY_VECTOR_MB=64
GEOINTEL_AOI_WORKER_ENABLED=true
GEOINTEL_AOI_WORKER_POLL_SECONDS=2
# Optional single-operator access gate. Never store a plaintext password here.
# Generate the password hash with AuthService.hash_password and use a unique,
# random session secret of at least 32 characters.
GEOINTEL_AUTH_ENABLED=true
GEOINTEL_AUTH_REQUIRE_HTTPS=true
GEOINTEL_AUTH_USERNAME=
GEOINTEL_AUTH_PASSWORD_HASH=
GEOINTEL_AUTH_SESSION_SECRET=
GEOINTEL_AUTH_SESSION_TTL_SECONDS=43200
# Optional additive Authentik OIDC login. Configure all fields together. The
# local operator credentials above remain the recovery login.
GEOINTEL_PUBLIC_BASE_URL=https://geointel.example.com
GEOINTEL_AUTHENTIK_ISSUER=
GEOINTEL_AUTHENTIK_CLIENT_ID=
GEOINTEL_AUTHENTIK_CLIENT_SECRET=
GEOINTEL_AUTHENTIK_ALLOWED_EMAIL=
# Guest access is opt-in. It opens the seeded GeoIntel demo in a temporary,
# API-enforced restricted session and belongs only on a dedicated demo install.
# LET OP: scripts/configure_operator_login.sh zet dit op false tenzij je
# expliciet --guest-access true meegeeft.
GEOINTEL_GUEST_ACCESS_ENABLED=false
GEOINTEL_GUEST_DISPLAY_NAME=Gast
GEOINTEL_GUEST_SESSION_TTL_SECONDS=7200
# Explicit, bounded acquisition from the official Digitaal Vlaanderen WMS.
ORTHOPHOTO_ENABLED=true
ORTHOPHOTO_WMS_URL=https://geo.api.vlaanderen.be/OMWRGBMRVL/wms
SPW_ORTHOPHOTO_WMS_URL=https://geoservices.wallonie.be/arcgis/services/IMAGERIE/ORTHO_LAST/MapServer/WMSServer
BRUSSELS_ORTHOPHOTO_WMS_URL=https://geoservices-grid.irisnet.be/geoserver/urbisgrid/ows
ORTHOPHOTO_WMS_LAYER=Ortho
ORTHOPHOTO_RESOLUTION_M=1.0
ORTHOPHOTO_MIN_SIDE_M=128
ORTHOPHOTO_MAX_SIDE_M=1024
ORTHOPHOTO_CACHE_TTL_HOURS=24
# Explicit read-only edition checks for GRB, orthophoto, Statbel and ALZ publications.
# No feature, raster, Statbel distribution or ALZ archive is downloaded by these probes.
SOURCE_CATALOG_PROBE_ENABLED=true
SOURCE_CATALOG_GRB_WFS_URL=https://geo.api.vlaanderen.be/GRB/wfs
SOURCE_CATALOG_STATBEL_DCAT_URL=https://doc.statbel.be/publications/DCAT/DCAT_opendata_datasets.ttl
SOURCE_CATALOG_STATBEL_MAX_RESPONSE_MB=5
SOURCE_CATALOG_ALZ_RELEASE_URL=https://landbouwcijfers.vlaanderen.be/open-geodata-landbouwgebruikspercelen
SOURCE_CATALOG_PROBE_TIMEOUT_SECONDS=10
SOURCE_CATALOG_PROBE_MAX_RESPONSE_MB=2
SOURCE_CATALOG_PROBE_CACHE_TTL_SECONDS=900
# Bounded official Flemish, Walloon and Brussels vectors, loaded only after a map selection.
OFFICIAL_VECTOR_ENABLED=true
BWK_WFS_URL=https://geo.api.vlaanderen.be/BWK/wfs
DOV_SOIL_WFS_URL=https://www.dov.vlaanderen.be/geoserver/wfs
SPW_PICC_ENABLED=true
SPW_PICC_MAPSERVER_URL=https://geoservices.wallonie.be/arcgis/rest/services/TOPOGRAPHIE/PICC_VDIFF/MapServer
SPW_FLOOD_HAZARD_ENABLED=true
SPW_FLOOD_HAZARD_MAPSERVER_URL=https://geoservices.wallonie.be/arcgis/rest/services/EAU/ALEA_INOND/MapServer
URBIS_ENABLED=true
URBIS_WFS_URL=https://geoservices-vector.irisnet.be/geoserver/urbisvector/ows
OFFICIAL_VECTOR_MIN_SIDE_M=10
OFFICIAL_VECTOR_MAX_SIDE_M=20000
OFFICIAL_VECTOR_PAGE_SIZE=1000
OFFICIAL_VECTOR_MAX_PAGES=200
OFFICIAL_VECTOR_MAX_FEATURES=100000
OFFICIAL_VECTOR_TIMEOUT_SECONDS=180
OFFICIAL_VECTOR_MAX_RESPONSE_MB=20
OFFICIAL_VECTOR_MAX_TOTAL_RESPONSE_MB=256
OFFICIAL_VECTOR_CACHE_TTL_HOURS=24
DHMV_ENABLED=true
DHMV_WCS_URL=https://geo.api.vlaanderen.be/DHMV/wcs
DHMV_RESOLUTION_M=5.0
DHMV_MIN_SIDE_M=10
DHMV_MAX_SIDE_M=20000
DHMV_MAX_PIXELS=12000000
DHMV_TIMEOUT_SECONDS=300
DHMV_MAX_RESPONSE_MB=160
FLOOD_HAZARD_ENABLED=true
FLOOD_HAZARD_WCS_URL=https://geoservice.waterinfo.be/OGRK/wcs
FLOOD_HAZARD_RESOLUTION_M=5.0
FLOOD_HAZARD_MIN_SIDE_M=10
FLOOD_HAZARD_MAX_SIDE_M=20000
FLOOD_HAZARD_MAX_PIXELS=12000000
FLOOD_HAZARD_TIMEOUT_SECONDS=300
FLOOD_HAZARD_MAX_RESPONSE_MB=160
BATHYMETRY_PROFILES_ENABLED=true
BATHYMETRY_PROFILES_LAYER_URL=https://vha.waterinfo.be/arcgis/rest/services/digitale_atlas/MapServer/0
BATHYMETRY_WATERCOURSE_LAYER_URL=https://vha.waterinfo.be/arcgis/rest/services/digitale_atlas/MapServer/1
BATHYMETRY_PROFILES_PAGE_SIZE=1000
BATHYMETRY_PROFILES_MAX_FEATURES=50000
BATHYMETRY_PROFILES_TIMEOUT_SECONDS=120
BATHYMETRY_PROFILES_MAX_RESPONSE_MB=32
MDK_BATHYMETRY_PROBE_ENABLED=true
MDK_BATHYMETRY_WCS_URL=https://bathy.agentschapmdk.be/spatialfusionserver/services/ows/wcs/EL_wcs
MDK_BATHYMETRY_PROBE_TIMEOUT_SECONDS=20
MDK_BATHYMETRY_PROBE_MAX_RESPONSE_MB=4
# Bounded MDK acquisition stays fail-closed until the readiness probe reports
# "reachable" and an advertised coverage id is configured explicitly.
MDK_BATHYMETRY_ACQUISITION_ENABLED=false
MDK_BATHYMETRY_COVERAGE_ID=
MDK_BATHYMETRY_REQUEST_CRS=EPSG:4326
MDK_BATHYMETRY_MAX_BBOX_DEG2=0.25
MDK_BATHYMETRY_ACQUISITION_TIMEOUT_SECONDS=120
MDK_BATHYMETRY_ACQUISITION_MAX_RESPONSE_MB=160
# Allowlisted Departement Omgeving policy rasters. Regional requests are
# transferred as fixed 10 km WCS tiles before exact Area clipping.
THEMATIC_RASTER_ENABLED=true
THEMATIC_RASTER_WCS_URL=https://www.mercator.vlaanderen.be/raadpleegdienstenmercatorpubliek/wcs
THEMATIC_RASTER_MIN_SIDE_M=100
THEMATIC_RASTER_MAX_SIDE_M=60000
THEMATIC_RASTER_MAX_PIXELS=30000000
THEMATIC_RASTER_TIMEOUT_SECONDS=300
THEMATIC_RASTER_MAX_RESPONSE_MB=160
WALOUS_ENABLED=true
WALOUS_SOURCE_DIR=/app/storage/source-cache/walous
WALOUS_ANALYSIS_RESOLUTION_M=10
WALOUS_MAX_SIDE_M=60000
WALOUS_MAX_PIXELS=36000000
SPW_TERRAIN_ENABLED=true
SPW_TERRAIN_SOURCE_DIR=/app/storage/source-cache/spw-terrain
SPW_TERRAIN_ANALYSIS_RESOLUTION_M=5
SPW_TERRAIN_MAX_SIDE_M=20000
SPW_TERRAIN_MAX_PIXELS=12000000
# Production releases always contain the pinned AI dependencies. Inference may
# remain disabled until an integrity-bound local model is mounted.
GEOINTEL_INSTALL_AI=true
YOLO_ENABLED=false
YOLO_MODELS_DIR=/app/models
YOLO_MODEL_PATH=
YOLO_MODEL_ID=yolo-configured
YOLO_MODEL_DISPLAY_NAME=Configured YOLO detector
YOLO_MODEL_VERSION=
YOLO_CONFIG_DIR=/app/storage/ultralytics
YOLO_DEVICE=cuda:0
YOLO_REQUIRE_CUDA=true
YOLO_MODEL_CLASSES=building
YOLO_ENFORCE_VALIDATION_SCOPE=true
YOLO_VALIDATION_SCOPE_MANIFEST_PATH=/app/storage/operator-data/model-validation-scopes/active-building-model.json
YOLO_VALIDATION_SCOPE_MANIFEST_SHA256=
# Deprecated display metadata; never used as an inference authorization gate.
YOLO_VALIDATED_AREA_NAMES=Mol,Kempen
YOLO_IMAGE_SIZE=640
YOLO_MAX_TILES=100
YOLO_MAX_DETECTIONS=1000
YOLO_DUPLICATE_IOU_THRESHOLD=0.5
YOLO_BATCH_SIZE=1
# Local segmentation models. GeoIntel never downloads model weights
# automatically; point these to existing local files to enable inference.
YOLO_SEG_ENABLED=false
YOLO_SEG_MODEL_PATH=
YOLO_SEG_MODEL_ID=yolo-seg-configured
YOLO_SEG_MODEL_DISPLAY_NAME=Configured YOLO segmentation
YOLO_SEG_MODEL_VERSION=
SAM_ENABLED=false
SAM_MODEL_PATH=
SAM_MODEL_ID=sam-configured
SAM_MODEL_DISPLAY_NAME=Configured SAM segmentation
SAM_MODEL_VERSION=
SEGMENTATION_MAX_MASKS_PER_TILE=300
SEGMENTATION_DUPLICATE_IOU_THRESHOLD=0.5
# Local Ollama assistant. The all-in-one container reaches the Unraid host
# through Docker's host-gateway mapping; no Ollama port is exposed by GeoIntel.
OLLAMA_ENABLED=true
OLLAMA_BASE_URL=http://host.docker.internal:11434
OLLAMA_DEFAULT_MODEL=qwen3.5:9b
OLLAMA_TIMEOUT_SECONDS=120
OLLAMA_MAX_OUTPUT_TOKENS=1200
OLLAMA_CONTEXT_TOKENS=16384
+15
View File
@@ -0,0 +1,15 @@
#!/bin/sh
set -eu
if [ "$#" -lt 2 ]; then
echo "usage: gosu user command [args...]" >&2
exit 64
fi
target_user="$1"
shift
exec setpriv \
--reuid="$target_user" \
--regid="$target_user" \
--init-groups \
"$@"
+107
View File
@@ -0,0 +1,107 @@
geo $geointel_trusted_forwarder {
default 0;
127.0.0.0/8 1;
::1/128 1;
# The outer Nginx Proxy Manager reaches this container through Docker's
# internal bridge; public/LAN clients are not trusted forwarders.
172.16.0.0/12 1;
}
map "$geointel_trusted_forwarder:$http_x_forwarded_proto" $geointel_forwarded_proto {
default $scheme;
"1:https" https;
"1:http" http;
}
server {
listen 80;
server_name _;
client_max_body_size __GEOINTEL_MAX_UPLOAD_MB__m;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
add_header Content-Security-Policy "frame-ancestors 'none'" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
root /usr/share/nginx/html;
index index.html;
location = /index.html {
add_header Cache-Control "no-cache";
add_header Content-Security-Policy "frame-ancestors 'none'" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
try_files /index.html =404;
}
location = /geointel-icon.svg {
add_header Cache-Control "public, max-age=3600";
add_header Content-Security-Policy "frame-ancestors 'none'" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
try_files /geointel-icon.svg =404;
}
location = /geointel-icon.png {
add_header Cache-Control "public, max-age=3600";
add_header Content-Security-Policy "frame-ancestors 'none'" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
# Preserve the stable DockerMan/public URL while the frontend keeps
# its explicit 32px and 180px icon variants.
try_files /geointel-icon-180.png =404;
}
location /assets/ {
add_header Cache-Control "no-cache";
add_header Content-Security-Policy "frame-ancestors 'none'" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
try_files $uri =404;
}
location /api/ {
proxy_pass http://127.0.0.1:8000/api/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $geointel_forwarded_proto;
}
location = /health {
proxy_pass http://127.0.0.1:8000/health;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $geointel_forwarded_proto;
}
location = /health/live {
proxy_pass http://127.0.0.1:8000/health/live;
proxy_http_version 1.1;
proxy_set_header Host $host;
}
location = /health/ready {
proxy_pass http://127.0.0.1:8000/health/ready;
proxy_http_version 1.1;
proxy_set_header Host $host;
}
location / {
try_files $uri $uri/ /index.html;
}
}
+323
View File
@@ -0,0 +1,323 @@
#!/usr/bin/env bash
set -euo pipefail
# Restore the production database from a verified pre-deploy dump while the
# normal GeoIntel container is stopped. This is intentionally a separate,
# explicitly confirmed operation: starting an older image against a schema
# migrated by a newer image is not a safe rollback strategy.
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$ROOT"
GEOINTEL_DEPLOY_LOCK_FILE="${GEOINTEL_DEPLOY_LOCK_FILE:-/tmp/geointel-release-deploy.lock}"
if [ "${GEOINTEL_DEPLOY_LOCK_HELD:-false}" != "true" ]; then
command -v flock >/dev/null 2>&1 || {
echo "GeoIntel database restore requires flock to prevent concurrent deployment." >&2
exit 2
}
exec 9>"$GEOINTEL_DEPLOY_LOCK_FILE"
if ! flock -n 9; then
echo "Another GeoIntel deployment or rollback is already running." >&2
exit 3
fi
fi
BACKUP_DIR=""
CONFIRMED="false"
RESTORE_IMAGE="${GEOINTEL_ROLLBACK_IMAGE:-}"
GEOINTEL_CONTAINER_NAME="${GEOINTEL_CONTAINER_NAME:-geointel}"
usage() {
cat <<'EOF'
Usage: bash deploy/unraid/restore-predeploy-database.sh \
--backup-dir PATH --confirm-production-database-restore [options]
Stops the normal GeoIntel container, starts an isolated PostGIS recovery
container on the same persistent database path, restores the checksum-verified
custom-format dump, validates Alembic/table counts, and stops recovery again.
The caller must start the rollback image after this command succeeds.
Options:
--image IMAGE Recovery image containing PostgreSQL/PostGIS tools
--container NAME Normal application container (default: geointel)
EOF
}
while [ "$#" -gt 0 ]; do
case "$1" in
--backup-dir) BACKUP_DIR="$2"; shift 2 ;;
--confirm-production-database-restore) CONFIRMED="true"; shift ;;
--image) RESTORE_IMAGE="$2"; shift 2 ;;
--container) GEOINTEL_CONTAINER_NAME="$2"; shift 2 ;;
--help|-h) usage; exit 0 ;;
*) echo "Unknown argument: $1" >&2; usage >&2; exit 2 ;;
esac
done
if [ "$CONFIRMED" != "true" ] || [ -z "$BACKUP_DIR" ]; then
echo "Explicit --confirm-production-database-restore and --backup-dir are required." >&2
exit 2
fi
for required in docker python3 sha256sum; do
command -v "$required" >/dev/null 2>&1 || {
echo "Missing required command: $required" >&2
exit 2
}
done
if [ -f .env ]; then
set -a
# shellcheck disable=SC1091
. ./.env
set +a
fi
GEOINTEL_BACKUPS_PATH="${GEOINTEL_BACKUPS_PATH:-/mnt/user/appdata/geointel/backups}"
GEOINTEL_POSTGIS_DATA_PATH="${GEOINTEL_POSTGIS_DATA_PATH:-/mnt/user/appdata/geointel/postgres-data}"
GEOINTEL_POSTGRES_DB="${GEOINTEL_POSTGRES_DB:-geointel}"
GEOINTEL_POSTGRES_USER="${GEOINTEL_POSTGRES_USER:-geointel}"
GEOINTEL_POSTGRES_PASSWORD="${GEOINTEL_POSTGRES_PASSWORD:-}"
if ! [[ "$GEOINTEL_POSTGRES_DB" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] \
|| ! [[ "$GEOINTEL_POSTGRES_USER" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then
echo "Configured PostGIS database and user names must be simple SQL identifiers." >&2
exit 2
fi
case "$GEOINTEL_POSTGRES_PASSWORD" in
''|geointel|postgres|password|changeme|change-me-before-shared-use)
echo "Refusing database restore with an empty or known-default PostGIS password." >&2
exit 2
;;
esac
test -f "$GEOINTEL_POSTGIS_DATA_PATH/PG_VERSION" || {
echo "Persistent PostGIS data path is not initialized: $GEOINTEL_POSTGIS_DATA_PATH" >&2
exit 3
}
GEOINTEL_BACKUPS_PATH="$(python3 -c 'import pathlib,sys; print(pathlib.Path(sys.argv[1]).expanduser().resolve())' "$GEOINTEL_BACKUPS_PATH")"
BACKUP_DIR="$(python3 -c 'import pathlib,sys; print(pathlib.Path(sys.argv[1]).expanduser().resolve())' "$BACKUP_DIR")"
python3 - "$GEOINTEL_BACKUPS_PATH" "$BACKUP_DIR" <<'PY'
import pathlib
import sys
root = pathlib.Path(sys.argv[1])
backup = pathlib.Path(sys.argv[2])
try:
backup.relative_to(root)
except ValueError as exc:
raise SystemExit(f"Backup directory must be below {root}") from exc
if backup == root:
raise SystemExit("Backup directory must identify one immutable backup")
PY
for required_file in manifest.json database.dump database.list database-metadata.tsv table-counts.tsv CHECKSUMS.sha256; do
test -s "$BACKUP_DIR/$required_file" || {
echo "Missing or empty backup artifact: $required_file" >&2
exit 3
}
done
(
cd "$BACKUP_DIR"
sha256sum -c CHECKSUMS.sha256
)
python3 "$ROOT/scripts/release_backup_snapshot.py" verify-backup --backup-dir "$BACKUP_DIR"
IFS=$'\t' read -r BACKUP_DB BACKUP_USER BACKUP_IMAGE_ID BACKUP_RELEASE_ID < <(
python3 - "$BACKUP_DIR/manifest.json" <<'PY'
import json
import pathlib
import sys
payload = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8"))
if payload.get("schema_version") != 1 or payload.get("read_only_source") is not True:
raise SystemExit("Unsupported or unsafe backup manifest")
print(
f"{payload.get('database_name', '')}\t{payload.get('database_user', '')}\t"
f"{payload.get('image_id', '')}\t{payload.get('release_id', '')}"
)
PY
)
if [ "$BACKUP_DB" != "$GEOINTEL_POSTGRES_DB" ] || [ "$BACKUP_USER" != "$GEOINTEL_POSTGRES_USER" ]; then
echo "Backup database identity does not match the configured production database." >&2
exit 3
fi
if ! [[ "$BACKUP_IMAGE_ID" =~ ^sha256:[0-9a-f]{64}$ ]]; then
echo "Backup manifest does not contain one immutable Docker image ID." >&2
exit 3
fi
if [ -z "$RESTORE_IMAGE" ]; then
RESTORE_IMAGE="$BACKUP_IMAGE_ID"
fi
docker image inspect "$RESTORE_IMAGE" >/dev/null
RESTORE_IMAGE_ID="$(docker image inspect --format '{{.Id}}' "$RESTORE_IMAGE")"
if [ -z "$BACKUP_IMAGE_ID" ] || [ "$BACKUP_IMAGE_ID" != "$RESTORE_IMAGE_ID" ]; then
echo "Backup image identity does not match the retained rollback image." >&2
exit 3
fi
case "$BACKUP_RELEASE_ID" in
predeploy-*) ;;
*) echo "Production rollback requires a predeploy backup." >&2; exit 3 ;;
esac
if docker ps -a --format '{{.Names}}' | grep -Fxq "$GEOINTEL_CONTAINER_NAME"; then
docker rm -f "$GEOINTEL_CONTAINER_NAME" >/dev/null
fi
RECOVERY_CONTAINER="geointel-db-restore-$(date -u +%Y%m%d%H%M%S)-$$"
RESTORE_PROOF_DB="geointel_restore_proof_$(date -u +%Y%m%d%H%M%S)_$$"
RECOVERY_DB="geointel_pre_restore_$(date -u +%Y%m%d%H%M%S)_$$"
FAILED_RESTORE_DB="geointel_failed_restore_$(date -u +%Y%m%d%H%M%S)_$$"
SWAP_COMPLETE="false"
cleanup_recovery() {
if [ "$SWAP_COMPLETE" != "true" ] \
&& [ "$(docker inspect -f '{{.State.Running}}' "$RECOVERY_CONTAINER" 2>/dev/null || true)" = "true" ]; then
docker exec "$RECOVERY_CONTAINER" dropdb --if-exists --force \
-U "$GEOINTEL_POSTGRES_USER" "$RESTORE_PROOF_DB" >/dev/null 2>&1 || true
fi
docker rm -f "$RECOVERY_CONTAINER" >/dev/null 2>&1 || true
}
trap cleanup_recovery EXIT
docker run -d \
--name "$RECOVERY_CONTAINER" \
--restart no \
-e PGDATA=/var/lib/postgresql/data \
-e PGPASSWORD="$GEOINTEL_POSTGRES_PASSWORD" \
-v "$GEOINTEL_POSTGIS_DATA_PATH:/var/lib/postgresql/data" \
-v "$BACKUP_DIR:/restore:ro" \
--entrypoint /bin/bash \
"$RESTORE_IMAGE" \
-c 'set -euo pipefail; chown postgres:postgres "$PGDATA"; exec gosu postgres postgres' \
>/dev/null
for attempt in $(seq 1 180); do
if docker exec "$RECOVERY_CONTAINER" pg_isready -h 127.0.0.1 -U "$GEOINTEL_POSTGRES_USER" -d postgres >/dev/null 2>&1; then
break
fi
if [ "$(docker inspect -f '{{.State.Running}}' "$RECOVERY_CONTAINER" 2>/dev/null || true)" != "true" ]; then
echo "Database recovery container exited before PostGIS became ready." >&2
docker logs "$RECOVERY_CONTAINER" >&2 || true
exit 4
fi
if [ "$attempt" -eq 180 ]; then
echo "PostGIS recovery did not become ready within six minutes." >&2
exit 4
fi
sleep 2
done
RESTORED_LIST="$(mktemp)"
trap 'rm -f -- "$RESTORED_LIST"; cleanup_recovery' EXIT
docker exec "$RECOVERY_CONTAINER" pg_restore --list /restore/database.dump > "$RESTORED_LIST"
cmp -s "$RESTORED_LIST" "$BACKUP_DIR/database.list" || {
echo "Recovery image reads a different PostgreSQL archive listing." >&2
exit 4
}
if ! docker exec "$RECOVERY_CONTAINER" psql -X -v ON_ERROR_STOP=1 \
-U "$GEOINTEL_POSTGRES_USER" -d postgres -Atqc \
"SELECT 1 FROM pg_database WHERE datname = '${GEOINTEL_POSTGRES_DB}';" | grep -Fxq 1; then
echo "Configured production database does not exist; refusing replacement." >&2
exit 4
fi
for generated_database in "$RESTORE_PROOF_DB" "$RECOVERY_DB" "$FAILED_RESTORE_DB"; do
if docker exec "$RECOVERY_CONTAINER" psql -X -v ON_ERROR_STOP=1 \
-U "$GEOINTEL_POSTGRES_USER" -d postgres -Atqc \
"SELECT 1 FROM pg_database WHERE datname = '${generated_database}';" | grep -Fxq 1; then
echo "Generated recovery database already exists: ${generated_database}" >&2
exit 4
fi
done
# Prove the complete archive in a separate database before touching production.
docker exec "$RECOVERY_CONTAINER" createdb \
-U "$GEOINTEL_POSTGRES_USER" "$RESTORE_PROOF_DB"
docker exec "$RECOVERY_CONTAINER" pg_restore \
--exit-on-error \
--no-owner \
--no-privileges \
-U "$GEOINTEL_POSTGRES_USER" \
-d "$RESTORE_PROOF_DB" \
/restore/database.dump
EXPECTED_HEAD="$(awk -F $'\t' '$1 == "alembic_head" { print $2 }' "$BACKUP_DIR/database-metadata.tsv")"
validate_restored_database() {
local database_name="$1"
local restored_head=""
restored_head="$(docker exec "$RECOVERY_CONTAINER" psql -X -v ON_ERROR_STOP=1 \
-U "$GEOINTEL_POSTGRES_USER" -d "$database_name" -Atqc \
'SELECT version_num FROM alembic_version;')"
if [ -z "$EXPECTED_HEAD" ] || [ "$restored_head" != "$EXPECTED_HEAD" ]; then
echo "Restored Alembic head '$restored_head' differs from backup head '$EXPECTED_HEAD'." >&2
return 1
fi
while IFS=$'\t' read -r table expected; do
[[ "$table" =~ ^[a-z_]+$ ]] || {
echo "Unsafe table name in retained counts: $table" >&2
return 1
}
actual="$(docker exec "$RECOVERY_CONTAINER" psql -X -v ON_ERROR_STOP=1 \
-U "$GEOINTEL_POSTGRES_USER" -d "$database_name" -Atqc \
"SELECT count(*) FROM public.${table};")"
if [ "$actual" != "$expected" ]; then
echo "Restored count mismatch for $table: expected $expected, got $actual." >&2
return 1
fi
done < "$BACKUP_DIR/table-counts.tsv"
}
validate_restored_database "$RESTORE_PROOF_DB"
echo "Isolated predeploy restore proof passed: ${RESTORE_PROOF_DB}"
docker exec "$RECOVERY_CONTAINER" psql -X -v ON_ERROR_STOP=1 \
-U "$GEOINTEL_POSTGRES_USER" -d postgres -c \
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname IN ('${GEOINTEL_POSTGRES_DB}', '${RESTORE_PROOF_DB}') AND pid <> pg_backend_pid();" \
>/dev/null
docker exec "$RECOVERY_CONTAINER" psql -X -v ON_ERROR_STOP=1 \
-U "$GEOINTEL_POSTGRES_USER" -d postgres -c \
"ALTER DATABASE ${GEOINTEL_POSTGRES_DB} RENAME TO ${RECOVERY_DB};"
if ! docker exec "$RECOVERY_CONTAINER" psql -X -v ON_ERROR_STOP=1 \
-U "$GEOINTEL_POSTGRES_USER" -d postgres -c \
"ALTER DATABASE ${RESTORE_PROOF_DB} RENAME TO ${GEOINTEL_POSTGRES_DB};"; then
echo "Restored database cutover failed; restoring the untouched production database name." >&2
docker exec "$RECOVERY_CONTAINER" psql -X -v ON_ERROR_STOP=1 \
-U "$GEOINTEL_POSTGRES_USER" -d postgres -c \
"ALTER DATABASE ${RECOVERY_DB} RENAME TO ${GEOINTEL_POSTGRES_DB};"
exit 4
fi
SWAP_COMPLETE="true"
if ! validate_restored_database "$GEOINTEL_POSTGRES_DB"; then
echo "Post-cutover validation failed; restoring the retained pre-restore database." >&2
docker exec "$RECOVERY_CONTAINER" psql -X -v ON_ERROR_STOP=1 \
-U "$GEOINTEL_POSTGRES_USER" -d postgres -c \
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '${GEOINTEL_POSTGRES_DB}' AND pid <> pg_backend_pid();" \
>/dev/null
docker exec "$RECOVERY_CONTAINER" psql -X -v ON_ERROR_STOP=1 \
-U "$GEOINTEL_POSTGRES_USER" -d postgres -c \
"ALTER DATABASE ${GEOINTEL_POSTGRES_DB} RENAME TO ${FAILED_RESTORE_DB};"
docker exec "$RECOVERY_CONTAINER" psql -X -v ON_ERROR_STOP=1 \
-U "$GEOINTEL_POSTGRES_USER" -d postgres -c \
"ALTER DATABASE ${RECOVERY_DB} RENAME TO ${GEOINTEL_POSTGRES_DB};"
SWAP_COMPLETE="false"
echo "Original production database was restored; failed restore retained as ${FAILED_RESTORE_DB}." >&2
exit 4
fi
if [ -z "$RECOVERY_DB" ]; then
echo "Recovery database identity was not retained." >&2
exit 4
fi
while IFS=$'\t' read -r table expected; do
[[ "$table" =~ ^[a-z_]+$ ]] || {
echo "Unsafe table name in retained counts: $table" >&2
exit 4
}
done < "$BACKUP_DIR/table-counts.tsv"
rm -f -- "$RESTORED_LIST"
cleanup_recovery
trap - EXIT
echo "Production database restored and verified from: $BACKUP_DIR"
echo "Pre-restore production database retained for operator recovery as: $RECOVERY_DB"
@@ -0,0 +1,97 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$ROOT"
GEOINTEL_DEPLOY_LOCK_FILE="${GEOINTEL_DEPLOY_LOCK_FILE:-/tmp/geointel-release-deploy.lock}"
if [ "${GEOINTEL_DEPLOY_LOCK_HELD:-false}" != "true" ]; then
command -v flock >/dev/null 2>&1 || {
echo "GeoIntel rollback requires flock to prevent concurrent deployment." >&2
exit 2
}
exec 9>"$GEOINTEL_DEPLOY_LOCK_FILE"
if ! flock -n 9; then
echo "Another GeoIntel deployment or rollback is already running." >&2
exit 3
fi
GEOINTEL_DEPLOY_LOCK_HELD="true"
export GEOINTEL_DEPLOY_LOCK_HELD
fi
GEOINTEL_ROLLBACK_IMAGE="${GEOINTEL_ROLLBACK_IMAGE:-}"
BACKUP_DIR="${GEOINTEL_ROLLBACK_BACKUP_DIR:-}"
CONFIRM_RESTORE="false"
usage() {
cat <<'EOF'
Usage: bash deploy/unraid/rollback-dockerman-container.sh \
--backup-dir PATH --confirm-production-database-restore
Restores the verified pre-deploy PostgreSQL dump first and only then starts the
retained previous image. Image-only rollback against an unknown migrated
schema is deliberately not supported.
EOF
}
while [ "$#" -gt 0 ]; do
case "$1" in
--backup-dir) BACKUP_DIR="$2"; shift 2 ;;
--confirm-production-database-restore) CONFIRM_RESTORE="true"; shift ;;
--help|-h) usage; exit 0 ;;
*) echo "Unknown argument: $1" >&2; usage >&2; exit 2 ;;
esac
done
if [ -z "$BACKUP_DIR" ] || [ "$CONFIRM_RESTORE" != "true" ]; then
echo "Rollback requires a verified pre-deploy backup and explicit database-restore confirmation." >&2
usage >&2
exit 2
fi
restore_image_args=()
if [ -n "$GEOINTEL_ROLLBACK_IMAGE" ]; then
restore_image_args=(--image "$GEOINTEL_ROLLBACK_IMAGE")
fi
echo "Restoring the pre-deploy database before starting its checksum-bound image..."
bash deploy/unraid/restore-predeploy-database.sh \
--backup-dir "$BACKUP_DIR" \
"${restore_image_args[@]}" \
--confirm-production-database-restore
if [ -z "$GEOINTEL_ROLLBACK_IMAGE" ]; then
GEOINTEL_ROLLBACK_IMAGE="$(python3 - "$BACKUP_DIR/manifest.json" <<'PY'
import json
import pathlib
import re
import sys
image_id = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8")).get("image_id", "")
if not re.fullmatch(r"sha256:[0-9a-f]{64}", image_id):
raise SystemExit("Backup manifest lacks an immutable rollback image ID")
print(image_id)
PY
)"
fi
docker image inspect "$GEOINTEL_ROLLBACK_IMAGE" >/dev/null
echo "Starting rollback image ${GEOINTEL_ROLLBACK_IMAGE} with the restored persistent database..."
GEOINTEL_IMAGE="$GEOINTEL_ROLLBACK_IMAGE" bash deploy/unraid/run-dockerman-container.sh
for attempt in $(seq 1 90); do
status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' geointel 2>/dev/null || true)"
if [ "$status" = "healthy" ]; then
LIVE_SMOKE_CONTAINER=geointel bash scripts/live_migration_smoke.sh
echo "Rollback completed with healthy image ${GEOINTEL_ROLLBACK_IMAGE}."
exit 0
fi
if [ "$status" = "unhealthy" ] || [ "$status" = "exited" ] || [ "$status" = "dead" ]; then
docker logs --tail 120 geointel >&2 || true
exit 1
fi
sleep 2
done
echo "Rollback container did not become healthy." >&2
docker logs --tail 120 geointel >&2 || true
exit 1
+527
View File
@@ -0,0 +1,527 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$ROOT"
GEOINTEL_CONTAINER_LOCK_FILE="${GEOINTEL_CONTAINER_LOCK_FILE:-/tmp/geointel-container-replacement.lock}"
if ! command -v flock >/dev/null 2>&1; then
echo "GeoIntel container replacement requires flock to prevent concurrent Docker mutations." >&2
exit 2
fi
exec 8>"$GEOINTEL_CONTAINER_LOCK_FILE"
if ! flock -w 300 8; then
echo "Timed out waiting for another GeoIntel container replacement to finish." >&2
exit 3
fi
if [ -f .env ]; then
set -a
# shellcheck disable=SC1091
. ./.env
set +a
fi
GEOINTEL_FRONTEND_PORT="${GEOINTEL_FRONTEND_PORT:-1202}"
GEOINTEL_IMAGE="${GEOINTEL_IMAGE:-geointel-all-in-one:latest}"
GEOINTEL_STORAGE_PATH="${GEOINTEL_STORAGE_PATH:-/mnt/user/appdata/geointel/storage}"
GEOINTEL_MODELS_PATH="${GEOINTEL_MODELS_PATH:-/mnt/user/appdata/geointel/models}"
GEOINTEL_BACKUPS_PATH="${GEOINTEL_BACKUPS_PATH:-/mnt/user/appdata/geointel/backups}"
GEOINTEL_POSTGIS_DATA_PATH="${GEOINTEL_POSTGIS_DATA_PATH:-/mnt/user/appdata/geointel/postgres-data}"
GEOINTEL_POSTGRES_DB="${GEOINTEL_POSTGRES_DB:-geointel}"
GEOINTEL_POSTGRES_USER="${GEOINTEL_POSTGRES_USER:-geointel}"
GEOINTEL_POSTGRES_PASSWORD="${GEOINTEL_POSTGRES_PASSWORD:-}"
GEOINTEL_CORS_ORIGINS="${GEOINTEL_CORS_ORIGINS:-http://localhost:${GEOINTEL_FRONTEND_PORT},http://127.0.0.1:${GEOINTEL_FRONTEND_PORT}}"
GEOINTEL_MAX_UPLOAD_MB="${GEOINTEL_MAX_UPLOAD_MB:-500}"
GEOINTEL_MAX_IN_MEMORY_VECTOR_MB="${GEOINTEL_MAX_IN_MEMORY_VECTOR_MB:-64}"
GEOINTEL_AOI_WORKER_ENABLED="${GEOINTEL_AOI_WORKER_ENABLED:-true}"
GEOINTEL_AOI_WORKER_POLL_SECONDS="${GEOINTEL_AOI_WORKER_POLL_SECONDS:-2}"
GEOINTEL_AUTH_ENABLED="${GEOINTEL_AUTH_ENABLED:-true}"
GEOINTEL_AUTH_REQUIRE_HTTPS="${GEOINTEL_AUTH_REQUIRE_HTTPS:-true}"
GEOINTEL_AUTH_USERNAME="${GEOINTEL_AUTH_USERNAME:-}"
GEOINTEL_AUTH_PASSWORD_HASH="${GEOINTEL_AUTH_PASSWORD_HASH:-}"
GEOINTEL_AUTH_SESSION_SECRET="${GEOINTEL_AUTH_SESSION_SECRET:-}"
GEOINTEL_AUTH_SESSION_TTL_SECONDS="${GEOINTEL_AUTH_SESSION_TTL_SECONDS:-43200}"
GEOINTEL_PUBLIC_BASE_URL="${GEOINTEL_PUBLIC_BASE_URL:-https://geointel.example.com}"
GEOINTEL_AUTHENTIK_ISSUER="${GEOINTEL_AUTHENTIK_ISSUER:-}"
GEOINTEL_AUTHENTIK_CLIENT_ID="${GEOINTEL_AUTHENTIK_CLIENT_ID:-}"
GEOINTEL_AUTHENTIK_CLIENT_SECRET="${GEOINTEL_AUTHENTIK_CLIENT_SECRET:-}"
GEOINTEL_AUTHENTIK_ALLOWED_EMAIL="${GEOINTEL_AUTHENTIK_ALLOWED_EMAIL:-}"
GEOINTEL_GUEST_ACCESS_ENABLED="${GEOINTEL_GUEST_ACCESS_ENABLED:-false}"
GEOINTEL_GUEST_DISPLAY_NAME="${GEOINTEL_GUEST_DISPLAY_NAME:-Gast}"
GEOINTEL_GUEST_SESSION_TTL_SECONDS="${GEOINTEL_GUEST_SESSION_TTL_SECONDS:-7200}"
ORTHOPHOTO_ENABLED="${ORTHOPHOTO_ENABLED:-true}"
ORTHOPHOTO_WMS_URL="${ORTHOPHOTO_WMS_URL:-https://geo.api.vlaanderen.be/OMWRGBMRVL/wms}"
SPW_ORTHOPHOTO_WMS_URL="${SPW_ORTHOPHOTO_WMS_URL:-https://geoservices.wallonie.be/arcgis/services/IMAGERIE/ORTHO_LAST/MapServer/WMSServer}"
BRUSSELS_ORTHOPHOTO_WMS_URL="${BRUSSELS_ORTHOPHOTO_WMS_URL:-https://geoservices-grid.irisnet.be/geoserver/urbisgrid/ows}"
ORTHOPHOTO_WMS_LAYER="${ORTHOPHOTO_WMS_LAYER:-Ortho}"
ORTHOPHOTO_RESOLUTION_M="${ORTHOPHOTO_RESOLUTION_M:-1.0}"
ORTHOPHOTO_MIN_SIDE_M="${ORTHOPHOTO_MIN_SIDE_M:-128}"
ORTHOPHOTO_MAX_SIDE_M="${ORTHOPHOTO_MAX_SIDE_M:-1024}"
ORTHOPHOTO_CACHE_TTL_HOURS="${ORTHOPHOTO_CACHE_TTL_HOURS:-24}"
SOURCE_CATALOG_PROBE_ENABLED="${SOURCE_CATALOG_PROBE_ENABLED:-true}"
SOURCE_CATALOG_GRB_WFS_URL="${SOURCE_CATALOG_GRB_WFS_URL:-https://geo.api.vlaanderen.be/GRB/wfs}"
SOURCE_CATALOG_STATBEL_DCAT_URL="${SOURCE_CATALOG_STATBEL_DCAT_URL:-https://doc.statbel.be/publications/DCAT/DCAT_opendata_datasets.ttl}"
SOURCE_CATALOG_STATBEL_MAX_RESPONSE_MB="${SOURCE_CATALOG_STATBEL_MAX_RESPONSE_MB:-5}"
SOURCE_CATALOG_ALZ_RELEASE_URL="${SOURCE_CATALOG_ALZ_RELEASE_URL:-https://landbouwcijfers.vlaanderen.be/open-geodata-landbouwgebruikspercelen}"
SOURCE_CATALOG_PROBE_TIMEOUT_SECONDS="${SOURCE_CATALOG_PROBE_TIMEOUT_SECONDS:-10}"
SOURCE_CATALOG_PROBE_MAX_RESPONSE_MB="${SOURCE_CATALOG_PROBE_MAX_RESPONSE_MB:-2}"
SOURCE_CATALOG_PROBE_CACHE_TTL_SECONDS="${SOURCE_CATALOG_PROBE_CACHE_TTL_SECONDS:-900}"
OFFICIAL_VECTOR_ENABLED="${OFFICIAL_VECTOR_ENABLED:-true}"
BWK_WFS_URL="${BWK_WFS_URL:-https://geo.api.vlaanderen.be/BWK/wfs}"
DOV_SOIL_WFS_URL="${DOV_SOIL_WFS_URL:-https://www.dov.vlaanderen.be/geoserver/wfs}"
SPW_PICC_ENABLED="${SPW_PICC_ENABLED:-true}"
SPW_PICC_MAPSERVER_URL="${SPW_PICC_MAPSERVER_URL:-https://geoservices.wallonie.be/arcgis/rest/services/TOPOGRAPHIE/PICC_VDIFF/MapServer}"
SPW_FLOOD_HAZARD_ENABLED="${SPW_FLOOD_HAZARD_ENABLED:-true}"
SPW_FLOOD_HAZARD_MAPSERVER_URL="${SPW_FLOOD_HAZARD_MAPSERVER_URL:-https://geoservices.wallonie.be/arcgis/rest/services/EAU/ALEA_INOND/MapServer}"
URBIS_ENABLED="${URBIS_ENABLED:-true}"
URBIS_WFS_URL="${URBIS_WFS_URL:-https://geoservices-vector.irisnet.be/geoserver/urbisvector/ows}"
OFFICIAL_VECTOR_MIN_SIDE_M="${OFFICIAL_VECTOR_MIN_SIDE_M:-10}"
OFFICIAL_VECTOR_MAX_SIDE_M="${OFFICIAL_VECTOR_MAX_SIDE_M:-20000}"
OFFICIAL_VECTOR_PAGE_SIZE="${OFFICIAL_VECTOR_PAGE_SIZE:-1000}"
OFFICIAL_VECTOR_MAX_PAGES="${OFFICIAL_VECTOR_MAX_PAGES:-200}"
OFFICIAL_VECTOR_MAX_FEATURES="${OFFICIAL_VECTOR_MAX_FEATURES:-100000}"
OFFICIAL_VECTOR_TIMEOUT_SECONDS="${OFFICIAL_VECTOR_TIMEOUT_SECONDS:-180}"
OFFICIAL_VECTOR_MAX_RESPONSE_MB="${OFFICIAL_VECTOR_MAX_RESPONSE_MB:-20}"
OFFICIAL_VECTOR_MAX_TOTAL_RESPONSE_MB="${OFFICIAL_VECTOR_MAX_TOTAL_RESPONSE_MB:-256}"
OFFICIAL_VECTOR_CACHE_TTL_HOURS="${OFFICIAL_VECTOR_CACHE_TTL_HOURS:-24}"
DHMV_ENABLED="${DHMV_ENABLED:-true}"
DHMV_WCS_URL="${DHMV_WCS_URL:-https://geo.api.vlaanderen.be/DHMV/wcs}"
DHMV_RESOLUTION_M="${DHMV_RESOLUTION_M:-5.0}"
DHMV_MIN_SIDE_M="${DHMV_MIN_SIDE_M:-10}"
DHMV_MAX_SIDE_M="${DHMV_MAX_SIDE_M:-20000}"
DHMV_MAX_PIXELS="${DHMV_MAX_PIXELS:-12000000}"
DHMV_TIMEOUT_SECONDS="${DHMV_TIMEOUT_SECONDS:-300}"
DHMV_MAX_RESPONSE_MB="${DHMV_MAX_RESPONSE_MB:-160}"
FLOOD_HAZARD_ENABLED="${FLOOD_HAZARD_ENABLED:-true}"
FLOOD_HAZARD_WCS_URL="${FLOOD_HAZARD_WCS_URL:-https://geoservice.waterinfo.be/OGRK/wcs}"
FLOOD_HAZARD_RESOLUTION_M="${FLOOD_HAZARD_RESOLUTION_M:-5.0}"
FLOOD_HAZARD_MIN_SIDE_M="${FLOOD_HAZARD_MIN_SIDE_M:-10}"
FLOOD_HAZARD_MAX_SIDE_M="${FLOOD_HAZARD_MAX_SIDE_M:-20000}"
FLOOD_HAZARD_MAX_PIXELS="${FLOOD_HAZARD_MAX_PIXELS:-12000000}"
FLOOD_HAZARD_TIMEOUT_SECONDS="${FLOOD_HAZARD_TIMEOUT_SECONDS:-300}"
FLOOD_HAZARD_MAX_RESPONSE_MB="${FLOOD_HAZARD_MAX_RESPONSE_MB:-160}"
BATHYMETRY_PROFILES_ENABLED="${BATHYMETRY_PROFILES_ENABLED:-true}"
BATHYMETRY_PROFILES_LAYER_URL="${BATHYMETRY_PROFILES_LAYER_URL:-https://vha.waterinfo.be/arcgis/rest/services/digitale_atlas/MapServer/0}"
BATHYMETRY_WATERCOURSE_LAYER_URL="${BATHYMETRY_WATERCOURSE_LAYER_URL:-https://vha.waterinfo.be/arcgis/rest/services/digitale_atlas/MapServer/1}"
BATHYMETRY_PROFILES_PAGE_SIZE="${BATHYMETRY_PROFILES_PAGE_SIZE:-1000}"
BATHYMETRY_PROFILES_MAX_FEATURES="${BATHYMETRY_PROFILES_MAX_FEATURES:-50000}"
BATHYMETRY_PROFILES_TIMEOUT_SECONDS="${BATHYMETRY_PROFILES_TIMEOUT_SECONDS:-120}"
BATHYMETRY_PROFILES_MAX_RESPONSE_MB="${BATHYMETRY_PROFILES_MAX_RESPONSE_MB:-32}"
MDK_BATHYMETRY_PROBE_ENABLED="${MDK_BATHYMETRY_PROBE_ENABLED:-true}"
MDK_BATHYMETRY_WCS_URL="${MDK_BATHYMETRY_WCS_URL:-https://bathy.agentschapmdk.be/spatialfusionserver/services/ows/wcs/EL_wcs}"
MDK_BATHYMETRY_PROBE_TIMEOUT_SECONDS="${MDK_BATHYMETRY_PROBE_TIMEOUT_SECONDS:-20}"
MDK_BATHYMETRY_PROBE_MAX_RESPONSE_MB="${MDK_BATHYMETRY_PROBE_MAX_RESPONSE_MB:-4}"
MDK_BATHYMETRY_ACQUISITION_ENABLED="${MDK_BATHYMETRY_ACQUISITION_ENABLED:-false}"
MDK_BATHYMETRY_COVERAGE_ID="${MDK_BATHYMETRY_COVERAGE_ID:-}"
MDK_BATHYMETRY_REQUEST_CRS="${MDK_BATHYMETRY_REQUEST_CRS:-EPSG:4326}"
MDK_BATHYMETRY_MAX_BBOX_DEG2="${MDK_BATHYMETRY_MAX_BBOX_DEG2:-0.25}"
MDK_BATHYMETRY_ACQUISITION_TIMEOUT_SECONDS="${MDK_BATHYMETRY_ACQUISITION_TIMEOUT_SECONDS:-120}"
MDK_BATHYMETRY_ACQUISITION_MAX_RESPONSE_MB="${MDK_BATHYMETRY_ACQUISITION_MAX_RESPONSE_MB:-160}"
THEMATIC_RASTER_ENABLED="${THEMATIC_RASTER_ENABLED:-true}"
THEMATIC_RASTER_WCS_URL="${THEMATIC_RASTER_WCS_URL:-https://www.mercator.vlaanderen.be/raadpleegdienstenmercatorpubliek/wcs}"
THEMATIC_RASTER_MIN_SIDE_M="${THEMATIC_RASTER_MIN_SIDE_M:-100}"
THEMATIC_RASTER_MAX_SIDE_M="${THEMATIC_RASTER_MAX_SIDE_M:-60000}"
THEMATIC_RASTER_MAX_PIXELS="${THEMATIC_RASTER_MAX_PIXELS:-30000000}"
THEMATIC_RASTER_TIMEOUT_SECONDS="${THEMATIC_RASTER_TIMEOUT_SECONDS:-300}"
THEMATIC_RASTER_MAX_RESPONSE_MB="${THEMATIC_RASTER_MAX_RESPONSE_MB:-160}"
WALOUS_ENABLED="${WALOUS_ENABLED:-true}"
WALOUS_SOURCE_DIR="${WALOUS_SOURCE_DIR:-/app/storage/source-cache/walous}"
WALOUS_ANALYSIS_RESOLUTION_M="${WALOUS_ANALYSIS_RESOLUTION_M:-10}"
WALOUS_MAX_SIDE_M="${WALOUS_MAX_SIDE_M:-60000}"
WALOUS_MAX_PIXELS="${WALOUS_MAX_PIXELS:-36000000}"
SPW_TERRAIN_ENABLED="${SPW_TERRAIN_ENABLED:-true}"
SPW_TERRAIN_SOURCE_DIR="${SPW_TERRAIN_SOURCE_DIR:-/app/storage/source-cache/spw-terrain}"
SPW_TERRAIN_ANALYSIS_RESOLUTION_M="${SPW_TERRAIN_ANALYSIS_RESOLUTION_M:-5}"
SPW_TERRAIN_MAX_SIDE_M="${SPW_TERRAIN_MAX_SIDE_M:-20000}"
SPW_TERRAIN_MAX_PIXELS="${SPW_TERRAIN_MAX_PIXELS:-12000000}"
YOLO_ENABLED="${YOLO_ENABLED:-false}"
YOLO_MODELS_DIR="${YOLO_MODELS_DIR:-/app/models}"
YOLO_MODEL_PATH="${YOLO_MODEL_PATH:-}"
YOLO_MODEL_ID="${YOLO_MODEL_ID:-yolo-configured}"
YOLO_MODEL_DISPLAY_NAME="${YOLO_MODEL_DISPLAY_NAME:-Configured YOLO detector}"
YOLO_MODEL_VERSION="${YOLO_MODEL_VERSION:-}"
YOLO_MODEL_CLASSES="${YOLO_MODEL_CLASSES:-building}"
YOLO_ENFORCE_VALIDATION_SCOPE="${YOLO_ENFORCE_VALIDATION_SCOPE:-true}"
YOLO_VALIDATION_SCOPE_MANIFEST_PATH="${YOLO_VALIDATION_SCOPE_MANIFEST_PATH:-}"
YOLO_VALIDATION_SCOPE_MANIFEST_SHA256="${YOLO_VALIDATION_SCOPE_MANIFEST_SHA256:-}"
YOLO_VALIDATED_AREA_NAMES="${YOLO_VALIDATED_AREA_NAMES:-Mol,Kempen}"
YOLO_CONFIG_DIR="${YOLO_CONFIG_DIR:-/app/storage/ultralytics}"
YOLO_DEVICE="${YOLO_DEVICE:-cuda:0}"
YOLO_REQUIRE_CUDA="${YOLO_REQUIRE_CUDA:-true}"
YOLO_IMAGE_SIZE="${YOLO_IMAGE_SIZE:-640}"
YOLO_MAX_TILES="${YOLO_MAX_TILES:-100}"
YOLO_MAX_DETECTIONS="${YOLO_MAX_DETECTIONS:-1000}"
YOLO_DUPLICATE_IOU_THRESHOLD="${YOLO_DUPLICATE_IOU_THRESHOLD:-0.5}"
YOLO_BATCH_SIZE="${YOLO_BATCH_SIZE:-1}"
YOLO_SEG_ENABLED="${YOLO_SEG_ENABLED:-false}"
YOLO_SEG_MODEL_PATH="${YOLO_SEG_MODEL_PATH:-}"
YOLO_SEG_MODEL_ID="${YOLO_SEG_MODEL_ID:-yolo-seg-configured}"
YOLO_SEG_MODEL_DISPLAY_NAME="${YOLO_SEG_MODEL_DISPLAY_NAME:-Configured YOLO segmentation}"
YOLO_SEG_MODEL_VERSION="${YOLO_SEG_MODEL_VERSION:-}"
SAM_ENABLED="${SAM_ENABLED:-false}"
SAM_MODEL_PATH="${SAM_MODEL_PATH:-}"
SAM_MODEL_ID="${SAM_MODEL_ID:-sam-configured}"
SAM_MODEL_DISPLAY_NAME="${SAM_MODEL_DISPLAY_NAME:-Configured SAM segmentation}"
SAM_MODEL_VERSION="${SAM_MODEL_VERSION:-}"
SEGMENTATION_MAX_MASKS_PER_TILE="${SEGMENTATION_MAX_MASKS_PER_TILE:-300}"
SEGMENTATION_DUPLICATE_IOU_THRESHOLD="${SEGMENTATION_DUPLICATE_IOU_THRESHOLD:-0.5}"
OLLAMA_ENABLED="${OLLAMA_ENABLED:-true}"
OLLAMA_BASE_URL="${OLLAMA_BASE_URL:-http://host.docker.internal:11434}"
OLLAMA_DEFAULT_MODEL="${OLLAMA_DEFAULT_MODEL:-qwen3.5:9b}"
OLLAMA_TIMEOUT_SECONDS="${OLLAMA_TIMEOUT_SECONDS:-120}"
OLLAMA_MAX_OUTPUT_TOKENS="${OLLAMA_MAX_OUTPUT_TOKENS:-1200}"
OLLAMA_CONTEXT_TOKENS="${OLLAMA_CONTEXT_TOKENS:-16384}"
validate_runtime_config() {
case "$GEOINTEL_FRONTEND_PORT" in
''|*[!0-9]*)
echo "GEOINTEL_FRONTEND_PORT must be a whole number." >&2
return 2
;;
esac
if [ "$GEOINTEL_FRONTEND_PORT" -lt 1 ] || [ "$GEOINTEL_FRONTEND_PORT" -gt 65535 ]; then
echo "GEOINTEL_FRONTEND_PORT must be between 1 and 65535." >&2
return 2
fi
case "$GEOINTEL_MAX_UPLOAD_MB" in
''|*[!0-9]*)
echo "GEOINTEL_MAX_UPLOAD_MB must be a whole number." >&2
return 2
;;
esac
if [ "$GEOINTEL_MAX_UPLOAD_MB" -lt 1 ] || [ "$GEOINTEL_MAX_UPLOAD_MB" -gt 2048 ]; then
echo "GEOINTEL_MAX_UPLOAD_MB must be between 1 and 2048." >&2
return 2
fi
case "$GEOINTEL_MAX_IN_MEMORY_VECTOR_MB" in
''|*[!0-9]*)
echo "GEOINTEL_MAX_IN_MEMORY_VECTOR_MB must be a whole number." >&2
return 2
;;
esac
if [ "$GEOINTEL_MAX_IN_MEMORY_VECTOR_MB" -lt 1 ] || [ "$GEOINTEL_MAX_IN_MEMORY_VECTOR_MB" -gt 256 ]; then
echo "GEOINTEL_MAX_IN_MEMORY_VECTOR_MB must be between 1 and 256." >&2
return 2
fi
case "$GEOINTEL_AUTH_ENABLED" in
true|false) ;;
*)
echo "GEOINTEL_AUTH_ENABLED must be true or false." >&2
return 2
;;
esac
case "$GEOINTEL_AUTH_REQUIRE_HTTPS" in
true|false) ;;
*)
echo "GEOINTEL_AUTH_REQUIRE_HTTPS must be true or false." >&2
return 2
;;
esac
if [ "$GEOINTEL_AUTH_ENABLED" = "true" ]; then
if [ -z "$GEOINTEL_AUTH_USERNAME" ] \
|| [ -z "$GEOINTEL_AUTH_PASSWORD_HASH" ] \
|| [ "${#GEOINTEL_AUTH_SESSION_SECRET}" -lt 32 ]; then
echo "Enabled operator authentication requires username, password hash and a 32+ character session secret." >&2
return 2
fi
case "$GEOINTEL_AUTH_PASSWORD_HASH" in
pbkdf2_sha256\$*) ;;
*)
echo "GEOINTEL_AUTH_PASSWORD_HASH must use the pbkdf2_sha256 format." >&2
return 2
;;
esac
if [ "$GEOINTEL_AUTH_REQUIRE_HTTPS" = "true" ]; then
case "$GEOINTEL_PUBLIC_BASE_URL" in
https://*) ;;
*)
echo "GEOINTEL_PUBLIC_BASE_URL must use HTTPS when operator login requires HTTPS." >&2
return 2
;;
esac
fi
fi
local authentik_count=0
local authentik_value
for authentik_value in \
"$GEOINTEL_AUTHENTIK_ISSUER" \
"$GEOINTEL_AUTHENTIK_CLIENT_ID" \
"$GEOINTEL_AUTHENTIK_CLIENT_SECRET" \
"$GEOINTEL_AUTHENTIK_ALLOWED_EMAIL"; do
if [ -n "$authentik_value" ]; then
authentik_count=$((authentik_count + 1))
fi
done
if [ "$authentik_count" -ne 0 ] && [ "$authentik_count" -ne 4 ]; then
echo "All GEOINTEL_AUTHENTIK_* values must be configured together." >&2
return 2
fi
if [ "$authentik_count" -eq 4 ]; then
if [ "$GEOINTEL_AUTH_ENABLED" != "true" ]; then
echo "GEOINTEL_AUTH_ENABLED must be true when Authentik is configured." >&2
return 2
fi
case "$GEOINTEL_AUTHENTIK_ISSUER" in
https://*) ;;
*) echo "GEOINTEL_AUTHENTIK_ISSUER must use HTTPS." >&2; return 2 ;;
esac
case "$GEOINTEL_PUBLIC_BASE_URL" in
https://*) ;;
*) echo "GEOINTEL_PUBLIC_BASE_URL must use HTTPS for Authentik." >&2; return 2 ;;
esac
fi
case "$GEOINTEL_GUEST_ACCESS_ENABLED" in
true|false) ;;
*)
echo "GEOINTEL_GUEST_ACCESS_ENABLED must be true or false." >&2
return 2
;;
esac
if [ -z "${GEOINTEL_GUEST_DISPLAY_NAME// }" ]; then
echo "GEOINTEL_GUEST_DISPLAY_NAME must not be blank." >&2
return 2
fi
case "$GEOINTEL_GUEST_SESSION_TTL_SECONDS" in
''|*[!0-9]*)
echo "GEOINTEL_GUEST_SESSION_TTL_SECONDS must be an integer." >&2
return 2
;;
esac
if [ "$GEOINTEL_GUEST_SESSION_TTL_SECONDS" -lt 900 ] || [ "$GEOINTEL_GUEST_SESSION_TTL_SECONDS" -gt 86400 ]; then
echo "GEOINTEL_GUEST_SESSION_TTL_SECONDS must be between 900 and 86400." >&2
return 2
fi
case "$GEOINTEL_POSTGRES_PASSWORD" in
''|geointel|postgres|password|changeme|change-me-before-shared-use)
echo "Refusing deployment with an empty or known-default PostGIS password." >&2
return 2
;;
esac
docker image inspect "$GEOINTEL_IMAGE" >/dev/null
}
install_dockerman_metadata() {
if [ -d /boot/config/plugins/dockerMan ]; then
mkdir -p /boot/config/plugins/dockerMan/templates-user /boot/config/plugins/dockerMan/images
cp deploy/unraid/geointel-unraid-template.xml /boot/config/plugins/dockerMan/templates-user/my-geointel.xml
cp deploy/unraid/geointel-icon.png /boot/config/plugins/dockerMan/images/geointel-icon.png
fi
}
migrate_compose_volume_if_needed() {
if [ -f "${GEOINTEL_POSTGIS_DATA_PATH}/PG_VERSION" ]; then
return 0
fi
local compose_volume_path
compose_volume_path="$(docker volume inspect geointel_geointel_postgis --format '{{ .Mountpoint }}' 2>/dev/null || true)"
if [ -z "$compose_volume_path" ] || [ ! -f "${compose_volume_path}/PG_VERSION" ]; then
return 0
fi
echo "Migrating existing Compose PostGIS volume to ${GEOINTEL_POSTGIS_DATA_PATH}..."
mkdir -p "$GEOINTEL_POSTGIS_DATA_PATH"
cp -a "${compose_volume_path}/." "$GEOINTEL_POSTGIS_DATA_PATH/"
}
validate_runtime_config
install_dockerman_metadata
# GeoIntel runs as ONE container started by docker run below.
# Tear down any leftover Compose stack first, including the multi-container
# development stack (db + backend + frontend) that also claims port 1202.
for compose_file in docker-compose.yml docker-compose.unraid.yml; do
if [ -f "$compose_file" ]; then
docker compose -f "$compose_file" down --remove-orphans || true
fi
done
if docker ps -a --format '{{.Names}}' | grep -qx geointel; then
docker rm -f geointel >/dev/null 2>&1 || true
for attempt in $(seq 1 60); do
if ! docker ps -a --format '{{.Names}}' | grep -qx geointel; then
break
fi
if [ "$attempt" -eq 60 ]; then
echo "GeoIntel container removal did not complete within 60 seconds." >&2
exit 1
fi
sleep 1
done
fi
mkdir -p "$GEOINTEL_STORAGE_PATH" "$GEOINTEL_MODELS_PATH" "$GEOINTEL_BACKUPS_PATH" "$GEOINTEL_POSTGIS_DATA_PATH"
migrate_compose_volume_if_needed
docker run -d \
--name geointel \
--gpus all \
--restart unless-stopped \
--label "com.docker.compose.project=${COMPOSE_PROJECT_NAME:-geointel}" \
--label "tech.itworx.commit=${GITEA_COMMIT_SHA:-unknown}" \
--label net.unraid.docker.managed=dockerman \
--label 'net.unraid.docker.webui=http://[IP]:[PORT:80]/' \
--label net.unraid.docker.icon=/boot/config/plugins/dockerMan/images/geointel-icon.png \
--add-host host.docker.internal:host-gateway \
-p "${GEOINTEL_FRONTEND_PORT}:80" \
-e GEOINTEL_POSTGRES_DB="$GEOINTEL_POSTGRES_DB" \
-e GEOINTEL_POSTGRES_USER="$GEOINTEL_POSTGRES_USER" \
-e GEOINTEL_POSTGRES_PASSWORD="$GEOINTEL_POSTGRES_PASSWORD" \
-e GEOINTEL_STORAGE_ROOT=/app/storage \
-e GEOINTEL_CORS_ORIGINS="$GEOINTEL_CORS_ORIGINS" \
-e GEOINTEL_MAX_UPLOAD_MB="$GEOINTEL_MAX_UPLOAD_MB" \
-e GEOINTEL_MAX_IN_MEMORY_VECTOR_MB="$GEOINTEL_MAX_IN_MEMORY_VECTOR_MB" \
-e GEOINTEL_AOI_WORKER_ENABLED="$GEOINTEL_AOI_WORKER_ENABLED" \
-e GEOINTEL_AOI_WORKER_POLL_SECONDS="$GEOINTEL_AOI_WORKER_POLL_SECONDS" \
-e GEOINTEL_AUTH_ENABLED="$GEOINTEL_AUTH_ENABLED" \
-e GEOINTEL_AUTH_REQUIRE_HTTPS="$GEOINTEL_AUTH_REQUIRE_HTTPS" \
-e GEOINTEL_AUTH_USERNAME="$GEOINTEL_AUTH_USERNAME" \
-e GEOINTEL_AUTH_PASSWORD_HASH="$GEOINTEL_AUTH_PASSWORD_HASH" \
-e GEOINTEL_AUTH_SESSION_SECRET="$GEOINTEL_AUTH_SESSION_SECRET" \
-e GEOINTEL_AUTH_SESSION_TTL_SECONDS="$GEOINTEL_AUTH_SESSION_TTL_SECONDS" \
-e GEOINTEL_PUBLIC_BASE_URL="$GEOINTEL_PUBLIC_BASE_URL" \
-e GEOINTEL_AUTHENTIK_ISSUER="$GEOINTEL_AUTHENTIK_ISSUER" \
-e GEOINTEL_AUTHENTIK_CLIENT_ID="$GEOINTEL_AUTHENTIK_CLIENT_ID" \
-e GEOINTEL_AUTHENTIK_CLIENT_SECRET="$GEOINTEL_AUTHENTIK_CLIENT_SECRET" \
-e GEOINTEL_AUTHENTIK_ALLOWED_EMAIL="$GEOINTEL_AUTHENTIK_ALLOWED_EMAIL" \
-e GEOINTEL_GUEST_ACCESS_ENABLED="$GEOINTEL_GUEST_ACCESS_ENABLED" \
-e GEOINTEL_GUEST_DISPLAY_NAME="$GEOINTEL_GUEST_DISPLAY_NAME" \
-e GEOINTEL_GUEST_SESSION_TTL_SECONDS="$GEOINTEL_GUEST_SESSION_TTL_SECONDS" \
-e ORTHOPHOTO_ENABLED="$ORTHOPHOTO_ENABLED" \
-e ORTHOPHOTO_WMS_URL="$ORTHOPHOTO_WMS_URL" \
-e SPW_ORTHOPHOTO_WMS_URL="$SPW_ORTHOPHOTO_WMS_URL" \
-e BRUSSELS_ORTHOPHOTO_WMS_URL="$BRUSSELS_ORTHOPHOTO_WMS_URL" \
-e ORTHOPHOTO_WMS_LAYER="$ORTHOPHOTO_WMS_LAYER" \
-e ORTHOPHOTO_RESOLUTION_M="$ORTHOPHOTO_RESOLUTION_M" \
-e ORTHOPHOTO_MIN_SIDE_M="$ORTHOPHOTO_MIN_SIDE_M" \
-e ORTHOPHOTO_MAX_SIDE_M="$ORTHOPHOTO_MAX_SIDE_M" \
-e ORTHOPHOTO_CACHE_TTL_HOURS="$ORTHOPHOTO_CACHE_TTL_HOURS" \
-e SOURCE_CATALOG_PROBE_ENABLED="$SOURCE_CATALOG_PROBE_ENABLED" \
-e SOURCE_CATALOG_GRB_WFS_URL="$SOURCE_CATALOG_GRB_WFS_URL" \
-e SOURCE_CATALOG_STATBEL_DCAT_URL="$SOURCE_CATALOG_STATBEL_DCAT_URL" \
-e SOURCE_CATALOG_STATBEL_MAX_RESPONSE_MB="$SOURCE_CATALOG_STATBEL_MAX_RESPONSE_MB" \
-e SOURCE_CATALOG_ALZ_RELEASE_URL="$SOURCE_CATALOG_ALZ_RELEASE_URL" \
-e SOURCE_CATALOG_PROBE_TIMEOUT_SECONDS="$SOURCE_CATALOG_PROBE_TIMEOUT_SECONDS" \
-e SOURCE_CATALOG_PROBE_MAX_RESPONSE_MB="$SOURCE_CATALOG_PROBE_MAX_RESPONSE_MB" \
-e SOURCE_CATALOG_PROBE_CACHE_TTL_SECONDS="$SOURCE_CATALOG_PROBE_CACHE_TTL_SECONDS" \
-e OFFICIAL_VECTOR_ENABLED="$OFFICIAL_VECTOR_ENABLED" \
-e BWK_WFS_URL="$BWK_WFS_URL" \
-e DOV_SOIL_WFS_URL="$DOV_SOIL_WFS_URL" \
-e SPW_PICC_ENABLED="$SPW_PICC_ENABLED" \
-e SPW_PICC_MAPSERVER_URL="$SPW_PICC_MAPSERVER_URL" \
-e SPW_FLOOD_HAZARD_ENABLED="$SPW_FLOOD_HAZARD_ENABLED" \
-e SPW_FLOOD_HAZARD_MAPSERVER_URL="$SPW_FLOOD_HAZARD_MAPSERVER_URL" \
-e URBIS_ENABLED="$URBIS_ENABLED" \
-e URBIS_WFS_URL="$URBIS_WFS_URL" \
-e OFFICIAL_VECTOR_MIN_SIDE_M="$OFFICIAL_VECTOR_MIN_SIDE_M" \
-e OFFICIAL_VECTOR_MAX_SIDE_M="$OFFICIAL_VECTOR_MAX_SIDE_M" \
-e OFFICIAL_VECTOR_PAGE_SIZE="$OFFICIAL_VECTOR_PAGE_SIZE" \
-e OFFICIAL_VECTOR_MAX_PAGES="$OFFICIAL_VECTOR_MAX_PAGES" \
-e OFFICIAL_VECTOR_MAX_FEATURES="$OFFICIAL_VECTOR_MAX_FEATURES" \
-e OFFICIAL_VECTOR_TIMEOUT_SECONDS="$OFFICIAL_VECTOR_TIMEOUT_SECONDS" \
-e OFFICIAL_VECTOR_MAX_RESPONSE_MB="$OFFICIAL_VECTOR_MAX_RESPONSE_MB" \
-e OFFICIAL_VECTOR_MAX_TOTAL_RESPONSE_MB="$OFFICIAL_VECTOR_MAX_TOTAL_RESPONSE_MB" \
-e OFFICIAL_VECTOR_CACHE_TTL_HOURS="$OFFICIAL_VECTOR_CACHE_TTL_HOURS" \
-e DHMV_ENABLED="$DHMV_ENABLED" \
-e DHMV_WCS_URL="$DHMV_WCS_URL" \
-e DHMV_RESOLUTION_M="$DHMV_RESOLUTION_M" \
-e DHMV_MIN_SIDE_M="$DHMV_MIN_SIDE_M" \
-e DHMV_MAX_SIDE_M="$DHMV_MAX_SIDE_M" \
-e DHMV_MAX_PIXELS="$DHMV_MAX_PIXELS" \
-e DHMV_TIMEOUT_SECONDS="$DHMV_TIMEOUT_SECONDS" \
-e DHMV_MAX_RESPONSE_MB="$DHMV_MAX_RESPONSE_MB" \
-e FLOOD_HAZARD_ENABLED="$FLOOD_HAZARD_ENABLED" \
-e FLOOD_HAZARD_WCS_URL="$FLOOD_HAZARD_WCS_URL" \
-e FLOOD_HAZARD_RESOLUTION_M="$FLOOD_HAZARD_RESOLUTION_M" \
-e FLOOD_HAZARD_MIN_SIDE_M="$FLOOD_HAZARD_MIN_SIDE_M" \
-e FLOOD_HAZARD_MAX_SIDE_M="$FLOOD_HAZARD_MAX_SIDE_M" \
-e FLOOD_HAZARD_MAX_PIXELS="$FLOOD_HAZARD_MAX_PIXELS" \
-e FLOOD_HAZARD_TIMEOUT_SECONDS="$FLOOD_HAZARD_TIMEOUT_SECONDS" \
-e FLOOD_HAZARD_MAX_RESPONSE_MB="$FLOOD_HAZARD_MAX_RESPONSE_MB" \
-e BATHYMETRY_PROFILES_ENABLED="$BATHYMETRY_PROFILES_ENABLED" \
-e BATHYMETRY_PROFILES_LAYER_URL="$BATHYMETRY_PROFILES_LAYER_URL" \
-e BATHYMETRY_WATERCOURSE_LAYER_URL="$BATHYMETRY_WATERCOURSE_LAYER_URL" \
-e BATHYMETRY_PROFILES_PAGE_SIZE="$BATHYMETRY_PROFILES_PAGE_SIZE" \
-e BATHYMETRY_PROFILES_MAX_FEATURES="$BATHYMETRY_PROFILES_MAX_FEATURES" \
-e BATHYMETRY_PROFILES_TIMEOUT_SECONDS="$BATHYMETRY_PROFILES_TIMEOUT_SECONDS" \
-e BATHYMETRY_PROFILES_MAX_RESPONSE_MB="$BATHYMETRY_PROFILES_MAX_RESPONSE_MB" \
-e MDK_BATHYMETRY_PROBE_ENABLED="$MDK_BATHYMETRY_PROBE_ENABLED" \
-e MDK_BATHYMETRY_WCS_URL="$MDK_BATHYMETRY_WCS_URL" \
-e MDK_BATHYMETRY_PROBE_TIMEOUT_SECONDS="$MDK_BATHYMETRY_PROBE_TIMEOUT_SECONDS" \
-e MDK_BATHYMETRY_PROBE_MAX_RESPONSE_MB="$MDK_BATHYMETRY_PROBE_MAX_RESPONSE_MB" \
-e MDK_BATHYMETRY_ACQUISITION_ENABLED="$MDK_BATHYMETRY_ACQUISITION_ENABLED" \
-e MDK_BATHYMETRY_COVERAGE_ID="$MDK_BATHYMETRY_COVERAGE_ID" \
-e MDK_BATHYMETRY_REQUEST_CRS="$MDK_BATHYMETRY_REQUEST_CRS" \
-e MDK_BATHYMETRY_MAX_BBOX_DEG2="$MDK_BATHYMETRY_MAX_BBOX_DEG2" \
-e MDK_BATHYMETRY_ACQUISITION_TIMEOUT_SECONDS="$MDK_BATHYMETRY_ACQUISITION_TIMEOUT_SECONDS" \
-e MDK_BATHYMETRY_ACQUISITION_MAX_RESPONSE_MB="$MDK_BATHYMETRY_ACQUISITION_MAX_RESPONSE_MB" \
-e THEMATIC_RASTER_ENABLED="$THEMATIC_RASTER_ENABLED" \
-e THEMATIC_RASTER_WCS_URL="$THEMATIC_RASTER_WCS_URL" \
-e THEMATIC_RASTER_MIN_SIDE_M="$THEMATIC_RASTER_MIN_SIDE_M" \
-e THEMATIC_RASTER_MAX_SIDE_M="$THEMATIC_RASTER_MAX_SIDE_M" \
-e THEMATIC_RASTER_MAX_PIXELS="$THEMATIC_RASTER_MAX_PIXELS" \
-e THEMATIC_RASTER_TIMEOUT_SECONDS="$THEMATIC_RASTER_TIMEOUT_SECONDS" \
-e THEMATIC_RASTER_MAX_RESPONSE_MB="$THEMATIC_RASTER_MAX_RESPONSE_MB" \
-e WALOUS_ENABLED="$WALOUS_ENABLED" \
-e WALOUS_SOURCE_DIR="$WALOUS_SOURCE_DIR" \
-e WALOUS_ANALYSIS_RESOLUTION_M="$WALOUS_ANALYSIS_RESOLUTION_M" \
-e WALOUS_MAX_SIDE_M="$WALOUS_MAX_SIDE_M" \
-e WALOUS_MAX_PIXELS="$WALOUS_MAX_PIXELS" \
-e SPW_TERRAIN_ENABLED="$SPW_TERRAIN_ENABLED" \
-e SPW_TERRAIN_SOURCE_DIR="$SPW_TERRAIN_SOURCE_DIR" \
-e SPW_TERRAIN_ANALYSIS_RESOLUTION_M="$SPW_TERRAIN_ANALYSIS_RESOLUTION_M" \
-e SPW_TERRAIN_MAX_SIDE_M="$SPW_TERRAIN_MAX_SIDE_M" \
-e SPW_TERRAIN_MAX_PIXELS="$SPW_TERRAIN_MAX_PIXELS" \
-e YOLO_ENABLED="$YOLO_ENABLED" \
-e YOLO_MODELS_DIR="$YOLO_MODELS_DIR" \
-e YOLO_MODEL_PATH="$YOLO_MODEL_PATH" \
-e YOLO_MODEL_ID="$YOLO_MODEL_ID" \
-e YOLO_MODEL_DISPLAY_NAME="$YOLO_MODEL_DISPLAY_NAME" \
-e YOLO_MODEL_VERSION="$YOLO_MODEL_VERSION" \
-e YOLO_MODEL_CLASSES="$YOLO_MODEL_CLASSES" \
-e YOLO_ENFORCE_VALIDATION_SCOPE="$YOLO_ENFORCE_VALIDATION_SCOPE" \
-e YOLO_VALIDATION_SCOPE_MANIFEST_PATH="$YOLO_VALIDATION_SCOPE_MANIFEST_PATH" \
-e YOLO_VALIDATION_SCOPE_MANIFEST_SHA256="$YOLO_VALIDATION_SCOPE_MANIFEST_SHA256" \
-e YOLO_VALIDATED_AREA_NAMES="$YOLO_VALIDATED_AREA_NAMES" \
-e YOLO_CONFIG_DIR="$YOLO_CONFIG_DIR" \
-e YOLO_DEVICE="$YOLO_DEVICE" \
-e YOLO_REQUIRE_CUDA="$YOLO_REQUIRE_CUDA" \
-e YOLO_IMAGE_SIZE="$YOLO_IMAGE_SIZE" \
-e YOLO_MAX_TILES="$YOLO_MAX_TILES" \
-e YOLO_MAX_DETECTIONS="$YOLO_MAX_DETECTIONS" \
-e YOLO_DUPLICATE_IOU_THRESHOLD="$YOLO_DUPLICATE_IOU_THRESHOLD" \
-e YOLO_BATCH_SIZE="$YOLO_BATCH_SIZE" \
-e YOLO_SEG_ENABLED="$YOLO_SEG_ENABLED" \
-e YOLO_SEG_MODEL_PATH="$YOLO_SEG_MODEL_PATH" \
-e YOLO_SEG_MODEL_ID="$YOLO_SEG_MODEL_ID" \
-e YOLO_SEG_MODEL_DISPLAY_NAME="$YOLO_SEG_MODEL_DISPLAY_NAME" \
-e YOLO_SEG_MODEL_VERSION="$YOLO_SEG_MODEL_VERSION" \
-e SAM_ENABLED="$SAM_ENABLED" \
-e SAM_MODEL_PATH="$SAM_MODEL_PATH" \
-e SAM_MODEL_ID="$SAM_MODEL_ID" \
-e SAM_MODEL_DISPLAY_NAME="$SAM_MODEL_DISPLAY_NAME" \
-e SAM_MODEL_VERSION="$SAM_MODEL_VERSION" \
-e SEGMENTATION_MAX_MASKS_PER_TILE="$SEGMENTATION_MAX_MASKS_PER_TILE" \
-e SEGMENTATION_DUPLICATE_IOU_THRESHOLD="$SEGMENTATION_DUPLICATE_IOU_THRESHOLD" \
-e OLLAMA_ENABLED="$OLLAMA_ENABLED" \
-e OLLAMA_BASE_URL="$OLLAMA_BASE_URL" \
-e OLLAMA_DEFAULT_MODEL="$OLLAMA_DEFAULT_MODEL" \
-e OLLAMA_TIMEOUT_SECONDS="$OLLAMA_TIMEOUT_SECONDS" \
-e OLLAMA_MAX_OUTPUT_TOKENS="$OLLAMA_MAX_OUTPUT_TOKENS" \
-e OLLAMA_CONTEXT_TOKENS="$OLLAMA_CONTEXT_TOKENS" \
-v "${GEOINTEL_POSTGIS_DATA_PATH}:/var/lib/postgresql/data" \
-v "${GEOINTEL_STORAGE_PATH}:/app/storage" \
-v "${GEOINTEL_MODELS_PATH}:/app/models" \
-v "${GEOINTEL_BACKUPS_PATH}:/app/backups:ro" \
"$GEOINTEL_IMAGE"
docker ps --filter name=geointel