NextChat on Ubuntu 24.04 on Azure User Guide
Overview
This guide covers the deployment and configuration of NextChat on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. NextChat (formerly ChatGPT-Next-Web) is an open source, self hosted web interface for chatting with large language models. It gives a team one fast, private chat front end that talks to the model providers you already pay for, instead of routing conversations through somebody else's hosted product. Start and organise conversations in a sidebar, pin reusable system prompts, pick from a built in prompt template library, switch model or provider per conversation, and export or share a transcript. It is a progressive web app, so it installs to a desktop or phone home screen and works well on a small screen.
NextChat is a front end, not a model host. It calls an LLM provider API, so it runs comfortably on a small CPU only VM and needs no GPU. It supports OpenAI and Azure OpenAI, Anthropic, Google Gemini, DeepSeek, Moonshot, Alibaba Qwen, ByteDance, iFlytek, xAI, ChatGLM and SiliconFlow, plus any OpenAI compatible endpoint, so one deployment can front several providers at once.
The cloudimg image ships the free and open source, MIT licensed NextChat as the official upstream container pinned by image digest, captured into the VM so your instance starts in seconds and never silently upgrades. No provider API key is ever baked into the image, and because NextChat's own access check is disabled whenever no access code is configured, a unique 32 character access code is generated for each VM on first boot and a preflight guard refuses to start the service without one. Backed by 24/7 cloudimg support.
NextChat is a trademark of its respective owner. This image is produced by cloudimg and is not affiliated with, endorsed by, or sponsored by the NextChat project. It ships the free and open source MIT licensed self hosted software, unmodified.

