Deployment guide

NextChat 2.16.1

Overview

This guide covers launching and verifying NextChat 2.16.1 on Ubuntu 24.04 from SecureAnyCloud on Amazon EC2. It is one version of the SecureAnyCloud Hardened Ollama, LiteLLM & Self Hosted LLM Server AMIs listing on AWS Marketplace. You pick it as the software version when you subscribe and launch. The listing's guide hub describes the other LLM servers, gateways and chat interfaces in the listing.

NextChat (formerly ChatGPT-Next-Web) is an open-source (MIT) web interface for chatting with large language models. It gives a team one fast chat front end for the providers you already use: OpenAI and Azure OpenAI, Anthropic, Google Gemini, DeepSeek, Moonshot, Alibaba Qwen, xAI and others. It also works with any OpenAI-compatible endpoint, such as an Ollama, LocalAI or llama.cpp server.

NextChat has a conversation sidebar, a prompt template library, per-conversation model switching, markdown and code rendering, and chat export.

NextChat is a chat client, not a model host. The models run at your providers or on your own inference server, so it needs no GPU. Conversations and settings are kept in your browser's local storage, not on the server.

What you get:

  • NextChat 2.16.1, the upstream container image yidadaa/chatgpt-next-web:v2.16.1, pinned by digest sha256:eaaa469ddeeb5fa58fb35f8767e9e096a2f1c8468c54b6703323624bf3071c5a, run by Docker CE (official Docker apt repository)
  • NextChat bound to loopback only (127.0.0.1:3000), with nginx as the only network-facing service
  • HTTPS on port 443 with a self-signed certificate created on your instance's first boot, and port 80 redirecting to HTTPS
  • A per-instance login (HTTP Basic authentication, user admin) and a per-instance NextChat access code, both generated on first boot into a root-only file
  • nextchat-set-hostname, a helper that serves NextChat under your own DNS name and re-issues the certificate for it

Default user: ubuntu. Architecture: x86_64. Root volume: 8 GiB gp3.

Security posture

  • No default credential. The login password, the access code and the session-gate token are generated on each instance's first boot. The password is stored only as a bcrypt hash (/etc/nginx/nextchat.htpasswd) and in a root-only file. None of them is in the image, so no two instances share one.
  • Every page needs the login. NextChat has no user accounts of its own, so nginx enforces a login in front of the UI. The API routes the UI uses also need a session cookie, which nginx issues only after a successful login. The only open path is nginx's own /healthz literal, which never reaches NextChat.
  • The open proxy is closed. Upstream NextChat has a route (/api/proxy) that fetches any address a caller puts in a request header, with no access check. It also has WebDAV and Upstash sync routes that fetch a caller-supplied endpoint. nginx allows only the provider routes (which call the endpoint configured on the server) and /api/config, and every other /api path returns 403. NextChat's plugin, cloud-sync and MCP features are therefore off. Page routes accept only GET, so Next.js server actions cannot be called.
  • The access code gates server-side keys. If you configure a provider key on the server, NextChat uses it only for requests that carry this instance's access code.
  • Fails closed. nginx will not start until first boot has finished and the password and session files exist. The container will not start without a full-length access code.
  • Only nginx faces the network. It answers only for the configured hostname (any other Host header gets its connection closed) and uses TLS 1.2 or 1.3. It refuses cross-origin POST requests and WebSocket upgrades (403), strips the upstream wildcard CORS headers, and never forwards a WebSocket upgrade to NextChat. It also replaces any client-supplied X-Forwarded-For, X-Real-IP or Forwarded header with the real peer address.
  • The hostname comes from configuration, never from a request. First boot reads it from EC2 instance metadata (IMDSv2), or from /etc/sac/nextchat-hostname if you set one.
  • No instance metadata access for the app. A Docker DOCKER-USER firewall rule blocks the container from the instance metadata service (169.254.169.254 and fd00:ec2::254), and the rule is in place before the container starts. systemd IPAddressDeny blocks nginx from it.
  • IMDSv2 required. The AMI is registered with ImdsSupport v2.0, so instances launched from it refuse token-less (IMDSv1) metadata requests.
  • No third-party analytics. The upstream image has its maintainer's Google Analytics ID built in, and an empty setting cannot switch it off. This image removes that ID from every page nginx serves. It also sends a Content Security Policy that only allows scripts from your instance (and the public npm CDNs used by HTML previews), so Google Analytics never loads. Next.js telemetry is also off.
  • Per-instance identity. SSH host keys, the machine ID and Docker's engine ID are generated fresh on every instance.

