【LLM】局域网内为容器服务启用HTTPS

OpenWebUI中,语音输入需要HTTPS才能使用麦克风等硬件资源,在局域网中通过NGINX转发实现HTTPS访问。

具体包含三个部分

  • 容器部署open-webui和nginx
  • 生成ssl证书
  • 修改nginx配置文件

1、容器部署

基于docker-compose,执行docker-compose up -d即可。

其中主要的就是映射conf文件夹和ssl的路径,不映射手动添加也一样。open-webui的环境变量就看个人需求了。

yaml 复制代码
services:
  open-webui:
    image: ghcr.io/open-webui/open-webui:main
    container_name: open-webui
    restart: always
    networks:
    - openwebui
  
  nginx:
    image: nginx
    container_name: nginx
    restart: always
    volumes:
    - /nginx/conf.d:/etc/nginx/conf.d
    - /nginx/ssl:/ssl
    ports:
    - "38080:8080"
    networks:
    - openwebui

# 为空即可
networks:
  openwebui:

2、生成ssl证书

这网上教程很多,基本一个命令就搞定,这里找GPT写了一个基于Flask的页面,方便访问(问就是因为懒得敲命令 )。具体代码放最后,有需要的可以复制。生成的文件放到上面配置的ssl文件夹里。

3.修改conf

conf.d文件夹下新建default.conf,写入内容如下,修改server_name以及ssl相关文件。

由于配置了network,localtion中访问地址可以直接用docker-compose中的app名字。

重启nginx容器即可访问。

shell 复制代码
server {
	listen 8080 ssl;
	server_name yourdomain.com;
	# 配置crt
	ssl_certificate "/ssl/xxx.crt";
	# 配置key
	ssl_certificate_key "/ssl/xxx.key";
	
	location /{
		proxy_pass http://open-webui:8080;
		proxy_set_header X-Real-IP $remote_addr;
		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
		proxy_set_header Host $http_host;
		proxy_set_header X-NginX-Proxy true;
		proxy_redirect default;
	}
}

附.生成ssl证书代码

pip install cryptography

python 复制代码
import os
from flask import Flask, jsonify, send_from_directory, request, render_template_string
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
from datetime import datetime, timedelta

app = Flask(__name__)

# SSL 证书文件夹
SSL_FOLDER = "ssl"
if not os.path.exists(SSL_FOLDER):
    os.makedirs(SSL_FOLDER)


# 生成 SSL 证书
def generate_ssl_cert(domain_name):
    key = rsa.generate_private_key(
        public_exponent=65537,
        key_size=2048,
        backend=default_backend()
    )

    subject = issuer = x509.Name([
        x509.NameAttribute(NameOID.COUNTRY_NAME, u"US"),
        x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, u"California"),
        x509.NameAttribute(NameOID.LOCALITY_NAME, u"San Francisco"),
        x509.NameAttribute(NameOID.ORGANIZATION_NAME, u"My Organization"),
        x509.NameAttribute(NameOID.COMMON_NAME, domain_name),
    ])

    cert = x509.CertificateBuilder().subject_name(
        subject
    ).issuer_name(
        issuer
    ).public_key(
        key.public_key()
    ).serial_number(
        x509.random_serial_number()
    ).not_valid_before(
        datetime.utcnow()
    ).not_valid_after(
        datetime.utcnow() + timedelta(days=365)
    ).add_extension(
        x509.SubjectAlternativeName([x509.DNSName(domain_name)]),
        critical=False,
    ).sign(key, hashes.SHA256(), default_backend())

    cert_filename = f"{domain_name}.crt"
    key_filename = f"{domain_name}.key"

    cert_path = os.path.join(SSL_FOLDER, cert_filename)
    key_path = os.path.join(SSL_FOLDER, key_filename)

    with open(key_path, "wb") as f:
        f.write(key.private_bytes(
            encoding=serialization.Encoding.PEM,
            format=serialization.PrivateFormat.TraditionalOpenSSL,
            encryption_algorithm=serialization.NoEncryption()
        ))

    with open(cert_path, "wb") as f:
        f.write(cert.public_bytes(serialization.Encoding.PEM))

    return cert_filename, key_filename


# 首页接口,显示 HTML 页面
@app.route("/", methods=["GET"])
def index():
    return render_template_string('''
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SSL Certificate Generator</title>
    <script>
        async function generateCert() {
            const domainName = document.getElementById('domain_name').value;
            const response = await fetch('/generate_cert', {
                method: 'POST',
                headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                body: new URLSearchParams({ domain_name: domainName })
            });

            if (response.ok) {
                const result = await response.json();
                document.getElementById('cert_link').href = `/download_cert/${result.certificate}`;
                document.getElementById('cert_link').innerText = result.certificate;
                document.getElementById('key_link').href = `/download_cert/${result.key}`;
                document.getElementById('key_link').innerText = result.key;
            } else {
                alert('Error generating certificate');
            }
        }
    </script>
</head>
<body>
    <h1>SSL Certificate Generator</h1>
    <form οnsubmit="event.preventDefault(); generateCert();">
        <label for="domain_name">Domain Name:</label>
        <input type="text" id="domain_name" name="domain_name" required>
        <button type="submit">Generate Certificate</button>
    </form>

    <h2>Generated Files</h2>
    <p>Certificate: <a id="cert_link" href="#" target="_blank">No certificate generated</a></p>
    <p>Private Key: <a id="key_link" href="#" target="_blank">No key generated</a></p>
</body>
</html>
    ''')


# 生成证书接口
@app.route("/generate_cert", methods=["POST"])
def generate_cert():
    domain_name = request.form.get("domain_name")
    if not domain_name:
        return jsonify({"error": "Domain name is required"}), 400

    cert_filename, key_filename = generate_ssl_cert(domain_name)
    return jsonify({"certificate": cert_filename, "key": key_filename})


# 下载证书接口
@app.route("/download_cert/<filename>", methods=["GET"])
def download_cert(filename):
    return send_from_directory(SSL_FOLDER, filename)


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000, debug=True)
相关推荐
真忒修斯之船5 小时前
大模型分布式训练并行技术(三)流水线并行
面试·llm·aigc
SpikeKing5 小时前
LLM - 使用 LLaMA-Factory 微调大模型 环境配置与训练推理 教程 (1)
人工智能·llm·大语言模型·llama·环境配置·llamafactory·训练框架
数据智能老司机1 天前
LLM工程师手册——监督微调
深度学习·架构·llm
AI_小站1 天前
LLM——10个大型语言模型(LLM)常见面试题以及答案解析
人工智能·程序人生·语言模型·自然语言处理·大模型·llm·大模型面试
waiting不是违停2 天前
LangChain Ollama实战文献检索助手(二)少样本提示FewShotPromptTemplate示例选择器
langchain·llm·ollama
我爱学Python!2 天前
AI Prompt如何帮你提升论文中的逻辑推理部分?
人工智能·程序人生·自然语言处理·chatgpt·llm·prompt·提示词
AI_小站2 天前
多模态大模型微调实践!PAI+LLaMA Factory搭建AI导游
人工智能·程序人生·语言模型·大模型·llm·产品经理·多模态大模型
AI_小站2 天前
【AI工作流】FastGPT - 深入解析FastGPT工作流编排:从基础到高级应用的全面指南
人工智能·程序人生·语言模型·大模型·llm·fastgpt·大模型应用
蚝油菜花3 天前
MeetingMind:AI 会议助手,支持自动转录音频并提取会议中的关键信息
人工智能·开源·llm
Agile.Zhou4 天前
给 Ollama 穿上 GPT 的外衣
llm·ollama