What is included:
- NextChat 2.16.1 (the MIT licensed upstream container
yidadaa/chatgpt-next-web), pinned by image digest, not by a moving tag - Docker Engine and the Compose plugin from the official Docker APT repository
- The web UI published on port
80(mapped to the container's port3000); nothing else is exposed nextchat.serviceandnextchat-firstboot.serviceas systemd units, enabled and active on boot- A unique 32 character access code generated per VM on first boot, never baked into the image
- A fail closed preflight guard: the service refuses to start if the access code is missing, empty or too short
- Every LLM provider credential shipped empty — you always supply your own
- MCP server side tool execution left disabled, the upstream default
- 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 NextChat listing on Azure Marketplace
- An API key for at least one LLM provider (OpenAI, Anthropic, Google Gemini, DeepSeek and others are supported). cloudimg does not supply one and never bundles one.
Recommended virtual machine size: Standard_B2s (2 vCPU, 4 GB RAM) is ample. NextChat is a single prebuilt Node.js server with no database and no local model, so it idles at roughly 150 MB of memory. Increase the size only for a large number of concurrent conversations. NSG inbound: allow 22/tcp from your management network and 80/tcp for the web UI from the networks that use it.
Step 1: Deploy from the Azure Portal
Sign in to the Azure Portal, choose Create a resource, search the Marketplace for NextChat 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="nextchat-prod"; LOCATION="eastus"; VM_NAME="nextchat-01"
GALLERY_IMAGE_ID="/subscriptions/<sub-id>/resourceGroups/azure-cloudimg/providers/Microsoft.Compute/galleries/cloudimgGallery/images/nextchat-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 nc-vnet --address-prefix 10.100.0.0/16 --subnet-name nc-subnet --subnet-prefix 10.100.1.0/24
az network nsg create -g "$RG" --name nc-nsg
az network nsg rule create -g "$RG" --nsg-name nc-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 nc-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_B2s --storage-sku StandardSSD_LRS \
--admin-username azureuser --ssh-key-values "$SSH_KEY" \
--vnet-name nc-vnet --subnet nc-subnet --nsg nc-nsg --public-ip-sku Standard
Step 3: Connect to your VM
ssh azureuser@<vm-ip>
Step 4: Confirm the services are running
NextChat runs as one container under nextchat.service, published on host port 80. Confirm the services are active and see the container:
sudo systemctl is-active docker nextchat-firstboot nextchat
sudo docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
All three services report active, and the container is Up with only 0.0.0.0:80->3000/tcp published:
active
active
active
NAMES STATUS PORTS
nextchat Up About a minute 0.0.0.0:80->3000/tcp, [::]:80->3000/tcp
Step 5: Read the per instance access code
A unique 32 character access code was generated for this VM on its first boot, before the app was reachable, and written to a root only file. Read it:
sudo cat /root/nextchat-credentials.txt
The file (mode 0600 root:root) holds NEXTCHAT_URL and NEXTCHAT_ACCESS_CODE. Neither ships in the image; the code is unique to this VM, so no two instances share one and there is no default to change.

Step 6: Understand the access code security model
This is the most important section of this guide. NextChat protects a server side provider key with an access code. Its own rule is that the check is enabled only when at least one code is configured — so an empty access code silently turns the gate off, and an instance with a working provider key would then let anyone who finds the address spend your credits. This image closes that: the code is generated per VM, and nextchat.service runs a preflight guard that refuses to start without one.
Confirm the gate is on and that requests without a valid code are refused:
B=http://localhost
curl -s $B/api/config | jq '{needCode, hideUserApiKey, disableGPT4}'
curl -s -o /dev/null -w 'no access code: HTTP %{http_code}\n' -X POST \
-H 'Content-Type: application/json' -d '{"model":"gpt-4o-mini","messages":[]}' \
$B/api/openai/v1/chat/completions
curl -s -X POST -H 'Content-Type: application/json' \
-d '{"model":"gpt-4o-mini","messages":[]}' $B/api/openai/v1/chat/completions | jq -c .
curl -s -o /dev/null -w 'wrong access code: HTTP %{http_code}\n' -X POST \
-H 'Content-Type: application/json' -H 'Authorization: Bearer nk-not-the-code' \
-d '{"model":"gpt-4o-mini","messages":[]}' $B/api/openai/v1/chat/completions
curl -s -X POST -H 'Content-Type: application/json' -H 'Authorization: Bearer nk-not-the-code' \
-d '{"model":"gpt-4o-mini","messages":[]}' $B/api/openai/v1/chat/completions | jq -c .
needCode is true (the gate is enabled), and both an absent and a wrong access code are refused with 401:
{
"needCode": true,
"hideUserApiKey": false,
"disableGPT4": false
}
no access code: HTTP 401
{"error":true,"msg":"empty access code"}
wrong access code: HTTP 401
{"error":true,"msg":"wrong access code"}

Now send the same request with your access code from Step 5:
CODE='<NEXTCHAT_ACCESS_CODE>'
curl -s -X POST -H 'Content-Type: application/json' \
-H "Authorization: Bearer nk-${CODE}" \
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"ping"}]}' \
http://localhost/api/openai/v1/chat/completions | jq -c .
Before you have set a provider key the reply is no longer a NextChat refusal — it is an error from the provider itself, which is how you know the access code was accepted and your request was forwarded:
{"error":{"message":"Incorrect API key provided: nk-...","type":"invalid_request_error","param":null,"code":"invalid_api_key"}}
An "error": true with a "msg" field is NextChat refusing you. An "error" object with a "message" field is your provider answering. Step 7 supplies the key that turns this into a real model response.
Step 7: Supply your own LLM provider API key
cloudimg never ships a provider key. There are two ways to supply yours.
Option A — each person brings their own key (no server configuration). Open the web UI, go to Settings, and paste a personal API key into the provider key field. Requests then use that person's key and their own billing. Nothing needs to change on the VM.
Option B — one key for everyone on this VM. Set it once in the environment file. Edit /var/lib/nextchat/.env with sudo nano /var/lib/nextchat/.env and fill in the provider you use:
# one of these, matching your provider
OPENAI_API_KEY=<your-token>
BASE_URL=https://api.openai.com
# or Anthropic
ANTHROPIC_API_KEY=<your-token>
# or Google Gemini
GOOGLE_API_KEY=<your-token>
Then restart the service to apply it:
sudo systemctl restart nextchat.service
With Option B the key lives only on your VM, and the access code from Step 5 is what stops anyone else using it. Any OpenAI compatible endpoint works: point BASE_URL at it and set OPENAI_API_KEY to that service's key.
The environment file is the whole configuration surface. Every provider credential ships empty:
sudo grep -vE '^\s*#' /var/lib/nextchat/.env | grep -v '^$' \
| sed -E 's|^(CODE=).*|\1<generated on this VM at first boot>|'
CODE=<generated on this VM at first boot>
OPENAI_API_KEY=
BASE_URL=https://api.openai.com
OPENAI_ORG_ID=
ANTHROPIC_API_KEY=
ANTHROPIC_API_VERSION=
ANTHROPIC_URL=
GOOGLE_API_KEY=
GOOGLE_URL=
DEEPSEEK_API_KEY=
SILICONFLOW_API_KEY=
SILICONFLOW_URL=
ENABLE_MCP=
HIDE_USER_API_KEY=
ENABLE_BALANCE_QUERY=
DISABLE_GPT4=
DISABLE_FAST_LINK=
CUSTOM_MODELS=
DEFAULT_MODEL=
Step 8: Open the web UI and enter the access code
Browse to http://<vm-ip>/ to reach NextChat. Open Settings and paste the NEXTCHAT_ACCESS_CODE from Step 5 into Access Code. The panel confirms Access control Enabled. This is also where a user pastes their own provider key if you chose Option A in Step 7.

