Troubleshooting Open WebUI (with OLLAMA) WebSocket Connection Failure via Apache Reverse Proxy and SSH Tunnel

ollama AI Open WebUI Troubleshooting

Last updated on March 9, 2025

Troubleshooting Open WebUI WebSocket Connection Failure via Apache Reverse Proxy and SSH Tunnel

**IMPORTANT UPDATE**

Open Web UI, as of March 2025, is a Single Page Application (SAP) that expects root domain requests like “<your-domain.com>/”. Serving the SAP with a path other than “root domain path” does not align with the SAP’s logic. So if you are trying to serve Open WebUI on a non-root path, the steps below will allow the app to partially function as expected, but does not fix 404s after a cookie is obtained by a logged-in Open WebUI user.

The work around for this is to use a subdomain (i.e. <open-webui>.<your-domain.com>/) rather than a non-root domain path (i.e. <your-domain.com>/<opem-webui). In other words, only root URL paths can be used to serve Open WebUI.

If you are using a root path to server Open WebUI, no adjustments are needed for apache.conf. Everything should work out-of-the-box using typical server setup or via ssh tunnel. This page will be kept up to inform others having similiar issues and provide some basis for those forking/changing Open WebUI’s logic.

Problem Description

(Note: Non-root paths as of March 2025 are not supported by Open WebUI, See “IMPORTANT UPDATE” at the top of this page)

When deploying Open WebUI (an open-source AI interface) on a local server and exposing it remotely through a cloud instance running Apache as a reverse proxy, the WebSocket connection at wss://<your-domain>/ws/socket.io/ failed. Locally, the app worked fine at http://localhost:<port>, but remotely, users encountered errors like WebSocket connection failed in the browser console, 403 Forbidden with wscat, and 500 Internal Server Error in Apache logs. The root cause was a combination of WebSocket routing misconfiguration, authentication issues, and middleware conflicts in Open WebUI’s backend.

This article outlines the steps to resolve this issue, based on a setup using an SSH tunnel between a local server and a cloud instance.

Environment Setup

