Application Servers Azure

Static Web Server on Ubuntu 24.04 on Azure User Guide

| Product: Static Web Server on Ubuntu 24.04 LTS on Azure

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 unprivileged sws account

  • 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 /health endpoint

  • A document root at /var/www/static with 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))

Both static-web-server units report active, static-web-server --version reports version 2.43.0 built for x86_64 unknown linux gnu under the MIT OR Apache 2.0 licence, and ss shows the server listening on port 443 for HTTPS and port 80 for HTTP on all interfaces

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.

curl over HTTPS to the loopback address returns HTTP/2 200 with strict transport security, x frame options DENY, x content type options nosniff and a content security policy header, and openssl shows the per VM self signed certificate with subject and issuer CN cloudimg-static-web-server and a ten year validity window

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.

curl requests showing HTTP and HTTPS both returning 200 for the root path, both returning 200 when an arbitrary domain name is supplied in the Host header proving the image serves any domain, and the health endpoint returning 200

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

the hardening defaults proven on a live instance: an index less directory returns 404 while a file inside it returns 200, the metrics endpoint returns 404, the configuration shows the HTTPS listener on port 443 with HTTP/2 and TLS against the per VM certificate plus security headers on and directory listing, hidden files, symbolic links and metrics all disabled, the four security headers are present on the response, and static-web-server runs as the unprivileged sws account whose shell is nologin

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. /metrics is 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/static is 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.