Static Web Server on Ubuntu 24.04 on Azure User Guide
Overview
This guide covers the deployment and configuration of Static Web Server on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. Static Web Server, also known as SWS, is a cross platform, high performance and asynchronous web server written in Rust for serving static files. It compiles to a single self contained binary with zero runtime dependencies, so it starts instantly, uses very little memory and is simple to operate.
The image installs Static Web Server 2.43.0 from the pinned upstream release, verified against a published SHA256 checksum, and runs it under systemd as a dedicated unprivileged sws account. There is no web interface, no database and no reverse proxy: readable TOML configuration is the whole configuration.
Two listeners, one site. static-web-server.service serves the document root over HTTPS on port 443, terminating TLS itself. static-web-server-http.service serves the same document root over HTTP on port 80. Because no host name is written into the image, both listeners answer for whatever address or domain you point at the VM, from the moment it boots. Once your domain is in place you can turn port 80 into a permanent HTTPS redirect, as described in Step 10.
Secure and lean by default. On the first boot of every VM a one shot service generates a unique self signed TLS certificate for that specific instance, so no certificate or private key is ever baked into the image. Directory listing is disabled, hidden dotfiles are never served, symbolic links are not followed out of the document root, and Strict Transport Security, X Frame Options, X Content Type Options and a Content Security Policy are sent on every response. The Prometheus metrics endpoint is switched off, because it is an unauthenticated information disclosure surface on a public web server. Static Web Server has no login, so no shared or default credential of any kind ships in the image.
What is included:
-
Static Web Server 2.43.0, a single static Rust binary at
/usr/local/bin/static-web-server, run under systemd as the dedicated unprivilegedswsaccount -
HTTPS on port 443 terminated by Static Web Server itself, and HTTP on port 80 serving the same document root
-
A unique self signed certificate generated for each instance on first boot into
/etc/static-web-server/certs, so no key material is shared across deployments -
A hardened default posture: directory listing disabled, dotfiles hidden, symbolic links blocked, security headers on every response and the metrics endpoint off
-
Content delivery tuned out of the box: on demand Gzip, Deflate, Brotli and Zstandard compression, precompressed file support, cache control headers, custom 404 and error pages, and a quiet
/healthendpoint -
A document root at
/var/www/staticwith a cloudimg landing page you replace with your own site -
A tightly sandboxed systemd unit whose only remaining privilege is the ability to bind the standard web ports
Prerequisites
-
Active Azure subscription, SSH public key, VNet and subnet in the target region
-
Subscription to the Static Web Server listing on Azure Marketplace
-
Network Security Group rules allowing TCP 22 for administration, and TCP 80 and TCP 443 from the clients that will reach the web server
Recommended virtual machine size: Standard_B2s (2 vCPU, 4 GB RAM) is ample for serving static sites or single page applications. Static Web Server is extremely light; larger sizes are only needed for very high request volumes.
Step 1: Deploy from the Azure Portal
Search Static Web Server in Marketplace, select the cloudimg publisher, and click Create. Configure the Network Security Group to allow TCP 443 (HTTPS) and TCP 80 (HTTP) from the clients that will reach the server, and TCP 22 for administration.
Step 2: Deploy from the Azure CLI
RG="web-prod"; LOCATION="eastus"; VM_NAME="sws1"
GALLERY_IMAGE_ID="/subscriptions/<sub-id>/resourceGroups/azure-cloudimg/providers/Microsoft.Compute/galleries/cloudimgGallery/images/static-web-server/versions/<version>"
SSH_KEY="$(cat ~/.ssh/id_rsa.pub)"
az group create --name "$RG" --location "$LOCATION"
az vm create \
--resource-group "$RG" --name "$VM_NAME" \
--image "$GALLERY_IMAGE_ID" \
--size Standard_B2s \
--admin-username azureuser \
--ssh-key-values "$SSH_KEY" \
--public-ip-sku Standard
az vm open-port --resource-group "$RG" --name "$VM_NAME" --port 443 --priority 1001
az vm open-port --resource-group "$RG" --name "$VM_NAME" --port 80 --priority 1002
az vm open-port --resource-group "$RG" --name "$VM_NAME" --port 22 --priority 1003
Step 3: First boot
On first boot the image generates a unique self signed TLS certificate for this VM into /etc/static-web-server/certs/sws.crt and sws.key, writes the informational note /root/static-web-server-info.txt, and starts both listeners. This completes within seconds. SSH in as azureuser and read the note:
sudo cat /root/static-web-server-info.txt
The note records the served HTTPS and HTTP URLs, the health endpoint, the document root, both configuration paths and the certificate path:
static_web_server.version=Version: 2.43.0
static_web_server.document_root=/var/www/static
static_web_server.config_https=/etc/static-web-server/config.toml
static_web_server.config_http=/etc/static-web-server/config-http.toml
static_web_server.tls_cert=/etc/static-web-server/certs/sws.crt
HTTPS_URL=https://10.0.0.17/
HTTP_URL=http://10.0.0.17/
HEALTH_URL=https://10.0.0.17/health
Static Web Server has no password, so the note holds no secret.
Step 4: Confirm the services are running
Both listener units are active, static-web-server --version reports the installed build, and ss confirms the server is listening on :443 for HTTPS and :80 for HTTP.
systemctl is-active static-web-server.service static-web-server-http.service
static-web-server --version
sudo ss -tlnp | grep -E ':80 |:443 '
Expected output:
active
active
Version: 2.43.0
Built: 2026-06-10 23:25:42 +02:00
Git commit: 74b0f3cd26beba31f2ac6d128b47c48439adfc8b
Build target: x86_64-unknown-linux-gnu
Rust version: rustc 1.96.0 (ac68faa20 2026-05-25)
License: MIT OR Apache-2.0
Homepage: https://static-web-server.net
LISTEN 0 128 *:443 *:* users:(("static-web-serv",pid=2793,fd=9))
LISTEN 0 128 *:80 *:* users:(("static-web-serv",pid=2794,fd=9))

