Hugging Face Chat UI on Ubuntu 24.04 on Azure User Guide
Overview
This guide covers the deployment and configuration of Hugging Face Chat UI on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. Chat UI is the open source codebase that powers HuggingChat, packaged here as a private, self hosted chat front end for your own team. It gives you a fast, familiar chat interface that talks to the model endpoint you choose, with conversations stored in your own database on your own VM rather than in somebody else's hosted product. Start conversations, keep them organised in a sidebar, switch model per conversation, upload images to multimodal models, watch replies stream token by token, and export your own data whenever you want.
Chat UI is a front end, not a model host. It calls an OpenAI compatible endpoint and discovers that endpoint's model list automatically, so it runs comfortably on a small CPU only VM and needs no GPU. Point it at the Hugging Face Inference Router for hundreds of open models, at OpenAI or Azure OpenAI, or at your own private Ollama, vLLM, TGI or LM Studio server, and the model picker fills itself in.
The cloudimg image ships the free and open source, Apache 2.0 licensed Chat UI as the official upstream container pinned by image digest, alongside MongoDB (also pinned by digest) for conversation history. No model provider token is ever baked into the image. Chat UI has no password login of its own, so the image puts an nginx access gateway in front of it: a unique 32 character credential is generated for each VM on first boot, and both chat-ui.service and nginx.service refuse to start without it. MongoDB listens on the loopback address only, with its own password generated per VM. Backed by 24/7 cloudimg support.
Hugging Face and Chat UI are trademarks of their respective owners. This image is produced by cloudimg and is not affiliated with, endorsed by, or sponsored by Hugging Face. It ships the free and open source Apache 2.0 licensed self hosted software, unmodified.

