Daniel Wiesmann commited on
Commit
7cc374f
·
1 Parent(s): 1d60967

Expose the API alongside the Streamlit demo in the HF Space via nginx

Browse files

Adds nginx as a reverse proxy in front of both the Streamlit demo and
the FastAPI app: / goes to Streamlit, /api/ goes to the API, both
managed by supervisord in the single HF Space container (nginx.conf)
and mirrored for local docker-compose (nginx.local.conf, using
container DNS names instead of localhost).

Fixes on top of the original approach:
- Dockerfile switched to USER root to install the nginx config/perms
but never switched back, so the whole container would've run as
root at runtime despite HF Spaces requiring UID 1000 - restored
USER user before CMD.
- GAZET_PUBLIC_API_URL was set but never read anywhere; wired it into
the Streamlit sidebar as a link to the API docs so the exposed API
is actually discoverable.
- The /api/ location was missing proxy_buffering off and generous
timeouts (present on the streamlit location but not copied over):
without them, GET /search/stream's incremental NDJSON would get
buffered by nginx until the response completes, and the default 60s
proxy_read_timeout is shorter than the LLM's documented 30-60s cold
start plus actual generation time.

Files changed (6) hide show
  1. Dockerfile +12 -1
  2. docker-compose.yml +10 -0
  3. gazet_demo.py +4 -1
  4. nginx.conf +52 -0
  5. nginx.local.conf +52 -0
  6. supervisord.conf +11 -1
Dockerfile CHANGED
@@ -4,7 +4,7 @@ FROM python:3.13-slim
4
 
5
  # Install system deps
6
  RUN apt-get update && apt-get install -y --no-install-recommends \
