前言
首先贴出配置文件。
相关配置文件因为比较杂乱所以都折叠起来了。可以点击后展开查看。
配置文件
docker-compose.yml
version: "3.7"
x-defaults: &defaults
restart: unless-stopped
networks:
- open-webui
extra_hosts:
- host.docker.internal:host-gateway
x-watchtower: &watchtower
labels:
- com.centurylinklabs.watchtower.enable=true
x-logging: &default-logging
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
services:
tts:
image: mzzsfy/tts:latest
container_name: tts
<<: [*defaults, *watchtower, *default-logging]
environment:
- TTS_STRICT_MODE=true
new-api:
# 因为始皇的 neo api 基于较旧的 new api 二次开发, 故切换为new api最新版本
image: calciumion/new-api:latest
#image: pengzhile/new-api:latest
container_name: new-api
<<: [*defaults, *watchtower, *default-logging]
command: --log-dir /app/logs
ports:
- "3001:3000"
environment:
- SQL_DSN=postgres://openwebui:mypasswd@postgres:5432/new-api
- REDIS_CONN_STRING=redis://redis
- SESSION_SECRET=server01
- TZ=Asia/Shanghai
volumes:
- ./new-api/data:/data
- ./new-api/logs:/app/logs
depends_on:
redis:
condition: service_healthy
tts:
condition: service_started
healthcheck:
test: ["CMD-SHELL", "wget -q -O - http://localhost:3000/api/status | grep -o '\"success\":\\s*true' | awk -F: '{print $2}'"]
interval: 30s
timeout: 10s
retries: 3
start_period: 15s
redis:
image: redis:7-alpine
container_name: redis
<<: [*defaults, *default-logging]
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
postgres:
container_name: postgres
image: postgres:17-alpine
<<: [*defaults, *default-logging]
environment:
POSTGRES_USER: openwebui
POSTGRES_PASSWORD: mypasswd
POSTGRES_DB: openwebui
POSTGRES_MULTIPLE_DATABASES: "\"new-api\",openwebui,openwebui_monitor"
volumes:
- ./postgres_data:/var/lib/postgresql/data
- ./postgres-init:/docker-entrypoint-initdb.d
ports:
- "5432:5432"
#user: postgres
healthcheck:
test: ["CMD-SHELL", "pg_isready -U openwebui | grep -o 'accepting connections'"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
open-webui:
container_name: open-webui
image: ghcr.io/open-webui/open-webui:main
<<: [*defaults, *default-logging]
ports:
- "4567:8080"
volumes:
- ./open-webui_data:/app/backend/data
- ./open-webui-assets-custom:/app/build/assets/custom
- ./open-webui-init.sh:/app/backend/open-webui-init.sh
environment:
- OLLAMA_BASE_URL=false
- SEARXNG_URL=false
- ENABLE_REALTIME_CHAT_SAVE=false
- DATABASE_URL=postgres://openwebui:mypasswd@postgres:5432/openwebui
- DATABASE_POOL_SIZE=10
- DATABASE_POOL_MAX_OVERFLOW=20
- ENABLE_OLLAMA_API=false
- AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST=5
#- AIOHTTP_CLIENT_TIMEOUT=10
- HF_ENDPOINT=https://hf-mirror.com
#- HOST="::"
depends_on:
postgres:
condition: service_healthy
new-api:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/health | grep -o '\"status\":\\s*true' | awk -F: '{print $2}'"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
command:
- /bin/bash
- -c
- |
bash /app/backend/open-webui-init.sh
openwebui-monitor:
container_name: openwebui-monitor
image: docker.io/variantconst/openwebui-monitor:latest
<<: [*defaults, *watchtower, *default-logging]
ports:
- "7878:3000"
env_file:
- .openwebui-monitor-env
environment:
- POSTGRES_HOST=postgres
- POSTGRES_PORT=5432
- POSTGRES_USER=openwebui
- POSTGRES_PASSWORD=mypasswd
- POSTGRES_DATABASE=openwebui_monitor
depends_on:
postgres:
condition: service_healthy
networks:
open-webui:
driver: bridge
enable_ipv6: true
ipam:
driver: default
config:
- subnet: fd00:db8:1234::/64
gateway: fd00:db8:1234::1
compose 配置文件中的./postgres-init/create-multiple-db.sh
./postgres-init/create-multiple-db.sh
#!/bin/bash
set -e
set -u
# 分割POSTGRES_MULTIPLE_DATABASES环境变量
if [ -n "$POSTGRES_MULTIPLE_DATABASES" ]; then
for db in $(echo $POSTGRES_MULTIPLE_DATABASES | tr ',' ' '); do
echo "Creating database: $db"
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL
CREATE DATABASE $db;
GRANT ALL PRIVILEGES ON DATABASE $db TO $POSTGRES_USER;
EOSQL
done
fi
./open-webui-assets-custom 文件夹 可以参照 【openwebui美化】实现Open-webui代码框的自动折叠、mac样式代码块,修改open WebUI字体样式。 的 #1 和 #45
附上一个我修改过的custom.js
custom.js
(function(){function checkIsEditPage(){return window.location.href.includes('/functions')}let isCurrentlyEditPage=checkIsEditPage();function updateImagesBasedOnTheme(){const isDarkMode=document.documentElement.classList.contains('dark');const imgTags=document.querySelectorAll('img');imgTags.forEach(img=>{const src=img.getAttribute('src');if(!src)return;if(isDarkMode){if(src.startsWith('https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light')){img.src=src.replace('https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light','https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/dark')}}else{if(src.startsWith('https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/dark')){img.src=src.replace('https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/dark','https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light')}}})}function onRouteChange(){isCurrentlyEditPage=checkIsEditPage();if(isCurrentlyEditPage){if(mutationObserverActive){mutationObserver.disconnect();mutationObserverActive=false}}else{initializeAllCodeBlocks();if(!mutationObserverActive){mutationObserver.observe(document.body,{childList:true,subtree:true});mutationObserverActive=true}}updateImagesBasedOnTheme()}const originalPushState=history.pushState;history.pushState=function(state,title,url){originalPushState.apply(history,arguments);onRouteChange()};window.addEventListener('popstate',onRouteChange);const observedCodeBlocks=new WeakSet();const resizeObserver=new ResizeObserver((entries)=>{if(isCurrentlyEditPage)return;for(const entry of entries){const editorRoot=entry.target;if(!editorRoot.classList.contains('cm-editor'))continue;updateCodeBlock(editorRoot)}});function updateCodeBlock(editorRoot){if(editorRoot.querySelector('.code-expand-btn'))return;const height=editorRoot.scrollHeight;if(height>400){editorRoot.id='collapsed';const expandBtn=document.createElement('button');expandBtn.className='code-expand-btn';expandBtn.id='collapsed';editorRoot.appendChild(expandBtn);editorRoot.style.height='400px'}}function initializeCodeBlock(editorRoot){if(observedCodeBlocks.has(editorRoot))return;observedCodeBlocks.add(editorRoot);resizeObserver.observe(editorRoot);updateCodeBlock(editorRoot)}function initializeAllCodeBlocks(){if(isCurrentlyEditPage)return;document.querySelectorAll('.cm-editor').forEach(initializeCodeBlock)}const mutationObserver=new MutationObserver((mutations)=>{if(isCurrentlyEditPage)return;let hasNewCodeBlocks=false;let hasNewImages=false;mutations.forEach((mutation)=>{mutation.addedNodes.forEach((node)=>{if(node.nodeType!==1)return;if(node.classList?.contains('cm-editor')){initializeCodeBlock(node);hasNewCodeBlocks=true}else{const matches=node.querySelectorAll?.('.cm-editor')||[];matches.forEach((el)=>{initializeCodeBlock(el);hasNewCodeBlocks=true})}if(node.tagName==='IMG'){hasNewImages=true}else{const imageMatches=node.querySelectorAll?.('img')||[];if(imageMatches.length>0){hasNewImages=true}}})});if(hasNewCodeBlocks)requestAnimationFrame(initializeAllCodeBlocks);if(hasNewImages)requestAnimationFrame(updateImagesBasedOnTheme)});let mutationObserverActive=false;document.addEventListener('click',function(evt){if(!evt.target.classList.contains('code-expand-btn'))return;const editorRoot=evt.target.closest('.cm-editor');if(!editorRoot)return;const isCollapsed=editorRoot.id==='collapsed';requestAnimationFrame(()=>{if(isCollapsed){const scroller=editorRoot.querySelector('.cm-scroller');editorRoot.style.height=`${scroller.scrollHeight}px`;editorRoot.id='expanded';evt.target.id='expanded'}else{editorRoot.style.height='400px';editorRoot.id='collapsed';evt.target.id='collapsed';const scrollTarget=editorRoot.closest('.relative.my-2')?.parentElement;scrollTarget?.scrollIntoView({behavior:'smooth',block:'start'})}})});function init(){isCurrentlyEditPage=checkIsEditPage();if(!isCurrentlyEditPage)initializeAllCodeBlocks();mutationObserver.observe(document.body,{childList:true,subtree:true});mutationObserverActive=true;updateImagesBasedOnTheme();setInterval(updateImagesBasedOnTheme,1000)}if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',init)}else{init()}const themeObserver=new MutationObserver((mutations)=>{mutations.forEach(mutation=>{if(mutation.attributeName==='class'&&mutation.target===document.documentElement){updateImagesBasedOnTheme()}})});themeObserver.observe(document.documentElement,{attributes:true});window.addEventListener('error',(error)=>{console.error('Code block error:',error)});window.addEventListener('unhandledrejection',(event)=>{console.error('Unhandled rejection:',event.reason)})})();
custom.css
@font-face { font-family: 'Dank Mono'; src:
url('../assets/fonts/DankMono-Regular.woff2')
format('woff2'); font-display: swap; font-style:
normal;
}
@font-face { font-family: 'Dank Mono'; src:
url('../assets/fonts/DankMono-Italic.woff2')
format('woff2'); font-display: swap; font-style:
italic;
}
@font-face { font-family: 'PingFangSC-Regular'; src:
url('../assets/fonts/PingFangSC-Regular.woff2')
format('woff2'); font-display: swap;
}
html { scroll-behavior: smooth;
}
body { font-family: 'PingFangSC-Regular', -apple-system,
BlinkMacSystemFont, sans-serif;
}
/* 代码块容器样式 */ .language-javascript,
[class*="language-"] {
background: #f6f8fa !important; border-radius: 10px
!important;
box-shadow: 0 10px 30px 0 rgba(0, 0, 0, 0.1)
!important;
position: relative; margin: 1.2em 0;
}
.dark .language-javascript, .dark [class*="language-"] {
background: #282c34 !important; box-shadow: 0 10px
30px 0 rgba(0, 0, 0, .4) !important;
}
/* 代码块顶部栏 */ .sticky.top-8 { background: #e1e4e8
!important;
height: 40px !important; display: flex; align-items:
center; border-radius: 10px 10px 0 0; padding: 0 15px
!important;
margin-bottom: -59px !important;
}
.dark .sticky.top-8 { background: #21252b !important;
}
/* 语言标识 */ .text-text-300 { position: absolute;
left: 60px; top: 2px; color: #abb2bf !important;
font-size: 17px !important; font-weight: 500
!important;
z-index: 11;
}
.dark .text-text-300 { color: #586069 !important;
}
/* 顶部按钮样式优化 */ .save-code-button,
.copy-code-button, .run-code-button {
background: #f8f9fa !important; color: #333
!important;
border: 1px solid #d1d5da !important; font-size: 12px
!important;
padding: 4px 12px !important; border-radius: 4px
!important;
transition: all 0.2s ease-in-out !important;
}
.dark .save-code-button, .dark .copy-code-button, .dark
.run-code-button {
background: #323842 !important; color: #abb2bf
!important;
border: 1px solid #3e4451 !important;
}
.save-code-button:hover, .copy-code-button:hover {
background: #e9ecef !important; color: #222
!important;
}
.dark .save-code-button:hover, .dark
.copy-code-button:hover {
background: #3e4451 !important; color: #fff
!important;
}
/* 代码块顶部装饰圆点 */ .language-javascript::before,
[class*="language-"]::before {
content: " "; position: absolute; border-radius: 50%;
background: #ff5f56; width: 12px; height: 12px; left:
15px; top: 14px; box-shadow: 20px 0 #ffbd2e, 40px 0
#27c93f;
z-index: 10;
}
.dakr .language-javascript::before, .dakr
[class*="language-"]::before {
background: #fc625d; box-shadow: 20px 0 #fdbc40, 40px
0 #35cd4b;
}
/* 代码内容区域 */ .cm-content { font-family: 'Dank
Mono', -apple-system, BlinkMacSystemFont, Inter,
ui-sans-serif, system-ui, 'Segoe UI', Roboto, Ubuntu,
Cantarell, 'Noto Sans', sans-serif, 'Helvetica Neue',
Arial, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe
UI Symbol', 'Noto Color Emoji'; font-size: 15px
!important;
line-height: 1.6em !important; padding: 20px 1.4em 1em
30px !important; color: #24292e !important;
}
.dark .cm-content { color: #abb2bf !important;
}
/* 代码语法高亮 */ .cm-line .ͼb { /* 关键字 */ color:
#d73a49 !important;
}
.dark .cm-line .ͼb { color: #c678dd !important;
}
.cm-line .ͼd { /* 数字 */ color: #a29bfe !important;
}
.dakr .cm-line .ͼd { color: #e5c07b !important;
}
.cm-line .ͼe { /* 字符串 */ color: #6a89cc !important;
}
.dakr .cm-line .ͼe { color: #98c379 !important;
}
.cm-line .ͼg { /* 变量 */ color: #2ca9e1 !important;
font-style: italic;
}
.dakr .cm-line .ͼg { color: #e3adb9 !important;
}
/* 代码语法高亮 - 扩展 */ .cm-comment { /* 注释 */
color: #7f848e !important; font-style: italic;
}
.cm-property { color: #61afef !important;
}
cm-tag { color: #e06c75 !important;
}
.cm-attribute { color: #d19a66 !important;
}
.cm-string { color: #98c379 !important;
}
.cm-operator { color: #56b6c2 !important;
}
span.ͼc { color: #7d5fff !important;
}
span.ͼl { color: #6bddcd !important;
}
span.ͼt { /* 暗色模式下的逗号 */ color: #ddb078
!important;
;
font-style: italic;
}
span.ͼr { /* 暗色模式下的函数名 */ font-style: italic;
}
span.ͼf { /* 亮色模式下奇怪的符号 */ color: #70a1ff;
}
span.ͼm { /* 亮色模式下的注释 */ color: #f29a76;
font-style: italic;
}
span.ͼw { /* 暗色模式下的注释 */ font-style: italic;
}
/* 滚动条样式 */ .cm-scroller::-webkit-scrollbar {
height: 10px !important; width: 10px !important;
background-color: #f6f8fa !important;
}
.dark .cm-scroller::-webkit-scrollbar { background-color:
#282c34 !important;
}
.cm-scroller::-webkit-scrollbar-track { box-shadow: inset
0 0 6px rgba(0, 0, 0, 0.1) !important; border-radius:
10px !important; background-color: #f6f8fa !important;
}
.dark .cm-scroller::-webkit-scrollbar-track { box-shadow:
inset 0 0 6px rgba(0, 0, 0, .3) !important;
background-color: #282c34 !important;
}
.cm-scroller::-webkit-scrollbar-thumb { border-radius:
10px !important; box-shadow: inset 0 0 6px rgba(0, 0,
0, .2) !important; background-color: #d1d5da
!important;
}
.dark .cm-scroller::-webkit-scrollbar-thumb { box-shadow:
inset 0 0 6px rgba(0, 0, 0, .5) !important;
background-color: #3e4451 !important;
}
/* 行号栏样式 */ .cm-gutters { background: #f6f8fa
!important;
border-right: 1px solid #d1d5da !important; color:
#586069 !important;
padding-right: 10px !important; font-family: "Dank
Mono";
}
.dark .cm-gutters { background: #282c34 !important;
border-right: 1px solid #3e4451 !important; color:
#495162 !important;
}
/* 当前行高亮 */ .cm-activeLine { background: #6699ff0b
!important;
}
.cm-gutterElement.cm-activeLineGutter { background-color:
#f9d3e3;
}
.dark .cm-gutterElement.cm-activeLineGutter {
background-color: #dd7694;
}
/* 添加代码选中样式 */ .cm-selectionBackground,
.cm-content ::selection {
background-color: rgba(122, 129, 255, 0.2) !important;
}
.cm-line.cm-selected { background-color: rgba(122, 129,
255, 0.2) !important;
}
/* 选中时的文本颜色保持原样,确保可读性 */ .cm-content
::selection {
color: rgba(62, 158, 111, 0.9) !important;
}
.dark .cm-content ::selection { color: rgba(245, 177, 255,
0.9) !important;
}
/* 匹配相同结果时的颜色 */ .cm-selectionMatch {
background-color: #9c88ff5a !important;
}
/* 当有多行选中时的样式 */
.cm-selectionLayer>.cm-selectionBackground {
background-color: rgba(122, 129, 255, 0.2) !important;
}
/* 代码块折叠/展开样式添加与修改 */ .cm-scroller {
background-color: #f6f8fa;
}
#collapsed>.cm-scroller, #expanded>.cm-scroller {
padding-bottom: 40px;
}
.dark .cm-scroller { background-color: #282c34;
}
.cm-scroller { overflow: auto !important;
}
.cm-editor { transition: height 1s cubic-bezier(0.4, 0,
0.2, 1); overflow: hidden !important;
}
/* 只给超高的代码块添加最大高度和内边距 */
.cm-editor#collapsed {
height: 400px;
}
/* .cm-editor#expanded { padding-bottom: 40px; } */
.code-expand-btn {
position: absolute; bottom: 10px; left: 50%;
transform: translateX(-50%); display: flex;
justify-content: center; align-items: center; padding:
6px 15px; border-radius: 15px; font-size: 12px;
cursor: pointer; border: none; color: #666;
background: rgba(255, 255, 255, 0.6); backdrop-filter:
blur(8px); box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
-webkit-backdrop-filter: blur(8px); z-index: 11;
transition: all 0.3s ease;
}
.dark .code-expand-btn { background: rgba(45, 45, 45,
0.6); color: #fff;
}
.code-expand-btn:hover { background: rgba(255, 255, 255,
0.8); backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px); transform:
translateX(-50%) translateY(-2px); box-shadow: 0 6px
8px rgba(0, 0, 0, 0.15);
}
.dark .code-expand-btn:hover { background: rgba(45, 45,
45, 0.8);
}
.code-expand-btn:active { transform: translateX(-50%)
translateY(0); box-shadow: 0 2px 4px rgba(0, 0, 0,
0.1);
}
.code-expand-btn::before { content: "⌄"; display:
inline-block; margin-right: 4px; font-size: 14px;
transition: transform 0.3s ease;
}
.code-expand-btn#expanded::before { transform:
rotate(180deg);
}
.code-expand-btn::after { content: "展开代码";
}
.code-expand-btn#expanded::after { content: "收起代码";
}
/* 渐变遮罩 */ .cm-editor#collapsed::after { content:
''; position: absolute; bottom: 0; left: 0; right: 0;
height: 100px; background: linear-gradient(transparent
0%, rgba(255, 255, 255, 0.3) 40%, rgba(255, 255, 255,
0.6) 80%, rgba(255, 255, 255, 0.8) 100%);
pointer-events: none; opacity: 0; transition: opacity
0.3s ease; z-index: 10; /* 确保遮罩层覆盖到滚动条 */
width: calc(100% + 17px); /* 17px是标准滚动条宽度 */
}
.dark .cm-editor#collapsed::after { background:
linear-gradient(transparent 0%, rgba(45, 45, 45, 0.3)
40%, rgba(45, 45, 45, 0.6) 80%, rgba(45, 45, 45, 0.8)
100%);
}
/* 只在折叠状态显示渐变遮罩 */ .cm-editor#collapsed::after
{
opacity: 1;
}
/* 隐藏原始的折叠按钮 */
div.flex.items-center.gap-0\.5 button.flex.gap-1.items-center:not(.run-code-button) {
display: none;
}
./open-webui-init.sh
sed -i '$d' /app/backend/start.sh
echo "WEBUI_SECRET_KEY=\"\$WEBUI_SECRET_KEY\" exec uvicorn open_webui.main:app --host \"0.0.0.0\" --port \"\$PORT\" --forwarded-allow-ips '*' &" >> /app/backend/start.sh
echo "WEBUI_SECRET_KEY=\"\$WEBUI_SECRET_KEY\" exec uvicorn open_webui.main:app --host \"::\" --port \"\$PORT\" --forwarded-allow-ips '*'" >> /app/backend/start.sh
#cat /app/backend/start.sh
cp /app/build/assets/custom/fonts/* /app/build/assets/fonts/
cp /app/build/assets/custom/custom.css /app/build/assets/
cp /app/build/assets/custom/custom.js /app/build/assets/
sed -i 's|</head>|<link rel="stylesheet" href="/assets/custom.css"></head>|' /app/build/index.html
sed -i 's|</body>|<script src="/assets/custom.js"></script></body>|' /app/build/index.html
bash /app/backend/start.sh
请参考 openwebui monitor 文档 进行设置
./openwebui-monitor-env
# OpenWebUI Configuration
#OPENWEBUI_DOMAIN=http://open-webui:4567 # OpenWebUI domain, e.g. https://chat.example.com
OPENWEBUI_DOMAIN=https://<chat.你的域名> # OpenWebUI domain, e.g. https://chat.example.com
OPENWEBUI_API_KEY= <open webui 的用户 api key> # OpenWebUI API key for fetching model list
# Access Control
ACCESS_TOKEN=<参照文档设置> # Used for Monitor page login
API_KEY=<参照文档设置> # Used for authentication when sending requests to Monitor
# Price Configuration (Optional, $/million tokens)
# DEFAULT_MODEL_INPUT_PRICE=60 # Default input price for models
# DEFAULT_MODEL_OUTPUT_PRICE=60 # Default output price for models# DEFAULT_MODEL_PER_MSG_PRICE=-1 # Default price per message for models, -1 means charging by tokens
INIT_BALANCE=200 # Initial balance for users, optional 根据具体情况设置
# COST_ON_INLET=0 # Pre-deduction amount on inlet, can be a fixed number (e.g. 0.1) or model-specific (e.g. gpt-4:0.32,gpt-3.5:0.01)
# PostgreSQL Database Configuration (Optional, configure these if using external database)
# POSTGRES_HOST=
# POSTGRES_PORT=
# POSTGRES_USER=
# POSTGRES_PASSWORD=
# POSTGRES_DATABASE=
open-webui-init.sh 之中的两行 echo 指令会同时启动两个 uvicorn 进程,host 分别为 0.0.0.0 和 ::,测试之后发现这样是可以同时使用 localhost:4567 和 [::1]:4567 来访问的。
1panel中openresty相关的设置:
网站 -> OpenResty -> 设置 -> 配置修改
user root;
worker_processes auto;
worker_rlimit_nofile 65535;
error_log /var/log/nginx/error.log notice;
error_log /dev/stdout notice;
pid /var/run/nginx.pid;
events {
worker_connections 10240;
multi_accept on;
use epoll;
}
http {
include mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"';
server_tokens off;
access_log /var/log/nginx/access.log main;
access_log /dev/stdout main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
server_names_hash_bucket_size 512;
client_header_buffer_size 1024k;
client_max_body_size 128m;
keepalive_timeout 300s;
keepalive_requests 100000;
gzip on;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_http_version 1.1;
gzip_comp_level 2;
gzip_types text/plain application/javascript application/x-javascript text/javascript text/css application/xml;
gzip_vary on;
gzip_proxied expired no-cache no-store private auth;
gzip_disable "MSIE [1-6]\.";
limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_conn_zone $server_name zone=perserver:10m;
include /usr/local/openresty/nginx/conf/conf.d/*.conf;
include /usr/local/openresty/1pwaf/data/conf/waf.conf;
proxy_temp_path /www/common/proxy/proxy_temp_dir;
proxy_cache_path /www/common/proxy/proxy_cache_dir levels=1:2 keys_zone=proxy_cache_panel:20m inactive=1d max_size=5g;
proxy_read_timeout 1200s;
proxy_connect_timeout 1200s;
}
替换配置文件中的<#chat.你的域名>为你配置的真实反代域名,并确认已经添加了dns解析。 更推荐使用1panel创建反代域名后参照这个配置,修改你自己的配置。不推荐 直接复制粘贴。
网站 -> <#chat.你的域名> -> 配置 -> 基本设置
| 域名 | 端口 |
|---|---|
| <chat.你的域名> | 80 |
网站 -> <#chat.你的域名> -> 配置 -> 配置文件
upstream openwebui_backend {
keepalive 32; # 保持32个连接在池中
server [::1]:4567;
}
server {
listen 80 ;
listen [::]:80 ;
listen 443 ssl http2 ;
listen [::]:443 ssl http2 ;
server_name <#chat.你的域名>;
index index.php index.html index.htm default.php default.htm default.html;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
proxy_set_header X-Real-IP $remote_addr;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
access_log /www/sites/<#chat.你的域名>/log/access.log main;
error_log /www/sites/<#chat.你的域名>/log/error.log;
location ^~ /.well-known/acme-challenge {
allow all;
root /usr/share/nginx/html;
}
if ($scheme = http) {
return 301 https://$host$request_uri;
}
ssl_certificate /www/sites/<#chat.你的域名>/ssl/fullchain.pem;
ssl_certificate_key /www/sites/<#chat.你的域名>/ssl/privkey.pem;
ssl_protocols TLSv1.3 TLSv1.2 TLSv1.1 TLSv1;
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:!aNULL:!eNULL:!EXPORT:!DSS:!DES:!RC4:!3DES:!MD5:!PSK:!KRB5:!SRP:!CAMELLIA:!SEED;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
error_page 497 https://$host$request_uri;
proxy_set_header X-Forwarded-Proto https;
add_header Strict-Transport-Security "max-age=31536000";
include /www/sites/<#chat.你的域名>/proxy/*.conf;
}
请确保已经在网站的配置文件中已经配置了openwebui_backend ,否则可能会报错。 如果你的openwebui需要修改到其他端口的话,请自行修改配置文件。
网站 -> <#chat.你的域名> -> 配置 -> 反向代理
| 名称 | 前端请求路径 | 后端代理地址 | 缓存 |
|---|---|---|---|
| api-models | /api/models | http://openwebui_backend | OFF |
| root | / | http://openwebui_backend | OFF |
| static-resources | .(jpg jpeg png gif ico svg woff woff2 ttf eot)(.*) | http://openwebui_backend | ON |
api-models -> 源文
location ^~ /api/models {
proxy_pass http://openwebui_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
add_header X-Cache $upstream_cache_status;
proxy_ssl_server_name off;
proxy_ssl_name $proxy_host;
# make nignx auto update cache content
proxy_cache_background_update on; # 这里要求 nginx 自动更新 api
proxy_cache_use_stale updating;
proxy_cache_revalidate on;
proxy_cache_min_uses 1;
proxy_buffering on;
proxy_buffer_size 4k; # 响应头缓冲区
proxy_buffers 8 4k; # 单个连接的缓冲区数量和大小
proxy_busy_buffers_size 8k; # 忙碌时缓冲区大小
proxy_max_temp_file_size 0; # 禁用临时文件(避免磁盘IO瓶颈)
add_header Cache-Control no-cache;
}
root -> 源文
location ^~ / {
proxy_pass http://openwebui_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
add_header X-Cache $upstream_cache_status;
proxy_ssl_server_name off;
proxy_ssl_name $proxy_host;
add_header Strict-Transport-Security "max-age=31536000";
add_header Cache-Control no-cache;
proxy_buffering on;
proxy_buffer_size 16k;
proxy_buffers 8 32k;
proxy_busy_buffers_size 64k;
proxy_max_temp_file_size 0;
}
static-resources -> 源文
location ~* \.(jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|eot)(.*) {
proxy_pass http://openwebui_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
add_header X-Cache $upstream_cache_status;
proxy_ssl_server_name off;
proxy_ssl_name $proxy_host;
proxy_buffering on;
proxy_buffer_size 4k; # 响应头缓冲区
proxy_buffers 8 4k; # 单个连接的缓冲区数量和大小
proxy_busy_buffers_size 8k; # 忙碌时缓冲区大小
proxy_max_temp_file_size 0; # 禁用临时文件(避免磁盘IO瓶颈)
if ( $uri ~* "\.(gif|png|jpg|css|js|woff|woff2)$" ) {
expires 1m;
}
proxy_ignore_headers Set-Cookie Cache-Control expires;
proxy_cache proxy_cache_panel;
proxy_cache_key $host$uri$is_args$args;
proxy_cache_valid 200 304 301 302 10m;
}
提醒
这里只对openwebui做了反向代理,其他的服务,比如,neo api,tts,openwebui-monitor等,都是通过cf tunnel进行转发的。cf tunnel需要自行设置。
docker 相关设置
如果需要添加proxies项,需要替换为正确的地址。
/etc/docker/daemon.json 部分配置
{
"ipv6": true,
"fixed-cidr-v6": "fd00:db8:1::/64",
"ip6tables": true,
"experimental": true,
"registry-mirrors": [
"https://docker.1ms.run",
"https://docker.xuanyuan.me"
],
"proxies": {
"http-proxy": "http://<proxy url>",
"https-proxy": "http://<proxy url>"
}
}
watchtower
其实不是特别推荐使用watchtower来更新容器。
毕竟更新完成之后可能会出问题导致应用无法运行。
如果需要去掉watchtower的label,可以去掉docker-compose.yml中的*watchtower。
如果需要使用watchtower,推荐使用docker login 登录。 可以防止受到限制。
Personal access tokens | Docker
请注意需要替换为正确的地址。
watchtower/docker-compose.yml
version: '3'
services:
watchtower:
image: containrrr/watchtower
container_name: watchtower
restart: unless-stopped
network_mode: host
volumes:
- <用户目录路径。即~的真实路径。cd ~ && pwd 查看>/.docker/config.json:/config.json
- /var/run/docker.sock:/var/run/docker.sock
- /etc/localtime:/etc/localtime:ro
environment:
http_proxy: http://<proxy url>
https_proxy: http://<proxy url>
WATCHTOWER_NOTIFICATIONS: email
WATCHTOWER_NOTIFICATION_EMAIL_FROM: <发件邮箱>
WATCHTOWER_NOTIFICATION_EMAIL_TO: <收件邮箱>
WATCHTOWER_NOTIFICATION_EMAIL_SERVER: <发件服务器>
WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER: <发件用户>
WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD: <发件密码>
WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG: watchtower
WATCHTOWER_INCLUDE_RESTARTING: true
WATCHTOWER_SCHEDULE: 0 0 */2 * * *
# WATCHTOWER_SCHEDULE: 0 0 */6 * * *
WATCHTOWER_CLEANUP: true
WATCHTOWER_TIMEOUT: 60s
WATCHTOWER_LABEL_ENABLE: true
# WATCHTOWER_RUN_ONCE: true
# WATCHTOWER_TRACE: true
WATCHTOWER_WARN_ON_HEAD_FAILURE: never
OpenWebUI模型的图片
可以参考这个帖子修改模型图标。我这里使用的是 lobehub/lobe-icons:
Lobe Icons - Popular AI / LLM Model Brand SVG Logo and Icon Collection.这个库中的图标。比如 图标链接 搭配上我修改过的custom.js可以自动在 日间/夜间 主题的时候切换 亮/暗 图标。
OpenWebUI 的设置
设置 → 通用
| 设置项 | 值 |
|---|---|
| 默认用户角色 | 待激活 |
| 允许新用户注册 | False |
| 在用户待激活界面中显示管理员邮箱等详细信息 | False |
| 启用 API 密钥(OpenWebUI Monitor需要) | True |
设置 → 外部连接
| 设置项 | 值 |
|---|---|
| Ollama API(如果没有使用则关闭) | False |
设置 → 竞技场评估
| 设置项 | 值 |
|---|---|
| 启用竞技场匿名评价模型 | False |
设置 → 界面
| 设置项 | 值 |
|---|---|
| 本地模型 | 当前模型 |
| 外部模型 | 可以使用deepseek v3或者4o mini等较便宜的或者是免费的模型 |
| 标题生成 | True |
| 标签生成 | True |
| 用于自动生成标题的提示词 | Openwebui v0.5.8 及以上版本标题生成 Prompt - 开发调优 - LINUX DO |
| 标签生成提示词 | 标签提示词 |
设置 → 语音
| 设置项 | 值 |
|---|---|
| 文本转语音引擎 | https://<api.你的域名>/v1 token使用neo api中创建的,确定渠道中已添加tts:8080,模型tts-1 |
| 文本转语音音色 | zh-CN-XiaoxiaoNeural |
| 文本转语音模型 | tts-1 |
编辑历史
2025年3月11日 01点16分:修改 mysql 至 postgresql 。
2025年3月12日 01点38分:注释掉user: postgres行,防止初始化时报错。
2025年3月12日 13点50分:修正一个/app/backend/start.sh的错误。
2025年3月12日 21点13分:修正一些空格和换行的问题。
2025年3月12日 23点12分:修复postgresql未创建new-api数据库的问题。
2025年3月19日 09点46分:修复一个因为http而导致语音功能无法使用的问题。
2025年3月22日 21点40分:docker compose增加日志配置,增加health check,切换neo api为new api,增加custom.css,修改不一致的反向代理配置内容。
2025年3月23日 10点28分:修改了一个pipeline导致的报错
2025年3月27日 14点18分:最近在使用gemini-2.5-pro-exp-03-25等思考模型的时候,发现如果长时间思考,但是没有返回内容,会导致反代超时。所以在OpenResty的设置里,设置超时时间为1200s。