What is included:
- Hugging Face Chat UI v0.9.6 (the Apache 2.0 licensed upstream container
ghcr.io/huggingface/chat-ui), pinned by image digest, not by a moving tag - MongoDB 8.0.28 (official Docker Library image, pinned by digest) as a single node replica set for conversation history
- Docker Engine and the Compose plugin from the official Docker APT repository
- An nginx access gateway on port
80, installed as a host package so unattended security upgrades keep the internet facing component patched - A unique 32 character web credential and a unique MongoDB password generated per VM on first boot, never baked into the image
- Fail closed guards: both the application service and the gateway refuse to start if the access credential is missing, empty or unhashed
- MongoDB published on
127.0.0.1only, with authorization enforced and a per VM keyFile - The model provider token shipped empty — you always supply your own
- Ubuntu 24.04 LTS base with latest security patches applied at build time
- Azure Linux Agent for seamless cloud integration and SSH key injection
- 24/7 cloudimg support with guaranteed 24 hour response SLA
Prerequisites
- Active Azure subscription, SSH public key, VNet + subnet in target region
- Subscription to the Hugging Face Chat UI listing on Azure Marketplace
- A model endpoint Chat UI can reach. The image ships pointed at the public Hugging Face Inference Router, whose model list is readable without a token, so the interface works the moment the VM boots. Sending a message needs your own API token, or your own OpenAI compatible endpoint. cloudimg does not supply one and never bundles one.
- Outbound HTTPS from the VM to whichever model endpoint you configure. Chat UI reads its model list at startup and will not serve until that endpoint answers.
Recommended virtual machine size: Standard_B2ms (2 vCPU, 8 GB RAM). Chat UI itself is a small Node.js server, but it shares the VM with MongoDB, which is where the extra memory goes as conversation history grows. Standard_B2s (2 vCPU, 4 GB) works for light single user use — the image is built and tested on it, with the MongoDB cache capped at 1 GB and no swap. NSG inbound: allow 22/tcp from your management network and 80/tcp from the networks that use the interface.
Step 1: Deploy from the Azure Portal
Sign in to the Azure Portal, choose Create a resource, search the Marketplace for Hugging Face Chat UI by cloudimg, and select Create. On Basics pick your subscription, resource group, region and size; under Administrator account choose SSH public key and paste your key; under Inbound port rules allow SSH (22) and HTTP (80). Then Review + create and Create.
Step 2: Deploy from the Azure CLI
RG="chat-ui-prod"; LOCATION="eastus"; VM_NAME="chat-ui-01"
GALLERY_IMAGE_ID="/subscriptions/<sub-id>/resourceGroups/azure-cloudimg/providers/Microsoft.Compute/galleries/cloudimgGallery/images/huggingface-chat-ui-ubuntu-24-04/versions/<version>"
SSH_KEY="$(cat ~/.ssh/id_rsa.pub)"
az group create --name "$RG" --location "$LOCATION"
az network vnet create -g "$RG" --name cu-vnet --address-prefix 10.100.0.0/16 --subnet-name cu-subnet --subnet-prefix 10.100.1.0/24
az network nsg create -g "$RG" --name cu-nsg
az network nsg rule create -g "$RG" --nsg-name cu-nsg --name allow-ssh --priority 100 \
--source-address-prefixes "<your-mgmt-cidr>" --destination-port-ranges 22 --access Allow --protocol Tcp
az network nsg rule create -g "$RG" --nsg-name cu-nsg --name allow-http --priority 110 \
--source-address-prefixes "<your-mgmt-cidr>" --destination-port-ranges 80 --access Allow --protocol Tcp
az vm create -g "$RG" --name "$VM_NAME" --image "$GALLERY_IMAGE_ID" \
--size Standard_B2ms --storage-sku StandardSSD_LRS \
--admin-username azureuser --ssh-key-values "$SSH_KEY" \
--vnet-name cu-vnet --subnet cu-subnet --nsg cu-nsg --public-ip-sku Standard
Step 3: Connect to your VM
ssh azureuser@<vm-ip>
Step 4: Confirm the services are running
Chat UI runs as two containers under chat-ui.service, behind an nginx gateway on host port 80. Confirm everything is active and see which images are running:
sudo systemctl is-active docker chat-ui-firstboot chat-ui nginx
sudo docker compose -f /var/lib/chat-ui/docker-compose.yml ps \
--format 'table {{.Name}}\t{{.Status}}'
All four services report active, and both containers are Up:
active
active
active
active
NAME STATUS
chat-ui Up 2 minutes
chat-ui-mongo Up 2 minutes (healthy)
Both images are pinned by digest, so the VM you booted is the exact artifact cloudimg tested:
sudo docker inspect --format '{{index .Config.Labels "org.opencontainers.image.version"}} {{index .Config.Labels "org.opencontainers.image.licenses"}} {{index .Config.Labels "org.opencontainers.image.revision"}}' \
ghcr.io/huggingface/chat-ui@sha256:41e450e64739b3ca45d7db1752de9a9eea11bb0829706f00b8dd7d3be016657b
The licence and the upstream commit are recorded in the image itself:
latest Apache-2.0 a33743a5c35ca0774ca82aadc2b49994a82e3d67
Commit a33743a5 is exactly what upstream git tag v0.9.6 points at.
Step 5: Read the per instance credentials
A unique 32 character web credential and a unique MongoDB password were generated for this VM on its first boot, before anything was reachable, and written to a root only file. Read them:
sudo cat /root/chat-ui-credentials.txt
The file (mode 0600 root:root) holds CHAT_UI_URL, CHAT_UI_USERNAME, CHAT_UI_PASSWORD, MONGODB_USERNAME and MONGODB_PASSWORD. None of them ship in the image; each is unique to this VM, so no two instances share one and there is no default to change.

Step 6: Open the web interface
Browse to the CHAT_UI_URL from Step 5 — http://<vm-ip>/. The browser asks for a username and password: enter chatui and the CHAT_UI_PASSWORD from the credentials file. Chat UI then loads with its model picker already populated from the configured endpoint.

Type a message and press Enter. The reply streams back token by token from the model endpoint, the conversation is titled automatically and appears in the sidebar, and everything is written to the MongoDB on this VM:

Step 7: Understand the access gate
This is the most important section of this guide. Chat UI has no password login of its own — upstream offers OpenID Connect single sign on, and nothing else. An instance left reachable on a public IP with a working model provider token would let anyone who finds the address spend your inference credit. This image closes that with an nginx gateway that denies every route by default, with a credential generated per VM.
Confirm the gate refuses anything but your own credential:
B=http://localhost
curl -s -o /dev/null -w 'no credentials: HTTP %{http_code}\n' $B/
curl -s -o /dev/null -w 'wrong password: HTTP %{http_code}\n' -u 'chatui:WRONGPASSWORD' $B/
curl -s -o /dev/null -w 'wrong username: HTTP %{http_code}\n' -u 'nobody:WRONGPASSWORD' $B/
curl -s -o /dev/null -w 'settings page: HTTP %{http_code}\n' $B/settings
curl -s -o /dev/null -w 'models API: HTTP %{http_code}\n' $B/api/models
echo -n 'liveness probe: '; curl -s $B/healthcheck; echo
Everything is refused with 401 except the static liveness probe, which returns the literal string OK and nothing else so a load balancer never needs to hold this VM's credential:
no credentials: HTTP 401
wrong password: HTTP 401
wrong username: HTTP 401
settings page: HTTP 401
models API: HTTP 401
liveness probe: OK

