untested Untested configuration
This configuration is provided as a starting point and reviewed against the Apache setup that Aetheus actually runs. It has not been executed on a real deployment, so treat every value as something to verify rather than as a proven recipe.
nginx
An nginx layout transposed from the Apache configuration Aetheus runs in production. Same two host names, same two rules on the frontend, same WebSocket requirement on the API.
The connection-upgrade map
WebSocket proxying in nginx needs a map from the incoming Upgrade header to the outgoing Connection header. It belongs in the http block, so put it in /etc/nginx/conf.d/upgrade.conf and declare it once:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
Skipping this is the single most common reason live output stops refreshing while everything else appears to work.
App host
Serves the frontend, either by proxying to the container or by serving the published wwwroot from disk. Both variants are shown.
server {
listen 443 ssl;
http2 on;
server_name app.example.com;
ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# A Blazor WebAssembly app boots from index.html and _framework/. Caching those means a
# browser keeps running the previous version after an upgrade.
location ~ ^/(index\.html|service-worker(\.published)?\.js|_framework/|_content/Radzen\.Blazor/) {
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
add_header Pragma "no-cache" always;
expires -1;
proxy_pass http://127.0.0.1:10025;
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 https;
}
location / {
proxy_pass http://127.0.0.1:10025;
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 https;
}
}
Serving the files directly instead of proxying
If you published the frontend to disk, replace both location blocks with:
root /var/www/aetheus-app;
index index.html;
location ~ ^/(index\.html|service-worker(\.published)?\.js|_framework/|_content/Radzen\.Blazor/) {
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
expires -1;
try_files $uri =404;
}
location / {
# Client-side routing: unknown paths must reach index.html, not a 404.
try_files $uri $uri/ /index.html;
}
Check that your build maps .wasm in mime.types; recent nginx releases do. If it does not, the runtime files are served as application/octet-stream and the app fails to boot.
API host
Serves the backend. The three upgrade lines are what allow live run output to work.
server {
listen 443 ssl;
http2 on;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
# Artefact uploads and downloads go through this host; the default 1m limit is far too low.
client_max_body_size 512m;
location / {
proxy_pass http://127.0.0.1:10026;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
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 https;
# Streamed run output must not be cut off mid-flight.
proxy_read_timeout 300s;
proxy_buffering off;
}
}
Redirect plain HTTP
server {
listen 80;
server_name app.example.com api.example.com;
return 301 https://$host$request_uri;
}
Apply
sudo nginx -t
sudo systemctl reload nginx
Always run nginx -t first: a reload with a broken configuration takes the site down rather than being ignored.
Verify
curl -sf https://api.example.com/health/ready
curl -sI https://app.example.com/ | grep -i cache-control
Then open the app host and sign in. A page that loads while every call fails points at the frontend's ApiBaseUrl or the backend's Cors__Origins__0, not at nginx.