librechat / Dockerfile
martynka's picture
Update Dockerfile
ff13e42 verified
raw
history blame
8.42 kB
# Use official LibreChat base image
FROM ghcr.io/danny-avila/librechat-dev:latest
# Install Python dependencies
USER root
RUN apk update && apk add --no-cache \
python3 \
py3-pip \
sqlite \
&& pip3 install flask werkzeug waitress requests --break-system-packages
# Setup directory structure
RUN mkdir -p /app/{admin/{templates,static},data,uploads,client/public/images,api/logs} \
&& mkdir -p /app/uploads/temp \
&& chown -R 1000:1000 /app \
&& chmod -R 777 /app/uploads \
&& chmod -R 777 /app/client/public/images \
&& chmod -R 777 /app/api/logs
# ===== Admin Templates =====
COPY <<"EOF" /app/admin/templates/login.html
<!DOCTYPE html>
<html>
<head>
<title>Admin Login</title>
<link rel="stylesheet" href="/static/admin.css">
</head>
<body>
<div class="container">
<h2>Admin Portal</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
# ===== Admin 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
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', () => {
const token = localStorage.getItem('sudo_token');
if (!token && !window.location.pathname.includes('login')) {
window.location.href = '/sudo';
} else if (token) {
loadUsers();
}
});
EOF
# ===== Combined Server =====
COPY <<"EOF" /app/combined_server.py
from flask import Flask, request, jsonify, render_template, make_response
from werkzeug.middleware.proxy_fix import ProxyFix
from werkzeug.security import generate_password_hash
import os
import sqlite3
from pathlib import Path
import subprocess
import requests
from waitress import serve
app = Flask(__name__,
template_folder='/app/admin/templates',
static_folder='/app/admin/static')
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1)
app.secret_key = os.getenv("FLASK_SECRET")
# Database setup
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
# Admin routes
@app.route('/sudo')
def admin_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")
])
@app.route('/sudo/remove_user', methods=['POST'])
def remove_user():
if request.headers.get('X-Sudo-Secret') != os.getenv("SUDO_SECRET"):
return jsonify({"error": "Unauthorized"}), 403
db = get_db()
db.execute("DELETE FROM users WHERE username=?", [request.json["username"]])
db.commit()
return jsonify({"status": "User removed"})
# LibreChat proxy
@app.route('/', defaults={'path': ''})
@app.route('/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE'])
def proxy(path):
if path.startswith('sudo/') or path == 'sudo':
return make_response("Not Found", 404)
resp = requests.request(
method=request.method,
url=f"http://localhost:3080/{path}",
headers={key: value for (key, value) in request.headers if key != 'Host'},
data=request.get_data(),
cookies=request.cookies,
allow_redirects=False
)
response = make_response(resp.content, resp.status_code)
for key, value in resp.headers.items():
if key.lower() not in ['content-encoding', 'content-length', 'transfer-encoding']:
response.headers[key] = value
return response
if __name__ == "__main__":
# Start LibreChat backend
subprocess.Popen(["npm", "run", "backend"], cwd="/app")
# Start combined server
serve(app, host='0.0.0.0', port=7860, threads=4)
EOF
# Startup script
COPY <<"EOF" /app/start.sh
#!/bin/sh
# Start the combined server
python3 /app/combined_server.py
EOF
RUN chmod +x /app/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" \
NODE_ENV=production
EXPOSE 7860
CMD ["/app/start.sh"]