> Markdown version of [Reverse Proxies](https://vaadin.com/docs/next/flow/production/reverse-proxy). Section index: [llms.txt](https://vaadin.com/docs/next/flow/llms.txt)

# Deploying Vaadin Applications Behind Reverse Proxies

Using a reverse proxy in front of a Java servlet container, such as Apache HTTPD with Tomcat or Jetty, is widely considered a best practice for deploying web applications. A reverse proxy acts as an intermediary between clients and the backend application server, offering numerous benefits that enhance performance, security, and scalability.

By offloading tasks like SSL termination, request routing, and caching to the reverse proxy, the servlet container can focus on serving application logic, resulting in a more efficient and maintainable deployment architecture. Additionally, reverse proxies provide a unified interface for serving multiple applications, enabling load balancing, URL rewriting, and seamless integration of static content alongside dynamic applications.

Security is another key advantage, as the reverse proxy can filter incoming requests, prevent direct exposure of the application server to the internet, and enforce access controls. This layer of abstraction not only simplifies scaling and maintenance but also enhances the reliability and robustness of your Java-based web applications.

This guide covers common deployment scenarios, configuration for reverse proxies, and special considerations for Vaadin’s server push functionality, session handling, and load balancing. It explains how to configure Vaadin applications to work seamlessly behind reverse proxies.

Various configuration scenarios are implemented using [`Apache HTTPD`](https://httpd.apache.org/) (2.4.47+) and [`nginx`](https://nginx.org/en/). The provided configurations are a working starting point that can be improved with specific customization for the production environment.

## <a id="apache-httpd"></a>Apache HTTPD

Proxying with Apache HTTPD is additionally expanded into two categories: HTTP and AJP backend protocols. Apache HTTPD should be configured to load [`mod_rewrite`](https://httpd.apache.org/docs/2.4/mod/mod_rewrite.html), [`mod_proxy`](https://httpd.apache.org/docs/2.4/mod/mod_proxy.html), and one or more proxy modules, such as [`mod_proxy_http`](https://httpd.apache.org/docs/2.4/mod/mod_proxy_http.html), [`mod_proxy_wstunnel`](https://httpd.apache.org/docs/2.4/mod/mod_proxy_wstunnel.html) or [`mod_proxy_ajp`](https://httpd.apache.org/docs/2.4/mod/mod_proxy_ajp.html).

```
LoadModule rewrite_module modules/mod_rewrite.so
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so
LoadModule proxy_wstunnel_module modules/mod_proxy_wstunnel.so

# Optional, for AJP backend protocol
# LoadModule proxy_ajp_module modules/mod_proxy_ajp.so
```

Example snippets are limited to the proxy configuration. In the simplest cases all relevant configurations are put into a `<Location>` directive. However, setups that require `mod_rewrite` should be directly used inside server config or virtual host definition. The proxying directives used in the examples are the following:

- `ProxyPass`: maps remote servers into the local server URL-space.

- `ProxyPassReverse`: adjusts the URL in HTTP response headers sent from a reverse proxied server.

- `ProxyPassReverseCookiePath`: adjusts the Path string in `Set-Cookie` headers from a reverse-proxied server.

`ProxyPass` directive can take a list of parameters in form of `key=value` pairs to tune the connection to the backend server. For simplicity, the examples won’t set any option, but with complex network setup, it might be useful to configure some of them:

- `keepalive=On`: should be used when you have a firewall between your Apache HTTPD and the backend server, which tends to drop inactive connections.

- `disablereuse=On`: force `mod_proxy` to immediately close a connection to the backend after being used, and thus, disable its persistent connection and pool for that backend. This helps in various situations where a firewall between Apache HTTPD and the backend server (regardless of protocol) tends to silently drop connections.

- `retry=0`: prevents Apache waiting for a while before sending request again to the backend server in case the worker is an error state.

Also, only `Location`, `Content-Location` and `URI` headers in the HTTP response is rewritten by `ProxyPassReverse`. Apache HTTPD won’t rewrite other response headers, nor does it by default rewrite URL references inside HTML pages. This means that if the proxied content contains absolute URL references, they’ll bypass the proxy. To rewrite HTML content to match the proxy, you must load and enable [`mod_proxy_html`](https://httpd.apache.org/docs/2.4/mod/mod_proxy_html.html).

### <a id="ajp-protocol"></a>AJP Protocol

The Apache JServ Protocol (AJP) is a binary protocol commonly used to connect web servers and application servers. It can be an efficient alternative to HTTP(S) in certain scenarios, such as when reducing overhead is important or when working with legacy systems.

In a Spring Boot application with embedded Apache Tomcat servlet container, AJP support can be configured as following:

```java
@ConditionalOnProperty("tomcat.ajp.port")
@Configuration
public class TomcatConfig implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {

    private static final String PROTOCOL = "AJP/1.3";

    @Value("${tomcat.ajp.port:8009}") //Defined on application.properties or as environment variable
    private int ajpPort;

    @Value("${tomcat.ajp.address:::}") //Defined on application.properties or as environment variable
    private InetAddress ajpAddress;

    @Value("${tomcat.ajp.secret}") // Defined on application.properties or as environment variable
    private String ajpSecret;


    @Override
    public void customize(TomcatServletWebServerFactory factory) {
        Connector ajpConnector = new Connector(PROTOCOL);
        ajpConnector.setPort(ajpPort);
        AbstractAjpProtocol<?> ajpProtocol = (AbstractAjpProtocol<?>) ajpConnector.getProtocolHandler();
        ajpProtocol.setSecret(ajpSecret);
        ajpProtocol.setAddress(ajpAddress);
        factory.addAdditionalConnectors(ajpConnector);
    }
}
```

To enhance security, the above snippet is setting the AJP protocol secret, that should be included with every request from the proxy server. In the Apache HTTPD configuration examples, the value of the secret is supposed to be stored in an environment variable named `VAADIN_APP_AJP_SECRET`.

For different setups, consult the documentation of the Servlet container.

AJP can’t carry WebSocket traffic, so the examples in this guide proxy the push endpoint over HTTP with a separate set of directives whenever WebSockets are in use. With the [Server-Sent Events](#server-sent-events) transport, that second channel isn’t needed: the push request travels over the AJP connection together with every other request, and the HTTP port of the backend server doesn’t have to be reachable from the proxy at all. The AJP worker has to be configured with `flushpackets=on`, because `mod_proxy_ajp` otherwise buffers the response body and holds back the stream.

Note that a `ws://` worker serves WebSocket traffic only, and rejects the plain `GET` request that Server-Sent Events uses. Where the examples route the push path to such a worker, the two transports are mutually exclusive: remove those rules to serve push over AJP. Where the upgrade is instead selected by a `RewriteCond` on the `Upgrade` request header, both transports are served by the same configuration, since requests without that header continue to the AJP worker.

## <a id="nginx"></a>`nginx`

The `nginx` examples are mostly based on the online [WebSocket proxying guide](https://nginx.org/en/docs/http/websocket.html). The provided code snippets are supposed to be placed into the [`http`](https://nginx.org/en/docs/http/ngx_http_core_module.html#http) block in the main configuration file.

```
user  nginx;
worker_processes  auto;

error_log  /var/log/nginx/error.log notice;
pid        /var/run/nginx.pid;

http {

    ## Example snippet goes here

}
```

A `map` directive is used to handle the connection upgrade, to set the value of the `Connection` header field in a request to the proxied server depending on the presence of the `Upgrade` field in the client request header.

```
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}
```

Other used directive are:

- `proxy_pass`: maps remote servers into the local server URL-space.

- `proxy_set_header`: redefines or appends fields to the request header passed to the proxied server.

- `proxy_redirect`: adjusts the URL in HTTP response headers sent from a reverse proxied server.

- `proxy_cookie_path`: adjusts the Path string in `Set-Cookie` headers from a reverse-proxied server.

## <a id="websockets-in-a-vaadin-application"></a>WebSockets in a Vaadin Application

WebSockets provide a persistent, full-duplex communication channel between a client and a server, unlike traditional HTTP requests, which follow a request-response model. In the context of a Vaadin application, WebSockets are **optional** but enhance user experience by enabling ([server push](https://vaadin.com/docs/next/flow/advanced/server-push.md)), allowing real-time UI updates without requiring clients to repeatedly poll the server.

WebSockets work by performing a protocol upgrade from HTTP to the WebSocket protocol (`ws://` or `wss://`) using the `Upgrade` and `Connection` headers.

In Apache HTTPD WebSocket proxying is usually achieved by adding the `upgrade=websocket` option to the `ProxyPass` directive. However, AJP does not support WebSockets because it is designed for traditional request-response communication and does not handle persistent bidirectional connections. The examples in this guide work around this limitation by proxying the upgrade requests over HTTP with a dedicated set of directives, which requires the HTTP port of the backend server to be reachable from the proxy. The [Server-Sent Events](#server-sent-events) transport is not affected, since it travels over an AJP connection like any other request.

To support WebSockets behind a reverse proxy, Apache must route WebSocket traffic ensuring proper handling of the upgrade process and maintaining the connection between the client and the backend Vaadin server, meaning that a specific configuration is required.

Similarly, `nginx` also needs to be configured to handle the protocol upgrade. In the proposed example, the WebSockets configuration blocks are marked with `Websockets only (begin)` and `Websockets only (end)` comments. If WebSockets support is not required by the application, the related configuration can be skipped. For Apache HTTPD `ProxyPass` directive remove the `upgrade` option.

> **Warning:** In Apache HTTPD, a `ProxyPass` rule for the push endpoint is ignored when a catch-all rule using the same URL scheme is declared before it. `mod_proxy` shares workers whose URLs overlap and keeps the one defined first, so the `upgrade=websocket` option on the push rule is silently discarded and the upgrade never happens. The [`mod_proxy` documentation](https://httpd.apache.org/docs/2.4/mod/mod_proxy.html) recommends sorting worker definitions by URL length, starting with the longest. For this reason, the examples in this guide declare the push locations before the catch-all. The `ProxyPassReverse` directive doesn’t create a worker and isn’t affected.

## <a id="server-sent-events"></a>Server-Sent Events in a Vaadin Application (since V25.3)

Server-Sent Events (SSE) is an alternative transport for `PUSH` communication, available behind the `ssePushTransport` [feature flag](https://vaadin.com/docs/next/flow/configuration/feature-flags.md). Rather than upgrading the connection, the browser sends a long-lived HTTP `GET` request to the push endpoint, and the server streams updates over the response until the connection is closed. Messages from the client to the server travel over regular XHR requests, as they do with the default `WEBSOCKET_XHR` transport.

Since SSE is plain HTTP, none of the WebSocket-specific configuration applies to it. Apache HTTPD needs neither `mod_proxy_wstunnel` nor the `upgrade` option on `ProxyPass`, and `nginx` needs neither the `map $http_upgrade` directive nor the `Upgrade` and `Connection` request headers. The push endpoint is the same `/VAADIN/push` path, so the location patterns already used for WebSockets match SSE traffic without changes.

What SSE requires instead is that the proxy forwards the response as it arrives. Proxy defaults tend to work against this, because a streaming response gets buffered, possibly compressed, and closed once it has been idle for long enough:

- **Response buffering** is the main obstacle. A buffering proxy waits for the buffer to fill, or for the response to end, before forwarding anything, and an SSE response never ends. In `nginx`, `proxy_buffering` is enabled by default and has to be turned off. In Apache HTTPD, `mod_proxy_http` forwards the backend flushes as they happen, but `mod_proxy_ajp` buffers the response body unless `flushpackets=on` is set on the worker.

- **Compression** doesn’t hold the stream back on its own, since `mod_deflate` and the `nginx` gzip filter both flush per event, but it should still be kept off the push response. See [Compression and Push](#compression-and-push).

- **The HTTP version to the backend** matters for `nginx`, which uses HTTP/1.0 upstream unless `proxy_http_version 1.1` is set.

- **Idle timeouts** close a stream that has been silent, as described in [Push Connection Timeout](#push-connection-timeout).

> **Warning:** A proxy that buffers the push response breaks server push silently. The connection is established, the server writes updates, and the browser receives them much later or not at all, with nothing in the logs to indicate a problem. Atmosphere pads the beginning of the stream with 2000 bytes to flush proxy buffers, but that’s less than the 4 KB default `proxy_buffer_size` of `nginx`, so it doesn’t compensate for a missing `proxy_buffering off`.

In the configuration examples, the SSE-specific configuration is marked with `Server-sent events only` comments: a `(begin)` and `(end)` pair where it consists of several directives, as in the `nginx` examples, and a single comment line where it’s one option added to an existing directive, as with `flushpackets=on` on the AJP workers. Apache HTTPD over HTTP carries no such marker, since its defaults already suit a streaming response, apart from the timeout.

When the proxy terminates TLS, `X-Forwarded-Proto` matters as much as it does for WebSockets, for a different reason. Vaadin builds the push URL from the advertised scheme, and a page served over `https` isn’t allowed to open an `http://` event stream, as the browser blocks it as mixed content. Configure the backend server to honor the forwarded headers, for example with `SERVER_FORWARD_HEADERS_STRATEGY=NATIVE` in a Spring Boot application.

Sticky sessions are required exactly as they are for the other transports. The SSE stream and the client-to-server XHR requests are separate connections that have to reach the same backend node. See [Load Balancing with Sticky Session](#load-balancing-sticky-session).

### <a id="browser-connection-limits"></a>Browser Connection Limits

Browsers allow a limited number of concurrent HTTP/1.1 connections to the same origin, typically six, and an open SSE stream occupies one of them for as long as the tab stays open. WebSocket connections aren’t counted against this pool, so the limit affects SSE and long polling only. With several tabs of the same application open, regular requests can end up queued behind the streams, and the application appears to freeze. Serving the application over HTTP/2 between the browser and the proxy removes the limit. The connection between the proxy and the backend server can stay on HTTP/1.1.

### <a id="hilla-reactive-endpoints"></a>Hilla Reactive Endpoints

Browser-callable Hilla methods that return a `Flux` use a separate push connection on the `/HILLA/push` path. That connection isn’t controlled by the Flow push configuration and always uses WebSockets. A proxy that can’t carry WebSocket traffic therefore supports Flow server push over SSE, but not Hilla reactive endpoints. Regular browser-callable methods aren’t affected, as they’re plain HTTP requests.

### <a id="verifying-the-push-transport"></a>Verifying the Push Transport

The push endpoint can be tested without a browser, by requesting it the way the client does:

```bash
curl -N -i "http://proxy/VAADIN/push?v-r=push&X-Atmosphere-Transport=sse&X-Atmosphere-tracking-id=0"
```

A correctly configured proxy responds with `200` and a `Content-Type: text/event-stream` header, and the response stays open. Compare the output with the same request sent directly to the backend server: if the proxy delivers in bursts what the backend server delivers gradually, the response is being buffered.

In the browser development tools, the push request must show the `text/event-stream` content type and remain pending while data arrives. Verifying that the application behaves correctly isn’t sufficient, because Atmosphere falls back to another transport when the selected one fails, and the application keeps working. To make such a failure visible while testing a proxy configuration, set the fallback transport to the same value as the transport.

Selecting a WebSocket transport against a proxy that’s configured for SSE only fails with `Error during WebSocket handshake: Unexpected response code: 501`. The `Upgrade` and `Connection` headers are hop-by-hop headers that the proxy doesn’t forward, so the backend server receives a request that claims the WebSocket transport without an actual upgrade, and rejects it. This is the expected behavior rather than a misconfiguration.

## <a id="compression-and-push"></a>Compression and Push

Compression can be left enabled, as long as it doesn’t apply to a streamed response. Vaadin `PUSH` answers with the `text/event-stream` content type over Server-Sent Events, and with `text/plain` over streaming and long polling. Keeping both out of the list of compressed content types is what matters, and it’s more dependable than excluding a path, since it holds wherever the push endpoint is mapped.

Apache HTTPD

```
<IfModule deflate_module>
    AddOutputFilterByType DEFLATE text/html text/css text/xml \
                                  application/javascript application/json \
                                  application/xml image/svg+xml

    # For paths whose response type isn't known in advance: an EventSource
    # always asks for text/event-stream, and the push endpoints stream
    # whatever they answer with.
    SetEnvIfNoCase Accept      text/event-stream      no-gzip=1
    SetEnvIfNoCase Request_URI "/(VAADIN|HILLA)/push" no-gzip=1
</IfModule>
```

`nginx`

```
gzip              on;
gzip_proxied      any;
gzip_types        text/css application/javascript application/json
                  application/xml image/svg+xml;
```

Avoid a wildcard such as `AddOutputFilterByType DEFLATE text/*`, which matches both of the content types used by push. In `nginx`, `text/html` is always compressed when `gzip` is enabled and can’t be removed from `gzip_types`, which is harmless here because a push response is never `text/html`.

Compressing the stream doesn’t stall it, because both compressors flush after each event, so the updates still arrive one at a time as long as `proxy_buffering off` or `flushpackets=on` is in place. It’s worth avoiding for two other reasons:

- A compression context stays allocated for the whole life of every push connection, which is the whole life of every open browser tab.

- The 2000 bytes of padding that Atmosphere writes when a stream opens exist to force a buffering intermediary to flush. Compressed, those 2000 identical characters shrink to a few dozen bytes and stop serving that purpose, so the deployment breaks as soon as another buffering hop appears in front of the proxy.

## <a id="deployment-scenarios"></a>Deployment Scenarios

The next sections provide configuration examples covering the following deployment scenarios:

| Scenario                                                          | Public URL            | Internal Vaadin Application URL                         |
| ----------------------------------------------------------------- | --------------------- | ------------------------------------------------------- |
| Web Server and Vaadin application on root context.                | `http(s)://proxy/`    | `http://vaadin-app:8080`                                |
| Web Server and Vaadin application on a sub context.               | `http(s)://proxy/app` | `http://vaadin-app:8080/app`                            |
| Web Server on root context and Vaadin application on sub context. | `http(s)://proxy/`    | `http://vaadin-app:8080/app`                            |
| Web Server on sub context and Vaadin application on root context. | `http(s)://proxy/app` | `http://vaadin-app:8080/`                               |
| Load Balancing with Sticky Session.                               | `http(s)://proxy/`    | `http://vaadin-app-1:8080/`,`http://vaadin-app-2:8080/` |

All the scenarios assume the Vaadin application is built for production and `PUSH` communication is enabled. Configuration that applies only to a specific push transport is marked with `Websockets only` and `Server-sent events only` comments. It’s usually better to deploy the application on the backend server at the same path as the proxy rather than to take this approach, to avoid potential issues with URLs sent back to the client as HTTP headers or in the response body.

### <a id="web-server-vaadin-on-root-context"></a>Web Server & Vaadin on Root Context

This is the most straightforward scenario, where a backend application served on the root context is published as-is on the internet, meaning that the browser requests to `http(s)://proxy/` are forwarded to `http://vaadin-app:8080`.

Apache HTTPD

```
<Location />
    ProxyPass         http://vaadin-app:8080/ upgrade=websocket
    ProxyPassReverse  http://vaadin-app:8080/
</Location>
```

Apache HTTPD - AJP

```
# -- Websockets only (begin)
RewriteEngine on
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule ^/?(.*) "ws://vaadin-app:8080/$1" [P,L]
# -- Websockets only (end)

# -- Server-sent events only: flushpackets=on prevents mod_proxy_ajp from buffering the push stream
ProxyPass   "/"  ajp://vaadin-app:8009/ secret=${VAADIN_APP_AJP_SECRET} flushpackets=on
```

`nginx`

```
# -- Websockets only (begin)
map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
}
# -- Websockets only (end)

server {

    listen       80;
    server_name  localhost;

    location / {
        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;

        # -- Websockets only (begin)
        proxy_set_header        Upgrade $http_upgrade;
        proxy_set_header        Connection $connection_upgrade;
        # -- Websockets only (end)

        # -- Server-sent events only (begin)
        proxy_http_version      1.1;
        proxy_buffering         off;
        proxy_read_timeout      300s;
        # -- Server-sent events only (end)

        proxy_pass http://vaadin-app:8080/;
        proxy_redirect $scheme://$host/ $scheme://$http_host/;

    }
}
```

As an alternative, the push endpoint can be given its own configuration, instead of applying the same settings to the whole application. This setup requires dedicated rules for both the Flow and the Hilla push endpoints. With WebSockets, it limits the protocol upgrade to those paths. With Server-Sent Events, it limits the cost of disabled response buffering to the streaming response.

Apache HTTPD

```
# -- Websockets only (begin)
# Must be declared before the catch-all rule, see the warning on mod_proxy worker sharing.
<Location /VAADIN/push>
    ProxyPass               http://vaadin-app:8080/VAADIN/push upgrade=websocket
</Location>

<Location /HILLA/push>
    ProxyPass               http://vaadin-app:8080/HILLA/push upgrade=websocket
</Location>
# -- Websockets only (end)

<Location />
    ProxyPass               http://vaadin-app:8080/
    ProxyPassReverse        http://vaadin-app:8080/
</Location>
```

Apache HTTPD - AJP

```
# -- Websockets only (begin)
<Location /VAADIN/push>
    ProxyPass               ws://vaadin-app:8080/VAADIN/push
</Location>

<Location /HILLA/push>
    ProxyPass               ws://vaadin-app:8080/HILLA/push
</Location>
# -- Websockets only (end)

<Location />
    # -- Server-sent events only: flushpackets=on prevents mod_proxy_ajp from buffering the push stream
    ProxyPass               ajp://vaadin-app:8009/ secret=${VAADIN_APP_AJP_SECRET} flushpackets=on
</Location>
```

`nginx`

```
# -- Websockets only (begin)
map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
}
# -- Websockets only (end)

server {

    listen       80;
    server_name  localhost;

    location / {
        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_pass http://vaadin-app:8080/;
        proxy_redirect $scheme://$host/ $scheme://$http_host/;
    }

    location ~* "(/VAADIN|HILLA)/push" {
        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;
        # -- Websockets only (begin)
        proxy_set_header        Upgrade $http_upgrade;
        proxy_set_header        Connection $connection_upgrade;
        # -- Websockets only (end)

        # -- Server-sent events only (begin)
        proxy_http_version      1.1;
        proxy_buffering         off;
        proxy_read_timeout      300s;
        # -- Server-sent events only (end)

        proxy_pass http://vaadin-app:8080;
    }
}
```

### <a id="web-server-vaadin-on-sub-context"></a>Web Server & Vaadin on Sub-Context

Similar to the previous scenario, but the Vaadin application is reachable on the same sub path on both the reverse proxy and the backend server. In this case `http(s)://proxy/app/` forwards to `http://vaadin-app:8080/app/`.

Apache HTTPD

```
<Location /app/>
    ProxyPass         http://vaadin-app:8080/app/ upgrade=websocket
    ProxyPassReverse  http://vaadin-app:8080/app/
</Location>
```

Apache HTTPD - AJP

```
# -- Websockets only (begin)
RewriteEngine on
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule ^/app/(.*) "ws://vaadin-app:8080/app/$1" [P,L]
# -- Websockets only (end)

# -- Server-sent events only: flushpackets=on prevents mod_proxy_ajp from buffering the push stream
ProxyPass   "/app/"      ajp://vaadin-app:8009/app/ secret=${VAADIN_APP_AJP_SECRET} flushpackets=on
```

`nginx`

```
# -- Websockets only (begin)
map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
}
# -- Websockets only (end)

server {

    listen       80;
    server_name  localhost;

    location /app/ {
        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;

        # -- Websockets only (begin)
        proxy_set_header        Upgrade $http_upgrade;
        proxy_set_header        Connection $connection_upgrade;
        # -- Websockets only (end)

        # -- Server-sent events only (begin)
        proxy_http_version      1.1;
        proxy_buffering         off;
        proxy_read_timeout      300s;
        # -- Server-sent events only (end)

        proxy_pass http://vaadin-app:8080/app/;
        proxy_redirect $scheme://$host/ $scheme://$http_host/;

    }
}
```

As an alternative, the push endpoint can be given its own configuration, instead of applying the same settings to the whole application. This setup requires dedicated rules for both the Flow and the Hilla push endpoints. With WebSockets, it limits the protocol upgrade to those paths. With Server-Sent Events, it limits the cost of disabled response buffering to the streaming response.

Apache HTTPD

```
# -- Websockets only (begin)
# Must be declared before the catch-all rule, see the warning on mod_proxy worker sharing.
<Location /app/VAADIN/push>
    ProxyPass               http://vaadin-app:8080/app/VAADIN/push upgrade=websocket
</Location>

<Location /app/HILLA/push>
    ProxyPass               http://vaadin-app:8080/app/HILLA/push upgrade=websocket
</Location>
# -- Websockets only (end)

<Location /app/>
    ProxyPass               http://vaadin-app:8080/app/
    ProxyPassReverse        http://vaadin-app:8080/app/
</Location>
```

Apache HTTPD - AJP

```
# -- Websockets only (begin)
<Location /app/VAADIN/push>
    ProxyPass               ws://vaadin-app:8080/app/VAADIN/push
</Location>

<Location /app/HILLA/push>
    ProxyPass               ws://vaadin-app:8080/app/HILLA/push
</Location>
# -- Websockets only (end)

<Location /app/>
    # -- Server-sent events only: flushpackets=on prevents mod_proxy_ajp from buffering the push stream
    ProxyPass               ajp://vaadin-app:8009/app/ secret=${VAADIN_APP_AJP_SECRET} flushpackets=on
</Location>
```

`nginx`

```
# -- Websockets only (begin)
map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
}
# -- Websockets only (end)

server {

    listen       80;
    server_name  localhost;

    location /app/ {
        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_pass http://vaadin-app:8080/;
        proxy_redirect $scheme://$host/ $scheme://$http_host/app/;
    }

    location ~* ^/(VAADIN|HILLA)/push$ {

        rewrite ^/(.*) /app/$1 break;

        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;
        # -- Websockets only (begin)
        proxy_set_header        Upgrade $http_upgrade;
        proxy_set_header        Connection $connection_upgrade;
        # -- Websockets only (end)

        # -- Server-sent events only (begin)
        proxy_http_version      1.1;
        proxy_buffering         off;
        proxy_read_timeout      300s;
        # -- Server-sent events only (end)

        proxy_pass http://vaadin-app:8080;
    }
}
```

### <a id="web-server-on-root-context-vaadin-on-sub-context"></a>Web Server on Root Context & Vaadin on Sub-Context

In this scenario the backend application is published on a sub context, but the proxy is reachable on the root context. Therefore, a request to `http(s)://proxy/` is forwarded to `http://vaadin-app/app/`. Since paths don’t match, the reverse proxy must also rewrite the cookie paths.

Apache HTTPD

```
<Location />
    ProxyPass         "http://vaadin-app:8080/app/" upgrade=websocket
    ProxyPassReverse  "http://vaadin-app:8080/app/"
    ProxyPassReverseCookiePath "/app" "/"
</Location>
```

Apache HTTPD - AJP

```
# -- Websockets only (begin)
RewriteEngine on
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule ^/(.*) "ws://vaadin-app:8080/app/$1" [P,L]
# -- Websockets only (end)

# -- Server-sent events only: flushpackets=on prevents mod_proxy_ajp from buffering the push stream
ProxyPass         "/"      ajp://vaadin-app:8009/app/ secret=${VAADIN_APP_AJP_SECRET} flushpackets=on
ProxyPassReverse  "/"      "/app/"
ProxyPassReverseCookiePath "/app" "/"
```

`nginx`

```
# -- Websockets only (begin)
map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
}
# -- Websockets only (end)

server {

    listen       80;
    server_name  localhost;

    location / {
        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;

        # -- Websockets only (begin)
        proxy_set_header        Upgrade $http_upgrade;
        proxy_set_header        Connection $connection_upgrade;
        # -- Websockets only (end)

        # -- Server-sent events only (begin)
        proxy_http_version      1.1;
        proxy_buffering         off;
        proxy_read_timeout      300s;
        # -- Server-sent events only (end)

        proxy_pass http://vaadin-app:8080/app/;
        proxy_redirect $scheme://$host/app/ $scheme://$http_host/;
        proxy_cookie_path /app /;

    }
}
```

As an alternative, the push endpoint can be given its own configuration, instead of applying the same settings to the whole application. This setup requires dedicated rules for both the Flow and the Hilla push endpoints. With WebSockets, it limits the protocol upgrade to those paths. With Server-Sent Events, it limits the cost of disabled response buffering to the streaming response.

Apache HTTPD

```
# -- Websockets only (begin)
# Must be declared before the catch-all rule, see the warning on mod_proxy worker sharing.
<Location /VAADIN/push>
    ProxyPass "http://vaadin-app:8080/app/VAADIN/push" upgrade=websocket
</Location>

<Location /HILLA/push>
    ProxyPass "http://vaadin-app:8080/app/HILLA/push" upgrade=websocket
</Location>
# -- Websockets only (end)

<Location />
    ProxyPass                  "http://vaadin-app:8080/app/"
    ProxyPassReverse           "http://vaadin-app:8080/app/"
    ProxyPassReverseCookiePath "/app" "/"
</Location>
```

Apache HTTPD - AJP

```
# -- Websockets only (begin)
<Location /VAADIN/push>
    ProxyPass               ws://vaadin-app:8080/app/VAADIN/push
</Location>

<Location /HILLA/push>
    ProxyPass               ws://vaadin-app:8080/app/HILLA/push
</Location>
# -- Websockets only (end)

<Location / >
    # -- Server-sent events only: flushpackets=on prevents mod_proxy_ajp from buffering the push stream
    ProxyPass         ajp://vaadin-app:8009/app/ secret=${VAADIN_APP_AJP_SECRET} flushpackets=on
    ProxyPassReverse  "/app/"
    ProxyPassReverseCookiePath "/app" "/"
</Location>
```

`nginx`

```
# -- Websockets only (begin)
map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
}
# -- Websockets only (end)

server {

    listen       80;
    server_name  localhost;

    location / {
        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_pass http://vaadin-app:8080/app/;
        proxy_redirect $scheme://$host/app/ $scheme://$http_host/;
        proxy_cookie_path /app /;
    }

    location ~* ^/(VAADIN|HILLA)/push$ {

        rewrite ^/(.*) /app/$1 break;

        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;
        # -- Websockets only (begin)
        proxy_set_header        Upgrade $http_upgrade;
        proxy_set_header        Connection $connection_upgrade;
        # -- Websockets only (end)

        # -- Server-sent events only (begin)
        proxy_http_version      1.1;
        proxy_buffering         off;
        proxy_read_timeout      300s;
        # -- Server-sent events only (end)

        proxy_pass http://vaadin-app:8080;
    }
}
```

### <a id="web-server-on-sub-context-vaadin-on-root-context"></a>Web Server on Sub-Context & Vaadin on Root Context

This is the opposite of the above scenario. The proxy server exposes the application on a sub context but it forwards the request to the backed server root path, for example `http(s)://proxy/app/` to `http://vaadin-app:8080/`. As in the previous case, the proxy server must rewrite the cookie path.

Apache HTTPD

```
<Location /app/>
    ProxyPass         "http://vaadin-app:8080/" upgrade=websocket
    ProxyPassReverse  "/"
    ProxyPassReverseCookiePath "/" "/app"
</Location>
```

Apache HTTPD - AJP

```
# -- Websockets only (begin)
RewriteEngine on
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule ^/app/(.*) "ws://vaadin-app:8080/$1" [P,L]
# -- Websockets only (end)

<Location /app/ >
    # -- Server-sent events only: flushpackets=on prevents mod_proxy_ajp from buffering the push stream
    ProxyPass         ajp://vaadin-app:8009/ secret=${VAADIN_APP_AJP_SECRET} flushpackets=on
    ProxyPassReverse  "/"
    ProxyPassReverseCookiePath "/" "/app"
</Location>
```

`nginx`

```
# -- Websockets only (begin)
map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
}
# -- Websockets only (end)

server {

    listen       80;
    server_name  localhost;

    location /app/ {
        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;

        # -- Websockets only (begin)
        proxy_set_header        Upgrade $http_upgrade;
        proxy_set_header        Connection $connection_upgrade;
        # -- Websockets only (end)

        # -- Server-sent events only (begin)
        proxy_http_version      1.1;
        proxy_buffering         off;
        proxy_read_timeout      300s;
        # -- Server-sent events only (end)

        proxy_pass http://vaadin-app:8080/;
        proxy_redirect $scheme://$host/ $scheme://$http_host/app/;
        proxy_cookie_path / /app;
    }
}
```

As an alternative, the push endpoint can be given its own configuration, instead of applying the same settings to the whole application. This setup requires dedicated rules for both the Flow and the Hilla push endpoints. With WebSockets, it limits the protocol upgrade to those paths. With Server-Sent Events, it limits the cost of disabled response buffering to the streaming response.

Apache HTTPD

```
# -- Websockets only (begin)
# Must be declared before the catch-all rule, see the warning on mod_proxy worker sharing.
<Location /app/VAADIN/push>
    ProxyPass "http://vaadin-app:8080/VAADIN/push" upgrade=websocket
</Location>

<Location /app/HILLA/push>
    ProxyPass "http://vaadin-app:8080/HILLA/push" upgrade=websocket
</Location>
# -- Websockets only (end)

<Location /app/>
    ProxyPass                  "http://vaadin-app:8080/"
    ProxyPassReverse           "/"
    ProxyPassReverseCookiePath "/" "/app"
</Location>
```

Apache HTTPD - AJP

```
# -- Websockets only (begin)
<Location /app/VAADIN/push>
    ProxyPass               ws://vaadin-app:8080/VAADIN/push
</Location>

<Location /app/HILLA/push>
    ProxyPass               ws://vaadin-app:8080/HILLA/push
</Location>
# -- Websockets only (end)

<Location /app/ >
    # -- Server-sent events only: flushpackets=on prevents mod_proxy_ajp from buffering the push stream
    ProxyPass         ajp://vaadin-app:8009/ secret=${VAADIN_APP_AJP_SECRET} flushpackets=on
    ProxyPassReverse  "/"
    ProxyPassReverseCookiePath "/" "/app"
</Location>
```

`nginx`

```
# -- Websockets only (begin)
map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
}
# -- Websockets only (end)

server {

    listen       80;
    server_name  localhost;

    location /app/ {
        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_pass http://vaadin-app:8080/;
        proxy_redirect $scheme://$host/ $scheme://$http_host/app/;
        proxy_cookie_path /app /;
    }

    location ~* ^/app/(VAADIN|HILLA)/push$ {

        rewrite ^/app/(.*)      /$1 break;

        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;
        # -- Websockets only (begin)
        proxy_set_header        Upgrade $http_upgrade;
        proxy_set_header        Connection $connection_upgrade;
        # -- Websockets only (end)

        # -- Server-sent events only (begin)
        proxy_http_version      1.1;
        proxy_buffering         off;
        proxy_read_timeout      300s;
        # -- Server-sent events only (end)

        proxy_pass http://vaadin-app:8080;
    }
}
```

### <a id="load-balancing-sticky-session"></a>Load Balancing with Sticky Session

Load balancing is a critical mechanism for ensuring high availability, scalability, and fault tolerance in web applications. By distributing incoming client requests across multiple backend servers, load balancing improves application responsiveness and prevents any single server from becoming a bottleneck.

For Vaadin applications, which maintain long-lived user sessions due to their stateful nature, implementing load balancing with sticky sessions becomes essential. Sticky sessions, also known as session affinity, ensure that each user’s requests are consistently routed to the same backend server, preserving the application state and avoiding issues caused by session deserialization across servers.

For Apache HTTPD, you need to load the [`mod_proxy_balancer`](https://httpd.apache.org/docs/2.4/mod/mod_proxy_balancer.html) module and at least one module providing a scheduler algorithm. The example in this guide use [`mod_lbmethod_byrequests`](https://httpd.apache.org/docs/2.4/mod/mod_lbmethod_byrequests.html) that distributes the requests among the various workers to ensure that each gets their configured share of the number of requests.

Depending on the Apache server global setup, you may need to load also [`mod_slotmem_shm`](https://httpd.apache.org/docs/2.4/mod/mod_slotmem_shm.html), used internally by other modules.

```
LoadModule slotmem_shm_module modules/mod_slotmem_shm.so
LoadModule proxy_balancer_module modules/mod_proxy_balancer.so
LoadModule lbmethod_byrequests_module modules/mod_lbmethod_byrequests.so
```

Sticky sessions are managed using a custom `ROUTEID` cookie, simplifying configuration and ensuring proper session affinity without relying on backend modifications like adding a `jvmRoute` to Tomcat configuration.

For `nginx`, cookie based [sticky](https://nginx.org/en/docs/http/ngx_http_upstream_module.html#sticky) sessions are available in the open-source version since nginx 1.29.6. In earlier versions, the `sticky` directive was available only as part of the commercial subscription.

If you’re running an nginx version older than 1.29.6, you can use the [`ip_hash`](https://nginx.org/en/docs/http/ngx_http_upstream_module.html#ip_hash) directive instead. It uses the client IP address as a hashing key to determine which server in a server group should be selected for the client requests. The main drawback of the `ip_hash` approach is that it doesn’t work well for clients behind proxies or NAT, since many clients share the same IP.

Apache HTTPD

```
<Proxy "balancer://application-balancer/">
    BalancerMember "http://vaadin-app-1:8080" route=1 upgrade=websocket
    BalancerMember "http://vaadin-app-2:8080" route=2 upgrade=websocket
    ProxySet stickysession=ROUTEID
    ProxySet lbmethod=byrequests
</Proxy>

<Location / >
    # Adding a cookie for session affinity instead of backend JSESSIONID because:
    # - additional configuration required on the backend server to add the route id
    #   in the cookie value (e.g. jvmRoute for Tomcat)
    # - The backend cookie might not be set on the very first request, causing unexpected behaviors
    Header add Set-Cookie "ROUTEID=.%{BALANCER_WORKER_ROUTE}e; path=/; HttpOnly" env=BALANCER_ROUTE_CHANGED

    ProxyPass         "balancer://application-balancer/"
    ProxyPassReverse  "balancer://application-balancer/"
</Location>
```

Apache HTTPD - AJP

```
<Proxy "balancer://application-balancer/">
    # -- Server-sent events only: flushpackets=on prevents mod_proxy_ajp from buffering the push stream
    BalancerMember "ajp://vaadin-app-1:8009" route=1 flushpackets=on
    BalancerMember "ajp://vaadin-app-2:8009" route=2 flushpackets=on
    ProxySet stickysession=ROUTEID
    ProxySet lbmethod=byrequests
</Proxy>

# -- Websockets only (begin)
<Proxy "balancer://application-websocket-balancer/">
    BalancerMember "ws://vaadin-app-1:8080" route=1
    BalancerMember "ws://vaadin-app-2:8080" route=2
    ProxySet stickysession=ROUTEID
    ProxySet lbmethod=byrequests
</Proxy>

RewriteEngine on
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule ^/?(.*) "balancer://application-websocket-balancer/$1" [P,L]
# -- Websockets only (end)

# Adding a cookie for session affinity instead of backend JSESSIONID because:
# - additional configuration required on the backend server to add the route id
#   in the cookie value (e.g. jvmRoute for Tomcat)
# - The backend cookie might not be set on the very first request, causing unexpected behaviors
Header add Set-Cookie "ROUTEID=.%{BALANCER_WORKER_ROUTE}e; path=/; HttpOnly" env=BALANCER_ROUTE_CHANGED

ProxyPass         / "balancer://application-balancer/"
ProxyPassReverse  / "balancer://application-balancer/"
```

`nginx`

```
# -- Websockets only (begin)
map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
}
# -- Websockets only (end)

upstream application_balancer {
    server vaadin-app-1:8080;
    server vaadin-app-2:8080;
    # Cookie based sticky sessions, available in open-source nginx since 1.29.6.
    # On older versions, replace the line below with `ip_hash;`.
    sticky cookie ROUTEID path=/ httponly;
}

server {

    listen       80;
    server_name  localhost;

    location / {
        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;

        # -- Websockets only (begin)
        proxy_set_header        Upgrade $http_upgrade;
        proxy_set_header        Connection $connection_upgrade;
        # -- Websockets only (end)

        # -- Server-sent events only (begin)
        proxy_http_version      1.1;
        proxy_buffering         off;
        proxy_read_timeout      300s;
        # -- Server-sent events only (end)

        proxy_pass http://application_balancer/;
        proxy_redirect $scheme://$host/ $scheme://$http_host/;
    }
}
```

## <a id="proxying-multiple-backend-vaadin-application"></a>Proxying Multiple Backend Vaadin Application

All proposed configurations can be applied when the reverse proxy exposes multiple backend Vaadin applications. In a similar setup, it’s important that all backend applications define different cookie names, otherwise the proxy overwrites the same cookie with different values, preventing the Vaadin applications from working correctly.

In a Spring Boot application, the cookie name can be set with the `server.servlet.session.cookie.name` property. Another possibility is to set programmatically the name in a Servlet listener by getting the `SessionCookieConfig` instance from the `ServletContext` and use the `setName(String)` method to change cookie name.

## <a id="push-connection-timeout"></a>Push Connection Timeout

By default, the push connection is closed if the proxied server doesn’t transmit any data within sixty seconds. Vaadin `PUSH` is configured to send a heartbeat message every sixty seconds, so the connection should not be closed. If the default is not working correctly, the timeout can be increased in both Apache HTTPD and `nginx` by applying the appropriate configuration.

The default of sixty seconds is exactly the heartbeat interval, which leaves no margin at all. For this reason, the examples raise it to three hundred seconds.

The timeout applies to every push transport, but the consequences differ. A WebSocket or long polling connection that the proxy closes is re-established by Atmosphere. A Server-Sent Events stream is reopened by the browser instead, which issues a new server request each time and can lose messages in between, making the problem easy to overlook.

Apache HTTPD

```
ProxyPass / http://vaadin-app:8080/ upgrade=websocket timeout=300

# In alternative, use ProxyTimeout directive
# ProxyTimeout 300
```

`nginx`

```
proxy_read_timeout 300s;
```

In `nginx`, a directive set in an enclosing block applies only when the same directive isn’t set at a more specific level. When the push endpoint has a `location` block of its own, set `proxy_read_timeout` inside that block.

`0C8F77AE-16A8-463B-8F43-1C9F3A7DF1E2`
