User Tools

Site Tools


computing:librechat

  • librechat
  • Jonathan Haack
  • Haack's Networking
  • webmaster@haacksnetworking.org

librechat


Introduction

This is a tutorial for Debian users to setup Librechat and attach a custom API to it. I chose to use Deepinfra for cost and speed reasons, but/and used the open weight DeepSeek v4 Flash model via the Deepinfra API. I also built an MCP server to create docx, xlsx, pptx, and pdf files using python under the hood. Lastly, I used the built-in and free keenable service so that the chat could scrape websites.To begin with, let's install required dependencies, webserver, database, and service manager.

sudo apt update && sudo apt upgrade -y
sudo apt install -y git build-essential curl gnupg python3-pip python3-venv

curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
sudo apt install -y nodejs
sudo npm install -g npm@11
node -v && npm -v

wget -qO - https://www.mongodb.org/static/pgp/server-8.0.asc | sudo gpg --dearmor -o /usr/share/keyrings/mongodb-server-8.0.gpg
echo "deb [ signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] https://repo.mongodb.org/apt/debian bookworm/mongodb-org/8.0 main" | sudo tee /etc/apt/sources.list.d/mongodb-org-8.0.list
sudo apt update
sudo apt install -y mongodb-org
sudo systemctl enable --now mongod

sudo npm install -g pm2

Now, let's clone the librechat repo and setup the virtual environment and the server configuration.

cd /opt
sudo git clone https://github.com/LibreChat-AI/LibreChat.git
sudo chown -R $USER:$USER LibreChat
cd LibreChat
cp .env.example .env
nano .env

Email and other add-ons and other niceties and such can all be set up later to one's liking and as they see fit. The bare bones requirements are:

MONGO_URI=mongodb://127.0.0.1:27017/LibreChat

JWT_SECRET=your_64_hex_secret
JWT_REFRESH_SECRET=your_64_hex_secret
CREDS_KEY=your_64_hex_secret
CREDS_IV=your_32_hex_iv

DOMAIN_CLIENT=https://chat.haacksnetworking.org
DOMAIN_SERVER=https://chat.haacksnetworking.org

TRUST_PROXY=1
HOST=127.0.0.1

# This creates an env variable for your deepinfra key, which gets called in librechat.yaml
DEEPINFRA_API_KEY=sk-di-your_key_here

Use openssl rand -hex 32 and openssl rand -hex 16 for the IV in order to generate secrets. Start service for first time and debug. Once it is functional and working - not before - then run the pm2 commands which will auto-generate systemd units and give you control of them with pm2 commands.

npm run reinstall
npm run backend #test, debug, then use pm2 instead       
pm2 start npm --name librechat -- run backend
pm2 save
pm2 startup          

You can optionally setup a firewall:

sudo ufw allow OpenSSH
sudo ufw allow 'Apache Full'
sudo ufw enable

Create or edit /opt/LibreChat/librechat.yaml:

# minimum information and parameters
version: 1.3.17
cache: true

endpoints:
  custom:
    - name: "DeepInfra DeepSeek"
      apiKey: "${DEEPINFRA_API_KEY}"
      baseURL: "https://api.deepinfra.com/v1/openai"
      models:
        default: ["deepseek-ai/DeepSeek-V4-Flash"]
        fetch: false
      titleConvo: true
      titleModel: "deepseek-ai/DeepSeek-V4-Flash"
      modelDisplayLabel: "DeepSeek V4 Flash"

# this is a built in and free in cost web scraper 
webSearch:
  searchProvider: "keenable"
  scraperProvider: "keenable"
  rerankerType: "none"

# don't add this block until after initial setup and when you later create the mcp server
mcpServers:
  document-tools:
    type: stdio
    command: /opt/mcp-docs/venv/bin/python
    args:
      - /opt/mcp-docs/server.py
    timeout: 60000

pm2 restart librechat

Once librechat is running properly under pm2, the next most important step is setting up the apache reverse proxy and setting up the admin account. I just use the default apache virtual host for the domain, let apache cut and generate the cert using the non-RP vhost config, and then once the librechat service is running locally, I swap the virtual hosts with the reverse proxy versions instead and then restart the service. Here are the packages and the cert cutting commands:

sudo a2enmod proxy proxy_http proxy_wstunnel ssl rewrite headers socache_shmcb
sudo systemctl restart apache2
sudo apt install certbot letsencrypt python3-certbot-apache
sudo certbot --authenticator standalone --installer apache -d chat.haacksnetworking.org --pre-hook "systemctl stop apache2" --post-hook "systemctl start apache2"

Here is your http vhost nano /etc/apache2/sites-available/chat.haacksnetworking.org.conf