Step 5: Confirm HTTPS and the per instance certificate
Static Web Server answers HTTPS on port 443 directly. Request the site over HTTPS on the loopback address (the -k flag accepts the self signed certificate), then inspect the certificate.
curl -ksI https://127.0.0.1/
sudo openssl x509 -in /etc/static-web-server/certs/sws.crt -noout -subject -issuer -dates
The response is served over HTTP/2 with the security headers applied, and the certificate subject is cloudimg-static-web-server, unique to this VM:
HTTP/2 200
content-type: text/html; charset=utf-8
cache-control: max-age=86400
strict-transport-security: max-age=63072000; includeSubDomains; preload
x-frame-options: DENY
x-content-type-options: nosniff
content-security-policy: frame-ancestors 'self'
subject=C = GB, O = cloudimg, CN = cloudimg-static-web-server
issuer=C = GB, O = cloudimg, CN = cloudimg-static-web-server
notBefore=Jul 25 18:24:14 2026 GMT
notAfter=Jul 22 18:24:14 2036 GMT
The certificate was generated on this instance's first boot, so no private key is shared with any other deployment.

Step 6: Confirm HTTP, any domain serving, and the health endpoint
The same site is served on port 80. Critically, no host name is baked into the image, so both listeners answer for any Host header, which means the site works immediately on the VM's public IP and on any domain you later point at it.
curl -s -o /dev/null -w 'HTTP GET / -> %{http_code}\n' http://127.0.0.1/
curl -ks -o /dev/null -w 'HTTPS GET / -> %{http_code}\n' https://127.0.0.1/
curl -ks -o /dev/null -w 'HTTPS Host www.example.com -> %{http_code}\n' -H 'Host: www.example.com' https://127.0.0.1/
curl -s -o /dev/null -w 'HTTP Host www.example.com -> %{http_code}\n' -H 'Host: www.example.com' http://127.0.0.1/
curl -ks -o /dev/null -w 'HTTPS GET /health -> %{http_code}\n' https://127.0.0.1/health
Every request returns 200, including the requests carrying an arbitrary domain name in the Host header:
HTTP GET / -> 200
HTTPS GET / -> 200
HTTPS Host www.example.com -> 200
HTTP Host www.example.com -> 200
HTTPS GET /health -> 200
The /health endpoint returns 200 and writes no log entry, which makes it ideal for Azure Load Balancer health probes and availability monitoring.

