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
| Symptom | Usual 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. |