Step 9: Start a conversation
Close Settings and you are on the conversation workspace: the conversation list on the left, the message pane in the middle, and the composer at the bottom. Press New Chat to start a fresh conversation at any time.

Type a message and press Send. The reply streams back into the conversation and renders as formatted markdown, with the model name shown above it. This is the whole product: your message goes from the browser to NextChat on your VM, out through its server side provider proxy to the model provider you configured, and the reply comes back and renders here.

Step 10: Use the prompt template library
Choose Mask in the sidebar to open the built in Prompt Template library — reusable system prompts that pre-configure a conversation for a task, each with its own model and settings. Filter by language, search, press Chat to start a conversation from a template, or Create to save your own.

Step 11: Confirm the pinned release and its licence
The container is pinned by image digest, not by a moving tag, so your instance ships an exact, reproducible artifact. The image's own OCI labels record the upstream release, its licence and the exact source commit it was built from:
NC_IMAGE='yidadaa/chatgpt-next-web@sha256:eaaa469ddeeb5fa58fb35f8767e9e096a2f1c8468c54b6703323624bf3071c5a'
sudo docker inspect --format '{{json .Config.Labels}}' "$NC_IMAGE" | jq '{
version: ."org.opencontainers.image.version",
licence: ."org.opencontainers.image.licenses",
source: ."org.opencontainers.image.source",
revision:."org.opencontainers.image.revision"}'
{
"version": "v2.16.1",
"licence": "MIT",
"source": "https://github.com/ChatGPTNextWeb/NextChat",
"revision": "557a2cce357749c6fb3176d42e03ff6f7de4d355"
}

