Add Unraid all-in-one runtime
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled

This commit is contained in:
Codex
2026-06-17 04:41:50 +02:00
parent ed083d1f3a
commit e553ce845b
17 changed files with 422 additions and 104 deletions
+23
View File
@@ -0,0 +1,23 @@
.git
.venv
venv
__pycache__
*.pyc
.pytest_cache
frontend/node_modules
frontend/dist
frontend/*.tsbuildinfo
backend/.pytest_cache
backend/**/*.pyc
backend/**/__pycache__
storage
postgres-data
datasets/raw
datasets/processed
datasets/cache
exports
models
.env
+1
View File
@@ -25,6 +25,7 @@ VITE_MAP_STYLE_URL=https://demotiles.maplibre.org/style.json
GEOINTEL_FRONTEND_PORT=1202 GEOINTEL_FRONTEND_PORT=1202
GEOINTEL_BACKEND_PORT=8000 GEOINTEL_BACKEND_PORT=8000
GEOINTEL_STORAGE_PATH=./storage GEOINTEL_STORAGE_PATH=./storage
GEOINTEL_POSTGIS_DATA_PATH=./postgres-data
GEOINTEL_POSTGRES_DB=geointel GEOINTEL_POSTGRES_DB=geointel
GEOINTEL_POSTGRES_USER=geointel GEOINTEL_POSTGRES_USER=geointel
GEOINTEL_POSTGRES_PASSWORD=geointel GEOINTEL_POSTGRES_PASSWORD=geointel
+10
View File
@@ -7,6 +7,16 @@
# Changelog # Changelog
## Sprint 32 Unraid all-in-one runtime (2026-06-17)
- Added `docker-compose.unraid.yml` for a single `geointel` container on Unraid.
- Added `deploy/unraid/Dockerfile.all-in-one`, embedding PostGIS, FastAPI, nginx and the built React frontend in one image.
- Added `deploy/unraid/all-in-one-start.sh` to start embedded PostGIS, apply Alembic migrations, start the backend and serve nginx.
- Added `deploy/unraid/nginx-all-in-one.conf` with localhost backend proxying inside the same container.
- Updated Tower deploy scripts to stop the old multi-container stack without removing volumes and start the all-in-one stack.
- Updated the Unraid template so the Docker can be edited from Unraid with one web port, storage path, PostGIS data path and app icon.
- No API contracts, migrations, product features, provider fetching or AI behavior were introduced.
## Sprint 31 Unraid deployment template (2026-06-17) ## 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. - Made Docker Compose ports, storage path, PostGIS credentials, CORS origins and upload limit configurable through `.env` defaults.
+6 -5
View File
@@ -69,32 +69,33 @@ make readiness
## Unraid / Tower deployment ## 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. GeoIntel runs on Unraid as an all-in-one Docker container. The container embeds PostGIS, runs the FastAPI backend internally, and serves the frontend through nginx on one editable web port.
Unraid template assets live in: Unraid template assets live in:
- `deploy/unraid/geointel.env.example` - `deploy/unraid/geointel.env.example`
- `deploy/unraid/geointel-unraid-template.xml` - `deploy/unraid/geointel-unraid-template.xml`
- `deploy/unraid/geointel-icon.svg` - `deploy/unraid/geointel-icon.svg`
- `docker-compose.unraid.yml`
Copy the Unraid env template to `.env` in the checkout and edit ports there: Copy the Unraid env template to `.env` in the checkout and edit ports/paths there:
```bash ```bash
cd /mnt/user/appdata/geointel cd /mnt/user/appdata/geointel
cp deploy/unraid/geointel.env.example .env cp deploy/unraid/geointel.env.example .env
nano .env nano .env
docker compose up -d --build docker compose -f docker-compose.unraid.yml up -d --build
``` ```
Common editable values: Common editable values:
```env ```env
GEOINTEL_FRONTEND_PORT=1202 GEOINTEL_FRONTEND_PORT=1202
GEOINTEL_BACKEND_PORT=8000
GEOINTEL_STORAGE_PATH=/mnt/user/appdata/geointel/storage GEOINTEL_STORAGE_PATH=/mnt/user/appdata/geointel/storage
GEOINTEL_POSTGIS_DATA_PATH=/mnt/user/appdata/geointel/postgres-data
``` ```
The database port is intentionally not exposed to the LAN. See `deploy/unraid/README.md` for full setup, port-change and cleanup notes. The backend and PostGIS ports are intentionally not exposed to the LAN in the all-in-one runtime. See `deploy/unraid/README.md` for full setup, port-change and cleanup notes.
## Sprint 2 quick start ## Sprint 2 quick start
+52 -11
View File
@@ -10,24 +10,25 @@ def test_unraid_template_documents_editable_runtime_settings() -> None:
template = (ROOT / "deploy" / "unraid" / "geointel-unraid-template.xml").read_text(encoding="utf-8") template = (ROOT / "deploy" / "unraid" / "geointel-unraid-template.xml").read_text(encoding="utf-8")
assert "<Name>GeoIntel Kempen</Name>" in template assert "<Name>GeoIntel Kempen</Name>" in template
assert "GeoIntel is a multi-container Docker Compose stack" in template assert "GeoIntel all-in-one runs the complete GeoIntel Kempen V1 stack in one Docker container" in template
assert "<WebUI>http://[IP]:[PORT:1202]/</WebUI>" in template assert "<Repository>geointel-all-in-one:latest</Repository>" in template
assert "<Icon>http://[IP]:[PORT:1202]/geointel-icon.svg</Icon>" in template assert "<WebUI>http://[IP]:[PORT:80]/</WebUI>" in template
assert 'Target="GEOINTEL_FRONTEND_PORT"' in template assert "<Icon>http://[IP]:[PORT:80]/geointel-icon.svg</Icon>" in template
assert 'Target="GEOINTEL_BACKEND_PORT"' in template assert 'Target="80"' in template
assert 'Target="GEOINTEL_STORAGE_PATH"' in template assert 'Target="/app/storage"' in template
assert 'Target="/var/lib/postgresql/data"' in template
assert 'Target="GEOINTEL_POSTGRES_PASSWORD"' in template assert 'Target="GEOINTEL_POSTGRES_PASSWORD"' in template
assert 'Mask="true">change-me-before-shared-use</Config>' in template assert 'Mask="true">change-me-before-shared-use</Config>' in template
def test_unraid_env_template_matches_compose_variables() -> None: def test_unraid_env_template_matches_single_container_compose_variables() -> None:
compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8") compose = (ROOT / "docker-compose.unraid.yml").read_text(encoding="utf-8")
env_template = (ROOT / "deploy" / "unraid" / "geointel.env.example").read_text(encoding="utf-8") env_template = (ROOT / "deploy" / "unraid" / "geointel.env.example").read_text(encoding="utf-8")
for key in ( for key in (
"GEOINTEL_FRONTEND_PORT", "GEOINTEL_FRONTEND_PORT",
"GEOINTEL_BACKEND_PORT",
"GEOINTEL_STORAGE_PATH", "GEOINTEL_STORAGE_PATH",
"GEOINTEL_POSTGIS_DATA_PATH",
"GEOINTEL_POSTGRES_DB", "GEOINTEL_POSTGRES_DB",
"GEOINTEL_POSTGRES_USER", "GEOINTEL_POSTGRES_USER",
"GEOINTEL_POSTGRES_PASSWORD", "GEOINTEL_POSTGRES_PASSWORD",
@@ -39,6 +40,14 @@ def test_unraid_env_template_matches_compose_variables() -> None:
assert "GEOINTEL_FRONTEND_PORT=1202" in env_template assert "GEOINTEL_FRONTEND_PORT=1202" in env_template
assert "GEOINTEL_STORAGE_PATH=/mnt/user/appdata/geointel/storage" in env_template assert "GEOINTEL_STORAGE_PATH=/mnt/user/appdata/geointel/storage" in env_template
assert "GEOINTEL_POSTGIS_DATA_PATH=/mnt/user/appdata/geointel/postgres-data" in env_template
assert "${GEOINTEL_POSTGIS_DATA_PATH:-geointel_postgis}:/var/lib/postgresql/data" in compose
assert "geointel_postgis:" in compose
assert "geointel:" in compose
assert "db:" not in compose
assert "backend:" not in compose
assert "frontend:" not in compose
assert '"${GEOINTEL_FRONTEND_PORT:-1202}:80"' in compose
def test_unraid_readme_explains_port_changes_and_safe_cleanup() -> None: def test_unraid_readme_explains_port_changes_and_safe_cleanup() -> None:
@@ -46,12 +55,44 @@ def test_unraid_readme_explains_port_changes_and_safe_cleanup() -> None:
assert "cp deploy/unraid/geointel.env.example .env" in readme assert "cp deploy/unraid/geointel.env.example .env" in readme
assert "GEOINTEL_FRONTEND_PORT=1203" in readme assert "GEOINTEL_FRONTEND_PORT=1203" in readme
assert "docker compose up -d --build" in readme assert "docker compose -f docker-compose.unraid.yml up -d --build" in readme
assert "bash scripts/live_migration_smoke.sh" in readme assert "curl -fsS" in readme
assert "docker builder prune -af" in readme assert "docker builder prune -af" in readme
assert "Avoid broad volume pruning" in readme assert "Avoid broad volume pruning" in readme
def test_unraid_all_in_one_runtime_starts_embedded_postgis_backend_and_nginx() -> None:
dockerfile = (ROOT / "deploy" / "unraid" / "Dockerfile.all-in-one").read_text(encoding="utf-8")
start_script = (ROOT / "deploy" / "unraid" / "all-in-one-start.sh").read_text(encoding="utf-8")
nginx_config = (ROOT / "deploy" / "unraid" / "nginx-all-in-one.conf").read_text(encoding="utf-8")
dockerignore = (ROOT / ".dockerignore").read_text(encoding="utf-8")
assert "FROM postgis/postgis:16-3.4 AS runtime" in dockerfile
assert "COPY --from=frontend-build /frontend/dist/ /usr/share/nginx/html/" in dockerfile
assert 'CMD ["/usr/local/bin/geointel-all-in-one-start"]' in dockerfile
assert "/usr/local/bin/docker-entrypoint.sh postgres &" in start_script
assert "python -m alembic upgrade head" in start_script
assert "uvicorn app.main:app --host 127.0.0.1 --port 8000 &" in start_script
assert 'exec nginx -g "daemon off;"' in start_script
assert "proxy_pass http://127.0.0.1:8000/api/" in nginx_config
assert "proxy_pass http://127.0.0.1:8000/health" in nginx_config
assert "frontend/node_modules" in dockerignore
assert "storage" in dockerignore
assert "postgres-data" in dockerignore
def test_tower_deploy_uses_single_container_unraid_compose() -> None:
powershell = (ROOT / "scripts" / "deploy_tower.ps1").read_text(encoding="utf-8")
bash = (ROOT / "scripts" / "deploy_tower.sh").read_text(encoding="utf-8")
for script in (powershell, bash):
assert "docker compose down --remove-orphans || true" in script
assert "docker compose -f docker-compose.unraid.yml config" in script
assert "docker compose -f docker-compose.unraid.yml build geointel" in script
assert "docker compose -f docker-compose.unraid.yml up -d" in script
assert "COMPOSE_FILE=docker-compose.unraid.yml bash scripts/live_migration_smoke.sh" in script
def test_frontend_and_unraid_icon_assets_are_present() -> None: def test_frontend_and_unraid_icon_assets_are_present() -> None:
deploy_icon = (ROOT / "deploy" / "unraid" / "geointel-icon.svg").read_text(encoding="utf-8") 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") frontend_icon = (ROOT / "frontend" / "public" / "geointel-icon.svg").read_text(encoding="utf-8")
+58
View File
@@ -0,0 +1,58 @@
FROM node:20-alpine AS frontend-build
WORKDIR /frontend
COPY frontend/package.json frontend/package-lock.json* ./
RUN npm install
COPY frontend/ ./
RUN npm run build
FROM postgis/postgis:16-3.4 AS runtime
ENV GEOINTEL_ENV=production \
GEOINTEL_API_PREFIX=/api/v1 \
GEOINTEL_STORAGE_ROOT=/app/storage \
STORAGE_ROOT=/app/storage \
GEOINTEL_POSTGRES_DB=geointel \
GEOINTEL_POSTGRES_USER=geointel \
GEOINTEL_POSTGRES_PASSWORD=geointel \
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 \
libgdal-dev \
libgeos-dev \
libpq-dev \
libproj-dev \
nginx \
proj-bin \
python3 \
python3-pip \
python3-venv \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY backend/ /app/
COPY fixtures/ /app/fixtures/
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 python3 -m venv /opt/geointel/venv \
&& pip install --no-cache-dir --upgrade pip setuptools \
&& pip install --no-cache-dir ".[gis]" \
&& python scripts/gis_import_smoke.py \
&& chmod +x /usr/local/bin/geointel-all-in-one-start \
&& mkdir -p /app/storage /run/nginx /var/log/nginx
VOLUME ["/var/lib/postgresql/data", "/app/storage"]
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=5s --retries=10 --start-period=60s \
CMD curl -fsS http://127.0.0.1/health >/dev/null || exit 1
CMD ["/usr/local/bin/geointel-all-in-one-start"]
+48 -50
View File
@@ -1,30 +1,38 @@
# GeoIntel Unraid template # GeoIntel Unraid all-in-one container
GeoIntel is a multi-container Docker Compose stack: GeoIntel can run on Unraid as one Docker container.
- `db`: PostGIS 16 / PostGIS 3.4 Inside that single container:
- `backend`: FastAPI/GIS runtime
- `frontend`: nginx-served React app with `/api` and `/health` proxying to the backend
The normal browser entrypoint is: - 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 ```text
http://<unraid-ip>:${GEOINTEL_FRONTEND_PORT} http://<unraid-ip>:${GEOINTEL_FRONTEND_PORT}
``` ```
## Files The app icon is served from the same container:
- `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 ```text
http://<unraid-ip>:${GEOINTEL_FRONTEND_PORT}/geointel-icon.svg http://<unraid-ip>:${GEOINTEL_FRONTEND_PORT}/geointel-icon.svg
``` ```
## First setup on Unraid ## Files
- `docker-compose.unraid.yml`: recommended single-container Compose stack.
- `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/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`: icon source.
## First setup with Compose Manager
From the Unraid shell: From the Unraid shell:
@@ -33,59 +41,46 @@ cd /mnt/user/appdata
git clone gitea-widefrog:NuklearRabbit/geointel.git geointel git clone gitea-widefrog:NuklearRabbit/geointel.git geointel
cd /mnt/user/appdata/geointel cd /mnt/user/appdata/geointel
cp deploy/unraid/geointel.env.example .env cp deploy/unraid/geointel.env.example .env
```
Edit `.env` before starting the stack:
```bash
nano .env nano .env
``` docker compose -f docker-compose.unraid.yml config
docker compose -f docker-compose.unraid.yml up -d --build
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: Validate:
```bash ```bash
bash scripts/live_migration_smoke.sh curl -fsS "http://192.168.10.150:${GEOINTEL_FRONTEND_PORT:-1202}/health"
bash scripts/verify_browser_runtime.sh "http://192.168.10.150:${GEOINTEL_FRONTEND_PORT:-1202}" curl -fsS "http://192.168.10.150:${GEOINTEL_FRONTEND_PORT:-1202}/api/v1/projects"
curl -I "http://192.168.10.150:${GEOINTEL_FRONTEND_PORT:-1202}/geointel-icon.svg"
``` ```
## Change ports ## Change the browser port
Change the web UI port: Edit `.env`:
```env ```env
GEOINTEL_FRONTEND_PORT=1203 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 GEOINTEL_CORS_ORIGINS=http://localhost:1203,http://127.0.0.1:1203,http://192.168.10.150:1203
``` ```
Apply: Apply:
```bash ```bash
docker compose up -d --build docker compose -f docker-compose.unraid.yml up -d --build
``` ```
The database port is intentionally not published to the LAN. The backend reaches it through the internal Compose service name `db`. ## 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.
## Update from Gitea ## Update from Gitea
@@ -93,11 +88,10 @@ The database port is intentionally not published to the LAN. The backend reaches
cd /mnt/user/appdata/geointel cd /mnt/user/appdata/geointel
git fetch origin main git fetch origin main
git reset --hard origin/main git reset --hard origin/main
docker compose up -d --build docker compose -f docker-compose.unraid.yml up -d --build
bash scripts/live_migration_smoke.sh
``` ```
## Cleanup notes ## Safe cleanup
Safe cache cleanup if Docker build cache fills the Unraid Docker image: Safe cache cleanup if Docker build cache fills the Unraid Docker image:
@@ -106,3 +100,7 @@ docker builder prune -af
``` ```
Avoid broad volume pruning unless you explicitly intend to remove persisted PostGIS data or GeoIntel artifacts. 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`.
+79
View File
@@ -0,0 +1,79 @@
#!/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}}"
mkdir -p "$PGDATA" "$STORAGE_ROOT" /run/nginx /var/log/nginx
chown -R 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..."
for attempt in $(seq 1 60); 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."
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", 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;"
+12 -12
View File
@@ -1,18 +1,18 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Container version="2"> <Container version="2">
<Name>GeoIntel Kempen</Name> <Name>GeoIntel Kempen</Name>
<Repository>Local Docker Compose stack</Repository> <Repository>geointel-all-in-one:latest</Repository>
<Registry>gitea-widefrog:NuklearRabbit/geointel.git</Registry> <Registry>gitea-widefrog:NuklearRabbit/geointel.git</Registry>
<Network>bridge</Network> <Network>bridge</Network>
<Shell>sh</Shell> <Shell>bash</Shell>
<Privileged>false</Privileged> <Privileged>false</Privileged>
<Support>http://192.168.10.150:1202</Support> <Support>http://192.168.10.150:1202</Support>
<Project>GeoIntel Kempen</Project> <Project>GeoIntel Kempen</Project>
<Overview>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.</Overview> <Overview>GeoIntel all-in-one runs the complete GeoIntel Kempen V1 stack 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> <Category>Productivity: Tools: GIS:</Category>
<WebUI>http://[IP]:[PORT:1202]/</WebUI> <WebUI>http://[IP]:[PORT:80]/</WebUI>
<TemplateURL>deploy/unraid/geointel-unraid-template.xml</TemplateURL> <TemplateURL>deploy/unraid/geointel-unraid-template.xml</TemplateURL>
<Icon>http://[IP]:[PORT:1202]/geointel-icon.svg</Icon> <Icon>http://[IP]:[PORT:80]/geointel-icon.svg</Icon>
<ExtraParams/> <ExtraParams/>
<PostArgs/> <PostArgs/>
<CPUset/> <CPUset/>
@@ -20,14 +20,14 @@
<DonateText/> <DonateText/>
<DonateLink/> <DonateLink/>
<Description> <Description>
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. 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> </Description>
<Config Name="Web UI Port" Target="GEOINTEL_FRONTEND_PORT" Default="1202" Mode="" Description="Host port mapped to the frontend nginx container port 80." Type="Variable" Display="always" Required="true" Mask="false">1202</Config> <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="Backend API Port" Target="GEOINTEL_BACKEND_PORT" Default="8000" Mode="" Description="Optional host port mapped to backend container port 8000. Normal browser traffic uses the frontend /api proxy." Type="Variable" Display="advanced" Required="true" Mask="false">8000</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="Storage Path" Target="GEOINTEL_STORAGE_PATH" 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="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="Internal PostGIS database name." Type="Variable" Display="advanced" Required="true" Mask="false">geointel</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="Internal PostGIS database user." 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="Internal PostGIS database password. Change before shared use." Type="Variable" Display="advanced" Required="true" Mask="true">change-me-before-shared-use</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="http://localhost:1202,http://127.0.0.1:1202,http://192.168.10.150:1202" Mode="" Description="Comma-separated browser origins allowed to call the backend directly." Type="Variable" Display="advanced" Required="false" Mask="false">http://localhost:1202,http://127.0.0.1:1202,http://192.168.10.150:1202</Config> <Config Name="CORS Origins" Target="GEOINTEL_CORS_ORIGINS" Default="http://localhost:1202,http://127.0.0.1:1202,http://192.168.10.150:1202" Mode="" Description="Comma-separated browser origins allowed to call the backend directly." Type="Variable" Display="advanced" Required="false" Mask="false">http://localhost:1202,http://127.0.0.1:1202,http://192.168.10.150:1202</Config>
<Config Name="Max Upload MB" Target="GEOINTEL_MAX_UPLOAD_MB" Default="500" Mode="" Description="Maximum upload size in MiB enforced by the backend settings." Type="Variable" Display="advanced" Required="true" Mask="false">500</Config> <Config Name="Max Upload MB" Target="GEOINTEL_MAX_UPLOAD_MB" Default="500" Mode="" Description="Maximum upload size in MiB enforced by the backend settings." Type="Variable" Display="advanced" Required="true" Mask="false">500</Config>
</Container> </Container>
+5 -6
View File
@@ -1,17 +1,16 @@
# GeoIntel Unraid/Compose environment template. # GeoIntel Unraid all-in-one environment template.
# Copy this file to /mnt/user/appdata/geointel/.env and edit values there. # Copy this file to /mnt/user/appdata/geointel/.env and edit values there.
# Browser URL: http://<unraid-ip>:<GEOINTEL_FRONTEND_PORT> # Browser URL: http://<unraid-ip>:<GEOINTEL_FRONTEND_PORT>
GEOINTEL_FRONTEND_PORT=1202 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. # Persisted application artifacts: uploads, tiles, masks, reports and exports.
GEOINTEL_STORAGE_PATH=/mnt/user/appdata/geointel/storage GEOINTEL_STORAGE_PATH=/mnt/user/appdata/geointel/storage
# Internal PostGIS database settings. The database is not published to the LAN. # 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.
GEOINTEL_POSTGRES_DB=geointel GEOINTEL_POSTGRES_DB=geointel
GEOINTEL_POSTGRES_USER=geointel GEOINTEL_POSTGRES_USER=geointel
GEOINTEL_POSTGRES_PASSWORD=change-me-before-shared-use GEOINTEL_POSTGRES_PASSWORD=change-me-before-shared-use
+44
View File
@@ -0,0 +1,44 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location = /index.html {
add_header Cache-Control "no-cache";
try_files /index.html =404;
}
location = /geointel-icon.svg {
add_header Cache-Control "public, max-age=3600";
try_files /geointel-icon.svg =404;
}
location /assets/ {
add_header Cache-Control "no-cache";
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 $scheme;
}
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 $scheme;
}
location / {
try_files $uri $uri/ /index.html;
}
}
+23
View File
@@ -0,0 +1,23 @@
services:
geointel:
build:
context: .
dockerfile: deploy/unraid/Dockerfile.all-in-one
image: geointel-all-in-one:latest
container_name: geointel
environment:
GEOINTEL_POSTGRES_DB: ${GEOINTEL_POSTGRES_DB:-geointel}
GEOINTEL_POSTGRES_USER: ${GEOINTEL_POSTGRES_USER:-geointel}
GEOINTEL_POSTGRES_PASSWORD: ${GEOINTEL_POSTGRES_PASSWORD:-geointel}
GEOINTEL_STORAGE_ROOT: /app/storage
GEOINTEL_CORS_ORIGINS: ${GEOINTEL_CORS_ORIGINS:-http://localhost:1202,http://127.0.0.1:1202}
GEOINTEL_MAX_UPLOAD_MB: ${GEOINTEL_MAX_UPLOAD_MB:-500}
ports:
- "${GEOINTEL_FRONTEND_PORT:-1202}:80"
volumes:
- ${GEOINTEL_POSTGIS_DATA_PATH:-geointel_postgis}:/var/lib/postgresql/data
- ${GEOINTEL_STORAGE_PATH:-./storage}:/app/storage
restart: unless-stopped
volumes:
geointel_postgis:
+29
View File
@@ -1,3 +1,32 @@
## Sprint 32 Unraid all-in-one runtime (2026-06-17)
Changed:
- Added `docker-compose.unraid.yml` for a single editable `geointel` container on Unraid.
- Added `deploy/unraid/Dockerfile.all-in-one` to build one image containing embedded PostGIS, backend GIS runtime, nginx and frontend static assets.
- Added `deploy/unraid/all-in-one-start.sh` to start embedded PostGIS, apply Alembic migrations, start FastAPI on internal localhost and serve nginx on container port 80.
- Added `deploy/unraid/nginx-all-in-one.conf` so `/api` and `/health` proxy to `127.0.0.1:8000` inside the same container.
- Updated `deploy/unraid/geointel.env.example`, Unraid XML template and README for one-container operation and editable web/storage/database paths.
- Updated Tower deploy scripts to stop the old multi-container stack without deleting volumes and start `docker-compose.unraid.yml`.
- Updated `scripts/live_migration_smoke.sh` to support both all-in-one `geointel` and legacy `backend` Compose services.
- Added root `.dockerignore` for all-in-one builds.
- Updated Sprint 31 tests to cover the all-in-one Dockerfile, startscript, nginx config, deploy scripts and template metadata.
Validation:
- `python -m pytest backend/tests/test_sprint31_unraid_template.py backend/tests/test_live_migration_smoke_script.py backend/tests/test_docker_runtime_config.py` passed: 26 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: 179 tests.
- `bash scripts/run_readiness_check.sh` passed: 179 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.
- `bash -n deploy/unraid/all-in-one-start.sh` passed.
Notes:
- No API contracts, migrations, product features, provider fetching or AI behavior changed.
- Local Windows environment does not have `docker` in PATH; Tower deployment should provide the live all-in-one Docker validation.
## Sprint 31 Unraid deployment template (2026-06-17) ## Sprint 31 Unraid deployment template (2026-06-17)
Changed: Changed:
+2
View File
@@ -11,6 +11,7 @@ This file now starts with the current implementation status. Older preparation/b
- [x] Add Docker build context ignores for backend and frontend. - [x] Add Docker build context ignores for backend and frontend.
- [x] Run Docker/PostGIS live validation on Tower/Unraid. - [x] Run Docker/PostGIS live validation on Tower/Unraid.
- [x] Add Unraid Compose template assets with editable ports, storage path and app icon. - [x] Add Unraid Compose template assets with editable ports, storage path and app icon.
- [x] Add single-container Unraid runtime with embedded PostGIS, backend and frontend.
## Current implementation status ## Current implementation status
@@ -43,6 +44,7 @@ This file now starts with the current implementation status. Older preparation/b
- [x] Dataset detail, raster controls and vector controls component decomposition. - [x] Dataset detail, raster controls and vector controls component decomposition.
- [x] QA/QC results and map workspace component decomposition. - [x] QA/QC results and map workspace component decomposition.
- [x] Docker Compose port/storage/database configuration via `.env` defaults for Unraid. - [x] Docker Compose port/storage/database configuration via `.env` defaults for Unraid.
- [x] Single-container `geointel` Unraid compose/template runtime.
- [ ] Further frontend component decomposition for export preview and shared workbench orchestration. - [ ] Further frontend component decomposition for export preview and shared workbench orchestration.
## Sprint 8 status ## Sprint 8 status
+8 -7
View File
@@ -30,16 +30,17 @@ fi
git remote get-url origin >/dev/null 2>&1 || git remote add origin '$RemoteRepo' git remote get-url origin >/dev/null 2>&1 || git remote add origin '$RemoteRepo'
git fetch origin '$RemoteBranch' git fetch origin '$RemoteBranch'
git reset --hard 'origin/$RemoteBranch' git reset --hard 'origin/$RemoteBranch'
git branch -M '$RemoteBranch' git branch -M '$RemoteBranch'
chmod +x scripts/*.sh backend/docker_start.sh || true chmod +x scripts/*.sh backend/docker_start.sh deploy/unraid/*.sh || true
docker compose config >/dev/null docker compose down --remove-orphans || true
docker compose build backend frontend docker compose -f docker-compose.unraid.yml config >/dev/null
docker compose up -d docker compose -f docker-compose.unraid.yml build geointel
docker compose ps docker compose -f docker-compose.unraid.yml up -d
docker compose -f docker-compose.unraid.yml ps
if [ -x scripts/live_migration_smoke.sh ]; then if [ -x scripts/live_migration_smoke.sh ]; then
bash scripts/live_migration_smoke.sh COMPOSE_FILE=docker-compose.unraid.yml bash scripts/live_migration_smoke.sh
fi fi
if [ -x scripts/verify_browser_runtime.sh ]; then if [ -x scripts/verify_browser_runtime.sh ]; then
+7 -5
View File
@@ -32,14 +32,16 @@ fi
git fetch origin "$REMOTE_BRANCH" git fetch origin "$REMOTE_BRANCH"
git checkout -B "$REMOTE_BRANCH" "origin/$REMOTE_BRANCH" git checkout -B "$REMOTE_BRANCH" "origin/$REMOTE_BRANCH"
chmod +x scripts/*.sh backend/docker_start.sh deploy/unraid/*.sh || true
docker compose config >/dev/null docker compose down --remove-orphans || true
docker compose build backend frontend docker compose -f docker-compose.unraid.yml config >/dev/null
docker compose up -d docker compose -f docker-compose.unraid.yml build geointel
docker compose ps docker compose -f docker-compose.unraid.yml up -d
docker compose -f docker-compose.unraid.yml ps
if [[ -x scripts/live_migration_smoke.sh ]]; then if [[ -x scripts/live_migration_smoke.sh ]]; then
bash scripts/live_migration_smoke.sh COMPOSE_FILE=docker-compose.unraid.yml bash scripts/live_migration_smoke.sh
fi fi
if [[ -x scripts/verify_browser_runtime.sh ]]; then if [[ -x scripts/verify_browser_runtime.sh ]]; then
+15 -8
View File
@@ -4,14 +4,16 @@ set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PYTHON_BIN="${PYTHON_BIN:-}" PYTHON_BIN="${PYTHON_BIN:-}"
LIVE_SMOKE_USE_DOCKER="${LIVE_SMOKE_USE_DOCKER:-auto}" LIVE_SMOKE_USE_DOCKER="${LIVE_SMOKE_USE_DOCKER:-auto}"
COMPOSE_FILE_PATH="${COMPOSE_FILE:-docker-compose.yml}"
run_docker_smoke() { run_docker_smoke() {
service_name="$1"
cd "$ROOT" cd "$ROOT"
echo "== GeoIntel live migration smoke ==" echo "== GeoIntel live migration smoke =="
echo "Using Docker Compose backend service." echo "Using Docker Compose service: ${service_name}."
docker compose exec -T backend sh <<'CONTAINER_SCRIPT' docker compose -f "$COMPOSE_FILE_PATH" exec -T "$service_name" sh <<'CONTAINER_SCRIPT'
set -eu set -eu
cd /app cd /app
@@ -76,13 +78,18 @@ echo "== Live migration smoke passed =="
CONTAINER_SCRIPT CONTAINER_SCRIPT
} }
if [ "$LIVE_SMOKE_USE_DOCKER" != "0" ] && command -v docker >/dev/null 2>&1 && [ -f "$ROOT/docker-compose.yml" ]; then if [ "$LIVE_SMOKE_USE_DOCKER" != "0" ] && command -v docker >/dev/null 2>&1 && [ -f "$ROOT/$COMPOSE_FILE_PATH" ]; then
if docker compose ps -q backend >/dev/null 2>&1 && [ -n "$(docker compose ps -q backend 2>/dev/null)" ]; then cd "$ROOT"
if [ "$LIVE_SMOKE_USE_DOCKER" = "1" ] || docker compose ps --status running backend | grep -q backend; then for service_name in geointel backend; do
run_docker_smoke if docker compose -f "$COMPOSE_FILE_PATH" ps -q "$service_name" >/dev/null 2>&1 \
exit 0 && [ -n "$(docker compose -f "$COMPOSE_FILE_PATH" ps -q "$service_name" 2>/dev/null)" ]; then
if [ "$LIVE_SMOKE_USE_DOCKER" = "1" ] \
|| docker compose -f "$COMPOSE_FILE_PATH" ps --status running "$service_name" | grep -q "$service_name"; then
run_docker_smoke "$service_name"
exit 0
fi
fi fi
fi done
fi fi
if [ -z "$PYTHON_BIN" ]; then if [ -z "$PYTHON_BIN" ]; then