docs/operations/reverse-proxy.md
RustFS speaks plain S3 over HTTP/1.1 and HTTP/2 and works behind reverse
proxies (Caddy, Nginx, HAProxy) and CDNs (Cloudflare). Most proxy problems are
not RustFS storage bugs — the same request sent directly to :9000
succeeds, while the proxied request fails. This page documents the request
semantics RustFS expects from the proxy layer and gives known-good
configurations.
Rule of thumb: if a request works against
http://<host>:9000directly but fails through the proxy, the fault is in the proxy/CDN request forwarding, not in RustFS object handling. Use the checklist below to find which forwarding behavior broke.
S3 clients sign requests with AWS SigV4. RustFS (via s3s) re-derives the
signature from the forwarded request, and streams the request body to storage.
For this to succeed the proxy must forward the request byte-for-byte with
respect to the signed material and the body:
Content-Length: N, exactly N body bytes
must reach RustFS. If fewer bytes arrive, RustFS waits for the rest per the
HTTP spec and the request appears to hang until the client aborts.Host and any x-amz-* / signed headers
must reach RustFS unchanged. Rewriting Host is fine only if the client
signed with that same host.Content-Length; avoid re-chunking large bodies. Some CDNs
drop Content-Length and switch to Transfer-Encoding: chunked, or buffer
the whole request body before forwarding — both change the timing and
framing RustFS sees.ETag from responses (breaks multipart completion).socket hang up on writesRustFS closes idle upstream HTTP/1.1 keep-alive connections after
RUSTFS_HTTP1_HEADER_READ_TIMEOUT seconds (default 75s; see
crates/config/src/constants/tls.rs). Reverse proxies keep a pool of upstream
connections and reuse them. If the proxy's upstream idle-keepalive window is
longer than RustFS's timeout, the proxy can pick a connection that RustFS
has already FIN'd, write a request onto the dead socket, and the client sees:
TimeoutError: socket hang up # ECONNRESET
AbortError: Request aborted
This is most visible on large PutObject uploads because:
PUT is non-idempotent, so proxies will not transparently retry it; andPick either side; doing both is safest:
RUSTFS_HTTP1_HEADER_READ_TIMEOUT (default 75s) above
the proxy's upstream idle-keepalive. To harden slowloris protection on a
directly-exposed node instead, lower it — but then also lower the proxy
keepalive below it.your-domain.example.com {
reverse_proxy http://127.0.0.1:9000 {
transport http {
# Talk HTTP/1.1 to RustFS.
versions 1.1
# Keep the proxy's upstream idle-keepalive BELOW RustFS's
# RUSTFS_HTTP1_HEADER_READ_TIMEOUT (default 75s) so Caddy never
# reuses a connection RustFS already closed. Set to 0 to disable
# upstream keep-alive entirely (simplest, slightly less efficient).
keepalive 30s
keepalive_idle_conns_per_host 0
# Never let the proxy compress/transform the request body.
compression off
# Generous timeouts for multi-MB single-request PUTs.
dial_timeout 30s
read_timeout 300s
write_timeout 300s
}
# Forward the body untouched; do not negotiate compression upstream.
header_up Accept-Encoding identity
# Preserve the host the client signed with.
header_up Host {upstream_hostport}
# Stream immediately instead of buffering.
flush_interval -1
}
}
location / {
proxy_pass http://127.0.0.1:9000;
proxy_http_version 1.1;
# Nginx default upstream keepalive is 60s; keep it under RustFS's 75s.
# (set `keepalive` in the matching `upstream {}` block)
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header Accept-Encoding "identity";
# Do not buffer/limit large uploads.
proxy_request_buffering off;
client_max_body_size 0;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
Cloudflare's proxy (orange cloud) may buffer the entire request body before
forwarding, and can rewrite requests to Transfer-Encoding: chunked, dropping
the client's Content-Length. Symptoms match this pattern exactly: tiny uploads
succeed, larger uploads fail with socket hang up.
Accept-Encoding: identity so nothing in the path negotiates
compression (see issues #609, #1492).Content-Length reaches RustFS; disable chunked re-encoding in tunnel
settings (see issue #934).Run each step and note where behavior diverges:
http://<host>:9000
directly. Success here confirms the fault is in the proxy/CDN path.RUSTFS_HTTP_REQUEST_BODY_READ_TIMEOUT (default 300s; 0 disables) and, on
timeout, logs a put_object_body_read_stalled event with the
received/expected byte counts — grep the server log for it to confirm a
truncated-body forwarding problem.Content-Length body
bytes with no compression/transformation.Host and x-amz-* headers must reach
RustFS unchanged; a SignatureDoesNotMatch (rather than a hang) points here.Accept-Encoding)SignatureDoesNotMatch on Cloudflare tunnel (Accept-Encoding)Content-Length)ETag stripping)