From a1e293b1d71ebee503aae91b8813b050526101e6 Mon Sep 17 00:00:00 2001 From: Oliver Hofmann Date: Thu, 7 May 2026 16:03:03 +0200 Subject: [PATCH] Add ADMIN_HOST env var, restructure docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docker-entrypoint.sh: Admin-API bindet auf ADMIN_HOST (default 0.0.0.0) statt hardcoded 0.0.0.0 — ermöglicht Einschränkung auf 127.0.0.1 - README: Zweck-Beschreibung, HTTPS-Reverse-Proxy-Abschnitt (Caddy/Nginx), Port-8001-Abschnitt korrigiert (Docker-Port-Mapping greift bei network_mode: host nicht), ADMIN_HOST in Konfig-Tabelle ergänzt - DOCKERHUB.md / DOCKERHUB.en.md: Auf drei Szenarien reduziert (network_mode: host, Ollama als Container + SQLite/PostgreSQL); host.docker.internal-Varianten entfernt - review_priorities.md: gelöscht (alle Punkte behoben) --- DOCKERHUB.en.md | 96 +++++++------------------------------------- DOCKERHUB.md | 96 +++++++------------------------------------- README.md | 72 ++++++++++++++++++++++++++++----- docker-entrypoint.sh | 2 +- 4 files changed, 94 insertions(+), 172 deletions(-) diff --git a/DOCKERHUB.en.md b/DOCKERHUB.en.md index dab26b7..73b9a44 100644 --- a/DOCKERHUB.en.md +++ b/DOCKERHUB.en.md @@ -2,8 +2,6 @@ A lightweight reverse proxy for [Ollama](https://ollama.com) that manages API keys with configurable token and request quotas. Incoming requests in OpenAI-compatible format are authenticated, checked against the quota, and forwarded to the configured Ollama server. -Ollama does not need to run on the same host — `OLLAMA_URL` can point to any reachable server: the Docker host itself, another machine on the network, or a remote server. - ## Features - OpenAI-compatible endpoint (`/v1/chat/completions`, `/v1/models`) @@ -21,16 +19,7 @@ Ollama does not need to run on the same host — `OLLAMA_URL` can point to any r | `8000` | Proxy endpoint (OpenAI API) | | `8001` | Admin API + web interface | -Port 8001 must be exposed because the container serves the admin interface directly on this port. All API endpoints require the `ADMIN_PASSWORD` — without a valid token, only the public frontend files (HTML/JS/CSS of the login page) are accessible. The password is therefore the primary protection. - -Additional hardening: binding to `127.0.0.1` restricts access to the local host and prevents direct network access: - -``` -ports: - - "127.0.0.1:8001:8001" # local access only - # or: - - "8001:8001" # network-wide, protected by ADMIN_PASSWORD only -``` +All API endpoints require the `ADMIN_PASSWORD` — without a valid token, only the public frontend files (HTML/JS/CSS of the login page) are accessible. The password is therefore the primary protection. ## Environment Variables @@ -46,80 +35,36 @@ ports: | `APP_TZ` | `Europe/Berlin` | Timezone for daily/monthly quota resets | | `LOG_FILE` | `logs/usage.log` | Path of the rotating usage log file | -## Docker Compose – External Ollama, SQLite +## Docker Compose – Ollama on the Host (Linux, recommended) -Use this when Ollama runs outside of Docker — on the Docker host or any other reachable server. Adjust `OLLAMA_URL` accordingly. +`network_mode: host` gives the container direct access to the host network stack. Ollama runs on the host and is reachable at `localhost:11434` — not visible from outside. The proxy and admin interface are available directly on host ports 8000 and 8001. ```yaml services: llmproxy: image: mediaeng/llmproxy:latest + container_name: llmproxy restart: unless-stopped - ports: - - "8000:8000" - - "127.0.0.1:8001:8001" - environment: - ADMIN_PASSWORD: changeme - OLLAMA_URL: http://host.docker.internal:11434 # or http://:11434 - DEFAULT_MODEL: llama3 - APP_TZ: Europe/Berlin + network_mode: host + env_file: .env volumes: - llmproxy-data:/app/backend - # On Linux, add extra_hosts since host.docker.internal is not - # available automatically: - # extra_hosts: - # - "host.docker.internal:host-gateway" volumes: llmproxy-data: ``` -## Docker Compose – External Ollama, PostgreSQL - -```yaml -services: - llmproxy: - image: mediaeng/llmproxy:latest - restart: unless-stopped - ports: - - "8000:8000" - - "127.0.0.1:8001:8001" - environment: - ADMIN_PASSWORD: changeme - OLLAMA_URL: http://host.docker.internal:11434 # or http://:11434 - DEFAULT_MODEL: llama3 - APP_TZ: Europe/Berlin - DATABASE_URL: postgresql://llmproxy:secret@db:5432/llmproxy - volumes: - - llmproxy-data:/app/backend - depends_on: - db: - condition: service_healthy - # extra_hosts: - # - "host.docker.internal:host-gateway" - - db: - image: postgres:16-alpine - restart: unless-stopped - environment: - POSTGRES_DB: llmproxy - POSTGRES_USER: llmproxy - POSTGRES_PASSWORD: secret - volumes: - - pg-data:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U llmproxy"] - interval: 5s - timeout: 5s - retries: 5 - -volumes: - pg-data: +`.env`: +```env +ADMIN_PASSWORD=changeme +OLLAMA_URL=http://localhost:11434 +DEFAULT_MODEL=llama3 +APP_TZ=Europe/Berlin ``` ## Docker Compose – Ollama as Container, SQLite -Ollama and llmproxy run together in Docker, data persisted in a volume. +Ollama and llmproxy run together in Docker. Ollama is not exposed externally. ```yaml services: @@ -128,7 +73,7 @@ services: restart: unless-stopped ports: - "8000:8000" - - "127.0.0.1:8001:8001" + - "8001:8001" environment: ADMIN_PASSWORD: changeme OLLAMA_URL: http://ollama:11434 @@ -161,7 +106,7 @@ services: restart: unless-stopped ports: - "8000:8000" - - "127.0.0.1:8001:8001" + - "8001:8001" environment: ADMIN_PASSWORD: changeme OLLAMA_URL: http://ollama:11434 @@ -200,17 +145,6 @@ volumes: ollama-data: ``` -## Quick Start - -```bash -docker run -d \ - -p 8000:8000 \ - -e ADMIN_PASSWORD=changeme \ - -e OLLAMA_URL=http://host.docker.internal:11434 \ - -v llmproxy-data:/app/backend \ - mediaeng/llmproxy:latest -``` - ## Client Configuration Configure the proxy as an OpenAI-compatible endpoint: diff --git a/DOCKERHUB.md b/DOCKERHUB.md index 1f7a046..85903b4 100644 --- a/DOCKERHUB.md +++ b/DOCKERHUB.md @@ -2,8 +2,6 @@ Ein schlanker Reverse-Proxy für [Ollama](https://ollama.com), der API-Keys mit konfigurierbaren Token- und Request-Quoten verwaltet. Eingehende Anfragen im OpenAI-kompatiblen Format werden authentifiziert, auf Quota geprüft und an den konfigurierten Ollama-Server weitergeleitet. -Ollama muss dabei nicht auf demselben Host laufen — `OLLAMA_URL` kann auf jeden erreichbaren Server zeigen, also auf den Docker-Host selbst, einen anderen Rechner im Netzwerk oder einen Remote-Server. - ## Funktionen - OpenAI-kompatibler Endpunkt (`/v1/chat/completions`, `/v1/models`) @@ -21,16 +19,7 @@ Ollama muss dabei nicht auf demselben Host laufen — `OLLAMA_URL` kann auf jede | `8000` | Proxy-Endpunkt (OpenAI-API) | | `8001` | Admin-API + Web-Oberfläche | -Port 8001 muss exposed werden, da der Container die Admin-Oberfläche selbst auf diesem Port ausliefert. Alle API-Endpunkte erfordern das `ADMIN_PASSWORD` — ein Zugriff ohne gültiges Token liefert nur die öffentlichen Frontend-Dateien (HTML/JS/CSS der Login-Seite). Das Passwort ist damit die primäre Schutzmaßnahme. - -Zusätzliche Härtung: Portbindung auf `127.0.0.1` beschränkt den Zugriff auf den lokalen Host und verhindert direkten Netzwerkzugriff: - -``` -ports: - - "127.0.0.1:8001:8001" # nur lokal erreichbar - # oder: - - "8001:8001" # netzwerkweit, Schutz nur durch ADMIN_PASSWORD -``` +Alle API-Endpunkte erfordern das `ADMIN_PASSWORD` — ein Zugriff ohne gültiges Token liefert nur die öffentlichen Frontend-Dateien (HTML/JS/CSS der Login-Seite). Das Passwort ist damit die primäre Schutzmaßnahme. ## Umgebungsvariablen @@ -46,80 +35,36 @@ ports: | `APP_TZ` | `Europe/Berlin` | Zeitzone für Tages-/Monats-Reset der Quoten | | `LOG_FILE` | `logs/usage.log` | Pfad der rotierenden Nutzungs-Logdatei | -## Docker Compose – Ollama extern, SQLite +## Docker Compose – Ollama auf dem Host (Linux, empfohlen) -Wenn Ollama außerhalb von Docker läuft — auf dem Docker-Host oder einem anderen erreichbaren Server. `OLLAMA_URL` entsprechend anpassen. +`network_mode: host` gibt dem Container direkten Zugriff auf das Host-Netzwerk. Ollama läuft auf dem Host und ist über `localhost:11434` erreichbar — nach außen nicht sichtbar. Proxy und Admin-Oberfläche sind direkt auf den Host-Ports 8000 und 8001 verfügbar. ```yaml services: llmproxy: image: mediaeng/llmproxy:latest + container_name: llmproxy restart: unless-stopped - ports: - - "8000:8000" - - "127.0.0.1:8001:8001" - environment: - ADMIN_PASSWORD: changeme - OLLAMA_URL: http://host.docker.internal:11434 # oder http://:11434 - DEFAULT_MODEL: llama3 - APP_TZ: Europe/Berlin + network_mode: host + env_file: .env volumes: - llmproxy-data:/app/backend - # Auf Linux extra_hosts ergänzen, da host.docker.internal dort - # nicht automatisch verfügbar ist: - # extra_hosts: - # - "host.docker.internal:host-gateway" volumes: llmproxy-data: ``` -## Docker Compose – Ollama extern, PostgreSQL - -```yaml -services: - llmproxy: - image: mediaeng/llmproxy:latest - restart: unless-stopped - ports: - - "8000:8000" - - "127.0.0.1:8001:8001" - environment: - ADMIN_PASSWORD: changeme - OLLAMA_URL: http://host.docker.internal:11434 # oder http://:11434 - DEFAULT_MODEL: llama3 - APP_TZ: Europe/Berlin - DATABASE_URL: postgresql://llmproxy:secret@db:5432/llmproxy - volumes: - - llmproxy-data:/app/backend - depends_on: - db: - condition: service_healthy - # extra_hosts: - # - "host.docker.internal:host-gateway" - - db: - image: postgres:16-alpine - restart: unless-stopped - environment: - POSTGRES_DB: llmproxy - POSTGRES_USER: llmproxy - POSTGRES_PASSWORD: secret - volumes: - - pg-data:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U llmproxy"] - interval: 5s - timeout: 5s - retries: 5 - -volumes: - pg-data: +`.env`: +```env +ADMIN_PASSWORD=changeme +OLLAMA_URL=http://localhost:11434 +DEFAULT_MODEL=llama3 +APP_TZ=Europe/Berlin ``` ## Docker Compose – Ollama als Container, SQLite -Ollama und llmproxy laufen gemeinsam in Docker, Daten in einem Volume. +Ollama und llmproxy laufen gemeinsam in Docker. Ollama ist nicht nach außen exposed. ```yaml services: @@ -128,7 +73,7 @@ services: restart: unless-stopped ports: - "8000:8000" - - "127.0.0.1:8001:8001" + - "8001:8001" environment: ADMIN_PASSWORD: changeme OLLAMA_URL: http://ollama:11434 @@ -161,7 +106,7 @@ services: restart: unless-stopped ports: - "8000:8000" - - "127.0.0.1:8001:8001" + - "8001:8001" environment: ADMIN_PASSWORD: changeme OLLAMA_URL: http://ollama:11434 @@ -200,17 +145,6 @@ volumes: ollama-data: ``` -## Schnellstart - -```bash -docker run -d \ - -p 8000:8000 \ - -e ADMIN_PASSWORD=changeme \ - -e OLLAMA_URL=http://host.docker.internal:11434 \ - -v llmproxy-data:/app/backend \ - mediaeng/llmproxy:latest -``` - ## Client-Konfiguration Den Proxy als OpenAI-kompatibler Endpunkt konfigurieren: diff --git a/README.md b/README.md index e190834..e57762f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Ollama Proxy mit API-Keys und Quotas -Ein Reverse-Proxy für Ollama mit API-Key-Authentifizierung, Quota-Management und Web-Admin-Oberfläche. +Ollama bietet von sich aus keine Authentifizierung — wer die API erreicht, kann sie nutzen. Dieses Projekt löst das Problem: Ollama bleibt an `localhost` gebunden und ist von außen nicht erreichbar. Vorgeschaltet läuft ein Proxy (Port 8000), der jeden Request auf einen gültigen API-Key prüft und optional Token- sowie Request-Quoten pro Key durchsetzt. Eine Web-Admin-Oberfläche (Port 8001) erlaubt das Verwalten von Keys, Quoten und Ollama-Einstellungen. ## Features @@ -19,7 +19,7 @@ Ein Reverse-Proxy für Ollama mit API-Key-Authentifizierung, Quota-Management un - Admin-Oberfläche passwortgeschützt (`ADMIN_PASSWORD`) — alle API-Endpunkte erfordern den Token - API-Keys als SHA-256-Hash in der DB — Plaintext nur einmalig bei Erstellung - Quota-Check atomar mit `SELECT FOR UPDATE` (kein TOCTOU-Race) -- Port 8001 kann optional auf `127.0.0.1` gebunden werden (zusätzliche Härtung) +- Admin-Port 8001 über `ADMIN_HOST=127.0.0.1` auf lokalen Zugriff beschränkbar ## Konfiguration @@ -48,6 +48,7 @@ LOG_FILE=logs/usage.log | `DEFAULT_MODEL` | `llama3` | Standard-Modell für `/v1/chat/completions` (auch in der UI änderbar) | | `APP_TZ` | `Europe/Berlin` | Zeitzone für tägliche/monatliche Quota-Resets | | `LOG_FILE` | `logs/usage.log` | Pfad der rotierenden Nutzungs-Logdatei | +| `ADMIN_HOST` | `0.0.0.0` | Bind-Adresse der Admin-API (z. B. `127.0.0.1` für lokalen Zugriff) | | `ALLOWED_ORIGINS` | `http://localhost:5173` | CORS-Origins (nur für Entwicklung relevant) | ## Entwicklung (lokal) @@ -86,7 +87,7 @@ Admin-Oberfläche: `http://localhost:5173` docker compose up -d ``` -Zieht das Image von DockerHub, lädt Variablen aus `.env` und verwendet die lokale SQLite-Datenbank. Weitere Compose-Varianten (PostgreSQL, Ollama als Container) siehe `DOCKERHUB.md`. +Zieht das Image von DockerHub, lädt Variablen aus `.env` und verwendet `network_mode: host` — der Container erreicht Ollama direkt über `localhost:11434`. Weitere Compose-Varianten (Ollama als Container, PostgreSQL) siehe `DOCKERHUB.md`. ### Image selbst bauen und pushen @@ -98,15 +99,68 @@ Das Script zeigt den aktuellen Git-Tag, bietet an einen neuen zu setzen, baut da ### Port 8001 (Admin) -Port 8001 muss exposed werden, da der Container die Admin-Oberfläche auf diesem Port ausliefert. Alle API-Endpunkte erfordern das `ADMIN_PASSWORD` — der Token ist der primäre Schutz. Optionale zusätzliche Härtung: Bindung auf `127.0.0.1`: +Alle Admin-Endpunkte erfordern das `ADMIN_PASSWORD` — der Token ist der primäre Schutz. Für zusätzliche Härtung lässt sich die Admin-API auf lokalen Zugriff beschränken: -```yaml -ports: - - "127.0.0.1:8001:8001" # nur lokal - # oder: - - "8001:8001" # netzwerkweit, Schutz durch ADMIN_PASSWORD +```env +ADMIN_HOST=127.0.0.1 ``` +Bei `network_mode: host` (Produktions-Standard) ist das die einzig wirksame Methode — Docker-Port-Mapping greift dort nicht. + +### HTTPS via Reverse-Proxy (ungetestet) + +Wer Proxy und Admin-Oberfläche per HTTPS bereitstellen will, kann einen weiteren Reverse-Proxy (z. B. Nginx oder Caddy) vorschalten. Bei `network_mode: host` lauschen beide Dienste direkt auf dem Host, Nginx/Caddy proxyen auf `localhost`. + +**Caddy** (empfohlen — automatisches TLS via Let's Encrypt): + +``` +llm.example.com { + reverse_proxy localhost:8000 { + flush_interval -1 + } +} + +llm-admin.example.com { + reverse_proxy localhost:8001 +} +``` + +**Nginx** (mit Certbot-Zertifikaten): + +```nginx +server { + listen 443 ssl; + server_name llm.example.com; + + ssl_certificate /etc/letsencrypt/live/llm.example.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/llm.example.com/privkey.pem; + + location / { + proxy_pass http://127.0.0.1:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_buffering off; # nötig für Streaming + proxy_cache off; + } +} + +server { + listen 443 ssl; + server_name llm-admin.example.com; + + ssl_certificate /etc/letsencrypt/live/llm-admin.example.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/llm-admin.example.com/privkey.pem; + + location / { + proxy_pass http://127.0.0.1:8001; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } +} +``` + +Clients konfigurieren dann `https://llm.example.com/v1` als Base URL. + ## Proxy-Endpunkte (Port 8000) Alle Endpunkte erfordern einen gültigen API-Key im `Authorization`-Header. diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 4376900..3054c88 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -10,7 +10,7 @@ uvicorn main:app \ PROXY_PID=$! uvicorn admin:app \ - --host "0.0.0.0" \ + --host "${ADMIN_HOST:-0.0.0.0}" \ --port "${ADMIN_PORT:-8001}" & ADMIN_PID=$!