Prerequisites

  • An AWS account and permission to launch EC2 instances
  • An EC2 key pair in the Region you launch in
  • An API key for at least one model provider, or a self-hosted OpenAI-compatible model server such as Ollama

Recommended instance type: m5.large. NextChat is a web client and holds no model, so 2 vCPUs and 8 GiB of memory are ample. The model work happens at your providers.

Connecting to your instance

Version Operating system SSH user

NextChat 2.16.1 on Ubuntu 24.04 Ubuntu 24.04 LTS ubuntu

1

Launch the instance

Option A: AWS Marketplace console

  • Open the SecureAnyCloud Hardened Ollama, LiteLLM & Self Hosted LLM Server AMIs listing on AWS Marketplace. Choose View purchase options, then Subscribe. One subscription covers every version in the listing.
  • When the subscription is active, choose Launch your software.
  • Choose One-click launch from AWS Marketplace (or Launch from EC2 Console for the full launch wizard). In Version, choose NextChat 2.16.1 on Ubuntu 24.04, then choose your Region.
  • Pick an instance type (m5.large recommended; the software charge is $0.04 per vCPU per hour) and your key pair.
  • Set the security group. The listing's default group opens SSH (22), HTTP (80) and HTTPS (443). Restrict SSH to your own IP address, and restrict 80 and 443 to the networks your users browse from.
  • Choose your VPC and subnet, leave the metadata defaults (the image already requires IMDSv2), then choose Launch.

Option B: AWS CLI

Run the following on your own workstation. It creates a security group that allows SSH from your management address and HTTPS from your users' network, then launches the instance.

Replace the AMI ID with the one shown for your Region on the Marketplace launch page. Replace the VPC, subnet, key pair and CIDR ranges with your own:

SG=$(aws ec2 create-security-group --group-name nextchat \
  --description "NextChat" --vpc-id vpc-0123456789abcdef0 \
  --query GroupId --output text)
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 22 --cidr <your-mgmt-cidr>
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 443 --cidr 203.0.113.0/24
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 80 --cidr 203.0.113.0/24
aws ec2 run-instances \
  --image-id ami-0123456789abcdef0 \
  --instance-type m5.large \
  --key-name my-key-pair \
  --security-group-ids "$SG" \
  --subnet-id subnet-0123456789abcdef0 \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=nextchat}]'

You do not need --metadata-options: the AMI already makes IMDSv2 required.

2

Connect and find your login

Connect as ubuntu with the key pair you chose:

ssh -i ~/.ssh/my-key-pair.pem ubuntu@<public-ip>

