Apache

This is the reverse-proxy configuration Aetheus actually runs in production, with the deployment-specific tokens replaced by example values. If you are unsure which proxy to use, use this one.

Enable the modules

sudo a2enmod ssl proxy proxy_http proxy_wstunnel rewrite headers
sudo systemctl restart apache2

proxy_wstunnel is not optional: without it the API host cannot upgrade to WebSocket, and live run output stops updating while everything else appears to work.

Get a certificate

One certificate covering both host names keeps the configuration simple:

sudo certbot certonly --apache \
    -d app.example.com \
    -d api.example.com

App host

Serves the frontend. Whether the files come from a container or from a published wwwroot, the two rules that matter are the same: never cache the boot files, and let client-side routing work.

<VirtualHost *:443>
    ServerName app.example.com

    SSLEngine On
    SSLCertificateFile /etc/letsencrypt/live/app.example.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/app.example.com/privkey.pem
    Include /etc/letsencrypt/options-ssl-apache.conf

    ProxyPreserveHost On
    ProxyRequests Off
    RequestHeader set X-Forwarded-Proto "https"

    # A Blazor WebAssembly app boots from index.html and _framework/. Caching those means a
    # browser keeps running the previous version after an upgrade.
    <LocationMatch "^/(index\.html|service-worker(\.published)?\.js|_framework/|_content/Radzen\.Blazor/)">
        Header always set Cache-Control "no-cache, no-store, must-revalidate"
        Header always set Pragma "no-cache"
        Header always set Expires "0"
    </LocationMatch>

    ProxyPass / http://127.0.0.1:10025/
    ProxyPassReverse / http://127.0.0.1:10025/

    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
    Header always set X-Content-Type-Options "nosniff"
    Header always set Referrer-Policy "strict-origin-when-cross-origin"

    ErrorLog ${APACHE_LOG_DIR}/app.example.com_ssl_error.log
    CustomLog ${APACHE_LOG_DIR}/app.example.com_ssl_access.log combined
</VirtualHost>

Serving the files directly instead of proxying

If you published the frontend to disk rather than running the container, replace the two ProxyPass lines with a document root and a fallback so unknown paths reach index.html:

    DocumentRoot /var/www/aetheus-app

    <Directory /var/www/aetheus-app>
        Require all granted
        Options -Indexes
        FallbackResource /index.html
    </Directory>

API host

Serves the backend. The rewrite block is what turns an upgrade request into a proxied WebSocket connection; it must come before ProxyPass.

<VirtualHost *:443>
    ServerName api.example.com

    SSLEngine On
    SSLCertificateFile /etc/letsencrypt/live/app.example.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/app.example.com/privkey.pem
    Include /etc/letsencrypt/options-ssl-apache.conf

    ProxyPreserveHost On
    ProxyRequests Off
    RequestHeader set X-Forwarded-Proto "https"

    RewriteEngine On
    RewriteCond %{HTTP:Upgrade} websocket [NC]
    RewriteCond %{HTTP:Connection} upgrade [NC]
    RewriteRule ^/?(.*) ws://127.0.0.1:10026/$1 [P,L]

    ProxyPass / http://127.0.0.1:10026/
    ProxyPassReverse / http://127.0.0.1:10026/

    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
    Header always set X-Content-Type-Options "nosniff"

    ErrorLog ${APACHE_LOG_DIR}/api.example.com_ssl_error.log
    CustomLog ${APACHE_LOG_DIR}/api.example.com_ssl_access.log combined
</VirtualHost>

Redirect plain HTTP

One virtual host per name, on port 80, redirecting to HTTPS:

<VirtualHost *:80>
    ServerName app.example.com
    RewriteEngine On
    RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
</VirtualHost>

<VirtualHost *:80>
    ServerName api.example.com
    RewriteEngine On
    RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
</VirtualHost>

Apply

sudo apachectl configtest
sudo systemctl reload apache2

Always run configtest first: a reload with a broken configuration takes the site down rather than being ignored.

Verify

# The API answers, and its database is reachable.
curl -sf https://api.example.com/health/ready

# The frontend is served and not cached.
curl -sI https://app.example.com/ | grep -i cache-control

Then open the app host in a browser and sign in. If the page loads but every call fails, the two settings to check are the frontend's ApiBaseUrl and the backend's Cors__Origins__0.

Common failures

SymptomUsual cause
The app loads, every API call fails with a CORS error.Cors__Origins__0 does not exactly match the app host, scheme included.
The app loads but calls go to the wrong address.ApiBaseUrl in the frontend's appsettings.json was overwritten by a redeploy.
Everything works but live output never refreshes.proxy_wstunnel is not enabled, or the rewrite block is missing on the API host.
A refresh on a deep link returns 404.No fallback to index.html when serving the files directly.
The browser still runs the previous version after an upgrade.The no-store rules on index.html and _framework/ are missing.