librechat / Dockerfile
martynka's picture
Update Dockerfile
a08bc08 verified
raw
history blame
8.08 kB
# Use official LibreChat base image
FROM ghcr.io/danny-avila/librechat-dev:latest
# Install system dependencies
USER root
RUN apk update && apk add --no-cache \
nginx \
python3 \
py3-pip \
sqlite \
&& pip3 install flask werkzeug --break-system-packages
# Setup directories within /app
RUN mkdir -p /app/admin/{templates,static} \
&& mkdir -p /app/data \
&& chmod -R 777 /app/client/public/images \
&& chmod -R 777 /app/api/logs
# ===== Integrated Admin Panel =====
COPY <<"EOF" /app/admin/app.py
from flask import Flask, request, jsonify, render_template
import os
import sqlite3
from werkzeug.security import generate_password_hash
from pathlib import Path
app = Flask(__name__,
template_folder=str(Path(__file__).parent/'templates'),
static_folder=str(Path(__file__).parent/'static'))
app.secret_key = os.getenv("FLASK_SECRET")
# SQLite in /app/data (ephemeral in free HF Spaces)
DB_PATH = '/app/data/admin.db'
def get_db():
Path(DB_PATH).parent.mkdir(exist_ok=True)
db = sqlite3.connect(DB_PATH)
db.execute('''CREATE TABLE IF NOT EXISTS users
(username TEXT PRIMARY KEY, password TEXT, role TEXT)''')
return db
@app.route('/sudo')
def home():
return render_template('login.html')
@app.route('/sudo/login', methods=['POST'])
def login():
if request.json.get('sudo_secret') == os.getenv("SUDO_SECRET"):
return jsonify({"status": "success"})
return jsonify({"error": "Invalid secret"}), 403
@app.route('/sudo/dashboard')
def dashboard():
return render_template('dashboard.html')
@app.route('/sudo/add_user', methods=['POST'])
def add_user():
if request.headers.get('X-Sudo-Secret') != os.getenv("SUDO_SECRET"):
return jsonify({"error": "Unauthorized"}), 403
db = get_db()
try:
db.execute("INSERT INTO users VALUES (?,?,?)", [
request.json["username"],
generate_password_hash(request.json["password"]),
"user"
])
db.commit()
return jsonify({"status": "User added"})
except sqlite3.IntegrityError:
return jsonify({"error": "User exists"}), 400
@app.route('/sudo/list_users', methods=['GET'])
def list_users():
if request.headers.get('X-Sudo-Secret') != os.getenv("SUDO_SECRET"):
return jsonify({"error": "Unauthorized"}), 403
db = get_db()
return jsonify([
{"username": row[0]} for row in db.execute("SELECT username FROM users")
])
EOF
# Admin templates
COPY <<"EOF" /app/admin/templates/login.html
<!DOCTYPE html>
<html>
<head>
<title>LibreChat Admin</title>
<link rel="stylesheet" href="/static/admin.css">
</head>
<body>
<div class="container">
<h2>LibreChat Admin</h2>
<form id="loginForm">
<input type="password" name="sudo_secret" placeholder="Admin Secret" required>
<button type="submit">Login</button>
</form>
</div>
<script src="/static/admin.js"></script>
</body>
</html>
EOF
COPY <<"EOF" /app/admin/templates/dashboard.html
<!DOCTYPE html>
<html>
<head>
<title>User Management</title>
<link rel="stylesheet" href="/static/admin.css">
</head>
<body>
<div class="container">
<h1>User Management</h1>
<div class="card">
<h2>Add User</h2>
<form id="addUserForm">
<input type="text" name="username" placeholder="Username" required>
<input type="password" name="password" placeholder="Password" required>
<button type="submit">Add User</button>
</form>
</div>
<div class="card">
<h2>Current Users</h2>
<ul id="userList"></ul>
</div>
</div>
<script src="/static/admin.js"></script>
</body>
</html>
EOF
# Static files
COPY <<"EOF" /app/admin/static/admin.css
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 800px;
margin: 0 auto;
}
.card {
background: white;
padding: 20px;
margin: 20px 0;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
input, button {
width: 100%;
padding: 10px;
margin: 8px 0;
box-sizing: border-box;
}
button {
background-color: #4CAF50;
color: white;
border: none;
cursor: pointer;
}
EOF
# Properly escaped JavaScript
COPY <<"EOF" /app/admin/static/admin.js
async function loadUsers() {
const response = await fetch('/sudo/list_users', {
headers: {'X-Sudo-Secret': localStorage.getItem('sudo_token')}
});
if (response.ok) {
const users = await response.json();
document.getElementById('userList').innerHTML = users.map(user =>
\`<li>\${user.username} <button onclick="deleteUser('\${user.username}')">Delete</button></li>\`
).join('');
}
}
async function deleteUser(username) {
if (confirm(\`Delete \${username}?\`)) {
await fetch('/sudo/remove_user', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Sudo-Secret': localStorage.getItem('sudo_token')
},
body: JSON.stringify({ username: username })
});
loadUsers();
}
}
document.getElementById('addUserForm').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const response = await fetch('/sudo/add_user', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Sudo-Secret': localStorage.getItem('sudo_token')
},
body: JSON.stringify({
username: formData.get('username'),
password: formData.get('password')
})
});
if (response.ok) {
e.target.reset();
await loadUsers();
}
});
document.addEventListener('DOMContentLoaded', () => {
if (!localStorage.getItem('sudo_token') && !window.location.pathname.includes('login')) {
window.location.href = '/sudo';
}
loadUsers();
});
EOF
# Add delete endpoint to app.py
RUN echo '@app.route("/sudo/remove_user", methods=["POST"])' >> /app/admin/app.py
RUN echo 'def remove_user():' >> /app/admin/app.py
RUN echo ' if request.headers.get("X-Sudo-Secret") != os.getenv("SUDO_SECRET"):' >> /app/admin/app.py
RUN echo ' return jsonify({"error": "Unauthorized"}), 403' >> /app/admin/app.py
RUN echo ' db = get_db()' >> /app/admin/app.py
RUN echo ' db.execute("DELETE FROM users WHERE username=?", [request.json["username"]])' >> /app/admin/app.py
RUN echo ' db.commit()' >> /app/admin/app.py
RUN echo ' return jsonify({"status": "User removed"})' >> /app/admin/app.py
# NGINX Configuration
COPY <<"EOF" /etc/nginx/nginx.conf
events { worker_connections 1024; }
http {
server {
listen 7860;
# LibreChat API
location / {
proxy_pass http://localhost:3080;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
}
# Admin Panel
location /sudo {
proxy_pass http://localhost:5000;
proxy_set_header X-Sudo-Secret \$http_x_sudo_secret;
}
# Static files
location /static {
alias /app/admin/static;
}
}
}
EOF
# Startup script
COPY <<"EOF" /start.sh
#!/bin/sh
# Start LibreChat
cd /app/api && npm run backend &
# Start Admin Panel
cd /app/admin && python3 app.py &
# Start NGINX
nginx -g "daemon off;"
# Keep container running
wait
EOF
RUN chmod +x /start.sh
# Environment variables
ENV HOST=0.0.0.0 \
PORT=3080 \
SESSION_EXPIRY=900000 \
REFRESH_TOKEN_EXPIRY=604800000 \
SEARCH=true \
MEILI_NO_ANALYTICS=true \
MEILI_HOST=https://martynka-meilisearch.hf.space \
CONFIG_PATH=/app/librechat.yaml \
CUSTOM_FOOTER=EasierIT \
MONGO_URI="$MONGO_URI" \
SUDO_SECRET="$SUDO_SECRET" \
FLASK_SECRET="$FLASK_SECRET"
EXPOSE 7860
CMD ["/start.sh"]