Now send the same request with your password. Read it straight out of the credentials file rather than typing it, so it never lands in your shell history:
P=$(sudo grep -m1 '^CHAT_UI_PASSWORD=' /root/chat-ui-credentials.txt | cut -d= -f2-)
curl -s -o /dev/null -w 'your credential: HTTP %{http_code}\n' -u "chatui:$P" http://localhost/
your credential: HTTP 200
The gate is fail closed, not merely configured. Both services carry a preflight guard that refuses to start if the credential file is missing, empty, holds a plaintext entry, or if the site configuration disables authentication anywhere but the liveness probe:
systemctl show -p ExecStartPre --value chat-ui.service | tr ';' '\n' | grep -o '/usr/local/sbin/[^ "]*'
systemctl show -p ExecStartPre --value nginx.service | tr ';' '\n' | grep -o '/usr/local/sbin/[^ "]*'
sudo /usr/local/sbin/chat-ui-preflight.sh --nginx --gate-file /dev/null; echo "exit=$?"
Both units run the guard, and the guard rejects an empty credential file rather than serving without one:
/usr/local/sbin/chat-ui-preflight.sh
/usr/local/sbin/chat-ui-preflight.sh
chat-ui-preflight: REFUSING TO START — /dev/null is empty — the HTTP Basic gate would accept nobody and protect nothing.
exit=1
Change the password at any time by running sudo htpasswd -B /etc/nginx/chat-ui.htpasswd chatui, which prompts for the new value. The gate reads the file on every request, so no restart is needed. To add a second person, run the same command with a different username. To use single sign on instead, configure OPENID_CLIENT_ID and OPENID_CLIENT_SECRET in /var/lib/chat-ui/.env.local; keep the gateway credential in place as well, since it is what the preflight guard checks.
Step 8: Point Chat UI at your own model endpoint
cloudimg never ships a model provider token. The image is configured for the public Hugging Face Inference Router, whose model list is readable without one — which is why the interface and the model picker work immediately:

To actually send messages, add your own token. The whole configuration surface is one file:
sudo grep -vE '^\s*#' /var/lib/chat-ui/.env.local | grep -v '^$' \
| sed -E 's|^(MONGODB_URL=).*|\1<generated on this VM at first boot>|'
OPENAI_BASE_URL=https://router.huggingface.co/v1
OPENAI_API_KEY=
MONGODB_URL=<generated on this VM at first boot>
MONGODB_DB_NAME=chat-ui
COOKIE_SAMESITE=lax
COOKIE_SECURE=false
PROTOCOL_HEADER=x-forwarded-proto
HOST_HEADER=x-forwarded-host
TASK_MODEL=
OPENID_CLIENT_ID=
OPENID_CLIENT_SECRET=
LLM_SUMMARIZATION=true
ENABLE_DATA_EXPORT=true
Edit that file with your preferred editor as root, set OPENAI_API_KEY to your own token, and restart:
# Hugging Face Inference Router (the shipped default)
OPENAI_BASE_URL=https://router.huggingface.co/v1
OPENAI_API_KEY=<your-token>
# or OpenAI
# OPENAI_BASE_URL=https://api.openai.com/v1
# or your own private endpoint - Ollama, vLLM, TGI, LM Studio
# OPENAI_BASE_URL=http://10.100.1.20:11434/v1
# OPENAI_API_KEY=not-used-but-must-not-be-empty
sudo systemctl restart chat-ui.service
Chat UI rediscovers the model list from ${OPENAI_BASE_URL}/models on every start, so whatever your endpoint serves is what appears in the picker. The configured endpoint is shown in the interface under Settings, along with the exact upstream commit this image was built from:

Your token lives only on this VM, and the access gate from Step 7 is what stops anyone else using it.
Step 9: The database
Conversation history lives in MongoDB on this VM. It is published on the loopback address only and is never reachable from the network:
sudo ss -lntp | grep -E ':(80|3000|27017) ' | awk '{print $1, $4}'
Only nginx is on a routable address. Chat UI itself and MongoDB are both loopback bound, so every request has to pass the gate:
LISTEN 0.0.0.0:80
LISTEN 127.0.0.1:3000
LISTEN 127.0.0.1:27017
LISTEN [::]:80

