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
+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
- `backend`: FastAPI/GIS runtime
- `frontend`: nginx-served React app with `/api` and `/health` proxying to the backend
Inside that single container:
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
http://<unraid-ip>:${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:
The app icon is served from the same container:
```text
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:
@@ -33,59 +41,46 @@ 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
docker compose -f docker-compose.unraid.yml config
docker compose -f docker-compose.unraid.yml 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}"
curl -fsS "http://192.168.10.150:${GEOINTEL_FRONTEND_PORT:-1202}/health"
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
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
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
@@ -93,11 +88,10 @@ The database port is intentionally not published to the LAN. The backend reaches
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
docker compose -f docker-compose.unraid.yml up -d --build
```
## Cleanup notes
## Safe cleanup
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.
## 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"?>
<Container version="2">
<Name>GeoIntel Kempen</Name>
<Repository>Local Docker Compose stack</Repository>
<Repository>geointel-all-in-one:latest</Repository>
<Registry>gitea-widefrog:NuklearRabbit/geointel.git</Registry>
<Network>bridge</Network>
<Shell>sh</Shell>
<Shell>bash</Shell>
<Privileged>false</Privileged>
<Support>http://192.168.10.150:1202</Support>
<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>
<WebUI>http://[IP]:[PORT:1202]/</WebUI>
<WebUI>http://[IP]:[PORT:80]/</WebUI>
<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/>
<PostArgs/>
<CPUset/>
@@ -20,14 +20,14 @@
<DonateText/>
<DonateLink/>
<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>
<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="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="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="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 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 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="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="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="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>
</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.
# Browser URL: http://<unraid-ip>:<GEOINTEL_FRONTEND_PORT>
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.
# 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_USER=geointel
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;
}
}