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:
- Serve HTTPS on the host in
APP_URLand forward tohttp://127.0.0.1:3000. - Pass
Hostand theX-Forwarded-Proto/X-Forwarded-Hostheaders. - Append the client's address to
X-Forwarded-For(not replace it with something the client sent). - Allow request bodies a bit larger than the upload chunk size (default 8 MB).
- 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;
}
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.
| Setup | TRUST_PROXY_HOPS |
|---|---|
| One proxy (Caddy, nginx or Traefik) in front of the app | 1 (default) |
| A CDN or load balancer plus your own proxy | 2 |
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).