Progressive enhancement chain: GeoIP auto-detect -> browser GPS -> manual entry -> works without any location. When set, the observer coordinates are injected into chat requests so the LLM can answer "Where is Jupiter?" with actual azimuth/elevation from the user's location instead of placeholder coordinates. Backend: - GeoIP service (MaxMind GeoLite2-City) with lazy init, private IP filtering, and IPv4-mapped IPv6 unwrapping - GET /api/geolocate endpoint (sync to avoid blocking event loop on mmap I/O, rightmost X-Forwarded-For for Caddy trust chain) - ObserverContext model on both chat endpoints with shared _observer_prefix() helper that sanitizes label against prompt injection Frontend: - Location bar between header and messages with pin icon, GPS button, edit/clear controls, and inline manual entry parser (accepts "40.7N 74.0W", decimal lat/lon, pg_orrery observer format) - GeoIP auto-detect on first visit, localStorage persistence - Observer coordinates sent with every chat request Infrastructure: - api-data volume for GeoIP database, Caddy handle_4 for /api/geolocate - update_geoip.sh using MaxMind Basic auth (key stays out of ps/proc)
145 lines
4.4 KiB
YAML
145 lines
4.4 KiB
YAML
services:
|
|
db:
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile.db
|
|
environment:
|
|
POSTGRES_DB: orrery_search
|
|
POSTGRES_USER: ${POSTGRES_USER:-orrery}
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
|
volumes:
|
|
- pg-data:/var/lib/postgresql/data
|
|
networks:
|
|
- internal
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U orrery -d orrery_search"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 5
|
|
start_period: 10s
|
|
restart: unless-stopped
|
|
|
|
pgai-install:
|
|
image: timescale/pgai-vectorizer-worker:latest
|
|
entrypoint: ["python", "-m", "pgai", "install", "-d", "postgresql://${POSTGRES_USER:-orrery}:${POSTGRES_PASSWORD}@db:5432/orrery_search"]
|
|
networks:
|
|
- internal
|
|
depends_on:
|
|
db:
|
|
condition: service_healthy
|
|
restart: "no"
|
|
|
|
vectorizer-worker:
|
|
image: timescale/pgai-vectorizer-worker:latest
|
|
environment:
|
|
PGAI_VECTORIZER_WORKER_DB_URL: postgresql://${POSTGRES_USER:-orrery}:${POSTGRES_PASSWORD}@db:5432/orrery_search
|
|
OPENAI_BASE_URL: ${GPU_BASE_URL:-https://orrery-search.gpu.supported.systems/v1}
|
|
OPENAI_API_KEY: ${GPU_API_KEY}
|
|
command: ["--poll-interval", "5s"]
|
|
networks:
|
|
- internal
|
|
depends_on:
|
|
db:
|
|
condition: service_healthy
|
|
pgai-install:
|
|
condition: service_completed_successfully
|
|
restart: unless-stopped
|
|
|
|
api-dev:
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile
|
|
target: dev
|
|
profiles: ["dev"]
|
|
env_file: .env
|
|
volumes:
|
|
- ./src:/app/src
|
|
- ./alembic:/app/alembic
|
|
- ../docs/src/content/docs:/data/content:ro
|
|
- api-data:/data/geoip
|
|
networks:
|
|
- internal
|
|
- caddy
|
|
depends_on:
|
|
db:
|
|
condition: service_healthy
|
|
pgai-install:
|
|
condition: service_completed_successfully
|
|
labels:
|
|
caddy: ${DOMAIN:-pg-orrery.warehack.ing}
|
|
caddy.handle: /api/search*
|
|
caddy.handle.0_reverse_proxy: "{{upstreams 8000}}"
|
|
caddy.handle_1: /health
|
|
caddy.handle_1.0_reverse_proxy: "{{upstreams 8000}}"
|
|
caddy.handle_2: /mcp*
|
|
caddy.handle_2.0_reverse_proxy: "{{upstreams 8000}}"
|
|
caddy.handle_3: /api/chat*
|
|
caddy.handle_3.0_reverse_proxy: "{{upstreams 8000}}"
|
|
caddy.handle_3.0_reverse_proxy.flush_interval: "-1"
|
|
caddy.handle_3.0_reverse_proxy.transport: "http"
|
|
caddy.handle_3.0_reverse_proxy.transport.read_timeout: "0"
|
|
caddy.handle_3.0_reverse_proxy.transport.write_timeout: "0"
|
|
caddy.handle_4: /api/geolocate
|
|
caddy.handle_4.0_reverse_proxy: "{{upstreams 8000}}"
|
|
healthcheck:
|
|
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health')"]
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 15s
|
|
restart: unless-stopped
|
|
|
|
api-prod:
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile
|
|
target: prod
|
|
profiles: ["prod"]
|
|
env_file: .env
|
|
volumes:
|
|
- ../docs/src/content/docs:/data/content:ro
|
|
- api-data:/data/geoip
|
|
networks:
|
|
- internal
|
|
- caddy
|
|
depends_on:
|
|
db:
|
|
condition: service_healthy
|
|
pgai-install:
|
|
condition: service_completed_successfully
|
|
labels:
|
|
caddy: ${DOMAIN:-pg-orrery.warehack.ing}
|
|
caddy.handle: /api/search*
|
|
caddy.handle.0_reverse_proxy: "{{upstreams 8000}}"
|
|
caddy.handle_1: /health
|
|
caddy.handle_1.0_reverse_proxy: "{{upstreams 8000}}"
|
|
caddy.handle_2: /mcp*
|
|
caddy.handle_2.0_reverse_proxy: "{{upstreams 8000}}"
|
|
caddy.handle_3: /api/chat*
|
|
caddy.handle_3.0_reverse_proxy: "{{upstreams 8000}}"
|
|
caddy.handle_3.0_reverse_proxy.flush_interval: "-1"
|
|
caddy.handle_3.0_reverse_proxy.transport: "http"
|
|
caddy.handle_3.0_reverse_proxy.transport.read_timeout: "0"
|
|
caddy.handle_3.0_reverse_proxy.transport.write_timeout: "0"
|
|
caddy.handle_4: /api/geolocate
|
|
caddy.handle_4.0_reverse_proxy: "{{upstreams 8000}}"
|
|
healthcheck:
|
|
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health')"]
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 15s
|
|
restart: unless-stopped
|
|
|
|
volumes:
|
|
pg-data:
|
|
api-data:
|
|
# GeoIP database must be provisioned manually:
|
|
# docker compose exec api-prod bash scripts/update_geoip.sh
|
|
# Requires MAXMIND_LICENSE_KEY env var (free tier)
|
|
|
|
networks:
|
|
internal:
|
|
caddy:
|
|
external: true
|