Network Deployment
Supported HTTP protocols
Runique natively supports HTTP/1.1 and HTTP/2 via Axum/Hyper.
HTTP/2 and the Host header
HTTP/2 does not send a Host header — it uses the :authority pseudo-header instead.
Runique handles this automatically: the allowed_hosts middleware and the HTTPS redirect
middleware first read the Host header, then fall back to request.uri().authority()
when absent.
This behavior covers HTTP/1.1, HTTP/2, and reverse proxies (nginx, Caddy, Cloudflare).
HTTP/3
HTTP/3 runs over QUIC (UDP) and is not natively supported by Axum/Hyper at this time. To benefit from it, two options:
| Option | Description |
|---|---|
| Cloudflare (recommended) | Terminates HTTP/3 on Cloudflare's side, proxies HTTP/2 to Runique. Zero server-side configuration. |
| Reverse proxy (Caddy, nginx) | Some reverse proxies support HTTP/3 and proxy HTTP/1.1 or HTTP/2 to Runique. |
Runique directly on the internet = HTTP/2 maximum.
ACME / Automatic TLS
The acme feature allows Runique to manage its own Let's Encrypt certificates
without a reverse proxy.
# Cargo.toml
runique = { features = ["acme"] }
# .env
ACME_ENABLED=true
ACME_DOMAIN=mydomain.com
ACME_EMAIL=admin@mydomain.com
ACME_CERTS_DIR=/absolute/path/to/certs # default: ./certs
ACME_CERTS_DIRshould be an absolute path in production. A relative path depends on the systemdWorkingDirectory— if not set correctly, the certificate is not found and the server crashes on every restart.If
ACME_ENABLED=truebut theacmefeature is not compiled in, Runique prints a warning at startup.
Limitation — one site per machine
ACME requires exclusive use of port 80. If multiple applications run on the same server, only one can use ACME — the others cannot obtain their certificates simultaneously.
In a multi-site setup, use a reverse proxy (nginx, Caddy) that manages Let's Encrypt itself, and run each Runique instance on a separate internal port with ACME_ENABLED=false.
Required ports
| Port | Usage |
|---|---|
| 80 | Let's Encrypt HTTP-01 challenge + HTTPS redirect |
| 443 | HTTPS (TLS) |
To listen on these ports without root, use CAP_NET_BIND_SERVICE:
# /etc/systemd/system/runique.service
[Service]
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE
Behind a reverse proxy
If a reverse proxy (nginx, Caddy, Cloudflare) handles TLS, Runique runs over HTTP
on an internal port (e.g. 3000) and does not require the acme feature.
ACME_ENABLED=false
PORT=3000
IP_SERVER=127.0.0.1
For HTTPS redirection, let the proxy handle it and disable ENFORCE_HTTPS on
Runique's side to avoid a double redirect.
Nginx — recommended production configuration
TLS hardening
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
server_tokens off;
Security headers on media files
Runique injects its security headers (CSP, HSTS, X-Content-Type-Options, etc.)
into every response it generates. But files served directly by Nginx via alias
(a location /media/ block) bypass Runique entirely — Nginx must add the
security headers itself.
location /media/ {
alias /var/www/myproject/media/;
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
}
The
alwaysflag is required: without it, Nginx only sends these headers on 2xx/3xx responses, not on 4xx/5xx errors.
Full example (multi-site)
server {
listen 80;
server_name mydomain.com www.mydomain.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name mydomain.com www.mydomain.com;
ssl_certificate /etc/letsencrypt/live/mydomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mydomain.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
server_tokens off;
client_max_body_size 10M;
location / {
proxy_pass http://127.0.0.1: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;
}
location /media/ {
alias /var/www/myproject/media/;
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
}
}