<VirtualHost *:80>
    ServerName chat.haacksnetworking.org
    Redirect permanent / https://chat.haacksnetworking.org/
</VirtualHost>

Here is your https vhost nano /etc/apache2/sites-available/chat.haacksnetworking.org-le-ssl.conf

<VirtualHost *:443>
    ServerName chat.haacksnetworking.org
    SSLEngine on
    SSLCertificateFile /etc/letsencrypt/live/chat.haacksnetworking.org/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/chat.haacksnetworking.org/privkey.pem
    Include /etc/letsencrypt/options-ssl-apache.conf

    ProxyPreserveHost On
    ProxyRequests Off

    # Exclude downloads from proxy - must come before ProxyPass /
    # You create this down below for MCP downloads
    ProxyPass /downloads !
    Alias /downloads /opt/LibreChat/generated_docs
    <Directory /opt/LibreChat/generated_docs>
        Options -Indexes
        Require all granted
    </Directory>

    ProxyPass / http://127.0.0.1:3080/
    ProxyPassReverse / http://127.0.0.1:3080/

    RequestHeader set X-Forwarded-Proto "https"
    RequestHeader set X-Forwarded-Port "443"
    RequestHeader set X-Real-IP "%{REMOTE_ADDR}s"
    RequestHeader set X-Forwarded-For "%{REMOTE_ADDR}s"

    RewriteEngine On
    RewriteCond %{HTTP:Upgrade} =websocket [NC]
    RewriteCond %{HTTP:Connection} upgrade [NC]
    RewriteRule ^/?(.*) "ws://127.0.0.1:3080/$1" [P,L]

    ProxyTimeout 300
    Timeout 300
    LimitRequestBody 26214400

    ErrorLog ${APACHE_LOG_DIR}/librechat-error.log
    CustomLog ${APACHE_LOG_DIR}/librechat-access.log combined
</VirtualHost>

Once you swap the vhosts with the reverse proxy configs you enable the vhosts, check the config, and reload the latest apache2 config for the currently running service.

sudo a2ensite librechat
sudo apache2ctl configtest
sudo systemctl reload apache2

You can now go to your domain.com in your web browser and setup your admin account. Make sure to verify it with mongodb on the backend. After setting up the account, run

mongosh --quiet --eval 'db = db.getSiblingDB("LibreChat"); db.users.find({}, {email:1, role:1}).pretty()'

If you want to disable public registration, add the following to /opt/LibreChat/.env:

ALLOW_REGISTRATION=false

Then create users with npm run create-user and just enter the parameters and restart the service. Once you are logged in, select the model you attached in the upper left drop down menu and test it for functionality. So long as it is working, you can now move on to creating an mcp server to do some common tasks like create docx, xlsx, pptx, and pdf files. Here's what I came up with:

cd /opt
mkdir -p /opt/mcp-docs
cd /opt/mcp-docs
python3 -m venv venv
source venv/bin/activate
pip install mcp python-docx openpyxl pandas reportlab python-pptx

Create /opt/mcp-docs/server.py and create python programs that do the various tasks on the host/vm running librechat:

#!/usr/bin/env python3
import json
import os
from mcp.server import MCPServer
from docx import Document
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill
from reportlab.lib.pagesizes import letter
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
from reportlab.lib.styles import getSampleStyleSheet
from pptx import Presentation

OUTPUT_DIR = "/opt/LibreChat/generated_docs"
os.makedirs(OUTPUT_DIR, exist_ok=True)

mcp = MCPServer("document-tools")

@mcp.tool()
def create_docx(title: str, content: list = None, body: str = None) -> str:
    """Create a Word (.docx) document.
    Prefer content as a list of objects: [{"type":"heading|paragraph|bullet", "text":"..."}].
    If that fails, pass body as a plain multi-line string.
    """
    doc = Document()
    doc.add_heading(title, 0)

    items = content or []
    if not items and body:
        for line in body.splitlines():
            line = line.strip()
            if not line:
                continue
            if line.startswith("#"):
                doc.add_heading(line.lstrip("# ").strip(), level=1)
            elif line.startswith("- ") or line.startswith("* "):
                doc.add_paragraph(line[2:].strip(), style="List Bullet")
            else:
                doc.add_paragraph(line)
    else:
        for item in items:
            if isinstance(item, str):
                doc.add_paragraph(item)
                continue
            if not isinstance(item, dict):
                continue
            t = (item.get("type") or "paragraph").lower()
            text = item.get("text") or item.get("content") or ""
            if not text:
                continue
            if t == "heading":
                doc.add_heading(text, level=1)
            elif t == "bullet":
                doc.add_paragraph(text, style="List Bullet")
            else:
                doc.add_paragraph(text)

    filename = f"{title.replace(' ', '_')}.docx"
    path = os.path.join(OUTPUT_DIR, filename)
    doc.save(path)
    return json.dumps({
        "success": True,
        "file": filename,
        "download": f"https://chat.haacksnetworking.org/downloads/{filename}"
    })

