Reverse proxy + TLS
A reference nginx and Caddy reverse-proxy setup that terminates TLS in front of Nosdesk, plus the proxy-related settings (FRONTEND_URL, WebAuthn origin, TRUSTED_PROXIES) that matter.
Nosdesk listens on plain HTTP on 127.0.0.1:8080 and expects something in front of it to terminate TLS. It runs in production mode with Secure cookies, so the browser must reach it over HTTPS or logins won’t stick.
The configuration below is one example, for a single host running its own reverse proxy. Other setups work just as well (an existing ingress controller, a cloud load balancer, a corporate WAF), and the specifics will differ. What every setup has to get right is the same:
- Terminate TLS and forward to
127.0.0.1:8080. - Pass through WebSocket upgrades, which the collaborative editor relies on.
- Forward
Host,X-Forwarded-For, andX-Forwarded-Protoso Nosdesk sees the real client. - Allow request bodies at least as large as
MAX_FILE_SIZE_MB(default 50), or it’ll reject uploads the app would have accepted.
DNS
Point an A record (and AAAA, if you serve IPv6) at the host. That public HTTPS origin is the value you’ll give Nosdesk as FRONTEND_URL, along with the other proxy settings covered in Tell Nosdesk it’s behind a proxy.
nginx
Obtain a certificate with Let’s Encrypt. Certbot’s nginx plugin handles issuance and renewal:
sudo apt install nginx certbot python3-certbot-nginx
sudo certbot certonly --nginx -d help.example.com Then drop in a server block. It redirects HTTP to HTTPS and proxies everything else. The API, frontend, and collab WebSocket are all same-origin, so a single location / handles them:
# Redirect HTTP to HTTPS
server {
listen 80;
listen [::]:80;
server_name help.example.com;
return 301 https://$host$request_uri;
}
# Lets the collab WebSocket upgrade cleanly while leaving plain
# HTTP requests on keep-alive.
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name help.example.com;
ssl_certificate /etc/letsencrypt/live/help.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/help.example.com/privkey.pem;
# Match MAX_FILE_SIZE_MB so the proxy doesn't reject an upload
# the backend would accept.
client_max_body_size 50m;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
# WebSocket upgrade for collaborative editing
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# Real client address + scheme, for rate limiting, audit
# logs, and correct redirects
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;
}
} Save it under /etc/nginx/sites-available/, symlink it into sites-enabled/, run sudo nginx -t to check it, and sudo systemctl reload nginx. Nosdesk sets its own Content-Security-Policy and security headers, so there’s no need to add those at the proxy. Add HSTS here if you want it.
Caddy
Caddy needs less configuration. It fetches and renews Let’s Encrypt certificates automatically, and proxies WebSockets and the X-Forwarded-* headers without extra setup. The whole Caddyfile is:
help.example.com {
reverse_proxy 127.0.0.1:8080
} For uploads larger than Caddy’s default cap, add a request_body { max_size 50MB } block to match MAX_FILE_SIZE_MB.
Tell Nosdesk it’s behind a proxy
Once the proxy terminates TLS, set these in your .env so Nosdesk knows its real public origin and can trust what the proxy tells it:
| Variable | Set it to | What it does |
|---|---|---|
FRONTEND_URL | Your public HTTPS origin, e.g. https://help.example.com | Drives CORS and cookie scope. It must be the address people browse to, not the internal http://localhost:8080. |
WEBAUTHN_RP_ORIGIN | The same origin, e.g. https://help.example.com | The origin passkeys are bound to. Only needed if you use passkeys; the browser rejects a registration whose origin doesn’t match. |
WEBAUTHN_RP_ID | The bare host, e.g. help.example.com | The domain passkeys are scoped to. Defaults to the host of FRONTEND_URL, so you can usually leave it unset. |
TRUSTED_PROXIES | The CIDR the proxy connects from, e.g. 172.18.0.1/32 | Whose X-Forwarded-For Nosdesk will trust. Without it, rate limiting and the audit log key on the proxy instead of the real client. Finding the value is covered below. |
A complete proxy-related block in .env:
# Public HTTPS origin: drives CORS and cookie scope
FRONTEND_URL=https://help.example.com
# Passkeys: bind WebAuthn to that origin (only if you use passkeys)
WEBAUTHN_RP_ID=help.example.com
WEBAUTHN_RP_ORIGIN=https://help.example.com
# Trust the proxy's X-Forwarded-For so the real client IP reaches
# rate limiting and the audit log (see below for the right value)
TRUSTED_PROXIES=172.18.0.1/32 The full set of variables, with defaults, lives in the Configuration reference.
Finding the TRUSTED_PROXIES value
TRUSTED_PROXIES is the range Nosdesk sees the proxy connecting from, which isn’t always the proxy’s own address. With the Compose setup here, a proxy on the host talks to the published 127.0.0.1:8080 port, and Docker NATs that connection, so the container sees the bridge gateway rather than 127.0.0.1. Find that gateway and trust it:
docker network inspect nosdesk_nosdesk-network \
--format '{{ (index .IPAM.Config 0).Gateway }}'
# e.g. 172.18.0.1 -> TRUSTED_PROXIES=172.18.0.1/32 Scope it as tightly as your network allows. If you run the proxy as another service inside the Compose network instead, trust that network’s subnet. Left unset, Nosdesk ignores X-Forwarded-For entirely, which is the safe default for a backend with nothing in front of it: a header anyone can set must not be believed.
Restart the stack after changing these (docker compose up -d), then load your HTTPS URL and continue to first-run setup.