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
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:
@@ -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"]
|
||||
@@ -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 |
|
||||
@@ -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`.
|
||||
@@ -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;"
|
||||
@@ -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 |
@@ -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 |
@@ -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>
|
||||
@@ -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
|
||||
@@ -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 \
|
||||
"$@"
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user