@mcp.tool()
def create_xlsx(name: str, headers: list, rows: list) -> str:
    """Create an Excel (.xlsx) spreadsheet."""
    wb = Workbook()
    ws = wb.active
    ws.title = name[:31]
    for col, h in enumerate(headers, 1):
        cell = ws.cell(1, col, h)
        cell.font = Font(bold=True, color="FFFFFF")
        cell.fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
    for r_idx, row in enumerate(rows, 2):
        for c_idx, val in enumerate(row, 1):
            ws.cell(r_idx, c_idx, val)
    filename = f"{name.replace(' ', '_')}.xlsx"
    path = os.path.join(OUTPUT_DIR, filename)
    wb.save(path)
    return json.dumps({
        "success": True,
        "file": filename,
        "download": f"https://chat.haacksnetworking.org/downloads/{filename}"
    })

@mcp.tool()
def create_pdf(title: str, content: list = None, body: str = None) -> str:
    """Create a PDF document. content is a list of dicts with type (heading/paragraph/bullet) and text, or pass body as plain text."""
    filename = f"{title.replace(' ', '_')}.pdf"
    path = os.path.join(OUTPUT_DIR, filename)
    doc = SimpleDocTemplate(path, pagesize=letter)
    styles = getSampleStyleSheet()
    story = []

    story.append(Paragraph(title, styles["Title"]))
    story.append(Spacer(1, 12))

    items = content or []
    if not items and body:
        for line in body.splitlines():
            line = line.strip()
            if not line:
                continue
            if line.startswith("#"):
                story.append(Paragraph(line.lstrip("# ").strip(), styles["Heading2"]))
            elif line.startswith("- ") or line.startswith("* "):
                story.append(Paragraph(f"• {line[2:].strip()}", styles["Normal"]))
            else:
                story.append(Paragraph(line, styles["Normal"]))
            story.append(Spacer(1, 6))
    else:
        for item in items:
            if isinstance(item, str):
                story.append(Paragraph(item, styles["Normal"]))
                story.append(Spacer(1, 6))
                continue
            if not isinstance(item, dict):
                continue
            t = (item.get("type") or "paragraph").lower()
            text = item.get("text") or item.get("content") or ""
            if not text:
                continue
            if t == "heading":
                story.append(Paragraph(text, styles["Heading2"]))
            elif t == "bullet":
                story.append(Paragraph(f"• {text}", styles["Normal"]))
            else:
                story.append(Paragraph(text, styles["Normal"]))
            story.append(Spacer(1, 6))

    doc.build(story)
    return json.dumps({
        "success": True,
        "file": filename,
        "download": f"https://chat.haacksnetworking.org/downloads/{filename}"
    })

@mcp.tool()
def create_pptx(title: str, slides: list = None) -> str:
    """Create a PowerPoint (.pptx). slides is a list of dicts with title and content (list of bullet strings)."""
    prs = Presentation()
    title_slide_layout = prs.slide_layouts[0]
    slide = prs.slides.add_slide(title_slide_layout)
    slide.shapes.title.text = title
    if len(slide.placeholders) > 1:
        try:
            slide.placeholders[1].text = "Generated by LibreChat"
        except Exception:
            pass

    bullet_layout = prs.slide_layouts[1]
    for s in (slides or []):
        slide = prs.slides.add_slide(bullet_layout)
        slide.shapes.title.text = s.get("title", "Slide")
        body = slide.placeholders[1]
        tf = body.text_frame
        tf.text = ""
        for bullet in s.get("content", []):
            p = tf.add_paragraph()
            p.text = str(bullet)
            p.level = 0

    filename = f"{title.replace(' ', '_')}.pptx"
    path = os.path.join(OUTPUT_DIR, filename)
    prs.save(path)
    return json.dumps({
        "success": True,
        "file": filename,
        "download": f"https://chat.haacksnetworking.org/downloads/{filename}"
    })

if __name__ == "__main__":
    mcp.run(transport="stdio")
chmod +x /opt/mcp-docs/server.py
mkdir -p /opt/LibreChat/generated_docs
deactivate
pm2 restart librechat

The python above is 4 programs that do each of the various tasks of creating doc files, spreadsheets, powerpoints, and pdfs. You can customize names to your preference. I have not gotten to the point of needing to update yet, but/and the barebones update pathway would be something like this:

cd /opt/LibreChat
pm2 stop librechat
git pull
npm run smart-reinstall
pm2 restart librechat

— oemb1905 2026/10/17 19:44

computing/librechat.txt · Last modified: by oemb1905