Step 7: Confirm the hardening defaults
Directory listing is disabled, so a directory with no index file returns the custom 404 page rather than exposing its contents, while individual files inside it are still served by their path. The Prometheus metrics endpoint is off, and the security headers are present on every response.
curl -ks -o /dev/null -w 'GET /downloads/ -> %{http_code} (index-less directory)\n' https://127.0.0.1/downloads/
curl -ks -o /dev/null -w 'GET /downloads/README.txt -> %{http_code} (file inside it)\n' https://127.0.0.1/downloads/README.txt
curl -ks -o /dev/null -w 'GET /metrics -> %{http_code} (Prometheus endpoint off)\n' https://127.0.0.1/metrics
curl -ksI https://127.0.0.1/ | grep -iE 'strict-transport|x-frame|x-content-type|content-security'
Expected output:
GET /downloads/ -> 404 (index-less directory)
GET /downloads/README.txt -> 200 (file inside it)
GET /metrics -> 404 (Prometheus endpoint off)
strict-transport-security: max-age=63072000; includeSubDomains; preload
x-frame-options: DENY
x-content-type-options: nosniff
content-security-policy: frame-ancestors 'self'
Hidden dotfiles such as .git, .env and .htpasswd are never served either, and a path component that is a symbolic link is never followed out of the document root.
Step 8: Review the configuration
The HTTPS listener is configured entirely by /etc/static-web-server/config.toml; the HTTP listener uses /etc/static-web-server/config-http.toml with the same hardening.
sudo grep -vE '^\s*#|^\s*$' /etc/static-web-server/config.toml
The effective settings are:
[general]
host = "::"
port = 443
root = "/var/www/static"
log-level = "info"
http2 = true
http2-tls-cert = "/etc/static-web-server/certs/sws.crt"
http2-tls-key = "/etc/static-web-server/certs/sws.key"
security-headers = true
directory-listing = false
ignore-hidden-files = true
disable-symlinks = true
index-files = "index.html, index.htm"
cache-control-headers = true
compression = true
compression-level = "default"
compression-static = true
redirect-trailing-slash = true
text-charset = true
page404 = "/var/www/static/404.html"
page50x = "/var/www/static/50x.html"
health = true
metrics = false
threads-multiplier = 1
grace-period = 5

