pg_orrery/search/docker-compose.yml
Ryan Malloy d22d451c69 Add observer location awareness to chat widget
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)
2026-03-01 23:34:14 -07:00

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