# How do I run docker-jitsi-meet behind Nginx, Traefik or Caddy?

> Terminate TLS on your proxy, set DISABLE_HTTPS=1, ENABLE_LETSENCRYPT=0 and PUBLIC_URL to your real https address, then forward all HTTP traffic to the web container's HTTP port, with WebSocket upgrade enabled for /xmpp-websocket. Media never goes through the proxy: UDP 10000 must reach the server directly and JVB_ADVERTISE_IPS must hold the public IP. Since stable-11146 the web container listens on 8000 and 8443 inside the container instead of 80 and 443, and the /colibri-ws route is gone, so older proxy configs that target the container directly need updating.

Source: https://jitsi.help/guides/jitsi-behind-reverse-proxy/
Updated: October 5, 2026
Publisher: Jitsi Help (https://jitsi.help/)

## Who this is for

You already run a reverse proxy (Nginx, Traefik or Caddy) on a server. It owns ports 80 and 443 and your certificates, and you want docker-jitsi-meet to sit behind it like any other app. Or you upgraded to stable-11146 or newer and your existing proxy setup stopped working.

The outcome: Jitsi served at `https://meet.example.com` through your proxy, signaling over WebSocket working, and audio and video flowing directly to the videobridge.

## How it works

A Jitsi meeting uses two very different kinds of traffic, and only one of them can go through an HTTP reverse proxy.

**Web and signaling go through the proxy.** The web container serves the app and forwards two signaling routes to Prosody: `/xmpp-websocket` (the default, a WebSocket) and `/http-bind` (BOSH, the HTTP polling fallback). The handbook names `/xmpp-websocket` as the one route that needs WebSocket handling. Everything else is plain HTTP to the web container.

**Media never goes through the proxy.** Audio and video go from browsers straight to the videobridge on `10000/udp`, which the jvb service publishes on the host. The handbook is explicit that `JVB_ADVERTISE_IPS` must be set whether or not you use a reverse proxy, because the advertised IP receives the media and the proxy never sees it. If it is wrong, calls break when a third person joins.

**The browser learns its URLs from `PUBLIC_URL`.** The generated config sets `config.websocket` to `wss://<your domain>/xmpp-websocket` and `config.bosh` to `https://<your domain>/http-bind`. If `PUBLIC_URL` is unset it falls back to `https://localhost:8443`, so every visitor's browser tries to connect to its own machine.

**What changed in stable-11146.** The web container now listens on unprivileged ports `8000` (HTTP) and `8443` (HTTPS) inside the container, instead of `80` and `443`, so it can run without root. The host side did not change: `HTTP_PORT` and `HTTPS_PORT` still default to 8000 and 8443, and compose maps them to the new container ports (`'${HTTP_PORT}:8000'`). Separately, the colibri WebSocket routes `/colibri-ws/` and `/colibri-relay-ws/` were removed, because the bridge channel now always uses SCTP data channels.

## Before you start

- docker-jitsi-meet stable-11146 or newer (the latest is stable-11248), with compose files and image tags from the same release. Coming from an older release? Follow [the rootless upgrade guide](/guides/upgrade-jitsi-docker-rootless/) first.
- A DNS A record for meet.example.com pointing at the server.
- Your proxy holds a valid certificate for meet.example.com. Caddy and Traefik can get one automatically.
- Firewall: `443/tcp` (and `80/tcp` for redirects) to the proxy, `10000/udp` to the server.
- Your server's public IP, used below as `203.0.113.10`.
- Do not serve Jitsi from a subdirectory such as example.com/meet. The handbook warns it does not work well.

## Steps

### 1. Configure .env

In the docker-jitsi-meet folder, edit `.env`. The first three lines are the handbook's reverse proxy settings:

```ini
DISABLE_HTTPS=1
ENABLE_HTTP_REDIRECT=0
ENABLE_LETSENCRYPT=0
PUBLIC_URL=https://meet.example.com
JVB_ADVERTISE_IPS=203.0.113.10
HTTP_PORT=8000
HTTPS_PORT=8443
```

With `DISABLE_HTTPS=1` the container does not start its HTTPS server block at all, so only HTTP on 8000 answers. Keep `ENABLE_HTTP_REDIRECT=0`, otherwise the container answers plain HTTP with a redirect to https and your proxy loops. Do not put a port in `PUBLIC_URL`. It must be the address users type.

### 2. Keep the container ports off the internet

Bind the published web ports to localhost so only the proxy can reach them. Edit the `web` service in `docker-compose.yml`:

```yaml
        ports:
            - '127.0.0.1:${HTTP_PORT}:8000'
            - '127.0.0.1:${HTTPS_PORT}:8443'
```

The official update procedure unzips the new release over your files, so re-apply this edit after every upgrade. Skip this step if your proxy runs in Docker and reaches the container over a shared network (Traefik below).

### 3. Restart Jitsi

```bash
docker compose down
docker compose up -d
```

### 4a. Nginx on the host

This server block follows the handbook routes, adds forwarded headers, and raises the read timeout. Nginx closes a proxied WebSocket that sends no data for 60 seconds by default. The 900 second value matches the handbook's Apache example.

```nginx
server {
    listen 80;
    server_name meet.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name meet.example.com;

    ssl_certificate     /etc/letsencrypt/live/meet.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/meet.example.com/privkey.pem;

    location = /xmpp-websocket {
        proxy_pass http://127.0.0.1:8000$request_uri;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 900s;
        tcp_nodelay on;
    }

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 120s;
    }
}
```

```bash
sudo nginx -t && sudo systemctl reload nginx
```

### 4b. Traefik with Docker labels

Traefik supports WebSocket connections with no extra configuration. Create `docker-compose.override.yml` next to `docker-compose.yml`. The entrypoint `websecure`, the resolver `letsencrypt` and the network `proxy` are placeholders: use the names from your own Traefik setup.

```yaml
services:
    web:
        labels:
            traefik.enable: "true"
            traefik.docker.network: "proxy"
            traefik.http.routers.jitsi.rule: "Host(`meet.example.com`)"
            traefik.http.routers.jitsi.entrypoints: "websecure"
            traefik.http.routers.jitsi.tls.certresolver: "letsencrypt"
            traefik.http.services.jitsi.loadbalancer.server.port: "8000"
        networks:
            meet.jitsi:
            proxy:

networks:
    proxy:
        external: true
```

Set the port label explicitly. When a container exposes several ports, Traefik picks the lowest one. That happens to be 8000 on new images (it was 80 on old ones), but an explicit label written for an old release says `80` and breaks after the upgrade.

### 4c. Caddy

Caddy proxies WebSockets automatically and sets `X-Forwarded-For`, `X-Forwarded-Proto` and `X-Forwarded-Host` by default. It also gets and renews the certificate for the site name on its own.

```caddy
meet.example.com {
    reverse_proxy 127.0.0.1:8000 {
        stream_close_delay 5m
    }
}
```

By default Caddy closes open WebSockets when its config reloads. `stream_close_delay` keeps them open for a while, so a reload does not drop everyone from their meetings. If Caddy runs in Docker on the Jitsi network, use `reverse_proxy web:8000` instead.

```bash
sudo systemctl reload caddy
```

### 5. Update a proxy config written for stable-11031 or older

- Any target that points at the container directly (`web:80`, `web:443`, or a Traefik port label of `80`) must change to `web:8000`.
- Delete any `/colibri-ws/` or `/colibri-relay-ws/` routes, and the `ENABLE_COLIBRI_WEBSOCKET`, `COLIBRI_WEBSOCKET_PORT` and `COLIBRI_WEBSOCKET_REGEX` variables. They no longer exist.
- Targets that use the host ports (`127.0.0.1:8000`) keep working, because the host defaults did not change.

## Configuration reference

| Setting | Where | Default | What it does |
|---|---|---|---|
| `PUBLIC_URL` | `.env` | `https://localhost:8443` if unset | Public address; builds the browser's websocket and BOSH URLs |
| `DISABLE_HTTPS` | `.env` | `0` | `1` removes the container's HTTPS server, for TLS on the proxy |
| `ENABLE_HTTP_REDIRECT` | `.env` | `0` | `1` makes container HTTP redirect to https; keep `0` behind a proxy |
| `ENABLE_LETSENCRYPT` | `.env` | off | Built-in certificates; needs host ports 80 and 443, so disable behind a proxy |
| `HTTP_PORT` | `.env` | `8000` | Host port mapped to container port 8000 |
| `HTTPS_PORT` | `.env` | `8443` | Host port mapped to container port 8443 |
| `JVB_ADVERTISE_IPS` | `.env` | unset | IPs the bridge gives browsers for media; set to the public IP |
| `JVB_PORT` | `.env` | `10000` | UDP media port, published directly on the host |
| `ENABLE_XMPP_WEBSOCKET` | `.env` | `1` | `0` makes the client fall back to BOSH on `/http-bind` |
| `BOSH_RELATIVE` | `.env` | `false` | Uses a relative `/http-bind` path instead of the full URL |

## Common mistakes

- **Joining fails or spins forever.** `PUBLIC_URL` is unset or has a port, so the browser connects to the wrong address. Set it to `https://meet.example.com` and restart.
- **502 Bad Gateway after upgrading.** The proxy still targets container port 80. Change it to 8000.
- **Redirect loop.** `ENABLE_HTTP_REDIRECT=1` with TLS on the proxy. Set it to `0`.
- **Two people work, the third breaks video.** UDP 10000 is blocked or `JVB_ADVERTISE_IPS` is wrong. The proxy cannot help here. See [video fails with 3 people](/troubleshooting/jitsi-video-not-working-3-participants/).
- **Meetings drop about a minute in.** The proxy closes idle WebSockets. Raise `proxy_read_timeout` in Nginx.
- **Let's Encrypt errors in the web logs.** `ENABLE_LETSENCRYPT=1` was left on while the proxy owns port 80. Set it to `0`.

## Verify

1. The browser config uses your domain:

```bash
curl -s https://meet.example.com/config.js | grep -E "config\.(websocket|bosh) ="
```

Expected:

```text
config.bosh = 'https://meet.example.com/http-bind';
config.websocket = 'wss://meet.example.com/xmpp-websocket';
```

2. The WebSocket upgrade passes through the proxy:

```bash
curl -s -o /dev/null -w "%{http_code}\n" --http1.1 \
  -H "Connection: Upgrade" -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: SGVsbG9KaXRzaSE=" \
  -H "Sec-WebSocket-Protocol: xmpp" \
  https://meet.example.com/xmpp-websocket
```

Expected: `101`. A `200`, `400` or `502` means the upgrade headers are not reaching Prosody.

3. In the browser, open the developer tools Network tab, filter by WS, join a meeting, and check that `xmpp-websocket` shows status `101`.

4. Start a meeting with three people on different networks. If video works for all three, media reaches UDP 10000 directly.

## If it still fails

- Read the web container log with `docker compose logs --tail=100 web` and look for upstream errors on `/xmpp-websocket`.
- Read Prosody with `docker compose logs --tail=100 prosody`. No new connections means traffic stops at the proxy.
- Check the container answers locally: `curl -sI http://127.0.0.1:8000/` should return `200`.
- If the web container fails with `open() "/run/web/config/nginx/nginx.conf" failed`, check you are on a stable release and not master, which uses nightly unstable images.
- For media problems, see [video fails with 3 people](/troubleshooting/jitsi-video-not-working-3-participants/). The proxy is not involved.

## What we have not confirmed yet

We checked everything above against the stable-11248 templates and compose file, the Jitsi handbook, the merged pull requests, and the Nginx, Traefik and Caddy documentation on 2026-10-05. We have not yet run every variant on a real server. These points are still open:

- The Traefik labels, the Caddy block and the Nginx timeouts have not been tested on a live stable-11248 server.
- The exact WebSocket curl response from Prosody (101 with the `xmpp` subprotocol and no room parameter) needs confirming on a real server.
- The handbook's "Disabling WebSocket connections" section still sets `ENABLE_SCTP=1`, but that variable no longer appears in the stable-11248 compose files or templates, since PR #2285 made SCTP always on. The handbook looks out of date there.
- The right `/http-bind` read timeout depends on the BOSH wait the client requests. 120 seconds is a safe value, not a measured one.
- Whether `docker-compose.override.yml` can remove the default public port mappings, instead of editing `docker-compose.yml`, was not checked.
- A reverse proxy in front of a Debian/Ubuntu package install, which ships its own Nginx on 443, is not covered here.

## Sources

- [docker-jitsi-meet PR #2261 refactor(web): use unprivileged ports 8000 and 8443](https://github.com/jitsi/docker-jitsi-meet/pull/2261), merged 2026-06-20, maintainer PR
- [docker-jitsi-meet release stable-11146](https://github.com/jitsi/docker-jitsi-meet/releases/tag/stable-11146), 2026-08-03, release note
- [Jitsi handbook, Self-Hosting Guide: Docker (quick start, external ports, Let's Encrypt, updating, NAT, running behind a reverse proxy)](https://jitsi.github.io/handbook/docs/devops-guide/devops-guide-docker), checked 2026-10-05, official doc
- [docker-jitsi-meet docker-compose.yml at stable-11248 (web and jvb ports)](https://github.com/jitsi/docker-jitsi-meet/blob/stable-11248/docker-compose.yml), source code
- [docker-jitsi-meet web/rootfs/defaults/meet.conf at stable-11248 (/http-bind and /xmpp-websocket routes)](https://github.com/jitsi/docker-jitsi-meet/blob/stable-11248/web/rootfs/defaults/meet.conf), source code
- [docker-jitsi-meet web/rootfs/defaults/system-config.js at stable-11248 (PUBLIC_URL, websocket and BOSH URLs)](https://github.com/jitsi/docker-jitsi-meet/blob/stable-11248/web/rootfs/defaults/system-config.js), source code
- [docker-jitsi-meet web/rootfs/defaults/default at stable-11248 (listen 8000 and 8443, DISABLE_HTTPS, ENABLE_HTTP_REDIRECT)](https://github.com/jitsi/docker-jitsi-meet/blob/stable-11248/web/rootfs/defaults/default), source code
- [docker-jitsi-meet PR #2285 remove deprecated colibri websocket support](https://github.com/jitsi/docker-jitsi-meet/pull/2285), merged 2026-07-29, maintainer PR
- [docker-jitsi-meet env.example at stable-11248](https://github.com/jitsi/docker-jitsi-meet/blob/stable-11248/env.example), source code
- [docker-jitsi-meet issue #2275 nginx.conf not found on master branch](https://github.com/jitsi/docker-jitsi-meet/issues/2275), 2026-06-29, community report with maintainer comments
- [docker-jitsi-meet release stable-11248](https://github.com/jitsi/docker-jitsi-meet/releases/tag/stable-11248), 2026-09-14, release note
- [nginx documentation, WebSocket proxying](https://nginx.org/en/docs/http/websocket.html), checked 2026-10-05, official doc
- [Caddy documentation, reverse_proxy directive](https://caddyserver.com/docs/caddyfile/directives/reverse_proxy), checked 2026-10-05, official doc
- [Caddy documentation, Automatic HTTPS](https://caddyserver.com/docs/automatic-https), checked 2026-10-05, official doc
- [Traefik documentation, Expose overview (WebSocket support)](https://doc.traefik.io/traefik/expose/overview/), checked 2026-10-05, official doc
- [Traefik documentation, Docker provider (port selection)](https://doc.traefik.io/traefik/reference/install-configuration/providers/docker/), checked 2026-10-05, official doc

## Need a hand?

If Jitsi still will not load through your proxy, [contact our engineers](/contact/?topic=troubleshooting) with your proxy config, your `.env` without secrets, and the output of `docker compose logs --tail=100 web prosody`. Our [support plans](/services/jitsi-support/) include reverse proxy setup and testing.

## Frequently asked questions

### Do I still need to open UDP 10000 if everything goes through my proxy?

Yes. Media never passes through an HTTP proxy. UDP 10000 must reach the server directly, and JVB_ADVERTISE_IPS must be the public IP.

### Can I keep Jitsi's built-in Let's Encrypt behind a proxy?

No. It needs host ports 80 and 443, which your proxy already owns. Set ENABLE_LETSENCRYPT=0 and let the proxy handle certificates.

### Can I host Jitsi at example.com/meet?

The Jitsi handbook says subdirectory deployments do not work well. Use a subdomain such as meet.example.com.

### Do I need to proxy /colibri-ws any more?

No. It was removed in stable-11146. The bridge channel now always uses SCTP data channels, so only /xmpp-websocket needs WebSocket handling.

### Should the proxy talk to Jitsi over HTTP or HTTPS?

HTTP on the same host is normal, with TLS ending at the proxy. That is what DISABLE_HTTPS=1 is for.


---

Jitsi Help is an independent service. It is not affiliated with, endorsed by or sponsored by 8x8, Inc. or the Jitsi project. Jitsi and Jitsi Meet are trademarks of 8x8, Inc., used here only to describe the software we host and support.
