使用docker compose ,搭建 OpenWebUI 教程,部分配置优化,启用ipv6

前言

首先贴出配置文件。
相关配置文件因为比较杂乱所以都折叠起来了。可以点击后展开查看。

配置文件

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 apittsopenwebui-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: :pretzel: 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 apinew api,增加custom.css,修改不一致的反向代理配置内容。
2025年3月23日 10点28分:修改了一个pipeline导致的报错
2025年3月27日 14点18分:最近在使用gemini-2.5-pro-exp-03-25等思考模型的时候,发现如果长时间思考,但是没有返回内容,会导致反代超时。所以在OpenResty的设置里,设置超时时间为1200s。

88 个赞

参考帖子

至于搭建的过程,参考了

应该还参考了其他的贴子,但是由于我没有把帖子都加入书签,导致我只能找到这几个姑且还记得住的帖子了。总之感谢以上大佬。

感谢

感谢 @funnycups @fl0w1nd 两位佬友给出的意见和建议。

12 个赞

不错 帮顶~先马后看

3 个赞

突然发现忘了附上Open web UI 的函数。

Context Clip Filter Function | Open WebUI Community

设置项名称 设置项说明
N Last Messages 可以设置最大对话轮数。我这里设置为了40。 40

OpenWebUI-Monitor

设置项名称 设置项说明
Api Endpoint 参照 文档进行设置 -
Api Key 参照 文档进行设置 -
Priority - 5
Show Cost 显示费用 True
Show Balance 显示余额 True
Show Spend Time 显示耗时 True
Show Tokens 显示Token数 True
Show Tokens Per Sec 显示每秒Token数 True
Language - zh
6 个赞

赞佬友!学习起来

3 个赞

感觉你这里面很多服务其实都可以分开来,没必要都在一个compose文件里定义吧?
而且为何要同时用两个数据库?如果是历史原因还好,如果从头开始部署,选一个用不是更好吗

3 个赞

是在哪里设置cf tunnel呢?

3 个赞

我是觉得既然是一套服务,干脆放在一起管理比较好。
至于数据库…其实是历史原因啦,刚开始都是用的sqlite。后来比较担心性能问题。于是按照new-api仓库的docker-compose配置文件加了mysql。
至于postgresql,记不起来了,不过应该也是按照某个教程帖子配的。

4 个赞

可以参考

https://linux.do/t/topic/293931

5 个赞

真不错,学习起来

6 个赞

原来如此,历史原因倒好理解,我自己也是因为某些原因,mysql和postgres共存的,一直没时间管。但如果是作为教程,最好还是统一一下,只用mysql或postgres比较好
我的newapi和数据库都有其他用处,所以全部都分开来配置的,分开来管理出了问题也好修改

4 个赞

数据库全用 postgresql 得了, new-api 配置环境变量SQL_DSN=postgres://newapi:password@postgres:5432/newapi

4 个赞

非常有道理,改了。

3 个赞

很详细:+1:

4 个赞

原来是另外配置,我以为要在教程的配置文件里面改 :sweat_smile:

5 个赞

太强了,大佬

4 个赞

太强了 学习一下。

4 个赞

很不错,有帮助

4 个赞

是不是少东西了
Failed to load /root/.openwebui-monitor-env: open /root/.openwebui-monitor-env: no such file or directory

4 个赞

补上了,刷新下应该就能看到了

3 个赞