Skip to content

Commit 93ddd41

Browse files
authored
feat: add reverse proxy support (#346)
* feat: add reverse proxy support * improve * improve * improve * improve * improve * fix: update integration test to use modern 'docker compose' command - Replace 'docker-compose' with 'docker compose' (Docker Compose v2+) - Add fallback to legacy docker-compose command for compatibility - Fixes test failures on systems using Docker Compose plugin * ci: trigger test rerun * fix: make docker-compose detection more robust for CI - Add get_docker_compose_command() to detect available command - Use shutil.which() to check command availability - Dynamically use correct command (docker compose vs docker-compose) - Should work in both modern and legacy Docker environments * fix: docker-compose networking in base path integration test Fix connection refused error in test_reverse_proxy_simple_config by handling host vs bridge networking modes correctly: - Linux (host mode): nginx listens on 18080 directly, no port mapping - Mac/Windows (bridge mode): nginx listens on 80, mapped to 18080 With host networking, port mappings in docker-compose don't work since the container binds directly to the host's network namespace.
1 parent 7ee229b commit 93ddd41

File tree

14 files changed

+1049
-11
lines changed

14 files changed

+1049
-11
lines changed

.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ HINDSIGHT_API_HOST=0.0.0.0
3131
HINDSIGHT_API_PORT=8888
3232
HINDSIGHT_API_LOG_LEVEL=info
3333

34+
# Base Path / Reverse Proxy Support (Optional)
35+
# Set these when deploying behind a reverse proxy with path-based routing
36+
# Example: To deploy at example.com/hindsight/, set both to "/hindsight"
37+
# HINDSIGHT_API_BASE_PATH=/hindsight
38+
# NEXT_PUBLIC_BASE_PATH=/hindsight
39+
3440
# Database (Optional - uses embedded pg0 by default)
3541
# HINDSIGHT_API_DATABASE_URL=postgresql://user:pass@host:5432/db
3642
# HINDSIGHT_API_DATABASE_SCHEMA=public # PostgreSQL schema name (default: public)
File renamed without changes.
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Nginx Reverse Proxy with Custom Base Path
2+
3+
Deploy Hindsight API under `/hindsight` (or any custom path) using Nginx reverse proxy.
4+
5+
## Quick Start (Published Image - API Only)
6+
7+
```bash
8+
docker-compose up
9+
```
10+
11+
- **API:** http://localhost:8080/hindsight/docs
12+
- **Control Plane:** http://localhost:9999 (direct access, not proxied)
13+
14+
## Full Stack with Custom Base Path (Requires Build)
15+
16+
**Important:** You cannot rebuild from the published image with build args. You must build from source.
17+
18+
### Build from Source with Custom Base Path
19+
20+
1. **Clone the repository** (if you haven't):
21+
```bash
22+
git clone https://github.com/vectorize-io/hindsight.git
23+
cd hindsight
24+
```
25+
26+
2. **Build with base path**:
27+
```bash
28+
docker build \
29+
--build-arg NEXT_PUBLIC_BASE_PATH=/hindsight \
30+
-f docker/standalone/Dockerfile \
31+
-t hindsight:custom \
32+
.
33+
```
34+
35+
3. **Update docker-compose.yml** to use your built image:
36+
```yaml
37+
services:
38+
hindsight:
39+
image: hindsight:custom # ← Change this
40+
environment:
41+
HINDSIGHT_API_BASE_PATH: /hindsight
42+
NEXT_PUBLIC_BASE_PATH: /hindsight
43+
```
44+
45+
4. **Update nginx.conf** to handle Control Plane routes (see below)
46+
47+
5. **Run**:
48+
```bash
49+
docker-compose up
50+
```
51+
52+
### Required nginx.conf for Full Stack
53+
54+
Replace the current `nginx.conf` with this to proxy both API and Control Plane:
55+
56+
```nginx
57+
events { worker_connections 1024; }
58+
59+
http {
60+
include /etc/nginx/mime.types;
61+
default_type application/octet-stream;
62+
63+
upstream hindsight_api { server hindsight:8888; }
64+
upstream hindsight_cp { server hindsight:9999; }
65+
66+
server {
67+
listen 80;
68+
69+
# API
70+
location ~ ^/hindsight/(docs|openapi\.json|health|metrics|v1|mcp) {
71+
proxy_pass http://hindsight_api;
72+
proxy_set_header Host $http_host;
73+
}
74+
75+
# Control Plane static files
76+
location ~ ^/hindsight/_next/ {
77+
proxy_pass http://hindsight_cp;
78+
proxy_set_header Host $http_host;
79+
}
80+
81+
# Control Plane UI
82+
location /hindsight {
83+
proxy_pass http://hindsight_cp;
84+
proxy_set_header Host $http_host;
85+
}
86+
87+
location = / { return 301 /hindsight; }
88+
}
89+
}
90+
```
91+
92+
### Why Build is Required
93+
94+
Next.js requires `basePath` at **build time**. The published image was built without a custom base path, so you must rebuild from source with the `NEXT_PUBLIC_BASE_PATH` build arg to deploy the Control Plane under a subpath.
95+
96+
The API works without rebuild because `HINDSIGHT_API_BASE_PATH` is a runtime environment variable.
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Hindsight API deployment with Nginx reverse proxy (API-only)
2+
#
3+
# This example deploys Hindsight API under the path /hindsight with:
4+
# - Hindsight standalone image (API + Control Plane + embedded pg0)
5+
# - Nginx reverse proxy (API only)
6+
#
7+
# Quick Start:
8+
# docker-compose -f docker/docker-compose/nginx/docker-compose.yml up
9+
#
10+
# Access:
11+
# API (via nginx): http://localhost:8080/hindsight/docs
12+
# Control Plane (direct): http://localhost:9999
13+
#
14+
# For full stack deployment (API + Control Plane both under /hindsight):
15+
# See README.md in this directory for instructions on building with basePath.
16+
#
17+
# Note: This configuration uses the published image (no build required).
18+
# Control Plane is served directly because Next.js basePath requires
19+
# build-time configuration. See README.md for the full stack option.
20+
21+
services:
22+
# Hindsight (API + Control Plane + embedded pg0)
23+
hindsight:
24+
image: ghcr.io/vectorize-io/hindsight:latest
25+
ports:
26+
- "9999:9999" # Control Plane (direct access, not proxied)
27+
environment:
28+
# API base path for reverse proxy
29+
HINDSIGHT_API_BASE_PATH: /hindsight
30+
31+
# LLM configuration
32+
# Using mock provider for testing (no API key needed)
33+
# For production, set OPENAI_API_KEY or ANTHROPIC_API_KEY and use a real provider
34+
HINDSIGHT_API_LLM_PROVIDER: ${HINDSIGHT_API_LLM_PROVIDER:-mock}
35+
HINDSIGHT_API_LLM_API_KEY: ${OPENAI_API_KEY:-not-needed-for-mock}
36+
HINDSIGHT_API_LLM_MODEL: ${HINDSIGHT_API_LLM_MODEL:-mock-model}
37+
38+
# Production examples (uncomment and set appropriate API key):
39+
# HINDSIGHT_API_LLM_PROVIDER: openai
40+
# HINDSIGHT_API_LLM_API_KEY: ${OPENAI_API_KEY}
41+
# HINDSIGHT_API_LLM_MODEL: gpt-4o-mini
42+
43+
# HINDSIGHT_API_LLM_PROVIDER: anthropic
44+
# HINDSIGHT_API_LLM_API_KEY: ${ANTHROPIC_API_KEY}
45+
# HINDSIGHT_API_LLM_MODEL: claude-sonnet-4-20250514
46+
47+
# Server config
48+
HINDSIGHT_API_HOST: 0.0.0.0
49+
HINDSIGHT_API_PORT: 8888
50+
HINDSIGHT_API_LOG_LEVEL: info
51+
52+
# Control Plane config
53+
HINDSIGHT_CP_DATAPLANE_API_URL: http://localhost:8888
54+
volumes:
55+
# Persist embedded pg0 database
56+
- hindsight_data:/app/data
57+
# Note: Ports not exposed - access via Nginx at localhost:8080/hindsight/
58+
# To debug directly, uncomment these ports:
59+
# ports:
60+
# - "8888:8888" # API
61+
# - "9999:9999" # Control Plane
62+
healthcheck:
63+
test: ["CMD", "curl", "-f", "http://localhost:8888/hindsight/health"]
64+
interval: 10s
65+
timeout: 5s
66+
retries: 3
67+
start_period: 30s
68+
networks:
69+
- hindsight
70+
71+
# Nginx reverse proxy
72+
nginx:
73+
image: nginx:alpine
74+
ports:
75+
- "8080:80"
76+
volumes:
77+
- ./nginx.conf:/etc/nginx/nginx.conf:ro
78+
depends_on:
79+
hindsight:
80+
condition: service_healthy
81+
networks:
82+
- hindsight
83+
84+
volumes:
85+
hindsight_data:
86+
87+
networks:
88+
hindsight:
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Nginx configuration for API-only reverse proxy
2+
# Control Plane accessed directly (not through nginx)
3+
4+
events {
5+
worker_connections 1024;
6+
}
7+
8+
http {
9+
include /etc/nginx/mime.types;
10+
default_type application/octet-stream;
11+
12+
# Logging
13+
access_log /var/log/nginx/access.log;
14+
error_log /var/log/nginx/error.log;
15+
16+
# Upstream - Hindsight API
17+
upstream hindsight_api {
18+
server hindsight:8888;
19+
}
20+
21+
server {
22+
listen 80;
23+
server_name _;
24+
25+
# API endpoints - forward with /hindsight prefix
26+
location /hindsight/ {
27+
proxy_pass http://hindsight_api;
28+
29+
proxy_set_header Host $http_host;
30+
proxy_set_header X-Real-IP $remote_addr;
31+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
32+
proxy_set_header X-Forwarded-Proto $scheme;
33+
}
34+
35+
# Redirect root to API docs
36+
location = / {
37+
return 301 /hindsight/docs;
38+
}
39+
}
40+
}

docker/standalone/Dockerfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ RUN rm -f package-lock.json && sed -i '/"@vectorize-io\/hindsight-client":/d' pa
112112
# Copy built SDK directly into node_modules (more reliable than npm link in Docker)
113113
COPY --from=sdk-builder /app/hindsight-clients/typescript ./node_modules/@vectorize-io/hindsight-client
114114

115+
# Accept base path as build argument for reverse proxy deployments
116+
# Usage: docker build --build-arg NEXT_PUBLIC_BASE_PATH=/hindsight ...
117+
ARG NEXT_PUBLIC_BASE_PATH=""
118+
115119
# Build Control Plane - run next build first, then custom standalone copy
116120
# (The build:standalone script expects a specific path structure that differs in Docker)
117121
RUN npm exec -- next build

hindsight-api/hindsight_api/api/http.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1491,6 +1491,9 @@ async def lifespan(app: FastAPI):
14911491
logging.info("Memory system closed")
14921492

14931493
from hindsight_api import __version__
1494+
from hindsight_api.config import get_config
1495+
1496+
config = get_config()
14941497

14951498
app = FastAPI(
14961499
title="Hindsight HTTP API",
@@ -1504,6 +1507,7 @@ async def lifespan(app: FastAPI):
15041507
"url": "https://www.apache.org/licenses/LICENSE-2.0.html",
15051508
},
15061509
lifespan=lifespan,
1510+
root_path=config.base_path,
15071511
)
15081512

15091513
# IMPORTANT: Set memory on app.state immediately, don't wait for lifespan

hindsight-api/hindsight_api/config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@
109109

110110
ENV_HOST = "HINDSIGHT_API_HOST"
111111
ENV_PORT = "HINDSIGHT_API_PORT"
112+
ENV_BASE_PATH = "HINDSIGHT_API_BASE_PATH"
112113
ENV_LOG_LEVEL = "HINDSIGHT_API_LOG_LEVEL"
113114
ENV_LOG_FORMAT = "HINDSIGHT_API_LOG_FORMAT"
114115
ENV_WORKERS = "HINDSIGHT_API_WORKERS"
@@ -230,6 +231,7 @@
230231

231232
DEFAULT_HOST = "0.0.0.0"
232233
DEFAULT_PORT = 8888
234+
DEFAULT_BASE_PATH = "" # Empty string = root path
233235
DEFAULT_LOG_LEVEL = "info"
234236
DEFAULT_LOG_FORMAT = "text" # Options: "text", "json"
235237
DEFAULT_WORKERS = 1
@@ -440,6 +442,7 @@ class HindsightConfig:
440442
# Server
441443
host: str
442444
port: int
445+
base_path: str
443446
log_level: str
444447
log_format: str
445448
mcp_enabled: bool
@@ -662,6 +665,7 @@ def from_env(cls) -> "HindsightConfig":
662665
# Server
663666
host=os.getenv(ENV_HOST, DEFAULT_HOST),
664667
port=int(os.getenv(ENV_PORT, DEFAULT_PORT)),
668+
base_path=os.getenv(ENV_BASE_PATH, DEFAULT_BASE_PATH),
665669
log_level=os.getenv(ENV_LOG_LEVEL, DEFAULT_LOG_LEVEL),
666670
log_format=os.getenv(ENV_LOG_FORMAT, DEFAULT_LOG_FORMAT).lower(),
667671
mcp_enabled=os.getenv(ENV_MCP_ENABLED, str(DEFAULT_MCP_ENABLED)).lower() == "true",

hindsight-api/hindsight_api/main.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ def main():
223223
reranker_litellm_model=config.reranker_litellm_model,
224224
host=args.host,
225225
port=args.port,
226+
base_path=config.base_path,
226227
log_level=args.log_level,
227228
log_format=config.log_format,
228229
mcp_enabled=config.mcp_enabled,

0 commit comments

Comments
 (0)