The compression middleware filter runs before route handlers, so the
res.getHeader('Content-Type') check in shouldSkipCompression is always
undefined at decision time. SSE exclusion relied entirely on the Accept
header, which non-standard clients (curl, fetch) may omit.
Add deterministic path-based exclusion for all known SSE routes so
compression is skipped regardless of client behavior. Also add a Caddy
reverse proxy example and a CDN double-compression warning to docs.
338 lines
9.4 KiB
Markdown
338 lines
9.4 KiB
Markdown
# Reverse Proxy Setup
|
|
|
|
Use this guide when running OpenChamber behind Nginx, Nginx Proxy Manager, Caddy, Cloudflare, or another reverse proxy.
|
|
|
|
## Before you proxy it
|
|
|
|
1. Confirm OpenChamber works directly first.
|
|
2. Open `http://<server-ip>:3000` or your custom port from the same network.
|
|
3. Only add the reverse proxy after the direct connection works.
|
|
|
|
## What the proxy must support
|
|
|
|
- WebSockets for live message transport:
|
|
- `/api/event/ws`
|
|
- `/api/global/event/ws`
|
|
- `/api/terminal/ws`
|
|
- SSE without buffering:
|
|
- `/api/event`
|
|
- `/api/global/event`
|
|
- `/api/notifications/stream`
|
|
- `/api/openchamber/events`
|
|
- `/api/terminal/:sessionId/stream`
|
|
- Large request bodies for attachments and file operations
|
|
- Long-lived read timeouts for live streams and terminal sessions
|
|
|
|
## Rules that matter
|
|
|
|
- Enable WebSocket proxying.
|
|
- Disable buffering on SSE routes.
|
|
- Disable gzip on the proxy if OpenChamber is already compressing responses.
|
|
- Keep compression enabled in only one layer.
|
|
- Forward normal proxy headers such as `Host`, `X-Forwarded-For`, and `X-Forwarded-Proto`.
|
|
- Increase body size limits if users upload files.
|
|
|
|
## Quick checklist
|
|
|
|
- OpenChamber reachable directly on LAN
|
|
- WebSockets enabled in the proxy
|
|
- SSE routes have buffering off
|
|
- `gzip off` on the proxy host, or proxy compression disabled another way
|
|
- `client_max_body_size` large enough for attachments
|
|
- `proxy_read_timeout` long enough for streams
|
|
|
|
## Example: Nginx
|
|
|
|
<details>
|
|
<summary>Show example config</summary>
|
|
|
|
```nginx
|
|
client_max_body_size 50M;
|
|
client_body_buffer_size 50M;
|
|
proxy_request_buffering off;
|
|
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Connection "";
|
|
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-Host $host;
|
|
|
|
gzip off;
|
|
|
|
location = /api/terminal/ws {
|
|
proxy_pass http://127.0.0.1:3000;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection "upgrade";
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
proxy_read_timeout 3600s;
|
|
proxy_send_timeout 3600s;
|
|
}
|
|
|
|
location = /api/global/event/ws {
|
|
proxy_pass http://127.0.0.1:3000;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection "upgrade";
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
proxy_read_timeout 3600s;
|
|
proxy_send_timeout 3600s;
|
|
}
|
|
|
|
location = /api/event/ws {
|
|
proxy_pass http://127.0.0.1:3000;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection "upgrade";
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
proxy_read_timeout 3600s;
|
|
proxy_send_timeout 3600s;
|
|
}
|
|
|
|
location ~ ^/api/(event|global/event|notifications/stream|openchamber/events)$ {
|
|
proxy_pass http://127.0.0.1:3000;
|
|
proxy_set_header Accept "text/event-stream";
|
|
proxy_set_header Cache-Control "no-cache";
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
gzip off;
|
|
add_header X-Accel-Buffering "no" always;
|
|
add_header Cache-Control "no-cache, no-transform" always;
|
|
proxy_read_timeout 3600s;
|
|
proxy_send_timeout 3600s;
|
|
}
|
|
|
|
location ~ ^/api/terminal/.+/stream$ {
|
|
proxy_pass http://127.0.0.1:3000;
|
|
proxy_set_header Accept "text/event-stream";
|
|
proxy_set_header Cache-Control "no-cache";
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
gzip off;
|
|
add_header X-Accel-Buffering "no" always;
|
|
add_header Cache-Control "no-cache, no-transform" always;
|
|
proxy_read_timeout 3600s;
|
|
proxy_send_timeout 3600s;
|
|
}
|
|
|
|
location /api {
|
|
proxy_pass http://127.0.0.1:3000;
|
|
proxy_read_timeout 3600s;
|
|
proxy_send_timeout 3600s;
|
|
}
|
|
|
|
location / {
|
|
proxy_pass http://127.0.0.1:3000;
|
|
}
|
|
```
|
|
|
|
</details>
|
|
|
|
## Example: Nginx Proxy Manager
|
|
|
|
<details>
|
|
<summary>Show Advanced tab example</summary>
|
|
|
|
```nginx
|
|
client_max_body_size 50M;
|
|
client_body_buffer_size 50M;
|
|
proxy_request_buffering off;
|
|
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Connection "";
|
|
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-Host $host;
|
|
|
|
gzip off;
|
|
|
|
location = /api/terminal/ws {
|
|
proxy_pass http://127.0.0.1:3000;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection "upgrade";
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
proxy_read_timeout 3600s;
|
|
proxy_send_timeout 3600s;
|
|
proxy_connect_timeout 30s;
|
|
}
|
|
|
|
location = /api/global/event/ws {
|
|
proxy_pass http://127.0.0.1:3000;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection "upgrade";
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
proxy_read_timeout 3600s;
|
|
proxy_send_timeout 3600s;
|
|
proxy_connect_timeout 30s;
|
|
}
|
|
|
|
location = /api/event/ws {
|
|
proxy_pass http://127.0.0.1:3000;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection "upgrade";
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
proxy_read_timeout 3600s;
|
|
proxy_send_timeout 3600s;
|
|
proxy_connect_timeout 30s;
|
|
}
|
|
|
|
location = /api/event {
|
|
proxy_pass http://127.0.0.1:3000;
|
|
proxy_set_header Accept "text/event-stream";
|
|
proxy_set_header Cache-Control "no-cache";
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
gzip off;
|
|
add_header X-Accel-Buffering "no" always;
|
|
add_header Cache-Control "no-cache, no-transform" always;
|
|
proxy_read_timeout 3600s;
|
|
proxy_send_timeout 3600s;
|
|
proxy_connect_timeout 30s;
|
|
}
|
|
|
|
location = /api/global/event {
|
|
proxy_pass http://127.0.0.1:3000;
|
|
proxy_set_header Accept "text/event-stream";
|
|
proxy_set_header Cache-Control "no-cache";
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
gzip off;
|
|
add_header X-Accel-Buffering "no" always;
|
|
add_header Cache-Control "no-cache, no-transform" always;
|
|
proxy_read_timeout 3600s;
|
|
proxy_send_timeout 3600s;
|
|
proxy_connect_timeout 30s;
|
|
}
|
|
|
|
location = /api/notifications/stream {
|
|
proxy_pass http://127.0.0.1:3000;
|
|
proxy_set_header Accept "text/event-stream";
|
|
proxy_set_header Cache-Control "no-cache";
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
gzip off;
|
|
add_header X-Accel-Buffering "no" always;
|
|
add_header Cache-Control "no-cache, no-transform" always;
|
|
proxy_read_timeout 3600s;
|
|
proxy_send_timeout 3600s;
|
|
proxy_connect_timeout 30s;
|
|
}
|
|
|
|
location = /api/openchamber/events {
|
|
proxy_pass http://127.0.0.1:3000;
|
|
proxy_set_header Accept "text/event-stream";
|
|
proxy_set_header Cache-Control "no-cache";
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
gzip off;
|
|
add_header X-Accel-Buffering "no" always;
|
|
add_header Cache-Control "no-cache, no-transform" always;
|
|
proxy_read_timeout 3600s;
|
|
proxy_send_timeout 3600s;
|
|
proxy_connect_timeout 30s;
|
|
}
|
|
|
|
location ~ ^/api/terminal/.+/stream$ {
|
|
proxy_pass http://127.0.0.1:3000;
|
|
proxy_set_header Accept "text/event-stream";
|
|
proxy_set_header Cache-Control "no-cache";
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
gzip off;
|
|
add_header X-Accel-Buffering "no" always;
|
|
add_header Cache-Control "no-cache, no-transform" always;
|
|
proxy_read_timeout 3600s;
|
|
proxy_send_timeout 3600s;
|
|
proxy_connect_timeout 30s;
|
|
}
|
|
|
|
location /api {
|
|
proxy_pass http://127.0.0.1:3000;
|
|
proxy_read_timeout 3600s;
|
|
proxy_send_timeout 3600s;
|
|
proxy_connect_timeout 30s;
|
|
}
|
|
|
|
location / {
|
|
proxy_pass http://127.0.0.1:3000;
|
|
}
|
|
```
|
|
|
|
</details>
|
|
|
|
Also enable `Websockets Support` in Nginx Proxy Manager for this host.
|
|
|
|
## Common failure signs
|
|
|
|
### Page loads, but sending messages fails
|
|
|
|
- WebSockets are not enabled in the proxy
|
|
- `/api/event/ws` or `/api/global/event/ws` is not passing through correctly
|
|
|
|
### Notifications or live status do not update
|
|
|
|
- one of the SSE routes is buffered or cached
|
|
- `X-Accel-Buffering "no"` is missing
|
|
|
|
### File uploads fail
|
|
|
|
- `client_max_body_size` is too small
|
|
|
|
### Everything works locally, but breaks only behind the proxy
|
|
|
|
- the proxy is compressing and buffering live traffic
|
|
- the proxy is missing WebSocket support
|
|
|
|
## Example: Caddy
|
|
|
|
<details>
|
|
<summary>Show example config</summary>
|
|
|
|
```caddy
|
|
reverse_proxy 127.0.0.1:3000 {
|
|
# WebSocket support is automatic in Caddy
|
|
|
|
# Flush SSE responses immediately
|
|
flush_interval -1
|
|
|
|
# Pass through Host and proxy headers
|
|
header_up Host {host}
|
|
header_up X-Real-IP {remote_host}
|
|
header_up X-Forwarded-For {remote_host}
|
|
header_up X-Forwarded-Proto {scheme}
|
|
|
|
# Increase timeouts for long-lived streams
|
|
transport http {
|
|
read_timeout 3600s
|
|
write_timeout 3600s
|
|
}
|
|
}
|
|
```
|
|
|
|
</details>
|
|
|
|
Caddy handles WebSocket upgrades automatically — no extra configuration needed. The `flush_interval -1` directive ensures SSE chunks are forwarded immediately without buffering.
|
|
|
|
## CDN and double-compression warning
|
|
|
|
If you place a CDN (such as Cloudflare) in front of your reverse proxy, be aware of double compression:
|
|
|
|
- OpenChamber compresses HTTP responses with gzip (threshold 1 KB).
|
|
- Cloudflare and other CDNs also compress responses by default.
|
|
- This can cause double-compressed responses or incorrect `Content-Encoding` headers.
|
|
|
|
To avoid this, disable compression at **one** layer:
|
|
|
|
- **Cloudflare:** Rules → Compression → disable (or use "Passthrough" mode).
|
|
- **Nginx:** `gzip off` (already shown in the examples above).
|
|
- **Caddy:** Caddy does not re-compress by default if the upstream already sends compressed content.
|
|
|
|
SSE streaming routes are excluded from compression by OpenChamber, but the CDN may still buffer them. Check your CDN documentation for how to disable buffering on SSE paths.
|