NextChat 2.16.1 on AWS User Guide
Overview
This guide covers launching and verifying NextChat 2.16.1 on Ubuntu 24.04 from cloudimg on Amazon EC2. It is one version of the cloudimg 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 other LLM servers, gateways and chat interfaces in the listing are described on the listing's guide hub.
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, or any OpenAI-compatible endpoint such as an Ollama, LocalAI or llama.cpp server. It 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 is on the image:
- NextChat 2.16.1, the upstream container image
yidadaa/chatgpt-next-web:v2.16.1, pinned by digestsha256: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; port 80 redirects 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 that nginx only issues after a successful login. The only open path is nginx's own
/healthzliteral, 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, plus 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; every other/apipath returns403. NextChat's plugin, cloud-sync and MCP features are therefore off. Page routes accept onlyGET, 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
Hostheader gets its connection closed), uses TLS 1.2 or 1.3, refuses cross-originPOSTrequests and WebSocket upgrades (403), strips the upstream wildcard CORS headers, never forwards a WebSocket upgrade to NextChat, and replaces any client-suppliedX-Forwarded-For,X-Real-IPorForwardedheader 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/cloudimg/nextchat-hostnameif you set one. - No instance metadata access for the app. The container is blocked from the instance metadata service (169.254.169.254 and fd00:ec2::254) by a Docker
DOCKER-USERfirewall rule, which is in place before the container starts. nginx is blocked from it by systemdIPAddressDeny. - 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, and 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 |
Step 1: Launch the instance
Option A: AWS Marketplace console
- Open the cloudimg Hardened Ollama, LiteLLM & Self Hosted LLM Server AMIs listing on AWS Marketplace and choose View purchase options, then Subscribe. One subscription covers every version in the listing.
- Once 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.largerecommended; the software charge is $0.04 per vCPU per hour) and your key pair. - 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 this 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, and 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.
Step 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, 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=
NEXTCHAT_ACCESS_CODE=
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 and store them in your password manager.
Step 3: Confirm NextChat is running
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 cloudimg-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
/healthz is answered by nginx itself and reveals nothing, so it is safe to use as a load balancer health check.
Step 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:
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.
Step 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 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. A chat request with no access code, or a wrong one, is refused by NextChat itself; with this instance's code it passes the gate and reaches the provider, which here rejects it 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
Step 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:

Step 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=
BASE_URL=
ANTHROPIC_API_KEY=
GOOGLE_API_KEY=
DEEPSEEK_API_KEY=
CUSTOM_MODELS=
DEFAULT_MODEL=
ENABLE_MCP=
HIDE_USER_API_KEY=
NEXT_TELEMETRY_DISABLED=
600 root:root
BASE_URL can point at any OpenAI-compatible server the instance can reach, for example 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: the container refuses to start without a full-length access code.
Step 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 carry none of your conversations or keys.
Step 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 it, updates nginx's accepted names and the cross-origin guard, records the name in /etc/cloudimg/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.
Step 10: Check the security controls
The certificate was made for this instance on first boot, 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 = cloudimg
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
Step 11: Change the login password or the access code
The password is read 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 and run sudo systemctl restart nextchat-container.service. Then put the new values in /root/nextchat-credentials.txt (or delete that file) so it does not hold stale ones.
Maintenance
- Logs:
sudo docker logs nextchatfor NextChat,/var/log/nginx/for HTTPS access,sudo journalctl -u nextchat-firstboot.servicefor first boot. - Restart:
sudo systemctl restart nextchat-container.service(recreates the container from/etc/nextchat/nextchat.env), orsudo 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 upgradein 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 and change the image reference in
/usr/local/sbin/nextchat-container-start, then restartnextchat-container.service. Keep the port published on127.0.0.1only and never use host networking: 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
cloudimg 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. cloudimg is not affiliated with or endorsed by them.