7
- curl supervisor libgomp1 && rm -rf /var/lib/apt/lists/*
8
 
9
  # Copy llama-server binary and backend .so files (must stay together)
10
  COPY --from=llama /app /usr/local/lib/llama
@@ -45,9 +45,20 @@ snapshot_download('developmentseed/gazet-geodata', repo_type='dataset', local_di
45
 
46
  COPY --chown=user supervisord.conf .
47
 
 
 
 
 
 
 
 
 
 
 
48
  ENV GAZET_DATA_DIR=$HOME/app/data \
49
  LLAMA_SERVER_URL=http://localhost:9000 \
50
  GAZET_API_URL=http://localhost:8000 \
 
51
  PATH="$HOME/app/.venv/bin:$PATH"
52
 
53
  EXPOSE 7860
 
4
 
5
  # Install system deps
6
  RUN apt-get update && apt-get install -y --no-install-recommends \
7
+ curl nginx supervisor libgomp1 && rm -rf /var/lib/apt/lists/*
8
 
9
  # Copy llama-server binary and backend .so files (must stay together)
10
  COPY --from=llama /app /usr/local/lib/llama
 
45
 
46
  COPY --chown=user supervisord.conf .
47
 
48
+ # Switch to root for nginx config and permissions
49
+ USER root
50
+ COPY nginx.conf /etc/nginx/nginx.conf
51
+ RUN chown -R user:user /var/log/nginx && \
52
+ chown -R user:user /var/lib/nginx && \
53
+ chmod 755 /var/log/nginx
54
+
55
+ # HF Spaces requires UID 1000 — drop back to the non-root user for runtime
56
+ USER user
57
+
58
  ENV GAZET_DATA_DIR=$HOME/app/data \
59
  LLAMA_SERVER_URL=http://localhost:9000 \
60
  GAZET_API_URL=http://localhost:8000 \
61
+ GAZET_PUBLIC_API_URL=/api \
62
  PATH="$HOME/app/.venv/bin:$PATH"
63
 
64
  EXPOSE 7860
docker-compose.yml CHANGED
@@ -1,4 +1,14 @@
1
  services:
 
 
 
 
 
 
 
 
 
 
2
  llama:
3
  image: ghcr.io/ggml-org/llama.cpp:server
4
  volumes:
 
1
  services:
2
+ nginx:
3
+ image: nginx:alpine
4
+ volumes:
5
+ - ./nginx.local.conf:/etc/nginx/nginx.conf:ro
6
+ ports:
7
+ - "7860:7860"
8
+ depends_on:
9
+ - app
10
+ - demo
11
+
12
  llama:
13
  image: ghcr.io/ggml-org/llama.cpp:server
14
  volumes:
gazet_demo.py CHANGED
@@ -141,6 +141,7 @@ def _render_map(geojson, placeholder):
141
 
142
 
143
  API = os.environ.get("GAZET_API_URL", "http://127.0.0.1:8000")
 
144
  PLAUSIBLE_SRC = os.environ.get(
145
  "PLAUSIBLE_SRC",
146
  "https://plausible.io/js/pa-rYXsbLvnb-tHwR72IDneF.js",
@@ -233,7 +234,7 @@ st.markdown(
233
  with st.sidebar:
234
  st.header("Learn how this was built")
235
  st.markdown(
236
- """
237
  **Gazet** turns plain English questions into geometries on a map.
238
 
239
  ### Behind the scenes
@@ -271,6 +272,8 @@ The model queries two open geographic datasets:
271
  [developmentseed/gazet](https://huggingface.co/spaces/developmentseed/gazet)
272
  - Source code:
273
  [developmentseed/gazet](https://github.com/developmentseed/gazet)
 
 
274
 
275
  ### Talk to us
276
 
 
141
 
142
 
143
  API = os.environ.get("GAZET_API_URL", "http://127.0.0.1:8000")
144
+ PUBLIC_API_URL = os.environ.get("GAZET_PUBLIC_API_URL", API)
145
  PLAUSIBLE_SRC = os.environ.get(
146
  "PLAUSIBLE_SRC",
147
  "https://plausible.io/js/pa-rYXsbLvnb-tHwR72IDneF.js",
 
234
  with st.sidebar:
235
  st.header("Learn how this was built")
236
  st.markdown(
237
+ f"""
238
  **Gazet** turns plain English questions into geometries on a map.
239
 
240
  ### Behind the scenes
 
272
  [developmentseed/gazet](https://huggingface.co/spaces/developmentseed/gazet)
273
  - Source code:
274
  [developmentseed/gazet](https://github.com/developmentseed/gazet)
275
+ - API docs:
276
+ [{PUBLIC_API_URL}/docs]({PUBLIC_API_URL}/docs)
277
 
278
  ### Talk to us
279
 
nginx.conf ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ worker_processes auto;
2
+ pid /tmp/nginx.pid;
3
+ error_log /var/log/nginx/error.log warn;
4
+
5
+ events {
6
+ worker_connections 1024;
7
+ }
8
+
9
+ http {
10
+ access_log /var/log/nginx/access.log;
11
+
12
+ upstream api {
13
+ server 127.0.0.1:8000;
14
+ }
15
+
16
+ upstream streamlit {
17
+ server 127.0.0.1:8501;
18
+ }
19
+
20
+ map $http_upgrade $connection_upgrade {
21
+ default upgrade;
22
+ '' close;
23
+ }
24
+
25
+ server {
26
+ listen 7860;
27
+ server_name _;
28
+
29
+ location /api/ {
30
+ proxy_pass http://api/;
31
+ proxy_set_header Host $host;
32
+ proxy_set_header X-Real-IP $remote_addr;
33
+ proxy_http_version 1.1;
34
+ # /search/stream emits NDJSON incrementally; buffering would hold it
35
+ # until the response completes, defeating the point of streaming.
36
+ proxy_buffering off;
37
+ proxy_read_timeout 600s;
38
+ proxy_send_timeout 600s;
39
+ }
40
+
41
+ location / {
42
+ proxy_pass http://streamlit;
43
+ proxy_http_version 1.1;
44
+ proxy_set_header Upgrade $http_upgrade;
45
+ proxy_set_header Connection $connection_upgrade;
46
+ proxy_set_header Host $host;
47
+ proxy_set_header X-Real-IP $remote_addr;
48
+ proxy_read_timeout 600s;
49
+ proxy_send_timeout 600s;
50
+ }
51
+ }
52
+ }
nginx.local.conf ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ worker_processes auto;
2
+ pid /tmp/nginx.pid;
3
+ error_log /var/log/nginx/error.log warn;
4
+
5
+ events {
6
+ worker_connections 1024;
7
+ }
8
+
9
+ http {
10
+ access_log /var/log/nginx/access.log;
11
+
12
+ upstream api_backend {
13
+ server app:8000;
14
+ }
15
+
16
+ upstream streamlit_backend {
17
+ server demo:8501;
18
+ }
19
+
20
+ map $http_upgrade $connection_upgrade {
21
+ default upgrade;
22
+ '' close;
23
+ }
24
+
25
+ server {
26
+ listen 7860;
27
+ server_name _;
28
+
29
+ location /api/ {
30
+ proxy_pass http://api_backend/;
31
+ proxy_set_header Host $host;
32
+ proxy_set_header X-Real-IP $remote_addr;
33
+ proxy_http_version 1.1;
34
+ # /search/stream emits NDJSON incrementally; buffering would hold it
35
+ # until the response completes, defeating the point of streaming.
36
+ proxy_buffering off;
37
+ proxy_read_timeout 600s;
38
+ proxy_send_timeout 600s;
39
+ }
40
+
41
+ location / {
42
+ proxy_pass http://streamlit_backend/;
43
+ proxy_http_version 1.1;
44
+ proxy_set_header Upgrade $http_upgrade;
45
+ proxy_set_header Connection $connection_upgrade;
46
+ proxy_set_header Host $host;
47
+ proxy_set_header X-Real-IP $remote_addr;
48
+ proxy_read_timeout 600s;
49
+ proxy_send_timeout 600s;
50
+ }
51
+ }
52
+ }
supervisord.conf CHANGED
@@ -26,7 +26,7 @@ stderr_logfile_maxbytes=0
26
  startsecs=10
27
 
28
  [program:streamlit]
29
- command=%(ENV_HOME)s/app/.venv/bin/streamlit run gazet_demo.py --server.port 7860 --server.address 0.0.0.0
30
  directory=%(ENV_HOME)s/app
31
  autostart=true
32
  autorestart=true
@@ -35,3 +35,13 @@ stdout_logfile_maxbytes=0
35
  stderr_logfile=/dev/stderr
36
  stderr_logfile_maxbytes=0
37
  startsecs=15
 
 
 
 
 
 
 
 
 
 
 
26
  startsecs=10
27
 
28
  [program:streamlit]
29
+ command=%(ENV_HOME)s/app/.venv/bin/streamlit run gazet_demo.py --server.port 8501 --server.address 0.0.0.0
30
  directory=%(ENV_HOME)s/app
31
  autostart=true
32
  autorestart=true
 
35
  stderr_logfile=/dev/stderr
36
  stderr_logfile_maxbytes=0
37
  startsecs=15
38
+
39
+ [program:nginx]
40
+ command=nginx -g 'daemon off;'
41
+ autostart=true
42
+ autorestart=true
43
+ stdout_logfile=/dev/stdout
44
+ stdout_logfile_maxbytes=0
45
+ stderr_logfile=/dev/stderr
46
+ stderr_logfile_maxbytes=0
47
+ startsecs=2