First boot finishes within a minute of launch. It generates the login password, the access code and the session token. It sets the hostname NextChat is served as (the instance's public DNS name when it has one), creates the TLS certificate for it, starts the NextChat container on loopback and writes /root/nextchat-credentials.txt (mode 0600, root). Then it starts nginx.

These are the entries in that file. The secrets are not shown here:

sudo grep -E '^NEXTCHAT_(URL|USERNAME)=' /root/nextchat-credentials.txt
sudo grep -oE '^[A-Z_]+=' /root/nextchat-credentials.txt
sudo stat -c '%a %U:%G' /root/nextchat-credentials.txt
NEXTCHAT_URL=https://ec2-13-221-234-168.compute-1.amazonaws.com/ NEXTCHAT_USERNAME=admin NEXTCHAT_URL= NEXTCHAT_USERNAME= NEXTCHAT_PASSWORD= <set-at-first-boot>= 600 root:root

Value What it is for

NEXTCHAT_URL The address of this instance's NextChat

NEXTCHAT_USERNAME Login username (always admin)

NEXTCHAT_PASSWORD Login password, generated on this instance

NEXTCHAT_ACCESS_CODE NextChat's access code, needed to use a provider key configured on the server

To read the secrets, run sudo cat /root/nextchat-credentials.txt in your own session, then store them in your password manager.

3

Confirm NextChat is running

Check that the first-boot unit, Docker, the metadata firewall rule and nginx are active. Only sshd and nginx listen beyond loopback. NextChat itself is on 127.0.0.1:3000, so the nginx login is the only way in.

systemctl is-active nextchat-firstboot.service docker.service sac-docker-imds-deny.service nginx.service
sudo docker ps --format '{{.Names}} {{.Ports}}'
sudo ss -Htlnp | awk '{print $4, $6}' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g' | sort -u curl -sk https://127.0.0.1/healthz
active active active active nextchat 127.0.0.1:3000->3000/tcp 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:3000 users:(("docker-proxy")) 127.0.0.53%lo:53 users:(("systemd-resolve")) 127.0.0.54:53 users:(("systemd-resolve")) [::]:22 users:(("sshd"),("systemd")) [::]:443 users:(("nginx"),("nginx"),("nginx")) [::]:80 users:(("nginx"),("nginx"),("nginx")) ok

nginx answers /healthz itself and it reveals nothing, so it is safe to use as a load balancer health check.

4

Prove the login and the web defaults

Anonymous requests to the UI and to the API are refused, and so is a password that is wrong by one character. This instance's own password is accepted. An unknown Host header gets no answer, and plain HTTP redirects to the configured HTTPS name. Run the checks:

H=$(sudo sed -n 's/^set \$app_host "\(.*\)";$/\1/p' /etc/nginx/nextchat-host.conf)
P=$(sudo grep '^NEXTCHAT_PASSWORD=' /root/nextchat-credentials.txt | cut -d= -f2-)
curl -sk -o /dev/null -w 'no password -> %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/"
curl -sk -o /dev/null -w 'no password, API -> %{http_code}\n' --resolve "$H:443:127.0.0.1" -X POST "https://$H/api/config"
curl -sk -o /dev/null -w 'wrong password -> %{http_code}\n' -K <(printf 'user = "admin:%sX"\n' "$P") --resolve "$H:443:127.0.0.1" "https://$H/"
curl -sk -o /dev/null -w 'this instance password -> %{http_code}\n' -K <(printf 'user = "admin:%s"\n' "$P") --resolve "$H:443:127.0.0.1" "https://$H/"
curl -sk -o /dev/null -w 'unknown Host -> %{http_code}\n' -H 'Host: evil.example' https://127.0.0.1/ || true
curl -s -o /dev/null -w 'http -> %{http_code} %{redirect_url}\n' -H "Host: $H" http://127.0.0.1/
no password -> 401 no password, API -> 401 wrong password -> 401 this instance password -> 200 unknown Host -> 000 http -> 301 https://ec2-13-221-234-168.compute-1.amazonaws.com/

The password is passed to curl through a config file on a file descriptor, so it never appears on a command line or in your shell history.

5

Prove the closed routes and the access code

Sign in once to get the session cookie (into a private temporary file), then try the routes that upstream leaves open.

The open proxy, WebDAV, Upstash and any unknown /api path are refused even with the login, and so is a cross-origin POST. /api/config reports that the access code is required.

NextChat itself refuses a chat request with no access code or a wrong one. With this instance's code, the request passes the gate and reaches the provider, which rejects it here because no provider key is configured yet:

H=$(sudo sed -n 's/^set \$app_host "\(.*\)";$/\1/p' /etc/nginx/nextchat-host.conf)
P=$(sudo grep '^NEXTCHAT_PASSWORD=' /root/nextchat-credentials.txt | cut -d= -f2-)
C=$(sudo grep '^NEXTCHAT_ACCESS_CODE=' /root/nextchat-credentials.txt | cut -d= -f2-)
J=$(mktemp); chmod 600 "$J" curl -sk -o /dev/null -c "$J" -K <(printf 'user = "admin:%s"\n' "$P") --resolve "$H:443:127.0.0.1" "https://$H/" for p in /api/proxy/v1/models /api/webdav/x /api/upstash/get/x /api/anything/x; do curl -sk -o /dev/null -w "$p -> %{http_code}\n" -b "$J" -K <(printf 'user = "admin:%s"\n' "$P") --resolve "$H:443:127.0.0.1" "https://$H$p" done curl -sk -o /dev/null -w 'cross-origin POST -> %{http_code}\n' -b "$J" --resolve "$H:443:127.0.0.1" -X POST -H 'Origin: https://evil.example' "https://$H/api/config" curl -sk -b "$J" --resolve "$H:443:127.0.0.1" -X POST -H "Origin: https://$H" "https://$H/api/config" | jq -c '{needCode}' REQ='{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hi"}]}' chat() { curl -sk -b "$J" --resolve "$H:443:127.0.0.1" -X POST -H "Origin: https://$H" -H 'Content-Type: application/json' "$@" -d "$REQ" "https://$H/api/openai/v1/chat/completions"; } echo "no code -> $(chat | jq -r '.msg')" echo "wrong code -> $(chat -K <(printf 'header = "Authorization: Bearer nk-%sX"\n' "$C") | jq -r '.msg')" echo "this code -> $(chat -K <(printf 'header = "Authorization: Bearer nk-%s"\n' "$C") | jq -r 'if .error|type=="object" then "passed the gate; provider says: " + (.error.code // .error.type // "error") else .msg end')" rm -f "$J"
/api/proxy/v1/models -> 403 /api/webdav/x -> 403 /api/upstash/get/x -> 403 /api/anything/x -> 403 cross-origin POST -> 403 {"needCode":true} no code -> empty access code wrong code -> wrong access code this code -> passed the gate; provider says: invalid_api_key
6

Sign in to NextChat

Browse to NEXTCHAT_URL (https://<public-dns-name>/). The certificate is self-signed, so your browser warns once. Accept it for this host, or install your own certificate (Step 9).

The browser then asks for a username and password. Use admin and NEXTCHAT_PASSWORD. NextChat opens a new conversation.

7

Connect your model providers

No provider key ships with the image. There are two ways to give NextChat one.

In the browser (per user). Open Settings (the gear at the bottom left), tick Custom Endpoint, choose the provider and paste your own API key. The key is kept in your browser's local storage and is sent with your requests. The server never stores it.

A custom endpoint address you type here (for example an Ollama server's OpenAI-compatible URL) is called by your browser, so it must be reachable from where you browse.

On the server (shared). Put the key in /etc/nextchat/nextchat.env (mode 0600, root) and restart the container. Every user then enters the instance's access code in Settings > Access Code instead of a key.

These are the settings the file holds (values not shown):

sudo grep -oE '^[A-Z_]+=' /etc/nextchat/nextchat.env
sudo stat -c '%a %U:%G' /etc/nextchat/nextchat.env
CODE= OPENAI_API_KEY= <set-at-first-boot>= ANTHROPIC_API_KEY= <set-at-first-boot>= DEEPSEEK_API_KEY= <set-at-first-boot>= DEFAULT_MODEL= ENABLE_MCP= HIDE_USER_API_KEY= <set-at-first-boot>= 600 root:root

BASE_URL can point at any OpenAI-compatible server the instance can reach. An example is the private address of an instance running the Ollama version of this listing (http://<private-ip>:11434). Allow that port from this instance in the model server's security group.

Edit the file, then apply it:

sudo nano /etc/nextchat/nextchat.env
sudo systemctl restart nextchat-container.service

Keep CODE at least 24 characters long, because the container refuses to start without a full-length access code.

8

Use the prompt templates

Choose Mask, then Find More, to browse NextChat's built-in prompt templates. Filter by language, start a chat from a template with Chat, or create your own.

Conversations stay in the browser you use. Use Settings > Local Data > Export to back them up, and Import to move them to another browser.

In the browser, NextChat also loads its interface font from Google Fonts and emoji images from jsDelivr, checks GitHub for newer NextChat releases, and requests its plugin catalogue. These are requests for public files, and they carry none of your conversations or keys.

9

Serve NextChat under your own DNS name

Point a DNS record at the instance (an Elastic IP keeps the address stable), then tell NextChat the name.

The helper re-issues the self-signed certificate for the name and updates nginx's accepted names and the cross-origin guard. It records the name in /etc/sac/nextchat-hostname (kept across reboots) and updates NEXTCHAT_URL:

sudo nextchat-set-hostname chat.example.com
nextchat-set-hostname: NextChat is served at https://chat.example.com/

To use a certificate from your own CA or from Let's Encrypt instead, replace /etc/ssl/nextchat/nextchat.crt and /etc/ssl/nextchat/nextchat.key (keep the key mode 0600, owner root) and run sudo systemctl reload nginx.

10

Check the security controls

First boot made the certificate for this instance. The container's traffic to the instance metadata service is dropped, and nginx is denied it too. The last line runs a request to the metadata service from inside the NextChat container:

H=$(sudo sed -n 's/^set \$app_host "\(.*\)";$/\1/p' /etc/nginx/nextchat-host.conf)
openssl s_client -connect 127.0.0.1:443 -servername "$H" </dev/null 2>/dev/null | openssl x509 -noout -subject -enddate sudo iptables -S DOCKER-USER | grep DROP systemctl show -p IPAddressDeny --value nginx.service
sudo docker exec nextchat node -e 'fetch("http://169.254.169.254/latest/api/token",{method:"PUT",headers:{"X-aws-ec2-metadata-token-ttl-seconds":"60"},signal:AbortSignal.timeout(5000)}).then(r=>console.log("reached: HTTP "+r.status)).catch(e=>console.log("blocked: "+((e.cause&&e.cause.code)||e.name)))'
subject=CN = ec2-13-221-234-168.compute-1.amazonaws.com, O = sac notAfter=Jan 22 05:25:59 2029 GMT -A DOCKER-USER -d 168.63.129.16/32 -j DROP -A DOCKER-USER -d 169.254.169.254/32 -j DROP 168.63.129.16/32 169.254.169.254/32 blocked: TimeoutError

The running container is the pinned release with telemetry and MCP off. The page carries the script Content Security Policy, and the Google Analytics ID built into the upstream image is not served to browsers (the count is 0):

sudo docker image inspect --format '{{range .RepoDigests}}{{println .}}{{end}}' "$(sudo docker inspect --format '{{.Image}}' nextchat)"
sudo docker image inspect --format '{{index .Config.Labels "org.opencontainers.image.version"}} {{index .Config.Labels "org.opencontainers.image.licenses"}}' "$(sudo docker inspect --format '{{.Image}}' nextchat)"
sudo docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' nextchat | grep -E '^(NEXT_TELEMETRY_DISABLED|ENABLE_MCP)='
H=$(sudo sed -n 's/^set \$app_host "\(.*\)";$/\1/p' /etc/nginx/nextchat-host.conf)
P=$(sudo grep '^NEXTCHAT_PASSWORD=' /root/nextchat-credentials.txt | cut -d= -f2-)
curl -sk -o /dev/null -D - -K <(printf 'user = "admin:%s"\n' "$P") --resolve "$H:443:127.0.0.1" "https://$H/" | grep -i '^content-security-policy' | cut -c1-60
curl -sk -K <(printf 'user = "admin:%s"\n' "$P") --resolve "$H:443:127.0.0.1" "https://$H/" | grep -c 'G-89WN60ZK2E' || true
yidadaa/chatgpt-next-web@sha256:eaaa469ddeeb5fa58fb35f8767e9e096a2f1c8468c54b6703323624bf3071c5a v2.16.1 MIT ENABLE_MCP=false NEXT_TELEMETRY_DISABLED=1 content-security-policy: script-src 'self' 'unsafe-inline' ' 0
11

Change the login password or the access code

The helper reads the password from standard input, so it never appears on a command line, in your shell history or in the system logs. Enter the new password when prompted, then reload nginx:

sudo htpasswd -i -B /etc/nginx/nextchat.htpasswd admin
sudo systemctl reload nginx

To change the access code, set a new CODE= value of at least 24 characters in /etc/nextchat/nextchat.env, then run sudo systemctl restart nextchat-container.service.

Then put the new values in /root/nextchat-credentials.txt, or delete that file, so that it does not hold stale ones.

Maintenance

  • Logs: sudo docker logs nextchat for NextChat, /var/log/nginx/ for HTTPS access, and sudo journalctl -u nextchat-firstboot.service for first boot.
  • Restart: sudo systemctl restart nextchat-container.service (recreates the container from /etc/nextchat/nextchat.env), or sudo systemctl restart nginx.
  • Operating system updates: Ubuntu's unattended security upgrades stay on, so nginx, Docker and the rest of the OS keep receiving patches. Apply the rest with sudo apt update && sudo apt upgrade in a maintenance window.
  • Upgrading NextChat: the container image is pinned by digest, and first boot never pulls. 2.16.1 is the newest upstream release. To upgrade deliberately, pull a newer release by digest, change the image reference in /usr/local/sbin/nextchat-container-start, then restart nextchat-container.service. Keep the port published on 127.0.0.1 only, and never use host networking, because that would bypass both the login and the metadata firewall rule. A newer release may add API routes or build in a different analytics ID, so recheck Step 5 and the last command of Step 10.
  • Your data: nothing is stored on the server. Conversations, templates and browser-side keys are in your browser. Export them from Settings to back them up.

Troubleshooting

The browser keeps asking for a password. Use admin and NEXTCHAT_PASSWORD from /root/nextchat-credentials.txt exactly, over https://.

NextChat says the access code is wrong or empty. Enter NEXTCHAT_ACCESS_CODE in Settings > Access Code, or paste your own provider key under Custom Endpoint.

nginx will not start. It waits for first boot to finish and for the password and session files to exist, by design. Check first boot with sudo journalctl -u nextchat-firstboot.service --no-pager | tail -20.

Plugins, WebDAV or Upstash sync do not work. They depend on routes that fetch any address a caller supplies, so this image refuses them (403).

Support

SecureAnyCloud provides 24/7 support for this image by email and live chat: launching and connecting, provider setup, TLS and hostnames, and upgrades.

NextChat is developed by the ChatGPTNextWeb project and contributors and is released under the MIT licence. Ubuntu is a registered trademark of Canonical Ltd. Docker is a trademark of Docker, Inc. All other product and company names are trademarks or registered trademarks of their respective holders. SecureAnyCloud is not affiliated with or endorsed by them.

Deploy on AWS

Launch NextChat 2.16.1 on Ubuntu 24.04 on AWS with 24/7 support from SecureAnyCloud.

View on Marketplace

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on AWS