Administer it from the VM, reading the password out of the credentials file so it never lands in your shell history:
MP=$(sudo grep -m1 '^MONGODB_PASSWORD=' /root/chat-ui-credentials.txt | cut -d= -f2-)
sudo docker exec chat-ui-mongo mongosh --quiet \
-u chatui_admin -p "$MP" --authenticationDatabase admin chat-ui \
--eval 'print("collections: " + db.getCollectionNames().length)' \
--eval 'print("conversations: " + db.conversations.countDocuments({}))'
collections: 14
conversations: 0
Back it up with mongodump, writing the archive out of the container:
MP=$(sudo grep -m1 '^MONGODB_PASSWORD=' /root/chat-ui-credentials.txt | cut -d= -f2-)
sudo docker exec chat-ui-mongo mongodump --quiet \
-u chatui_admin -p "$MP" --authenticationDatabase admin \
--db chat-ui --archive=/tmp/chat-ui-backup.archive
sudo docker cp chat-ui-mongo:/tmp/chat-ui-backup.archive /var/backups/chat-ui-backup.archive
sudo ls -lh /var/backups/chat-ui-backup.archive
The data directory itself is /var/lib/chat-ui/mongodb on the OS disk, so an Azure disk snapshot of the VM captures it too.
Step 10: Day to day operation
sudo systemctl status chat-ui.service --no-pager | head -8
sudo docker compose -f /var/lib/chat-ui/docker-compose.yml logs --tail 15 chat-ui
Restart the application after any configuration change, and reload the gateway after any nginx change:
sudo systemctl restart chat-ui.service
sudo nginx -t && sudo systemctl reload nginx
Ubuntu security updates are applied automatically by unattended-upgrades, which covers the nginx gateway. The two container images are pinned by digest and are deliberately not auto updated: to move to a newer Chat UI release, edit the image: digest in /var/lib/chat-ui/docker-compose.yml, then sudo systemctl restart chat-ui.service.
Security hardening
- Restrict port 80 in the network security group to the networks that actually use the interface. The access gate is a credential, not a substitute for a firewall.
- Put TLS in front. The gate uses HTTP Basic authentication, which sends the credential base64 encoded, not encrypted — so on an untrusted network it must run over HTTPS. Terminate TLS on an Azure Application Gateway, an Azure Front Door, or with certbot on this VM. Then set
COOKIE_SECURE=truein/var/lib/chat-ui/.env.localand restart, so session cookies are only sent over the encrypted connection. LeavePROTOCOL_HEADERandHOST_HEADERas shipped: they make Chat UI follow whatever scheme actually reached the gateway, so its own links stay correct either way. - Rotate the web credential with
sudo htpasswd -B /etc/nginx/chat-ui.htpasswd chatuiwhenever someone leaves. - Keep MongoDB where it is. It is loopback bound with authorization enforced; publishing it on a routable address would expose every conversation on the VM.
- Watch your inference spend at the provider. The gate stops strangers, but anyone with the credential can use your token, so use your provider's own usage limits as a second control.
Troubleshooting
The interface returns 502. Chat UI is not answering on its loopback port. The usual cause is that the VM cannot reach the configured model endpoint — Chat UI reads its model list at startup and refuses to serve until that endpoint answers:
sudo docker compose -f /var/lib/chat-ui/docker-compose.yml logs --tail 20 chat-ui | tail -20
Look for [models] Using OpenAI-compatible base URL followed by a non 200 fetch status. Fix OPENAI_BASE_URL (and OPENAI_API_KEY if your endpoint requires one for its model list) in /var/lib/chat-ui/.env.local and restart.
nginx did not start. The gateway refuses to run without a credential. Check the guard's own verdict:
sudo /usr/local/sbin/chat-ui-preflight.sh --nginx; echo "exit=$?"
Messages fail with a provider error. The gate accepted you and the request reached your provider — set or correct OPENAI_API_KEY. An HTTP 401 from the gate looks quite different: it never reaches Chat UI at all.
Conversations disappear after a restart. Confirm Chat UI is really using this VM's MongoDB rather than falling back to an in-memory database:
sudo docker compose -f /var/lib/chat-ui/docker-compose.yml logs chat-ui 2>/dev/null \
| grep -ci 'in-memory server' || echo 0
0 is correct. Anything higher means MONGODB_URL is not reaching the container.
Support
cloudimg provides 24/7 support with a guaranteed 24 hour response SLA for this image. Contact support@cloudimg.co.uk with your Azure subscription ID and the VM name.
Upstream project documentation lives at github.com/huggingface/chat-ui.