Self-host Open WebUI
A private ChatGPT for you and your team. No API keys.
Shelley handles the setup. About 10 minutes.
Prefer a terminal?
curl -fsSL https://exe-staging.dev/deploy/open-webui/prompt.txt | ssh exe.dev new --prompt=/dev/stdinOpen WebUI is the friendly chat app you might know from ChatGPT: a model picker, chat history, file uploads, and voice input. Here it runs on your own exe.dev VM, with models from exe.dev's LLM gateway and no API keys anywhere. Your exe.dev login is the sign-in, so there's no extra password to forget.
Share the VM with your team and each teammate gets their own account and chat history. Everything lives on the VM's disk, backed up every night.
What you get#
- Open WebUI at your VM's private URL, signed in with exe.dev, with you as admin.
- Dozens of models from exe.dev's LLM gateway. No API keys to paste.
- An account made automatically for each teammate you share the VM with.
- Chat with documents, DuckDuckGo web search, and voice input, ready to use.
- Starts again after a reboot; nightly backups, with one restored and checked.
What's running, and where
https://my-open-webui.exe.xyz/
Your chats, signed in with your exe.dev account
https://my-open-webui.exe.xyz/
Teammates you share the VM with, each with their own account
Deploy with Shelley#
Click Deploy and Shelley will set up Open WebUI on a new VM, then check that it works. No config-file wrangling required.
Prefer a terminal?
curl -fsSL \
https://exe-staging.dev/deploy/open-webui/prompt.txt \
| ssh exe.dev new --prompt=/dev/stdinWhen it's done, run this on your computer (only you can):
ssh exe.dev share add my-open-webui teammate@example.comWhat Shelley is told
Set up Open WebUI on this VM by following the exe.dev recipe at https://exe-staging.dev/deploy/open-webui.md from top to bottom.
The recipe's example VM name is my-open-webui. This VM's name is the output of `hostname`: use it wherever the recipe writes my-open-webui. Skip the step that creates the VM; you are on it.
You are done when all of these are true. Check each one yourself:
- Open WebUI at your VM's private URL, signed in with exe.dev, with you as admin.
- Dozens of models from exe.dev's LLM gateway. No API keys to paste.
- An account made automatically for each teammate you share the VM with.
- Chat with documents, DuckDuckGo web search, and voice input, ready to use.
- Starts again after a reboot; nightly backups, with one restored and checked.
Read the whole recipe, including the closed details. For initial setup,
follow "Install by hand" first, skipping VM creation and SSH: Docker,
the Compose stack, the owner's admin account, the chat check, the
documents/search/voice check, the backup timer, and the backup check.
Those checks are enough; don't install extra tools to test further.
The walkthrough at the top is for the owner afterwards. Do not run
Restore, Update, or "Run a small model on the VM".
Work as exedev in ~/open-webui. Never overwrite an existing .env.
Make the VM's owner the admin, using the email from the reflection
integration; if it prints nothing, ask the user for their exe.dev email.
The VM is private until it's shared, so start Open WebUI as written;
there's no need to hide port 8000 first. Don't share the VM or make it
public. Keep test chats to a word or two.
Verify docker.service is enabled and the container uses unless-stopped;
do not reboot.
Finish with the URLs I should open and these commands, which only I can run, from my own computer (with this VM's name in them):
ssh exe.dev share add my-open-webui teammate@example.com
Also at /deploy/open-webui/prompt.txt. The recipe it follows is /deploy/open-webui.md.
Start chatting#
Open the URL Shelley gives you. It'll look like
https://my-open-webui.exe.xyz/. Sign in to exe.dev if you're asked, and
Open WebUI signs you in automatically. You're its admin.
Pick a model at the top of the chat, or keep the default, and say hello. The model menu has a search box, and the list includes GPT models plus GLM, Kimi, DeepSeek, and Grok. A small model writes chat titles and suggested follow-ups.
Admin settings live under your name, bottom left: Settings → Admin.
Bring your team#
On your computer, share the VM with a teammate:
ssh exe.dev share add my-open-webui teammate@example.com
They get an email invitation. Once they've signed in to exe.dev, they open
the same URL and land in their own Open WebUI account, with access to all the
models. Their chats are separate from yours, but as an admin you can read
them; tell your team. ssh exe.dev share add my-open-webui team shares
with everyone in your exe.dev team.
People you haven't shared with see exe.dev's login screen and never reach
Open WebUI. ssh exe.dev share remove my-open-webui teammate@example.com
locks someone out again. Their account and chats stay until you delete them
in Settings → Admin → Users, which is also where you make someone admin.
Warning
Admins can install Functions and Tools, which run Python on your VM. Only make someone admin if you'd give them a shell. Everyone's chats count against your LLM allocation, so don't make the VM public with
share set-public: anyone signed in to exe.dev would get an account.
Documents, web search, and voice#
- Documents: click + in the message box to attach a PDF, Word file,
or text file, then ask about it. Collections you reuse go in
Workspace → Knowledge; type
#in a chat to pull one in. - Web search: click the integrations button next to + and turn on Web Search. It uses DuckDuckGo, with no key. If DuckDuckGo starts refusing requests, choose another engine in Settings → Admin → Web Search.
- Voice: the microphone button transcribes speech through the gateway.
Keep your chats#
Backups run nightly into ~/open-webui-backups. Each one includes your chats,
accounts, settings, and uploaded files. Keep a copy off the VM. On your
computer:
scp -r my-open-webui.exe.xyz:open-webui-backups .
Keep these files private. To restore one, or to update Open WebUI, see the
sections below, or ask Shelley to help. Something stuck? On the VM, try
cd ~/open-webui && docker compose logs --tail=100.
The nuts and bolts#
Happy clicking Deploy? You can stop here. These are the same setup steps Shelley follows, plus the maintenance commands for later.
Install by hand
Create a VM#
On your computer (pick another name if my-open-webui is taken):
ssh exe.dev new --name=my-open-webui
ssh my-open-webui.exe.xyz
All remaining installation commands run on the VM, as exedev.
Commands use hostname, so your VM can have any name.
Get Docker ready#
Open WebUI recommends Docker for servers; its image bundles everything,
including document readers and a small local model for searching
documents. Enable Docker at boot, or exe.dev's on-demand Docker won't
restart Open WebUI after a reboot. Install jq if needed:
sudo systemctl enable --now docker.service
systemctl is-enabled docker.service
command -v jq >/dev/null || sudo apt-get install -y jq
The check should print enabled.
Choose the models#
The gateway lists its models at https://llm.int.exe.xyz/models.json.
Open WebUI talks to it with OpenAI's Responses API; Anthropic-only models
and very expensive -pro models are left out. This writes .env once,
with a secret key for Open WebUI's sessions, the model list, a default
model, and a small model for chat titles:
mkdir -p ~/open-webui/data
cd ~/open-webui
if [ ! -e .env ]; then
catalog=$(curl -fsS https://llm.int.exe.xyz/models.json)
models=$(jq -c '[.models[] | select((.apis // []) | index("openai_responses"))
| .id | sub("^openai/"; "") | select(test("-pro$") | not)]' <<<"$catalog")
default_model=$(jq -r '.preferred_model // "gpt-5.5" | sub("^openai/"; "")' <<<"$catalog")
task_model=$(jq -r 'map(select(test("^gpt-[0-9.]+-(mini|nano)$")))[0] // "gpt-5.4-mini"' <<<"$models")
touch .env && chmod 600 .env
cat >> .env <<EOF
# Open WebUI copies these into its database on the FIRST start only.
# After that, change them in Settings -> Admin.
WEBUI_URL=https://$(hostname).exe.xyz
WEBUI_SECRET_KEY=$(openssl rand -hex 32)
DEFAULT_MODELS=$default_model
TASK_MODEL_EXTERNAL=$task_model
OPENAI_API_CONFIGS='{"0":{"enable":true,"api_type":"responses","model_ids":$models}}'
EOF
fi
grep -E '^(DEFAULT_MODELS|TASK_MODEL_EXTERNAL)=' .env
sed -n "s/^OPENAI_API_CONFIGS='\(.*\)'$/\1/p" .env | jq '."0".model_ids | length'
Expect the default model, the title model, and a count of a few dozen models.
Start Open WebUI#
The Compose file signs people in with the email exe.dev's proxy sends in
the X-ExeDev-Email header (see Login with exe),
and turns off Open WebUI's own sign-up form. New people start as ordinary
users who can use every model. The gateway needs no key, so Open WebUI
gets a placeholder.
cat > ~/open-webui/compose.yaml <<'EOF'
# Open WebUI behind exe.dev's HTTPS proxy, with models from the LLM gateway.
# Secrets and first-start settings come from .env in this directory.
name: open-webui
services:
open-webui:
# 0.12 follows the newest 0.12.x patch release.
image: ghcr.io/open-webui/open-webui:0.12
restart: unless-stopped
ports:
- "8000:8080"
env_file: .env
extra_hosts:
# Lets Open WebUI reach an optional Ollama on the VM.
- "host.docker.internal:host-gateway"
environment:
# exe.dev login is the sign-in. Port 8000 is only reachable
# through exe.dev's proxy, which sets this header.
WEBUI_AUTH_TRUSTED_EMAIL_HEADER: X-ExeDev-Email
ENABLE_LOGIN_FORM: "false"
DEFAULT_USER_ROLE: user
BYPASS_MODEL_ACCESS_CONTROL: "true"
# Models from exe.dev's LLM gateway; it needs no real key.
OPENAI_API_BASE_URL: https://llm.int.exe.xyz/v1
OPENAI_API_KEY: exe-llm-gateway
ENABLE_OLLAMA_API: "false"
# Web search without an API key.
ENABLE_WEB_SEARCH: "true"
WEB_SEARCH_ENGINE: duckduckgo
# Voice input, transcribed by the gateway.
AUDIO_STT_ENGINE: openai
AUDIO_STT_OPENAI_API_BASE_URL: https://llm.int.exe.xyz/v1
AUDIO_STT_OPENAI_API_KEY: exe-llm-gateway
AUDIO_STT_MODEL: gpt-transcribe
volumes:
- ./data:/app/backend/data
EOF
Start it and wait for {"status":true}. The first start downloads a
large image (about 6 GB on disk) and the small model for documents, so give
it a few minutes. If it takes more than ten, stop the loop with Ctrl-C and
check the logs.
cd ~/open-webui
docker compose up -d
until curl -fs http://localhost:8000/health; do sleep 5; done; echo
docker compose ps
The container should be Up and, after a minute, healthy.
Make yourself the admin#
The first account Open WebUI creates becomes its admin. The VM is private until you share it, so nobody else can get there first, but create your account now, before you share it. Find your exe.dev email:
owner=$(curl -fsS https://reflection.int.exe.xyz/email | jq -r '.email // empty')
echo "$owner"
If that prints nothing, set it yourself: owner=you@example.com, using the
email you sign in to exe.dev with. Then sign in the way exe.dev's proxy
will:
curl -fsS -X POST http://localhost:8000/api/v1/auths/signin \
-H 'Content-Type: application/json' -H "X-ExeDev-Email: $owner" \
-d '{"email":"","password":""}' | jq '{email, role}'
Expect your email and "role": "admin".
Check a chat#
Ask the title model for one word, through Open WebUI and the gateway:
cd ~/open-webui
owner=${owner:-$(curl -fsS https://reflection.int.exe.xyz/email | jq -r '.email // empty')}
token=$(curl -fsS -X POST http://localhost:8000/api/v1/auths/signin \
-H 'Content-Type: application/json' -H "X-ExeDev-Email: $owner" \
-d '{"email":"","password":""}' | jq -r .token)
curl -fsS http://localhost:8000/api/models -H "Authorization: Bearer $token" | jq '.data | length'
model=$(sed -n 's/^TASK_MODEL_EXTERNAL=//p' .env)
curl -fsS http://localhost:8000/api/chat/completions \
-H "Authorization: Bearer $token" -H 'Content-Type: application/json' \
-d "{\"model\":\"$model\",\"stream\":false,\"messages\":[{\"role\":\"user\",\"content\":\"Reply with one word: pong\"}]}" \
| jq -r '.choices[0].message.content'
Expect the number of models and pong. Without exe.dev's header, nobody
gets in. This should print 400:
curl -s -o /dev/null -w '%{http_code}\n' -X POST http://localhost:8000/api/v1/auths/signin \
-H 'Content-Type: application/json' -d '{"email":"","password":""}'
Check documents, web search, and voice#
Upload a one-line document and ask about it, run a web search, and send a second of silence to voice transcription. Each goes through Open WebUI as you, and the test file is deleted at the end:
cd ~/open-webui
owner=${owner:-$(curl -fsS https://reflection.int.exe.xyz/email | jq -r '.email // empty')}
auth="Authorization: Bearer $(curl -fsS -X POST http://localhost:8000/api/v1/auths/signin \
-H 'Content-Type: application/json' -H "X-ExeDev-Email: $owner" \
-d '{"email":"","password":""}' | jq -r .token)"
model=$(sed -n 's/^TASK_MODEL_EXTERNAL=//p' .env)
echo 'The project codename is BLUE PELICAN.' > /tmp/codename.txt
file=$(curl -fsS -X POST 'http://localhost:8000/api/v1/files/?process=true' \
-H "$auth" -F file=@/tmp/codename.txt | jq -r .id)
until curl -fsS "http://localhost:8000/api/v1/files/$file/process/status" -H "$auth" \
| jq -e '.status != "pending"' >/dev/null; do sleep 2; done
curl -fsS http://localhost:8000/api/chat/completions \
-H "$auth" -H 'Content-Type: application/json' \
-d "{\"model\":\"$model\",\"stream\":false,\"files\":[{\"type\":\"file\",\"id\":\"$file\"}],
\"messages\":[{\"role\":\"user\",\"content\":\"What is the codename? Two words.\"}]}" \
| jq -r '.choices[0].message.content'
curl -fsS -X POST http://localhost:8000/api/v1/retrieval/process/web/search \
-H "$auth" -H 'Content-Type: application/json' -d '{"queries":["exe.dev"]}' \
| jq '.items | length'
python3 -c 'import wave; w = wave.open("/tmp/silence.wav", "wb"); w.setnchannels(1)
w.setsampwidth(2); w.setframerate(16000); w.writeframes(bytes(32000)); w.close()'
curl -fsS -X POST http://localhost:8000/api/v1/audio/transcriptions \
-H "$auth" -F 'file=@/tmp/silence.wav;type=audio/wav' | jq -c '{text}'
curl -fsS -X DELETE "http://localhost:8000/api/v1/files/$file" -H "$auth"; echo
rm /tmp/codename.txt /tmp/silence.wav
Expect BLUE PELICAN (perhaps with a [1] citation), a number of search
results, {"text":""} for the silence, and a deleted-file message. If the
search fails, DuckDuckGo is limiting requests from the VM's address; try
again later. Everything else still works.
Set up nightly backups#
The backup stops Open WebUI for a few seconds, so the database and the
document index are copied in a consistent state. It runs as root because
the container writes data as root, and skips data/cache, which Open
WebUI downloads again.
cat > ~/open-webui/backup.sh <<'EOF'
#!/usr/bin/env bash
# Back up Open WebUI: data (chats, accounts, settings, uploads, document
# index), .env (the secret key), and the Compose file. Run with sudo.
set -euo pipefail
umask 077
cd "$(dirname "$(readlink -f "$0")")"
owner=$(stat -c %U:%G .)
dest="$(dirname "$PWD")/open-webui-backups"
mkdir -p "$dest"
out="$dest/open-webui-$(date -u +%Y%m%d-%H%M%S).tar.gz"
# A short stop keeps the SQLite database and document index consistent.
docker compose stop open-webui
trap 'docker compose start open-webui' EXIT
tar -czf "$out.part" --exclude=data/cache .env compose.yaml backup.sh data
docker compose start open-webui
trap - EXIT
mv "$out.part" "$out"
chown -R "$owner" "$dest"
# Keep the 14 newest backups.
ls -1t "$dest"/open-webui-*.tar.gz | tail -n +15 | xargs -r rm --
echo "$out"
EOF
chmod +x ~/open-webui/backup.sh
Schedule it for 03:30 UTC (or so), then take a first backup:
sudo tee /etc/systemd/system/open-webui-backup.service >/dev/null <<EOF
[Unit]
Description=Back up Open WebUI to ~/open-webui-backups
Requires=docker.service
After=docker.service
[Service]
Type=oneshot
ExecStart=$HOME/open-webui/backup.sh
EOF
sudo tee /etc/systemd/system/open-webui-backup.timer >/dev/null <<'EOF'
[Unit]
Description=Back up Open WebUI every night
[Timer]
OnCalendar=*-*-* 03:30:00
RandomizedDelaySec=30m
Persistent=true
[Install]
WantedBy=timers.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now open-webui-backup.timer
sudo systemctl start open-webui-backup.service
systemctl list-timers open-webui-backup.timer
ls -lh ~/open-webui-backups
until curl -fs http://localhost:8000/health; do sleep 5; done; echo
Check that the timer has a next run, the directory has a tarball, and Open
WebUI is back. For failures: journalctl -u open-webui-backup.service.
Test a backup#
Unpack the newest backup into a scratch directory and check its database, without touching the running copy:
latest=$(ls -1t ~/open-webui-backups/open-webui-*.tar.gz | head -n 1)
mkdir /tmp/open-webui-restore-test
sudo tar -xzf "$latest" -C /tmp/open-webui-restore-test
cd /tmp/open-webui-restore-test
sudo ls -A . data
docker compose -p open-webui-restore-test run --rm --no-deps -T open-webui python3 -c '
import sqlite3
db = sqlite3.connect("/app/backend/data/webui.db")
print(db.execute("pragma integrity_check").fetchone()[0])
for table in ("user", "chat"):
print(table, db.execute(f"select count(*) from {table}").fetchone()[0])
'
Expect .env, compose.yaml, and data with webui.db, then ok and
the number of accounts and chats. Remove only the scratch copy:
cd /tmp/open-webui-restore-test && docker compose -p open-webui-restore-test down
cd ~ && sudo rm -rf /tmp/open-webui-restore-test
Installation done. Leave sharing to the owner; continue with Start chatting.
Back up and restore
On the VM, sudo ~/open-webui/backup.sh makes an extra backup and prints
its path. Copy backups off the VM as shown in
Keep your chats.
Restore#
On a new VM, get Docker ready as in Install. Create
~/open-webui-backups there, then copy your backup over from your computer:
scp open-webui-20261017-033012.tar.gz my-open-webui.exe.xyz:open-webui-backups/
Use your backup's actual filename in this and the commands below. All remaining commands run on the VM. If Open WebUI already runs there, stop it and keep its directory aside first:
cd ~/open-webui && docker compose down
sudo mv ~/open-webui ~/open-webui.before-restore
Unpack the backup and start it:
mkdir ~/open-webui
sudo tar -xzf ~/open-webui-backups/open-webui-20261017-033012.tar.gz -C ~/open-webui
cd ~/open-webui
docker compose up -d
until curl -fs http://localhost:8000/health; do sleep 5; done; echo
Open the site and check your chats are there. Sign in with the same exe.dev
account to stay admin. On a new VM, recreate the backup timer from
Install, and if its name changed, update
WebUI URL in Settings → Admin → General. Keep
~/open-webui.before-restore until you're happy everything is back.
Update Open WebUI
The 0.12 image tag follows the newest 0.12.x release. On the VM, back up,
then pull and restart:
cd ~/open-webui
sudo ./backup.sh
docker compose pull
docker compose up -d
until curl -fs http://localhost:8000/health; do sleep 5; done; echo
curl -fsS http://localhost:8000/api/version; echo
docker image prune -f
Check the version, then reload the site with Ctrl+Shift+R (Cmd+Shift+R on a Mac) so your browser picks up the new pages. The last command frees the old image's disk space.
For a new minor version, such as 0.13, read the
release notes first,
then change the image: tag in compose.yaml and run the same commands.
To roll back, restore the backup: changing the tag alone won't
undo database migrations.
New models on the gateway don't show up by themselves. Add their IDs in
Settings → Admin → Connections, with the gear next to
https://llm.int.exe.xyz/v1.
Run a small model on the VM (optional)
Ollama runs open models on the VM's own CPU, so a chat never leaves the VM. On the default VM (2 vCPUs, 8 GB), we measured these speeds, in tokens (word pieces) per second:
gemma3:1b: about 28. Fine for quick, simple jobs.llama3.2:3b: about 19. A bit smarter, still quick.- 7-8B models: about 7, and they crowd Open WebUI out of memory.
They are much weaker than the gateway's models, and slow to read long
documents. For 7-8B models, give the VM more room first, on your computer:
ssh exe.dev resize my-open-webui --cpu=4 --memory=16GB.
On the VM, install Ollama (about 2 GB) and let Open WebUI's container reach it. exe.dev's proxy only forwards ports 3000-9999, so port 11434 stays off the internet.
curl -fsSL https://ollama.com/install.sh | sh
sudo mkdir -p /etc/systemd/system/ollama.service.d
printf '[Service]\nEnvironment=OLLAMA_HOST=0.0.0.0:11434\n' \
| sudo tee /etc/systemd/system/ollama.service.d/override.conf
sudo systemctl daemon-reload
sudo systemctl restart ollama
ollama pull gemma3:1b
cd ~/open-webui
docker compose exec open-webui curl -fsS http://host.docker.internal:11434/api/version; echo
Expect a version number. Then, in Open WebUI, go to
Settings → Admin → Connections and switch on Ollama API. Its address is
already http://host.docker.internal:11434. gemma3:1b appears in the model
menu. Add models with ollama pull, or from the download icon next to the
Ollama connection.
Something not working?
On the VM:
cd ~/open-webui
docker compose ps
docker compose logs --tail=100 open-webui
- 502 right after starting or updating: Open WebUI takes up to a
minute to start. Wait for
curl -fsS http://localhost:8000/health. - An error mentioning
/v1/chat/completions: the gateway connection was switched to Chat Completions. In Settings → Admin → Connections, open the gear and set API Type back to Responses. - I changed
.envand nothing happened: most settings are read only on the first start, then kept in Open WebUI's database. Change them in Settings → Admin instead. - A teammate sees no models: check the gateway connection is switched on in Settings → Admin → Connections, and that the model isn't hidden in Settings → Admin → Models.
- Web search fails or finds nothing: DuckDuckGo limits automated searches at times. Pick another engine in Settings → Admin → Web Search; most need an API key.
- Down after a reboot: run
sudo systemctl enable --now docker.service. - Disk full: each image is about 6 GB. Check
df -h /and rundocker image prune -f.
Frequently asked questions#
How much does it cost?
Open WebUI is free. You pay for your VM plan, and chats use your exe.dev LLM allocation, including your teammates' chats. Voice transcription with gpt-transcribe is free.
Which models can I use? Is Claude there?
Everything the gateway serves through OpenAI's API: GPT models plus GLM, Kimi, DeepSeek, Grok, and others. Claude isn't, because the gateway offers it only through Anthropic's Messages API, which Open WebUI doesn't speak to custom servers. Shelley and Claude Code can use it.
Can I use my own API key or ChatGPT subscription?
Yes, without touching Open WebUI. Point your LLM integration at your own provider key or ChatGPT account, and the same address serves your models.
Who can read my chats?
Each person's chats are their own, but Open WebUI admins can open anyone's chats, and the VM's owner can read its disk. Prompts and replies go to the model provider through exe.dev's gateway. Run a small local model to keep a chat entirely on the VM.
Does it work with Ollama?
Yes. Small 1-3B models run fast enough on the default VM's CPU; bigger ones want a bigger VM. See Run a small model on the VM.
Is it free for my company?
Yes, Open WebUI's license allows commercial use. It adds one rule: don't remove the Open WebUI name and logo, unless your instance has 50 or fewer users in any 30 days or you have an enterprise license.
Updated Oct 17, 2026 · tested end to end Oct 17, 2026 · Markdown · manifest.json · Something wrong? Tell us