Migrate from NGINX
This page moves an NGINX configuration that proxies HTTP to backends: server blocks with location, proxy_pass, TLS, authentication and limits. ReverseDock does not serve static files or cache responses in v0.1; keep a small web server as a backend for files.
The NGINX facts below come from the official module documentation at nginx.org (checked 2026-10-27): core, proxy, upstream, realip, limit_req, auth_basic and WebSocket proxying.
Concept map
Section titled “Concept map”| NGINX | ReverseDock |
|---|---|
listen in a server block |
listeners (reference): one per address, shared by every site |
server_name |
Host(`…`) in the route’s match |
location |
the path part of match (Path, PathPrefix, PathRegexp) and priority |
proxy_pass |
the route’s service |
upstream with server … weight=N |
services.<name>.backends with weight |
proxy_pass https://…, proxy_ssl_trusted_certificate, proxy_ssl_name |
scheme: https, tls.ca_file, tls.server_name |
ssl_certificate, ssl_certificate_key |
certificates (chosen by SNI, no per-site reference needed) |
ssl_protocols |
listeners.<name>.tls.policy: intermediate (TLS 1.2 and 1.3) or modern (TLS 1.3) |
return 301 https://$host$request_uri |
automatic (defaults.redirect_http_to_https) |
return 301 …, rewrite … redirect |
redirect middleware |
rewrite … break |
rewrite middleware |
proxy_pass with a URI part |
strip_prefix, add_prefix or replace_path |
proxy_set_header Host $host |
the default (pass_host_header: true) |
proxy_set_header X-Forwarded-For, X-Real-IP, X-Forwarded-Proto |
added automatically |
other proxy_set_header, add_header |
headers middleware (request.set, response.set, hsts) |
auth_basic, auth_basic_user_file |
basic_auth (realm, users_file) |
auth_request |
forward_auth |
allow, deny |
ip_allow, ip_deny |
limit_req_zone, limit_req |
rate_limit |
gzip |
compress |
error_page |
errors |
client_max_body_size |
security.limits.max_body_bytes (one value for all routes) |
large_client_header_buffers |
security.limits.max_header_bytes, max_headers, max_uri_bytes |
client_header_timeout, keepalive_timeout |
listeners.<name>.timeouts.request_header, idle |
proxy_connect_timeout, proxy_read_timeout, proxy_send_timeout |
services.<name>.timeouts or routes.<name>.timeouts (connect, read, write) |
set_real_ip_from, real_ip_header |
security.trusted_proxies, security.client_ip_header |
access_log |
observability.access_log |
nginx -t |
rdock validate |
nginx -s reload |
automatic on file change, or rdock reload, or SIGHUP |
stream module |
tcp_routes planned for v0.6 |
Five common setups
Section titled “Five common setups”Each pair shows the NGINX configuration and a complete rdock.yaml that passes rdock validate --strict.
1. One HTTPS site with an HTTP redirect
Section titled “1. One HTTPS site with an HTTP redirect”server { listen 80; server_name app.example.com; return 301 https://$host$request_uri;}
server { listen 443 ssl; server_name app.example.com; ssl_certificate /etc/nginx/certs/example.pem; ssl_certificate_key /etc/nginx/certs/example.key; ssl_protocols TLSv1.2 TLSv1.3;
location / { proxy_pass http://10.0.0.5:3000; 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; }}Routes attach to the websecure listener by default, the redirect to HTTPS is automatic, and the forwarding headers are written for you.
listeners: web: { address: ":80" } websecure: { address: ":443", tls: { policy: intermediate } }
certificates: example: cert_file: /etc/rdock/certs/example.pem key_file: /etc/rdock/certs/example.key
routes: app: match: "Host(`app.example.com`)" service: app
services: app: backends: [{ address: "10.0.0.5:3000" }]The automatic redirect answers 301 for GET and 308 for other methods, so a POST keeps its method and body.
2. Several sites and an API under a path
Section titled “2. Several sites and an API under a path”proxy_pass with a URI part replaces the matched location prefix: /api/users reaches the API as /users.
server { listen 443 ssl; server_name www.example.com; ssl_certificate /etc/nginx/certs/example.pem; ssl_certificate_key /etc/nginx/certs/example.key;
location / { proxy_pass http://10.0.0.10:8080; proxy_set_header Host $host; } location /api/ { proxy_pass http://10.0.0.11:9000/; proxy_set_header Host $host; }}
server { listen 443 ssl; server_name blog.example.com; ssl_certificate /etc/nginx/certs/example.pem; ssl_certificate_key /etc/nginx/certs/example.key;
location / { proxy_pass http://10.0.0.12:2368; proxy_set_header Host $host; }}listeners: web: { address: ":80" } websecure: { address: ":443", tls: {} }
certificates: example: cert_file: /etc/rdock/certs/example.pem key_file: /etc/rdock/certs/example.key
routes: www: match: "Host(`www.example.com`)" service: www api: match: "Host(`www.example.com`) && PathPrefix(`/api/`)" service: api middlewares: [strip-api] blog: match: "Host(`blog.example.com`)" service: blog
middlewares: strip-api: { type: strip_prefix, prefixes: ["/api"] }
services: www: backends: [{ address: "10.0.0.10:8080" }] api: backends: [{ address: "10.0.0.11:9000" }] blog: backends: [{ address: "10.0.0.12:2368" }]The api route is tried before www because its rule is longer (see Location matching). strip_prefix removes /api and keeps the slash after it, so the API receives /users, and it adds X-Forwarded-Prefix: /api. One certificate covers both sites because ReverseDock picks it by SNI.
3. Load balancing and WebSockets
Section titled “3. Load balancing and WebSockets”NGINX needs the Upgrade and Connection headers passed explicitly for WebSockets, and proxy_http_version 1.1 before NGINX 1.29.7.
upstream chat { server 10.0.0.30:8080 weight=2; server 10.0.0.31:8080;}
server { listen 443 ssl; server_name chat.example.com; ssl_certificate /etc/nginx/certs/example.pem; ssl_certificate_key /etc/nginx/certs/example.key;
location / { proxy_pass http://chat; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 1h; }}ReverseDock passes WebSocket upgrades without extra settings. Long-lived connections that stay silent need longer read timeouts on both sides: the route’s timeouts.read towards the backend, and the listener’s timeouts.read towards the client.
listeners: web: { address: ":80" } websecure: address: ":443" tls: {} timeouts: { read: 1h }
certificates: example: cert_file: /etc/rdock/certs/example.pem key_file: /etc/rdock/certs/example.key
routes: chat: match: "Host(`chat.example.com`)" service: chat timeouts: { read: 1h }
services: chat: backends: - { address: "10.0.0.30:8080", weight: 2 } - { address: "10.0.0.31:8080" }Both proxies use weighted round-robin by default. NGINX also marks a server unavailable after failed attempts (max_fails 1, fail_timeout 10 s); ReverseDock health checks are planned for v0.4, so until then a backend that is down still receives its share and those requests get 502. See WebSockets, SSE and gRPC.
4. Basic authentication, security headers and an upload limit
Section titled “4. Basic authentication, security headers and an upload limit”server { listen 443 ssl; server_name admin.example.com; ssl_certificate /etc/nginx/certs/example.pem; ssl_certificate_key /etc/nginx/certs/example.key;
client_max_body_size 50m; auth_basic "Admin"; auth_basic_user_file /etc/nginx/admins.htpasswd; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header X-Frame-Options DENY always;
location / { proxy_pass http://10.0.0.20:8000; proxy_set_header Host $host; }}listeners: web: { address: ":80" } websecure: { address: ":443", tls: {} }
certificates: example: cert_file: /etc/rdock/certs/example.pem key_file: /etc/rdock/certs/example.key
security: limits: { max_body_bytes: 50MiB }
routes: admin: match: "Host(`admin.example.com`)" service: admin middlewares: [admin-auth, secure-headers]
middlewares: admin-auth: type: basic_auth users_file: /etc/rdock/admins.htpasswd realm: Admin secure-headers: type: headers hsts: { max_age: 365d, include_subdomains: true } response: set: X-Frame-Options: DENY
services: admin: backends: [{ address: "10.0.0.20:8000" }]max_body_bytes applies to every route, not to one site; requests over it get 413, also when the body is chunked. HSTS from the headers middleware is sent on HTTPS responses only. htpasswd files with Apache MD5 ($apr1$), MD5-crypt ($1$) or {SHA} hashes load with a warning, which --strict turns into an error; other crypt() formats and {SSHA} are refused. Re-create entries with rdock hash --user NAME (see Basic auth).
5. Behind a load balancer: real client IP, allow-list and rate limit
Section titled “5. Behind a load balancer: real client IP, allow-list and rate limit”limit_req_zone $binary_remote_addr zone=api:10m rate=100r/s;
server { listen 443 ssl; server_name api.example.com; ssl_certificate /etc/nginx/certs/example.pem; ssl_certificate_key /etc/nginx/certs/example.key;
set_real_ip_from 10.0.0.0/24; real_ip_header X-Forwarded-For; real_ip_recursive on;
location / { allow 203.0.113.0/24; deny all; limit_req zone=api burst=200 nodelay; proxy_pass http://10.0.0.40:8080; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }}listeners: web: { address: ":80" } websecure: { address: ":443", tls: {} }
certificates: example: cert_file: /etc/rdock/certs/example.pem key_file: /etc/rdock/certs/example.key
security: trusted_proxies: [10.0.0.0/24] client_ip_header: x-forwarded-for
routes: api: match: "Host(`api.example.com`)" service: api middlewares: [office-only, api-limit]
middlewares: office-only: { type: ip_allow, cidrs: [203.0.113.0/24] } api-limit: { type: rate_limit, average: 100, period: 1s, burst: 200 }
services: api: backends: [{ address: "10.0.0.40:8080" }]ReverseDock walks X-Forwarded-For from the right and skips trusted addresses, like real_ip_recursive on. The resolved address is used by ip_allow, rate_limit and the access log. A blocked client gets 403; a client over the limit gets 429 with Retry-After. See Behind a proxy and Rate limiting.
Differences and gotchas
Section titled “Differences and gotchas”Location matching and route priority
Section titled “Location matching and route priority”NGINX selects the longest matching prefix location, then checks regular-expression locations in the order they appear and uses the first that matches; = ends the search on an exact match and ^~ skips the regular expressions.
ReverseDock has no separate prefix and regex passes. Every route has one priority number: priority when set, else the length of the rule text. Routes are tried from the highest number down; ties go to the route name. A request that matches no route gets ReverseDock’s 404 page.
| NGINX | ReverseDock rule |
|---|---|
location = /health |
Path(`/health`) |
location /api/ |
PathPrefix(`/api/`) (a plain string prefix: /api/x but not /api) |
location ^~ /static/ |
PathPrefix(`/static/`) with a priority above your regex routes |
location ~ \.php$ |
PathRegexp(`\.php$`); unanchored, so add ^ or $ as needed |
location ~* \.(png|jpg)$ |
PathRegexp(`(?i)\.(png|jpg)$`) |
server_name a.example.com b.example.com |
Host(`a.example.com`) || Host(`b.example.com`) |
server_name *.example.com |
Host(`*.example.com`) (exactly one label; on HTTPS the route names its certificate with tls: { certificate: <name> } until automatic certificates arrive) |
default_server |
a catch-all route such as PathPrefix(`/`) with priority: 1 (on HTTPS also tls: { certificate: <name> }), and defaults.certificate for TLS clients whose name no certificate covers |
Because a regex location does not automatically win over a prefix in ReverseDock, give routes that must win an explicit priority. Regular expressions use the Rust regex syntax, which has no look-around and no back-references. See Routing and priority and Rules.
Paths sent to the backend
Section titled “Paths sent to the backend”NGINX passes the request URI as the client sent it when proxy_pass has no URI part. ReverseDock normalises the path before routing (unreserved escapes decoded, dot segments and // removed) and forwards the normalised path, so the backend sees exactly the path that was routed. Path middlewares replace the URI part of proxy_pass: strip_prefix for location /api/ { proxy_pass http://b/; }, replace_path for a fixed target, add_prefix for proxy_pass http://b/prefix/ under location /, and rewrite for regular expressions.
proxy_set_header versus automatic headers
Section titled “proxy_set_header versus automatic headers”- Host: NGINX sends
Host: $proxy_host(the upstream’s name) unless you setproxy_set_header Host $host. ReverseDock sends the client’sHostby default. Setpass_host_header: falseon the service to send the backend’s address instead. - Forwarding headers: NGINX adds none unless configured. ReverseDock always writes
X-Forwarded-For(the peer appended),X-Real-IP(the resolved client IP),X-Forwarded-Proto(http,https,wsorwss),X-Forwarded-HostandX-Forwarded-Port, and addsX-Request-ID. From clients that are not insecurity.trusted_proxies, incoming forwarding headers (X-Forwarded-*,X-Real-IP,Forwarded,CF-Connecting-IP, also with_) are removed first, so they cannot be spoofed. - Inheritance: NGINX inherits
proxy_set_headerfrom the outer level only when the inner level sets none. ReverseDock has no levels: list the sameheadersmiddleware on each route that needs it. - Connection and Upgrade: handled for you; WebSocket and HTTP/2 need no header settings.
Limits
Section titled “Limits”| NGINX default | ReverseDock default |
|---|---|
client_max_body_size 1m (413) |
no body limit; set security.limits.max_body_bytes (413) |
large_client_header_buffers 4 8k |
64 KiB of request line and headers, 100 headers (431), 8 KiB request target (414) |
client_header_timeout 60s (408) |
listeners.<name>.timeouts.request_header 10 s |
keepalive_timeout 75s |
listeners.<name>.timeouts.idle 60 s |
proxy_connect_timeout 60s |
services.<name>.timeouts.connect 5 s |
proxy_read_timeout 60s, proxy_send_timeout 60s, between two operations |
read 60 s, write 60 s, also per operation |
The body limit is global in v0.1. An upload endpoint that needs more than the rest of the site raises the limit for every route. See Timeouts and limits and HTTP behaviour.
Other behaviour
Section titled “Other behaviour”- Rate limiting:
limit_reqdelays requests over the rate until the burst is used up and rejects with 503 by default (limit_req_status).rate_limitnever delays: requests over the bucket get 429 andRetry-Afterat once.burstis the bucket size and defaults toaverage. - Buffering: NGINX buffers backend responses by default (
proxy_buffering on). ReverseDock streams them, so server-sent events and gRPC work without extra settings. - Redirects:
redirectdefaults topermanent: false, which answers 302 for GET and 307 for other methods;permanent: truegives 301 and 308. - Strict requests: HTTP/1.1 requests without
Hostget 400,Expectvalues other than100-continueget 417, and bare-LF line endings in request headers close the connection. - No variables or
if: conditions are matchers in the route’smatch(Header,Query,Method,ClientIP), and per-request values come from the middlewares. - Reloads: ReverseDock reloads when
rdock.yamlor a file it refers to changes. A configuration with errors is rejected and the running one stays; at start, a broken file falls back to the last-known-good copy. See Reloads. - Settings for later versions:
rdock validatewarns about settings planned for a later version, such as health checks orleast_conn, andrdock runrefuses them.
Cutover
Section titled “Cutover”Run ReverseDock next to NGINX on other ports, compare the answers, then move the ports. NGINX keeps its configuration, so rolling back means starting it again.
-
Write
rdock.yaml, then validate it:Terminal window rdock validate --strict rdock.yaml -
Copy it to
rdock-test.yaml, change only the listener addresses to free ports (for example:8081and:8444), and start a test instance with its own data directory:Terminal window rdock run --config rdock-test.yaml --data-dir ./rdock-test-data -
Send the same requests to both and compare status lines and headers.
--resolvekeeps the real host name while connecting to the local port:Terminal window curl -sk -o /dev/null -D - --resolve app.example.com:443:127.0.0.1 https://app.example.com/curl -sk -o /dev/null -D - --resolve app.example.com:8444:127.0.0.1 https://app.example.com:8444/Cover every
location: exact paths, prefixes with and without the trailing slash, regex locations, authenticated pages (-u user:password), an upload near the body limit, and a WebSocket endpoint. For the HTTP redirect,curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' -H 'Host: app.example.com' http://127.0.0.1:8081/should print301 https://app.example.com:8444/. -
Check the test instance’s access log for 404, 502 and 504 answers. Each line names the
route,serviceandupstream, which shows which rule caught a request. -
Switch: stop NGINX, then start ReverseDock with the real
rdock.yamlon ports 80 and 443. With systemd:systemctl stop nginx, thensystemctl enable --now rdock. -
Watch the first minutes with
rdock statusandcurl -s http://127.0.0.1:2080/metrics(rdock_http_requests_totalby route and status class,rdock_upstream_errors_totalby service). -
Roll back if needed:
systemctl stop rdock, thensystemctl start nginx.
Checklist
Section titled “Checklist”- Every
server_nameandlocationpair has a route, and routes that must win over longer rules have apriority. - Regex locations are rewritten as
PathRegexpwith explicit^and$where NGINX relied on them. proxy_passURI parts are replaced bystrip_prefix,replace_pathoradd_prefix.- Backends that expect
Host: $proxy_hosthavepass_host_header: false. set_real_ip_fromnetworks are insecurity.trusted_proxies.client_max_body_sizeis carried over tosecurity.limits.max_body_bytes(NGINX’s default was 1 MiB; ReverseDock has none).- Long
proxy_read_timeoutvalues are carried over totimeouts.readon the route, and on the listener for WebSockets. htpasswdentries are argon2id or bcrypt (rdock hash), or the MD5 and SHA-1 warnings are accepted without--strict.- Every certificate is under
certificates, anddefaults.certificateis set if clients connect without SNI. rdock validate --strict rdock.yamlexits 0, responses were compared on the test ports, and the rollback steps are written down.
Related: Concepts · Route a domain · Path routing · Configuration reference