Skip to main content

Reverse proxy and TLS

The app listens on plain HTTP, port 3000. Put a reverse proxy in front of it that terminates TLS (HTTPS) on your domain. Don't expose port 3000 to the internet directly — the proxy is what makes client addresses trustworthy.

What every proxy must do:

  1. Serve HTTPS on the host in APP_URL and forward to http://127.0.0.1:3000.
  2. Pass Host and the X-Forwarded-Proto / X-Forwarded-Host headers.
  3. Append the client's address to X-Forwarded-For (not replace it with something the client sent).
  4. Allow request bodies a bit larger than the upload chunk size (default 8 MB).
  5. Allow long-lived responses for downloads and exports (no short read timeouts).

Caddy​

Caddy gets and renews certificates by itself and sets the forwarding headers by default.

flow.example.com {
encode zstd gzip
reverse_proxy 127.0.0.1:3000
}

Caddy has no request body limit by default.

nginx​

server {
listen 443 ssl;
http2 on;
server_name flow.example.com;

ssl_certificate /etc/letsencrypt/live/flow.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/flow.example.com/privkey.pem;

# uploads arrive in chunks (default 8 MB): allow a bit more, and stream them through
client_max_body_size 16m;
proxy_request_buffering off;

location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 300s;
}
}

server {
listen 80;
server_name flow.example.com;
return 301 https://$host$request_uri;
}
nginx's default body limit is 1 MB

Without client_max_body_size, uploads fail as soon as a chunk is larger than 1 MB. If you turn chunking off in Admin → General → Uploads, the limit must be above the largest file you allow.

Traefik​

With the Docker provider, add labels to the app service (and remove its ports: mapping):

labels:
- traefik.enable=true
- traefik.http.routers.sol2flow.rule=Host(`flow.example.com`)
- traefik.http.routers.sol2flow.entrypoints=websecure
- traefik.http.routers.sol2flow.tls.certresolver=letsencrypt
- traefik.http.services.sol2flow.loadbalancer.server.port=3000

Traefik appends to X-Forwarded-For and has no body limit by default.

Client IP addresses​

sol2flow uses the client's IP address for rate limits (for example against password guessing) and shows it in the sessions list. Each proxy appends the address it received the request from to X-Forwarded-For; sol2flow takes the entry TRUST_PROXY_HOPS places from the right. Entries further left come from the client and can be forged.

SetupTRUST_PROXY_HOPS
One proxy (Caddy, nginx or Traefik) in front of the app1 (default)
A CDN or load balancer plus your own proxy2

If the value is too low, everyone appears to come from your proxy's address and shares one rate limit. If it is too high, clients can pick their own address.

Sub-paths​

sol2flow must run at the root of its own host name (https://flow.example.com/), not under a path such as https://example.com/flow/.

After changing the domain​

APP_URL must match the new address. Two things are tied to the host name:

  • Passkeys only work on the host they were created for: people have to add them again after a domain change (they can still sign in with their password or single sign-on).
  • Single sign-on: update the redirect URI registered with each identity provider (shown in Admin → Authentication).