Step 12: Confirm the fail closed guard
nextchat.service runs a preflight guard before every start. If the access code is ever removed, emptied or shortened, the service refuses to start rather than come up ungated. Confirm the guard is wired in, and see it refuse:
sudo systemctl show -p ExecStartPre --value nextchat.service | head -1
printf 'CODE=\n' | sudo tee /tmp/demo.env >/dev/null
sudo /usr/local/sbin/nextchat-preflight.sh /tmp/demo.env; echo "exit=$?"
sudo rm -f /tmp/demo.env
The unit shows the guard in its ExecStartPre, and the guard exits 1 on an empty access code:
exit=1
nextchat-preflight: REFUSING TO START — CODE is empty in /tmp/demo.env. NextChat would run with NO access-code gate, leaving any configured provider key open to the internet. Set a long random CODE and try again.
Step 13: Rotate the access code
The access code is a shared secret for this instance. To roll it, write a new long random value and restart:
NEW_CODE="$(openssl rand -base64 64 | tr -dc 'A-Za-z0-9' | cut -c1-32)"
sudo sed -i "s|^CODE=.*|CODE=${NEW_CODE}|" /var/lib/nextchat/.env
sudo sed -i "s|^NEXTCHAT_ACCESS_CODE=.*|NEXTCHAT_ACCESS_CODE=${NEW_CODE}|" /root/nextchat-credentials.txt
sudo systemctl restart nextchat.service
sudo systemctl is-active nextchat.service
Everyone then re-enters the new code in Settings. CODE accepts a comma separated list, so you can also issue a different code per person and revoke one without disturbing the others.
Step 14: Server components
| Component | Version / Detail |
|---|---|
| Application | NextChat 2.16.1 (yidadaa/chatgpt-next-web), pinned by image digest |
| Upstream source | github.com/ChatGPTNextWeb/NextChat, commit 557a2cce (git tag v2.16.1) |
| Runtime | Node.js 18 (prebuilt Next.js standalone server) inside the container |
| Front door | container port 3000, published on host :80 |
| Orchestration | Docker Compose under nextchat.service |
| Access code | per instance, 32 characters, generated first boot into /var/lib/nextchat/.env and /root/nextchat-credentials.txt |
| Fail closed guard | /usr/local/sbin/nextchat-preflight.sh as the unit's ExecStartPre |
| Provider credentials | none shipped; supplied by you in /var/lib/nextchat/.env or per user in the browser |
| Server side data | none; conversations are stored in the browser |
| MCP tool execution | disabled (ENABLE_MCP empty, the upstream default) |
| Operating system | Ubuntu 24.04 LTS (patched at build) |
| License | MIT (NextChat) |
Step 15: Managing the service
sudo systemctl status nextchat --no-pager | head -12
sudo docker compose -f /var/lib/nextchat/docker-compose.yml logs --tail 40 nextchat
Restart the app with sudo systemctl restart nextchat.service, follow the logs live with sudo docker compose -f /var/lib/nextchat/docker-compose.yml logs -f nextchat, and view configuration in /var/lib/nextchat/.env. After changing the environment file, restart the service to apply it.
Conversations are stored in the browser, not on the server, so there is no server side database to back up. Each user can export their conversations from the NextChat UI.
Step 16: Use your own domain and HTTPS (production)
The image serves the web UI over plain HTTP on port 80, which is convenient behind a load balancer or private network. For production you should terminate TLS in front of the VM so the access code and your conversations travel encrypted:
- Front the VM with Azure Application Gateway, a load balancer with a managed certificate, or a CDN, and forward to the app on port
80. - Keep the NSG rule for port
80restricted to the front door, not the whole internet.
Step 17: Security recommendations
- Restrict the NSG. Allow TCP 80 (and 22 for admin) only from the networks that use the app. The access code is the application level control; the NSG is the network level one, and you want both.
- Terminate TLS in front of the app (Step 16) so the access code and conversation content are encrypted in transit.
- Rotate the access code (Step 13) when someone leaves, and issue per person codes using the comma separated list if you want to revoke individually.
- Never remove the access code. The service will refuse to start without one, which is deliberate — an ungated instance with a provider key configured is an open relay on your credits.
- Set a provider key rather than leaving one unset. With the access code accepted but no server side key configured, NextChat forwards your request to the provider using the access code as the API key, so the code reaches your provider in a failed request. Configuring a real key in Step 7 (or having each user supply their own in the browser) avoids that.
- Consider
HIDE_USER_API_KEY=1if you want everyone to use the server key only and to stop users supplying their own in the browser. - Leave MCP disabled unless you specifically need it; enabling it lets the server execute tools on this VM.
- Watch your provider spend. The access code protects the key, but anyone holding the code can spend on it. Set a usage limit in your provider's console.
- Keep the OS patched. Unattended security upgrades remain enabled on the running VM.
Step 18: Support and Licensing
NextChat is distributed under the MIT License. This cloudimg image bundles the unmodified official open source release, pinned by image digest; cloudimg provides the packaging, the per instance access code generation, the fail closed service guard, the paired deploy guide, and 24/7 support with a guaranteed 24 hour response SLA. There is no per user or per seat software fee. You pay your own model provider directly for token usage. NextChat is an independent open source project and this image is not affiliated with or endorsed by it.