Nginx UI on Ubuntu 24.04 on Azure User Guide
Overview
This guide covers deploying and using Nginx UI on Ubuntu 24.04 LTS from cloudimg on Microsoft Azure.
Nginx UI is a web interface for nginx. From a browser you create and edit sites, streams and upstreams, edit any nginx configuration file with syntax checking, test, reload and restart nginx, issue and renew Let's Encrypt certificates, and read nginx's access and error logs. Every change is checked with nginx -t before it is applied.
What the cloudimg image adds:
- No default login. The image contains no Nginx UI account, no database and none of Nginx UI's secrets. On the first boot of your VM a one-time service generates an admin password unique to the VM, completes Nginx UI's first-run setup through its local API with the network shut off, proves the password works and that guessable ones are refused, and only then opens the web interface. Nginx UI's setup page is never reachable from the network, and the password is written only to a file readable by root.
- HTTPS on port 443, from the first boot. nginx serves Nginx UI on port 443 with a certificate generated on your VM. Port 80 redirects to HTTPS, and a request for a host name the VM does not serve is dropped.
- Nginx UI on localhost only. Nginx UI listens on
127.0.0.1:9000, behind nginx. Port 9000 is not open to the network and the image's network rules do not include it. If anyone moves Nginx UI's listener (Preferences, or an edit of its settings file), it is put back on127.0.0.1:9000the next time the service starts. - The browser terminal is off. Nginx UI includes a terminal that is a shell on the VM in your browser. It is disabled on this image; Step 9 explains how to turn it on if you want it, and what that means.
- Hardened front door. nginx hands Nginx UI the real client address and the configured host name (never values a client sends), refuses a request that changes something when it carries another site's
Origin(and every cross-origin WebSocket), refuses Nginx UI's setup routes, rate limits the login routes on top of Nginx UI's own lockout, marks every cookieSecure, and keeps WebSocket session tokens out of the access log. - Azure metadata blocked. Nginx UI, nginx and the first-boot service cannot reach the Azure Instance Metadata Service or the Azure WireServer. Nginx UI fetches URLs you give it (certificate authorities, DNS provider APIs, S3 backup targets, cluster nodes, upstream checks), so a mistaken or malicious setting can never reach the VM's metadata or a managed identity token. The Azure agent keeps its own access.
- The current Nginx UI release. Nginx UI 2.9.1, outside every published Nginx UI security advisory (69 at the time of the build, including the critical ones that affect 2.4.2), compiled by cloudimg from the unmodified upstream source with one newer Go dependency (details in Step 12).
- Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.
What is on the image: Nginx UI 2.9.1 (built from the v2.9.1 source with Go 1.27.3), nginx 1.24 (from Ubuntu 24.04), cloud-init and the Azure Linux agent.
Default user: azureuser · Architecture: x64 (Gen2) · Recommended VM size: Standard_B2s (2 vCPU, 4 GiB)