(Note: Non-root paths as of March 2025 are not supported by Open WebUI, See “IMPORTANT UPDATE” at the top of this page)

  • Local Server:
    • Runs Open WebUI in a Docker container. Ollama and desired models installed inside container.
    • Docker image: Custom image using Open WebUI vanilla container with ollama models installed inside. Docker commit was used to create the custom image <your-image-name>.
    • Ports: <host-port> (mapped to container’s <8080>) and an additional port for the AI API.
    • No public IP; relies on SSH tunnel for external access.
    • Docker command with proper environment variables
      • [root@server-b ~]# docker run -d –gpus all -p 3000:8080 -p 11435:11434 -v open-webui-14b:/app/backend/data -v ollama-14b:/root/.ollama -e WEBUI_URL=https://<MY-DOMAIN.com/MYWEBUI> -e SOCKETIO_PATH=/socket.io –name open-webui-ollama-14b –restart unless-stopped my-open-webui-ollama-14b
  • Cloud Instance:
    • Runs a web server stack with Apache (e.g., Bitnami WordPress).
    • Public IP: <cloud-public-ip> Use for ssh tunnel and webserving.
    • Domain: <your-domain> with SSL/https enabled.
    • Acts as reverse proxy to forward traffic to the local server via SSH tunnel.
  • SSH Tunnel:
    • Forwards cloud instance’s localhost:<local-port> to local server’s localhost:<local-port>.
  • Symptoms:
    • Local access (http://localhost:<local-port>/) worked.
    • Remote access (https://<your-domain>/<app-path>) loaded the UI but failed WebSocket connections. UI loaded after proxy of static assets as seen in the Apache configuration below (e.g. /_app, /static & /api).
    • Apache logs showed 500 errors for /ws/socket.io/.
    • Docker logs showed AssertionError in starlette/staticfiles.py.

Root Cause

(Note: Non-root paths as of March 2025 are not supported by Open WebUI, See “IMPORTANT UPDATE” at the top of this page)

The issue stemmed from multiple factors:

  1. Path Mismatch: Apache proxied /ws/socket.io/ to ws://localhost:<local-port>/socket.io/ instead of ws://localhost:<local-port>/ws/socket.io/, missing Open WebUI’s expected WebSocket endpoint.
  2. Authentication: Open WebUI’s Socket.IO required a JWT token, causing 403 errors with unauthenticated tools like wscat.
  3. Routing Conflict: WebSocket requests hit Open WebUI’s static files middleware instead of the Socket.IO handler due to mount order in the backend configuration.

Solution

(Note: Non-root paths as of March 2025 are not supported by Open WebUI, See “IMPORTANT UPDATE” at the top of this page)

The fix involved correcting the Apache proxy configuration, ensuring proper WebSocket routing, and verifying authentication. Here are the steps that resolved the issue:

Step 1: Update Apache Proxy Configuration

Apache’s reverse proxy rules were misaligned with Open WebUI’s WebSocket endpoint (/ws/socket.io/). We updated the virtual host to proxy correctly.

Edit Virtual Host File:

On the cloud instance, open the Apache configuration file (path varies by setup, e.g., <apache-config-dir>/vhosts/<site>-vhost.conf):

sudo vim <apache-config-dir>/vhosts/<site>-vhost.conf

Replace <VirtualHost *:443> Section:

Use this updated configuration (NOTE: the configuration includes proxy for static assets running on the local server (e.g. /static, /_app, & /api):

<VirtualHost *:443>
  ServerName <your-domain>
  SSLEngine on
  SSLCertificateFile "<ssl-cert-path>/<your-domain>.crt"
  SSLCertificateKeyFile "<ssl-cert-path>/<your-domain>.key"
  
  # BEGIN: Configuration for SSL (e.g., Let’s Encrypt)
  Include "<ssl-config-path>/httpd-prefix.conf"
  
  # BEGIN: Support domain renewal when using mod_proxy without Location
  <IfModule mod_proxy.c>
    ProxyPass /.well-known !
  </IfModule>
  
  # Reverse Proxy for Open WebUI
  ProxyPreserveHost On
  ProxyPass /static http://localhost:<local-port>/static timeout=60 retry=0
  ProxyPassReverse /static http://localhost:<local-port>/static
  ProxyPass /_app http://localhost:<local-port>/_app timeout=60 retry=0
  ProxyPassReverse /_app http://localhost:<local-port>/_app
  ProxyPass /api http://localhost:<local-port>/api timeout=60 retry=0
  ProxyPassReverse /api http://localhost:<local-port>/api
  
  # WebSocket Proxy for Socket.IO
  RewriteEngine On
  RewriteCond %{HTTP:Upgrade} websocket [NC]
  RewriteCond %{HTTP:Connection} upgrade [NC]
  RewriteRule ^/ws/socket.io/(.*) ws://localhost:<local-port>/ws/socket.io/$1 [P,L]
  ProxyPass /ws/socket.io ws://localhost:<local-port>/ws/socket.io timeout=300 retry=0 keepalive=On
  ProxyPassReverse /ws/socket.io ws://localhost:<local-port>/ws/socket.io
  ProxyPass /<app-path> http://localhost:<local-port>/ timeout=60 retry=0
  ProxyPassReverse /<app-path> http://localhost:<local-port>/
  RequestHeader set X-Forwarded-Proto "https"
  RequestHeader set X-Forwarded-Port "443"

  # Chat URL Proxy
  RewriteCond %{REQUEST_URI} ^/c(/[0-9a-f-]{36})$
  RewriteRule ^/c/([0-9a-f-]{36})$ /$1 [P,L]
  ProxyPass /c/ http://localhost:<local-port>/ timeout=60 retry=0
  ProxyPassReverse /c/ http://localhost:<local-port>/
  
  # Auth Proxy
  RewriteCond %{REQUEST_URI} ^/auth(/|$)
  RewriteRule ^/auth(/(.*))?$ /$2 [P,L]
  ProxyPass /auth http://localhost:<local-port>/ timeout=60 retry=0
  ProxyPassReverse /auth http://localhost:<local-port>/
  
  # Block direct AI API requests (optional)
  RewriteCond %{REQUEST_URI} ^/<api-path>(/|$)
  RewriteRule ^ - [R=403,L]
  
  # Web Server DocumentRoot (e.g., WordPress)
  DocumentRoot <web-root-dir>
  <Directory "<web-root-dir>">
    Options -Indexes +FollowSymLinks -MultiViews
    AllowOverride None
    Require all granted
    RewriteEngine On
    RewriteRule ^<web-root-prefix>(/.*) $1 [L]
    RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
    RewriteBase /
    RewriteRule ^index\.php$ - [L]
    RewriteCond %{REQUEST_URI} !^/<app-path>
    RewriteCond %{REQUEST_URI} !^/c/
    RewriteCond %{REQUEST_URI} !^/auth
    RewriteCond %{REQUEST_URI} !^/static
    RewriteCond %{REQUEST_URI} !^/_app
    RewriteCond %{REQUEST_URI} !^/api
    RewriteCond %{REQUEST_URI} !^/ws/socket.io
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule . /index.php [L]
  </Directory>
  Include "<htaccess-config-path>/<site>-htaccess.conf"
  
  # BEGIN: Support domain renewal when using mod_proxy within Location
  <Location /.well-known>
    <IfModule mod_proxy.c>
      ProxyPass !
    </IfModule>
  </Location>
  
  # CORS Headers
  <IfModule mod_headers.c>
    Header always set Access-Control-Allow-Origin "*"
    Header always set Access-Control-Allow-Methods "GET, POST, OPTIONS, DELETE, PUT"
    Header always set Access-Control-Allow-Headers "Content-Type, Authorization"
  </IfModule>
</VirtualHost>

Replace placeholders:

(Note: Non-root paths as of March 2025 are not supported by Open WebUI, See “IMPORTANT UPDATE” at the top of this page)

  • <your-domain>: Your domain (e.g., example.com).
  • <ssl-cert-path>: Path to your SSL certificates.
  • <ssl-config-path>: Path to SSL config (e.g., Let’s Encrypt).
  • <local-port>: Port mapped to Open WebUI (e.g., 3000).
  • <app-path>: Open WebUI path (e.g., app).
  • <api-path>: AI API path to block (e.g., ollama).
  • <web-root-dir>: Web server root (e.g., /var/www/html).
  • <web-root-prefix>: Prefix to strip (e.g., html).
  • <htaccess-config-path>: Path to htaccess config.

Key changes:

(Note: Non-root paths as of March 2025 are not supported by Open WebUI, See “IMPORTANT UPDATE” at the top of this page)

  • Added ProxyPass /ws/socket.io ws://localhost:<local-port>/ws/socket.io.
  • Used RewriteRule for WebSocket upgrades.

Restart Apache:

sudo <apache-control-script> restart apache

Example: sudo /opt/bitnami/ctlscript.sh restart apache.

Step 2: Verify SSH Tunnel

(Note: Non-root paths as of March 2025 are not supported by Open WebUI, See “IMPORTANT UPDATE” at the top of this page)

The SSH tunnel forwards traffic from the cloud instance to the local server. Ensure it’s running:

On Local Server:

Kill any existing ssh tunnel:

pkill ssh

Start new ssh tunnl from local to cloud:

ssh -v -i <ssh-key-path>/<key-file> -R <local-port>:localhost:<local-port> <cloud-user>@<cloud-public-ip> -N &Copied

Replace placeholders:

  • <ssh-key-path>: Path to SSH key (e.g., /home/user/.ssh).
  • <key-file>: SSH key file (e.g., id_rsa).
  • <local-port>: Port (e.g., 3000).
  • <cloud-user>: Cloud instance user (e.g., admin).
  • <cloud-public-ip>: Cloud IP (e.g., 192.168.1.100).

Check verbose output for connected to localhost port <local-port>.

Step 3: Update Open WebUI Configuration

(Note: Non-root paths as of March 2025 are not supported by Open WebUI, See “IMPORTANT UPDATE” at the top of this page)

Open WebUI’s CORS and WebSocket settings needed adjustment for remote access:

Restart Docker Container:

Stop and remove the existing container:

docker stop <container-name>
docker rm <container-name>Copied

Run with updated environment variables:

docker run -d \
  --gpus all \
  -p <local-port>:<container-port> \
  -p <api-port>:<api-container-port> \
  -v <data-volume>:/app/backend/data \
  -v <api-volume>:/root/.ollama \
  -e WEBUI_URL=https://<your-domain>/<app-path> \
  -e CORS_ALLOW_ORIGIN=https://<your-domain> \
  --name <container-name> \
  --restart unless-stopped \
  <your-image-name>Copied

Replace placeholders:

  • <local-port>: Host port (e.g., 3000).
  • <container-port>: Container port (e.g., 8080).
  • <api-port>: AI API host port (e.g., 11434).
  • <api-container-port>: AI API container port (e.g., 11434).
  • <data-volume>: Volume for data (e.g., open-webui-data).
  • <api-volume>: Volume for API (e.g., ollama-data).
  • <your-domain>: Your domain (e.g., example.com).
  • <app-path>: App path (e.g., app).
  • <container-name>: Container name (e.g., open-webui).
  • <your-image-name>: Docker image (e.g., my-open-webui).

Step 4: Test WebSocket Connection

(Note: Non-root paths as of March 2025 are not supported by Open WebUI, See “IMPORTANT UPDATE” at the top of this page)

Local Test:

On local server:

wscat -c ws://localhost:<local-port>/ws/socket.io/?EIO=4

Replace <local-port> with host port that was forwarded from container in docker run command (e.g. <local(host)-port>:8080 (container-port). Expect 403 without a token (authentication required).

Remote Test:

Open https://<your-domain>/<app-path> in a browser.

Check Developer Tools (F12) > Network > WS for wss://<your-domain>/ws/socket.io/. Look for 101 Switching Protocols.

With Token (optional):

Extract token from browser cookies (token under Application > Cookies) or WebSocket headers (Authorization: Bearer <token>).

test:

wscat -c ws://localhost:<local-port>/ws/socket.io/?EIO=4 -H "Authorization: Bearer <your-token>"

Step 5: Verify Logs

(Note: Non-root paths as of March 2025 are not supported by Open WebUI, See “IMPORTANT UPDATE” at the top of this page)

  • Docker Logs:
docker logs <container-name>

Ensure no StaticFiles errors; look for Socket.IO connection logs.

Apache Logs:

sudo tail -f <apache-log-dir>/access_log
sudo tail -f <apache-log-dir>/error_logCopied

Replace <apache-log-dir> (e.g., /var/log/apache2).

Confirm /ws/socket.io/ requests return 101, not 500.

Outcome

(Note: Non-root paths as of March 2025 are not supported by Open WebUI, See “IMPORTANT UPDATE” at the top of this page)

After applying these steps:

  • The browser successfully connected to wss://<your-domain>/ws/socket.io/, showing 101 Switching Protocols.
  • Chat responses streamed correctly over WebSocket.
  • The WebSocket issue was fully resolved, enabling remote access to Open WebUI.
Additional Notes
  • Authentication: Open WebUI’s Socket.IO requires a JWT token, sent automatically by the browser after login. For manual testing, extract the token from browser cookies or headers.
  • Minor Errors404 errors for assets (e.g., /manifest.json) may persist but don’t affect functionality. Fix by ensuring files exist in the Docker container’s build directory. We will post a fixes for this in a later post.
  • Debugging: Add -e GLOBAL_LOG_LEVEL=DEBUG to the Docker run command for detailed backend logs if issues recur.

This solution leverages an SSH tunnel and Apache proxy, making it applicable to similar local-to-cloud setups. Documented on March 5, 2025, based on Open WebUI v0.5.19. Should you have any questions or concerns, please comment below. Thank you!

Related Articles

Responses