diff --git a/.env.example b/.env.example
index d80a6c69..95b5da12 100644
--- a/.env.example
+++ b/.env.example
@@ -20,3 +20,13 @@ OSM_OVERPASS_URL=https://overpass-api.de/api/interpreter
VITE_API_BASE_URL=
VITE_API_PROXY_TARGET=http://localhost:8000
VITE_MAP_STYLE_URL=https://demotiles.maplibre.org/style.json
+
+# Docker Compose / Unraid
+GEOINTEL_FRONTEND_PORT=1202
+GEOINTEL_BACKEND_PORT=8000
+GEOINTEL_STORAGE_PATH=./storage
+GEOINTEL_POSTGRES_DB=geointel
+GEOINTEL_POSTGRES_USER=geointel
+GEOINTEL_POSTGRES_PASSWORD=geointel
+GEOINTEL_CORS_ORIGINS=http://localhost:1202,http://127.0.0.1:1202
+GEOINTEL_MAX_UPLOAD_MB=500
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d68bdc44..42e884ea 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,15 @@
# Changelog
+## Sprint 31 Unraid deployment template (2026-06-17)
+
+- Made Docker Compose ports, storage path, PostGIS credentials, CORS origins and upload limit configurable through `.env` defaults.
+- Added `deploy/unraid/geointel.env.example` for Unraid/Tower setup.
+- Added `deploy/unraid/geointel-unraid-template.xml` documenting editable Unraid settings for the multi-container Compose stack.
+- Added GeoIntel SVG icon assets for Unraid/template use and frontend favicon serving.
+- Added regression coverage for Compose env defaults, Unraid template settings, README instructions and icon availability.
+- No API contracts, backend behavior, migrations, product features, provider fetching or AI behavior were introduced.
+
## Sprint 30 workbench component decomposition (2026-06-17)
- Moved persisted QA/QC result rendering into `QualityResultsPanel`.
diff --git a/README.md b/README.md
index cd6ec5ee..595e26bb 100644
--- a/README.md
+++ b/README.md
@@ -67,6 +67,35 @@ This is a documentation-driven engineering repo. The documentation is not decora
make readiness
```
+## Unraid / Tower deployment
+
+GeoIntel runs on Unraid as a Docker Compose stack with PostGIS, backend and frontend services. The frontend is the browser entrypoint and proxies `/api` and `/health` to the backend.
+
+Unraid template assets live in:
+
+- `deploy/unraid/geointel.env.example`
+- `deploy/unraid/geointel-unraid-template.xml`
+- `deploy/unraid/geointel-icon.svg`
+
+Copy the Unraid env template to `.env` in the checkout and edit ports there:
+
+```bash
+cd /mnt/user/appdata/geointel
+cp deploy/unraid/geointel.env.example .env
+nano .env
+docker compose up -d --build
+```
+
+Common editable values:
+
+```env
+GEOINTEL_FRONTEND_PORT=1202
+GEOINTEL_BACKEND_PORT=8000
+GEOINTEL_STORAGE_PATH=/mnt/user/appdata/geointel/storage
+```
+
+The database port is intentionally not exposed to the LAN. See `deploy/unraid/README.md` for full setup, port-change and cleanup notes.
+
## Sprint 2 quick start
- Update dependencies:
diff --git a/backend/tests/test_docker_runtime_config.py b/backend/tests/test_docker_runtime_config.py
index 8b91db91..dd93e643 100644
--- a/backend/tests/test_docker_runtime_config.py
+++ b/backend/tests/test_docker_runtime_config.py
@@ -41,15 +41,18 @@ def test_compose_does_not_require_missing_root_env_file() -> None:
compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8")
assert "env_file:" not in compose
- assert "DATABASE_URL: postgresql+psycopg://geointel:geointel@db:5432/geointel" in compose
+ assert "DATABASE_URL: postgresql+psycopg://${GEOINTEL_POSTGRES_USER:-geointel}" in compose
-def test_compose_exposes_frontend_on_host_port_1202_with_cors_origin() -> None:
+def test_compose_exposes_frontend_on_configurable_host_port_with_cors_origin() -> None:
compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8")
env_example = (ROOT / ".env.example").read_text(encoding="utf-8")
- assert '"1202:80"' in compose
- assert "CORS_ORIGINS: http://localhost:1202,http://127.0.0.1:1202" in compose
+ assert '"${GEOINTEL_FRONTEND_PORT:-1202}:80"' in compose
+ assert '"${GEOINTEL_BACKEND_PORT:-8000}:8000"' in compose
+ assert "CORS_ORIGINS: ${GEOINTEL_CORS_ORIGINS:-http://localhost:1202,http://127.0.0.1:1202}" in compose
+ assert "GEOINTEL_FRONTEND_PORT=1202" in env_example
+ assert "GEOINTEL_BACKEND_PORT=8000" in env_example
assert "http://localhost:1202" in env_example
assert "http://127.0.0.1:1202" in env_example
@@ -92,7 +95,7 @@ def test_compose_does_not_publish_postgis_on_default_host_port() -> None:
def test_compose_waits_for_healthy_database_and_applies_migrations() -> None:
compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8")
- assert "pg_isready -U geointel -d geointel" in compose
+ assert "pg_isready -U ${GEOINTEL_POSTGRES_USER:-geointel} -d ${GEOINTEL_POSTGRES_DB:-geointel}" in compose
assert "condition: service_healthy" in compose
assert "sh /app/docker_start.sh" in compose
diff --git a/backend/tests/test_sprint31_unraid_template.py b/backend/tests/test_sprint31_unraid_template.py
new file mode 100644
index 00000000..85998e48
--- /dev/null
+++ b/backend/tests/test_sprint31_unraid_template.py
@@ -0,0 +1,63 @@
+from __future__ import annotations
+
+from pathlib import Path
+
+
+ROOT = Path(__file__).resolve().parents[2]
+
+
+def test_unraid_template_documents_editable_runtime_settings() -> None:
+ template = (ROOT / "deploy" / "unraid" / "geointel-unraid-template.xml").read_text(encoding="utf-8")
+
+ assert "GeoIntel Kempen" in template
+ assert "GeoIntel is a multi-container Docker Compose stack" in template
+ assert "http://[IP]:[PORT:1202]/" in template
+ assert "http://[IP]:[PORT:1202]/geointel-icon.svg" in template
+ assert 'Target="GEOINTEL_FRONTEND_PORT"' in template
+ assert 'Target="GEOINTEL_BACKEND_PORT"' in template
+ assert 'Target="GEOINTEL_STORAGE_PATH"' in template
+ assert 'Target="GEOINTEL_POSTGRES_PASSWORD"' in template
+ assert 'Mask="true">change-me-before-shared-use' in template
+
+
+def test_unraid_env_template_matches_compose_variables() -> None:
+ compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8")
+ env_template = (ROOT / "deploy" / "unraid" / "geointel.env.example").read_text(encoding="utf-8")
+
+ for key in (
+ "GEOINTEL_FRONTEND_PORT",
+ "GEOINTEL_BACKEND_PORT",
+ "GEOINTEL_STORAGE_PATH",
+ "GEOINTEL_POSTGRES_DB",
+ "GEOINTEL_POSTGRES_USER",
+ "GEOINTEL_POSTGRES_PASSWORD",
+ "GEOINTEL_CORS_ORIGINS",
+ "GEOINTEL_MAX_UPLOAD_MB",
+ ):
+ assert key in compose
+ assert f"{key}=" in env_template
+
+ assert "GEOINTEL_FRONTEND_PORT=1202" in env_template
+ assert "GEOINTEL_STORAGE_PATH=/mnt/user/appdata/geointel/storage" in env_template
+
+
+def test_unraid_readme_explains_port_changes_and_safe_cleanup() -> None:
+ readme = (ROOT / "deploy" / "unraid" / "README.md").read_text(encoding="utf-8")
+
+ assert "cp deploy/unraid/geointel.env.example .env" in readme
+ assert "GEOINTEL_FRONTEND_PORT=1203" in readme
+ assert "docker compose up -d --build" in readme
+ assert "bash scripts/live_migration_smoke.sh" in readme
+ assert "docker builder prune -af" in readme
+ assert "Avoid broad volume pruning" in readme
+
+
+def test_frontend_and_unraid_icon_assets_are_present() -> None:
+ deploy_icon = (ROOT / "deploy" / "unraid" / "geointel-icon.svg").read_text(encoding="utf-8")
+ frontend_icon = (ROOT / "frontend" / "public" / "geointel-icon.svg").read_text(encoding="utf-8")
+ index = (ROOT / "frontend" / "index.html").read_text(encoding="utf-8")
+
+ assert "' in index
diff --git a/deploy/unraid/README.md b/deploy/unraid/README.md
new file mode 100644
index 00000000..b0396dfc
--- /dev/null
+++ b/deploy/unraid/README.md
@@ -0,0 +1,108 @@
+# GeoIntel Unraid template
+
+GeoIntel is a multi-container Docker Compose stack:
+
+- `db`: PostGIS 16 / PostGIS 3.4
+- `backend`: FastAPI/GIS runtime
+- `frontend`: nginx-served React app with `/api` and `/health` proxying to the backend
+
+The normal browser entrypoint is:
+
+```text
+http://:${GEOINTEL_FRONTEND_PORT}
+```
+
+## Files
+
+- `geointel.env.example`: copy this to the repository checkout as `.env`.
+- `geointel-unraid-template.xml`: Unraid/DockerMan-style metadata for the editable settings.
+- `geointel-icon.svg`: icon source for template/app metadata.
+
+The frontend also serves the same icon at:
+
+```text
+http://:${GEOINTEL_FRONTEND_PORT}/geointel-icon.svg
+```
+
+## First setup on Unraid
+
+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
+```
+
+Edit `.env` before starting the stack:
+
+```bash
+nano .env
+```
+
+Common values:
+
+```env
+GEOINTEL_FRONTEND_PORT=1202
+GEOINTEL_BACKEND_PORT=8000
+GEOINTEL_STORAGE_PATH=/mnt/user/appdata/geointel/storage
+GEOINTEL_POSTGRES_PASSWORD=change-me-before-shared-use
+GEOINTEL_CORS_ORIGINS=http://localhost:1202,http://127.0.0.1:1202,http://192.168.10.150:1202
+```
+
+Start or rebuild:
+
+```bash
+docker compose config
+docker compose up -d --build
+```
+
+Validate:
+
+```bash
+bash scripts/live_migration_smoke.sh
+bash scripts/verify_browser_runtime.sh "http://192.168.10.150:${GEOINTEL_FRONTEND_PORT:-1202}"
+```
+
+## Change ports
+
+Change the web UI port:
+
+```env
+GEOINTEL_FRONTEND_PORT=1203
+```
+
+If you expose a different web port, also update CORS origins:
+
+```env
+GEOINTEL_CORS_ORIGINS=http://localhost:1203,http://127.0.0.1:1203,http://192.168.10.150:1203
+```
+
+Apply:
+
+```bash
+docker compose up -d --build
+```
+
+The database port is intentionally not published to the LAN. The backend reaches it through the internal Compose service name `db`.
+
+## Update from Gitea
+
+```bash
+cd /mnt/user/appdata/geointel
+git fetch origin main
+git reset --hard origin/main
+docker compose up -d --build
+bash scripts/live_migration_smoke.sh
+```
+
+## Cleanup notes
+
+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.
diff --git a/deploy/unraid/geointel-icon.svg b/deploy/unraid/geointel-icon.svg
new file mode 100644
index 00000000..46084f65
--- /dev/null
+++ b/deploy/unraid/geointel-icon.svg
@@ -0,0 +1,20 @@
+
diff --git a/deploy/unraid/geointel-unraid-template.xml b/deploy/unraid/geointel-unraid-template.xml
new file mode 100644
index 00000000..59bade22
--- /dev/null
+++ b/deploy/unraid/geointel-unraid-template.xml
@@ -0,0 +1,33 @@
+
+
+ GeoIntel Kempen
+ Local Docker Compose stack
+ gitea-widefrog:NuklearRabbit/geointel.git
+ bridge
+ sh
+ false
+ http://192.168.10.150:1202
+ GeoIntel Kempen
+ GeoIntel is a multi-container Docker Compose stack for the GeoIntel Kempen GeoAI Workbench: PostGIS, FastAPI and React/MapLibre. Use this template together with deploy/unraid/geointel.env.example and docker-compose.yml; it documents the editable Unraid settings for the Compose stack.
+ Productivity: Tools: GIS:
+ http://[IP]:[PORT:1202]/
+ deploy/unraid/geointel-unraid-template.xml
+ http://[IP]:[PORT:1202]/geointel-icon.svg
+
+
+
+
+
+
+
+ GeoIntel Kempen runs as a Docker Compose stack with services db, backend and frontend. Edit the matching .env variables to change ports and paths, then run docker compose up -d --build from /mnt/user/appdata/geointel.
+
+ 1202
+ 8000
+ /mnt/user/appdata/geointel/storage
+ geointel
+ geointel
+ change-me-before-shared-use
+ http://localhost:1202,http://127.0.0.1:1202,http://192.168.10.150:1202
+ 500
+
diff --git a/deploy/unraid/geointel.env.example b/deploy/unraid/geointel.env.example
new file mode 100644
index 00000000..fa6043f9
--- /dev/null
+++ b/deploy/unraid/geointel.env.example
@@ -0,0 +1,23 @@
+# GeoIntel Unraid/Compose environment template.
+# Copy this file to /mnt/user/appdata/geointel/.env and edit values there.
+
+# Browser URL: http://:
+GEOINTEL_FRONTEND_PORT=1202
+
+# Optional direct backend API port. The frontend proxies /api and /health, so this
+# does not need to be exposed to normal browser users.
+GEOINTEL_BACKEND_PORT=8000
+
+# Persisted application artifacts: uploads, tiles, masks, reports and exports.
+GEOINTEL_STORAGE_PATH=/mnt/user/appdata/geointel/storage
+
+# Internal PostGIS database settings. The database is not published to the LAN.
+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.
+GEOINTEL_CORS_ORIGINS=http://localhost:1202,http://127.0.0.1:1202,http://192.168.10.150:1202
+
+# Upload guard in MiB.
+GEOINTEL_MAX_UPLOAD_MB=500
diff --git a/docker-compose.yml b/docker-compose.yml
index 91a75ab6..fb8e5326 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -2,13 +2,13 @@ services:
db:
image: postgis/postgis:16-3.4
environment:
- POSTGRES_DB: geointel
- POSTGRES_USER: geointel
- POSTGRES_PASSWORD: geointel
+ POSTGRES_DB: ${GEOINTEL_POSTGRES_DB:-geointel}
+ POSTGRES_USER: ${GEOINTEL_POSTGRES_USER:-geointel}
+ POSTGRES_PASSWORD: ${GEOINTEL_POSTGRES_PASSWORD:-geointel}
volumes:
- geointel_postgis:/var/lib/postgresql/data
healthcheck:
- test: ["CMD-SHELL", "pg_isready -U geointel -d geointel"]
+ test: ["CMD-SHELL", "pg_isready -U ${GEOINTEL_POSTGRES_USER:-geointel} -d ${GEOINTEL_POSTGRES_DB:-geointel}"]
interval: 5s
timeout: 5s
retries: 10
@@ -17,13 +17,14 @@ services:
build:
context: ./backend
environment:
- DATABASE_URL: postgresql+psycopg://geointel:geointel@db:5432/geointel
+ DATABASE_URL: postgresql+psycopg://${GEOINTEL_POSTGRES_USER:-geointel}:${GEOINTEL_POSTGRES_PASSWORD:-geointel}@db:5432/${GEOINTEL_POSTGRES_DB:-geointel}
STORAGE_ROOT: /app/storage
- CORS_ORIGINS: http://localhost:1202,http://127.0.0.1:1202
+ CORS_ORIGINS: ${GEOINTEL_CORS_ORIGINS:-http://localhost:1202,http://127.0.0.1:1202}
+ MAX_UPLOAD_MB: ${GEOINTEL_MAX_UPLOAD_MB:-500}
ports:
- - "8000:8000"
+ - "${GEOINTEL_BACKEND_PORT:-8000}:8000"
volumes:
- - ./storage:/app/storage
+ - ${GEOINTEL_STORAGE_PATH:-./storage}:/app/storage
- ./fixtures:/app/fixtures:ro
command: sh /app/docker_start.sh
depends_on:
@@ -40,7 +41,7 @@ services:
build:
context: ./frontend
ports:
- - "1202:80"
+ - "${GEOINTEL_FRONTEND_PORT:-1202}:80"
depends_on:
backend:
condition: service_healthy
diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md
index 947c46ba..8214e2e7 100644
--- a/docs/CODEX_EXECUTION_LOG.md
+++ b/docs/CODEX_EXECUTION_LOG.md
@@ -1,3 +1,30 @@
+## Sprint 31 Unraid deployment template (2026-06-17)
+
+Changed:
+- Made `docker-compose.yml` configurable through `.env` defaults for frontend port, backend port, storage path, PostGIS database/user/password, CORS origins and upload limit.
+- Added `deploy/unraid/geointel.env.example` for Unraid/Tower runtime configuration.
+- Added `deploy/unraid/geointel-unraid-template.xml` as Unraid/DockerMan-style metadata for the editable Compose stack settings.
+- Added `deploy/unraid/geointel-icon.svg` and served the same icon through `frontend/public/geointel-icon.svg`.
+- Added the frontend favicon link for the GeoIntel icon.
+- Added Sprint 31 tests for Unraid template coverage, compose variable coverage, docs and icon availability.
+- Updated root README, TODO and changelog docs.
+
+Validation:
+- `python -m pytest backend/tests/test_sprint31_unraid_template.py backend/tests/test_docker_runtime_config.py` passed: 22 tests.
+- `cd frontend && npm run typecheck` passed.
+- `cd frontend && npm run build` passed.
+- `python -m compileall backend/app` passed.
+- `cd backend && python -m pytest` passed: 177 tests.
+- `bash scripts/run_readiness_check.sh` passed: 177 backend tests, frontend typecheck/build, Alembic head check and script syntax checks.
+- `cd backend && python -m alembic heads` passed: single head `202606120900`.
+- `cd backend && python -m alembic upgrade head --sql` passed.
+- `bash -n scripts/live_migration_smoke.sh` passed.
+
+Notes:
+- Local Windows environment does not have `docker` in PATH, so local `docker compose config` could not be run from this machine.
+- The Tower deployment pass should validate Docker Compose config and live runtime after commit.
+- No API contracts, backend behavior, migrations, product features, provider fetching or AI behavior changed.
+
## Sprint 30 workbench component decomposition (2026-06-17)
Changed:
diff --git a/docs/TODO.md b/docs/TODO.md
index d0cc8c0f..892aba6d 100644
--- a/docs/TODO.md
+++ b/docs/TODO.md
@@ -10,6 +10,7 @@ This file now starts with the current implementation status. Older preparation/b
- [x] Fix Docker backend package install order and remove mandatory root `.env` dependency.
- [x] Add Docker build context ignores for backend and frontend.
- [x] Run Docker/PostGIS live validation on Tower/Unraid.
+- [x] Add Unraid Compose template assets with editable ports, storage path and app icon.
## Current implementation status
@@ -41,6 +42,7 @@ This file now starts with the current implementation status. Older preparation/b
- [x] Dataset, raster and vector workflow hook extraction beyond Sprint 10.
- [x] Dataset detail, raster controls and vector controls component decomposition.
- [x] QA/QC results and map workspace component decomposition.
+- [x] Docker Compose port/storage/database configuration via `.env` defaults for Unraid.
- [ ] Further frontend component decomposition for export preview and shared workbench orchestration.
## Sprint 8 status
diff --git a/frontend/index.html b/frontend/index.html
index 4b95ca2c..6d9f8095 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -3,6 +3,7 @@
+
GeoIntel Kempen
diff --git a/frontend/public/geointel-icon.svg b/frontend/public/geointel-icon.svg
new file mode 100644
index 00000000..46084f65
--- /dev/null
+++ b/frontend/public/geointel-icon.svg
@@ -0,0 +1,20 @@
+