diff --git a/docs/nginx-subpath-template.conf b/docs/nginx-subpath-template.conf new file mode 100644 index 00000000..15f4f067 --- /dev/null +++ b/docs/nginx-subpath-template.conf @@ -0,0 +1,218 @@ +# CloudCLI UI Nginx subpath deployment template. +# +# Purpose: +# Serve CloudCLI UI from a path prefix such as: +# http://localhost/ai/ +# https://example.com/ai/ +# +# CloudCLI itself still runs at the root of its own HTTP server, for example: +# http://127.0.0.1:3001/ +# +# Nginx receives public requests under /ai, strips that prefix, and forwards the +# remaining path to CloudCLI. For example: +# /ai/ -> / +# /ai/session/abc -> /session/abc +# /ai/assets/index.js -> /assets/index.js +# +# Important Nginx limitation: +# Nginx does not allow variables in `location` matchers or `rewrite` regexes. +# The configurable variables below are still useful for proxy/filter values, +# but if you change /ai to a different subpath, also update every line marked: +# [SUBPATH LITERAL] +# +# To use a different subpath, replace these literal matchers: +# location = /ai +# location ^~ /ai/ +# rewrite ^/ai(?/.*)$ ... +# +# Recommended deployment shape: +# CloudCLI is the only app using /ai, while root paths /api, /ws, and /shell +# are also proxied because the current frontend still calls those endpoints +# with root-relative URLs. + +worker_processes 1; + +events { + # Maximum simultaneous connections handled by each worker process. + # The default is enough for local testing and small self-hosted deployments. + worker_connections 1024; +} + +http { + # WebSocket requests include an Upgrade header. Normal HTTP requests do not. + # This map gives us the right Connection header for both cases: + # Upgrade present -> "upgrade" + # Upgrade absent -> "close" + map $http_upgrade $connection_upgrade { + default upgrade; + '' close; + } + + server { + # For HTTPS deployments, replace this with `listen 443 ssl http2;` and + # add ssl_certificate / ssl_certificate_key lines. + listen 80 default_server; + + # Use your real hostname in production, for example: + # server_name cloudcli.example.com; + server_name localhost 127.0.0.1; + + # ---- User settings ------------------------------------------------- + # + # Public path prefix where users access CloudCLI. + # Do not add a trailing slash. + # + # This variable can be used in redirects and response rewrites. It + # cannot be used in `location` matchers, so update the [SUBPATH LITERAL] + # lines too if you change it. + set $cloudcli_subpath /ai; + + # Private upstream URL where the CloudCLI server is listening. + # For a default local server this is usually http://127.0.0.1:3001. + set $cloudcli_upstream http://127.0.0.1:3001; + + # Allow larger file uploads through the code editor/project file APIs. + client_max_body_size 100m; + + # Redirect /ai to /ai/ so relative browser URL resolution is stable. + # [SUBPATH LITERAL] Change `/ai` if you change $cloudcli_subpath. + location = /ai { + return 301 $cloudcli_subpath/; + } + + # Main prefixed CloudCLI UI route. + # + # [SUBPATH LITERAL] Change `/ai/` and the `^/ai` rewrite if you change + # $cloudcli_subpath. + location ^~ /ai/ { + # Strip the public subpath before proxying. CloudCLI expects to see + # root paths such as /, /session/:id, /assets/..., /manifest.json. + rewrite ^/ai(?/.*)$ $cloudcli_path break; + + # Forward the rewritten request to the private CloudCLI server. + proxy_pass $cloudcli_upstream; + + # Use HTTP/1.1 so WebSocket upgrade requests can pass through if a + # browser reaches a socket endpoint under the subpath. + proxy_http_version 1.1; + + # Preserve useful request metadata for logs and future app support. + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Prefix $cloudcli_subpath; + + # WebSocket upgrade headers. Harmless for normal HTTP requests. + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + + # Long-running agent and terminal sessions can stay open for a long + # time, so avoid closing idle proxied connections too aggressively. + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + + # Disable gzip from the upstream response so sub_filter can inspect + # and rewrite HTML/JSON/JS response bodies. + proxy_set_header Accept-Encoding ""; + + # Rewrite browser-visible root-relative URLs so the runtime can + # discover that the app is mounted under the subpath. + # + # Examples: + # href="/manifest.json" -> href="/ai/manifest.json" + # src="/assets/app.js" -> src="/ai/assets/app.js" + # + # These rewrites are important for React Router basename detection. + sub_filter_once off; + sub_filter_types + application/json + application/manifest+json + application/javascript + text/javascript; + + sub_filter 'href="/' 'href="$cloudcli_subpath/'; + sub_filter 'src="/' 'src="$cloudcli_subpath/'; + + # The production HTML and JS register the service worker at /sw.js. + # Rewrite that registration so the worker is served from /ai/sw.js. + sub_filter "register('/sw.js')" "register('$cloudcli_subpath/sw.js')"; + sub_filter 'register("/sw.js")' 'register("$cloudcli_subpath/sw.js")'; + + # The manifest and service worker contain root-relative paths too. + # Rewriting them keeps PWA metadata and cached manifest requests + # under the same public subpath. + sub_filter '"start_url": "/"' '"start_url": "$cloudcli_subpath/"'; + sub_filter '"scope": "/"' '"scope": "$cloudcli_subpath/"'; + sub_filter '"src": "/' '"src": "$cloudcli_subpath/'; + sub_filter "'/manifest.json'" "'$cloudcli_subpath/manifest.json'"; + sub_filter '"/manifest.json"' '"$cloudcli_subpath/manifest.json"'; + } + + # Root API proxy. + # + # The current CloudCLI frontend calls APIs with root-relative URLs such + # as /api/auth/login. Keep this location unless the frontend becomes + # fully prefix-aware for API requests. + location ^~ /api/ { + proxy_pass $cloudcli_upstream; + + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Prefix $cloudcli_subpath; + + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + } + + # Main app WebSocket proxy. + # + # The frontend opens /ws for realtime chat/session/task updates. + location /ws { + proxy_pass $cloudcli_upstream; + + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Prefix $cloudcli_subpath; + + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + } + + # Shell WebSocket proxy. + # + # The browser terminal uses /shell. It requires the same WebSocket + # upgrade handling as /ws. + location /shell { + proxy_pass $cloudcli_upstream; + + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Prefix $cloudcli_subpath; + + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + } + + # Optional health endpoint proxy used by the frontend version checker. + location = /health { + proxy_pass $cloudcli_upstream; + + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Prefix $cloudcli_subpath; + } + } +}