Prerequisites
- An Azure subscription, an SSH key pair, and a virtual network and subnet in the target region.
- A network security group that allows TCP 22 (SSH) from your admin network and TCP 443 (the Nginx UI web interface and your HTTPS sites) from the networks that need it, plus TCP 80 for HTTP sites, the redirect to HTTPS and Let's Encrypt HTTP validation. Never open 9000: Nginx UI listens on the loopback address only.
Step 1: Deploy the VM
In the Azure portal, open Create a resource, search for Nginx UI on Ubuntu 24.04 LTS by cloudimg and select Create. On the Basics tab choose Standard_B2s and SSH public key authentication with azureuser as the user name; on the Networking tab allow 22, 80 and 443 as above.
With the Azure CLI (replace the values in angle brackets; the image URN is shown on the offer's Usage information + support tab):
az vm create --resource-group <resource-group> --name nginx-ui --image <image-urn-from-the-offer> \
--size Standard_B2s --admin-username azureuser --ssh-key-values ~/.ssh/id_rsa.pub \
--public-ip-sku Standard --nsg-rule SSH
az network nsg rule create --resource-group <resource-group> --nsg-name nginx-uiNSG --name web \
--priority 1010 --access Allow --protocol Tcp --destination-port-ranges 80 443 --source-address-prefixes <your-cidr>
First boot takes under a minute after the VM is running.
Step 2: Connect to your VM
ssh azureuser@<public-ip>
Step 3: Check the services
systemctl is-active nginx-ui-address nginx-ui nginx
sudo test -f /var/lib/cloudimg/nginx-ui-firstboot.done && echo FIRSTBOOT_DONE
active
active
active
FIRSTBOOT_DONE
nginx-ui-address reads the VM's IP addresses from the Azure Instance Metadata Service at each boot, nginx-ui-firstboot ran once on the first boot, and nginx-ui and nginx are the web interface and the web server it manages. Only SSH and nginx listen on network ports; Nginx UI listens on the loopback address:
sudo ss -lntp | awk 'NR>1 {print $4, $6}' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g' | grep -v '127.0.0.5' | sort
0.0.0.0:22 users:(("sshd"),("systemd"))
0.0.0.0:443 users:(("nginx"),("nginx"),("nginx"))
0.0.0.0:80 users:(("nginx"),("nginx"),("nginx"))
127.0.0.1:9000 users:(("nginx-ui"))
[::]:22 users:(("sshd"),("systemd"))
[::]:443 users:(("nginx"),("nginx"),("nginx"))
[::]:80 users:(("nginx"),("nginx"),("nginx"))

Step 4: Get your URL and admin password
The URL and the admin password are in a file only root can read:
sudo cat /root/nginx-ui-credentials.txt
# Nginx UI 2.9.1 - generated on first boot by nginx-ui-firstboot.service.
# Every value below is unique to this VM; nothing was baked into the image. Keep this file private.
# Web interface (HTTPS, self-signed certificate unique to this VM): sign in as admin with the password below,
# then change it (top right -> Account).
NGINX_UI_URL=https://<public-ip>/
NGINX_UI_ADMIN_USERNAME=admin
NGINX_UI_ADMIN_PASSWORD=<NGINX_UI_ADMIN_PASSWORD>
NGINX_UI_HOST=<public-ip>
# The certificate served on 443 (/etc/nginx/cloudimg/tls/nginx-ui.crt), to compare with your browser's warning:
TLS_CERT_SHA256=<certificate-fingerprint>
# Self-test: sudo nginx-ui-selftest Serve the UI under your own DNS name: sudo nginx-ui-set-host <dns-name>
# Guide: https://www.cloudimg.co.uk/guides/nginx-ui-on-ubuntu-24-04-azure/
sudo stat -c '%a %U %n' /root/nginx-ui-credentials.txt
sudo grep -c '^NGINX_UI_ADMIN_PASSWORD=' /root/nginx-ui-credentials.txt
600 root /root/nginx-ui-credentials.txt
1
The URL uses the VM's public IP address as the Azure Instance Metadata Service reports it (the private address when the VM has no public IP). The certificate also names the private IP address. To serve Nginx UI under your own DNS name, see Step 8.
Step 5: Sign in
Open https://<public-ip>/ in your browser. The certificate is self signed (generated on your VM): compare its SHA-256 fingerprint with TLS_CERT_SHA256, accept it, and sign in as admin with NGINX_UI_ADMIN_PASSWORD.

Change the password at once from the account menu at the top right (Account), then update the file so the self-test keeps working (Step 11). The dashboard (shown at the top of this guide) reports the server's load and the nginx status.
Step 6: Manage nginx
Everything Nginx UI changes is the system nginx on this VM: your sites live in /etc/nginx/sites-available and /etc/nginx/sites-enabled, streams in /etc/nginx/streams-available and /etc/nginx/streams-enabled, exactly as on any Ubuntu server.
Manage Sites lists your sites. The image ships the stock Ubuntu default site, disabled, as a template. Add a site, edit it in the guided editor or as plain configuration, then enable it: Nginx UI writes the file, runs nginx -t and reloads nginx.


How this fits with the HTTPS front door:
- The server blocks that serve Nginx UI itself are in
/etc/nginx/conf.d/cloudimg-nginx-ui.conf. They are not one of your sites and do not appear in Manage Sites, so saving, enabling or deleting a site never touches them. They answer only for the VM's own names (the public and private IP addresses, or your own name after Step 8); every other host name on ports 80 and 443 is yours to serve. - A site of yours that claims
default_serveron port 80 or 443 conflicts with them:nginx -tfails, and Nginx UI refuses to enable the site and puts it back the way it was, while the web interface stays up. - Manage Configs can edit any file under
/etc/nginx, includingcloudimg-nginx-ui.conf. Editing or removing that file deliberately can take the web interface off line. If that happens, reach Nginx UI through an SSH tunnel and fix it there:
ssh -L 9000:127.0.0.1:9000 azureuser@<public-ip>
Then open http://127.0.0.1:9000/ on your own computer (the tunnel carries it over SSH).
Nginx UI can also reload and restart nginx (the buttons on the dashboard): nginx always runs as its own nginx.service, with the same metadata block.
Step 7: Certificates
To issue Let's Encrypt certificates from Nginx UI, first set the email address for your ACME account under Preferences then Certificate (the image ships none, so no ACME account exists until you choose one). Then open a site, switch on Encrypt with Let's Encrypt and follow the prompts. HTTP validation needs TCP 80 open to the internet in the network security group and a DNS record for the site pointing at the VM; DNS validation needs your DNS provider's API credentials (Certificates then DNS Credentials).
Step 8: Your own DNS name for the web interface
Point a DNS record (for example an Azure DNS A record, or the public IP's DNS name label <label>.<region>.cloudapp.azure.com) at the VM, then tell the image to serve Nginx UI under that name. This issues a new self signed certificate for it, updates nginx, Nginx UI's base URL and passkey settings and the credentials file:
sudo nginx-ui-set-host nginx-ui.<your-domain>
sudo nginx-ui-set-host with no name returns to the VM's own IP address. To use a certificate from your own CA or from Let's Encrypt for the web interface, replace /etc/nginx/cloudimg/tls/nginx-ui.crt and /etc/nginx/cloudimg/tls/nginx-ui.key (keep the key mode 0600) and run sudo systemctl reload nginx.
Step 9: The web terminal
Nginx UI's Terminal page is a shell on this VM in your browser: as shipped, a root shell, available to anyone signed in to Nginx UI. It is disabled on this image, and the page prints a notice instead:

Use SSH instead. If you want the terminal, keep in mind that anyone who can sign in to Nginx UI (or steal a signed-in session) then has a shell on the VM. To turn it on with a login prompt (the person must sign in with a Linux account and password on the VM), set StartCmd = login under [terminal] in /etc/nginx-ui/app.ini and restart Nginx UI:
sudo sed -i 's#^StartCmd = .*#StartCmd = login#' /etc/nginx-ui/app.ini && sudo systemctl restart nginx-ui
StartCmd = bash gives a root shell with no further prompt; we do not recommend it. The web interface cannot change this setting; only the file can. To turn the terminal off again, set it back to /usr/local/sbin/nginx-ui-terminal-disabled.
Step 10: Security checks
nginx-ui-selftest re-proves the appliance with this VM's credentials: the services, Nginx UI on 127.0.0.1:9000 only, HTTPS with this VM's certificate, the setup window closed and refused at nginx, the API refusing anonymous calls, a wrong and the default password refused, the password in the credentials file accepted, and Nginx UI reading the nginx status:
sudo nginx-ui-selftest
SELFTEST_OK nginx-ui+nginx active; Nginx UI 127.0.0.1:9000 only; https 200 (Nginx UI 2.9.1); setup closed and refused; API 403 anonymous; wrong/default password refused; per-VM password OK; host <public-ip>

The metadata block is a systemd IPAddressDeny rule on Nginx UI, nginx and the first-boot service, covering the Azure Instance Metadata Service (169.254.169.254) and the Azure WireServer (168.63.129.16). nginx-ui-address, which only reads the VM's IP addresses before anything else starts, and the Azure agent keep their access:
for u in nginx-ui nginx nginx-ui-firstboot nginx-ui-address walinuxagent; do printf '%s: ' $u; systemctl show -p IPAddressDeny --value $u.service | tr ' ' '\n' | sort | tr '\n' ' '; echo; done
nginx-ui: 168.63.129.16/32 169.254.169.254/32 ::ffff:168.63.129.16/128 ::ffff:169.254.169.254/128 fd00:ec2::254/128
nginx: 168.63.129.16/32 169.254.169.254/32 ::ffff:168.63.129.16/128 ::ffff:169.254.169.254/128 fd00:ec2::254/128
nginx-ui-firstboot: 168.63.129.16/32 169.254.169.254/32 ::ffff:168.63.129.16/128 ::ffff:169.254.169.254/128 fd00:ec2::254/128
nginx-ui-address:
walinuxagent:
The front door, from the VM itself (a state-changing request from another site's page, the setup routes, the setup status and an anonymous API call):
H=$(sudo sed -n 's/^NGINX_UI_HOST="\(.*\)"$/\1/p' /etc/nginx/cloudimg/nginx-ui-host.env)
C=(--cacert /etc/nginx/cloudimg/tls/nginx-ui.crt --resolve "$H:443:127.0.0.1")
sudo curl -s "${C[@]}" -o /dev/null -w '%{http_code}\n' -X POST -H 'Origin: https://evil.example' "https://$H/api/nginx/test"
sudo curl -s "${C[@]}" -w '\n' -X POST "https://$H/api/setup/install"
sudo curl -s "${C[@]}" -w '\n' "https://$H/api/install"
sudo curl -s "${C[@]}" -w '\n' "https://$H/api/user"
403
{"message":"Nginx UI setup is completed locally on this VM (cloudimg)"}
{"lock":true,"timeout":false}
{"message":"Authorization failed"}

Nginx UI authenticates every API call with the Authorization header (and a WebSocket with a short-lived token in its URL); it never accepts a browser cookie as a login, so a link on another site cannot act with your session.
Step 11: Passwords and access
- Change the admin password in the web interface (Account, top right), then put the new password into
/root/nginx-ui-credentials.txt(sudo nano /root/nginx-ui-credentials.txt) songinx-ui-selftestkeeps working, or delete the file once you have stored the password elsewhere. - Two-factor authentication. Nginx UI supports TOTP and passkeys (Account, then 2FA / Passkeys); once enabled, sensitive actions ask for a second factor. Passkeys need a DNS name (Step 8).
- More users. Add users under Users. Every Nginx UI user can change the nginx configuration of this server.
- Lockout. After
MaxAttemptswrong passwords from one address (10 by default,[auth]in/etc/nginx-ui/app.ini) Nginx UI blocks that address forBanThresholdMinutes; nginx also limits the login routes to 10 attempts a minute per address.
Step 12: Versions, provenance and file locations
nginx-ui --version | head -1
sed -n '1,2p' /usr/share/doc/nginx-ui/BUILD-PROVENANCE.txt
grep 'golang.org/x/net' /usr/share/doc/nginx-ui/go.mod
nginx -v
nginx-ui 2.9.1 2(576) 35e500f9 (go1.27.3 linux/amd64)
Nginx UI 2.9.1 on this image was compiled by cloudimg from the unmodified upstream source with one dependency
raised (golang.org/x/net v0.62.0). Nginx UI is licensed under the GNU AGPL-3.0 (LICENSE and SOURCE in this directory).
golang.org/x/net v0.62.0
nginx version: nginx/1.24.0 (Ubuntu)
Why a source build. Nginx UI 2.9.1 is the current upstream release. Two advisories in the Go networking library it uses (GO-2026-6860 and GO-2026-6866, golang.org/x/net/html) were published after it, and the vulnerability scanner of the Go project finds the first one reachable in the official release binary. cloudimg compiled the unmodified v2.9.1 source with upstream's own toolchain and build command and upstream's own (signature verified) web frontend, raising only golang.org/x/net to the fixed v0.62.0. /usr/share/doc/nginx-ui/BUILD-PROVENANCE.txt lists every input and checksum. The in-app Upgrade page installs upstream's official release binaries.
Known scanner finding. Scanners report GO-2026-4883 and GO-2026-4887 against the github.com/docker/docker module that Nginx UI links for its optional "nginx in a Docker container" mode. Both describe bugs in the Docker daemon (an authorization plugin bypass and a plugin privilege check), list no fixed version for that module, and cannot be reached here: Nginx UI only uses the Docker client library, and the image runs no Docker daemon.
| Path | What it is |
|---|---|
/etc/nginx-ui/app.ini |
Nginx UI settings (root only); its database database.db is next to it |
/root/nginx-ui-credentials.txt |
URL and admin password (root only) |
/etc/nginx/sites-available, /etc/nginx/sites-enabled |
your sites, as managed by Nginx UI |
/etc/nginx/conf.d/cloudimg-nginx-ui.conf |
the HTTPS front door for Nginx UI; certificate and key in /etc/nginx/cloudimg/tls/ |
/run/cloudimg-nginx-ui/address.env |
this VM's public and private IP addresses, read from Azure IMDS at each boot |
/usr/share/doc/nginx-ui/ |
LICENSE, SOURCE (the AGPL-3.0 source offer), BUILD-PROVENANCE.txt, go.mod and go.sum |
journalctl -u nginx-ui |
Nginx UI's log (also nginx, nginx-ui-firstboot, nginx-ui-address) |
Nginx UI's own log lists the requests it serves, including WebSocket URLs that carry a short-lived session token. The journal is readable only by root and the adm group; signing out ends those sessions.

Step 13: If you deployed before 28 October 2026
VMs deployed from the earlier version of this image run Nginx UI 2.4.2 on plain HTTP on port 9000, opened to the internet by the image's network rules, with the browser terminal on as a root shell. Nginx UI 2.4.2 is affected by 30 published Nginx UI security advisories, fixed in releases 2.5.0 to 2.9.0, including three rated critical: GHSA-qq4p-5m6w-ww57 (the node secret leaked into access logs), GHSA-7mm5-v557-38qq (remote code execution through backup restore) and GHSA-p393-cf76-4jmr (remote code execution through a restored settings file), and GHSA-w8p3-r29g-jg3p (a static node secret giving full admin without a login). Your VM keeps running as it is; nothing changes on it unless you act. To check your version:
/usr/local/bin/nginx-ui --version | head -1
If it reports nginx-ui 2.4.2, do this now:
- Close port 9000 to the internet. In the VM's network security group, delete the rule for TCP 9000, or limit its source to your own IP address. Until you redeploy, reach the interface through an SSH tunnel instead (
ssh -L 9000:127.0.0.1:9000 azureuser@<public-ip>, thenhttp://127.0.0.1:9000/). - Upgrade in place to 2.9.1 (the official release, checked against its published SHA-256):
cd /tmp && curl -fsSLO https://github.com/0xJacky/nginx-ui/releases/download/v2.9.1/nginx-ui-linux-64.tar.gz
echo '8caa38db01a1b97b742c07015d0e19269c2cd13fdcd08aa9e948a5435d35fb08 nginx-ui-linux-64.tar.gz' | sha256sum -c -
tar -xzf nginx-ui-linux-64.tar.gz nginx-ui
sudo systemctl stop nginx-ui
sudo cp -a /usr/local/bin/nginx-ui /usr/local/bin/nginx-ui.2.4.2.bak && sudo cp -a /usr/local/etc/nginx-ui /usr/local/etc/nginx-ui.2.4.2.bak
sudo install -o root -g root -m 0755 nginx-ui /usr/local/bin/nginx-ui && rm -f nginx-ui nginx-ui-linux-64.tar.gz
sudo systemctl start nginx-ui && /usr/local/bin/nginx-ui --version | head -1
- Turn the browser terminal off: set
StartCmd = /bin/falseunder[terminal]in/usr/local/etc/nginx-ui/app.ini, thensudo systemctl restart nginx-ui. - Rotate the secrets the old version may have exposed. Change the admin password in the web interface (and in
/root/nginx-ui-credentials.txt), and give Nginx UI a new node secret and session secret (every user is signed out):
sudo python3 -I -c '
import re, secrets
p = "/usr/local/etc/nginx-ui/app.ini"
s = open(p).read()
s = re.sub(r"(?m)^(JwtSecret\s*=).*$", lambda m: m.group(1) + " " + secrets.token_hex(32), s, count=1)
s = re.sub(r"(?ms)(^\[node\].*?^Secret\s*=).*?$", lambda m: m.group(1) + " " + secrets.token_hex(32), s, count=1)
open(p, "w").write(s)
' && sudo systemctl restart nginx-ui
- Look for activity you did not cause: users you did not create (Users), sites, streams or configuration you did not write, and requests in
sudo journalctl -u nginx-uito/api/restore,/api/backup,/api/installor/mcpthat were not yours. If you find any, treat the VM as compromised: build a new one from the current image and move your sites to it.
Or redeploy from the current image version (recommended: it adds HTTPS on 443, Nginx UI on localhost only, the metadata block and the rest of this guide). Copy your sites across with scp from /etc/nginx/sites-available, /etc/nginx/streams-available and your certificates (/etc/nginx/ssl if you issued them in Nginx UI), enable them on the new VM in Manage Sites, then delete the old VM. You do not need to contact anyone to do any of this; cloudimg support is there if you want help (support@cloudimg.co.uk).
Step 14: Backup and maintenance
- Back up
/etc/nginx(your sites, streams and certificates) and/etc/nginx-ui(Nginx UI's settings and database). Azure Backup of the VM's OS disk covers both. Nginx UI also has its own Backup page and scheduled Auto Backup (to a local directory or S3-compatible storage). - Ubuntu security updates, including nginx, install automatically (unattended-upgrades). Nginx UI updates are installed from its Upgrade page (upstream's official release binaries) or by deploying a newer image version.
Step 15: Security recommendations
- Limit TCP 443 in the network security group to the networks that need the web interface and your sites, and TCP 22 to your admin network. Nginx UI controls this server's web configuration: treat it as an administrative console, ideally reachable over a VPN, Azure Bastion or a private network only. If your sites must be public, consider serving them on this VM and reaching Nginx UI only through the SSH tunnel (Step 6).
- Change the admin password on first sign in, switch on two-factor authentication and give each person their own user (Step 11).
- Serve the web interface under your own DNS name with a trusted certificate (Step 8).
- Leave the browser terminal off unless you need it (Step 9), and keep Nginx UI on
127.0.0.1:9000.
Support and licensing
cloudimg provides 24/7 technical support for this image by email and live chat: support@cloudimg.co.uk. Nginx UI is licensed under the GNU Affero General Public License v3.0. Its licence and the offer of its complete corresponding source are on the VM in /usr/share/doc/nginx-ui/ (LICENSE and SOURCE); the source is the upstream tag v2.9.1 with the go.mod and go.sum shipped in that directory, and cloudimg provides it on request. nginx is licensed under the 2-clause BSD licence. NGINX is a trademark of F5, Inc. cloudimg is not affiliated with or endorsed by F5, Inc. or the Nginx UI project.