Step 9: Serve your own site
Replace the cloudimg landing page with your own site by writing your files into the document root /var/www/static, or point root at a different directory in both configuration files. Keep the content readable by the sws account. The commands below are illustrative; substitute your own content.
# copy your static site into the document root, then fix ownership:
# sudo rsync -a ./my-site/ /var/www/static/
# sudo chown -R root:sws /var/www/static
# sudo find /var/www/static -type d -exec chmod 0755 {} +
# sudo find /var/www/static -type f -exec chmod 0644 {} +
#
# or change `root` in BOTH config files to another path, then apply:
# sudo systemctl restart static-web-server.service static-web-server-http.service
For a single page application, set page-fallback to your index.html so client side routes that do not exist on disk are served the application shell with a 200 status:
# add to the [general] section of BOTH config files:
# page-fallback = "/var/www/static/index.html"
# then apply:
# sudo systemctl restart static-web-server.service static-web-server-http.service
Step 10: Install a browser trusted certificate for your domain
The image ships with a self signed certificate so HTTPS works immediately. For a browser trusted certificate, point your domain's DNS A record at this VM's public IP, obtain a certificate for <your-domain> from a certificate authority, then point Static Web Server at the new certificate and key.
# 1) obtain a certificate + key for <your-domain> (for example with acme.sh or certbot)
# 2) install them and set in /etc/static-web-server/config.toml:
# http2-tls-cert = "/etc/static-web-server/certs/<your-domain>.crt"
# http2-tls-key = "/etc/static-web-server/certs/<your-domain>.key"
# 3) keep the key readable by the sws account only:
# sudo chown root:sws /etc/static-web-server/certs/<your-domain>.key
# sudo chmod 0640 /etc/static-web-server/certs/<your-domain>.key
# 4) apply the change:
# sudo systemctl restart static-web-server.service
Static Web Server loads the certificate at start up, so a restart is required to serve a replacement certificate.
Step 11: Optionally redirect HTTP to HTTPS
Once your domain resolves to this VM you can turn port 80 into a permanent redirect instead of a second copy of the site. Static Web Server's redirect sends every request to one fixed canonical host and rejects any Host header not on its allow list with 400 Bad Request, so only enable it once you know your domain, and list every host name you serve.
# 1) stop the plain HTTP listener so port 80 is free for the redirect server:
# sudo systemctl disable --now static-web-server-http.service
# 2) uncomment and edit these lines in /etc/static-web-server/config.toml,
# replacing the placeholder host names with your own:
# https-redirect = true
# https-redirect-host = "www.example.com"
# https-redirect-from-port = 80
# https-redirect-from-hosts = "www.example.com,example.com"
# 3) apply the change:
# sudo systemctl restart static-web-server.service
To go back to serving the site on both ports, comment the four lines out again and run sudo systemctl enable --now static-web-server-http.service.
Step 12: Optional features
Static Web Server supports a number of features that are available but not enabled by default. Add them to the [general] or [advanced] sections of the configuration files and restart the services.
# HTTP basic authentication (supply your own bcrypt hash; nothing is seeded in the image):
# basic-auth = "myuser:$2y$10$<your-own-bcrypt-hash>"
#
# Prometheus metrics — an unauthenticated endpoint, so restrict it at the network layer first:
# metrics = true
#
# Directory listing for a downloads site:
# directory-listing = true
# directory-listing-order = 1
#
# CORS for an assets or API origin:
# cors-allow-origins = "https://app.internal.test"
#
# Custom headers, redirects, rewrites and virtual hosts live in [advanced];
# see https://static-web-server.net/ for the full reference.
Step 13: Security recommendations
-
Restrict the NSG to your use case. Allow TCP 443 and TCP 80 only from the networks that need to reach the site, and keep TCP 22 restricted to your administration network.
-
Replace the self signed certificate for public sites. The per VM self signed certificate secures the transport immediately, but browsers will warn on it; install a CA issued certificate as in Step 10 for public facing sites.
-
Keep directory listing disabled. The cloudimg default returns the 404 page for a directory with no index file, so directory contents are never exposed; only enable listing for a path where you deliberately want it.
-
Leave the metrics endpoint off unless you need it.
/metricsis unauthenticated; if you enable it, restrict access at the network layer. -
Never place secrets in the document root. Dotfiles are ignored and symbolic links are not followed, but anything else under
/var/www/staticis public. -
Keep the OS patched. Unattended security upgrades remain enabled on the running VM.
Step 14: Support and Licensing
Static Web Server is an open source project dual licensed under the Apache License 2.0 or the MIT License, at your option. This cloudimg image installs the unmodified upstream release binary, verified against its published SHA256 checksum, and ships both licence texts at /usr/share/doc/static-web-server/. cloudimg provides the packaging, the systemd hardening, the secure by default TLS configuration, the per instance certificate automation, the paired deploy guide, and 24/7 support with a guaranteed 24 hour response SLA.
cloudimg is not affiliated with or endorsed by the Static Web Server project. Static Web Server is a mark of its respective owner and is used here only to identify the software.
Deploy on Azure
Find Static Web Server on Ubuntu 24.04 LTS on the Azure Marketplace, published by cloudimg. Deploy from the Portal or the Azure CLI as shown above.
Need Help?
Email support@cloudimg.co.uk for deployment help, configuration questions, or licensing enquiries.