Spaces:
Running
Running
# 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 \ | |
&& mkdir -p /var/lib/nginx/tmp \ | |
&& chown -R nginx:nginx /var/lib/nginx \ | |
&& chmod -R 770 /var/lib/nginx \ | |
&& mkdir -p /app/admin/{templates,static} \ | |
&& mkdir -p /app/data \ | |
&& chown -R 1000:1000 /app \ | |
&& mkdir -p /app/uploads/temp \ | |
&& chmod -R 777 /app/uploads/temp \ | |
&& chmod -R 777 /app/client/public/images \ | |
&& chmod -R 777 /app/api/logs \ | |
&& mkdir -p /app/{nginx/{logs,tmp,client_body},admin/{templates,static},data,uploads,client/public/images,api/logs} \ | |
&& mkdir -p /app/nginx/client_body \ | |
&& chmod -R 777 /app/nginx/client_body | |
# 2. Recompile NGINX with custom paths (critical fix) | |
RUN apk add --no-cache --virtual .build-deps \ | |
build-base \ | |
linux-headers \ | |
openssl-dev \ | |
pcre-dev \ | |
zlib-dev \ | |
&& wget http://nginx.org/download/nginx-1.24.0.tar.gz \ | |
&& tar xzf nginx-1.24.0.tar.gz \ | |
&& cd nginx-1.24.0 \ | |
&& ./configure \ | |
--prefix=/app/nginx \ | |
--sbin-path=/usr/sbin/nginx \ | |
--conf-path=/app/nginx/nginx.conf \ | |
--error-log-path=/app/nginx/logs/error.log \ | |
--pid-path=/app/nginx/nginx.pid \ | |
--lock-path=/app/nginx/nginx.lock \ | |
--http-log-path=/app/nginx/logs/access.log \ | |
--http-client-body-temp-path=/app/nginx/client_body \ | |
--http-proxy-temp-path=/app/nginx/tmp \ | |
--http-fastcgi-temp-path=/app/nginx/tmp \ | |
--user=nginx \ | |
--group=nginx \ | |
&& make \ | |
&& make install \ | |
&& apk del .build-deps \ | |
&& rm -rf /nginx-1.24.0* | |
# ===== 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')) | |
# Validate secrets | |
required_secrets = ['FLASK_SECRET', 'SUDO_SECRET'] | |
for secret in required_secrets: | |
if not os.getenv(secret): | |
raise RuntimeError(f"Missing required secret: {secret}") | |
app.secret_key = os.getenv("FLASK_SECRET") | |
# SQLite database | |
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") | |
]) | |
@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"}) | |
if __name__ == "__main__": | |
app.run(host="0.0.0.0", port=5000) | |
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 | |
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 | |
COPY <<"EOF" /app/nginx/nginx.conf | |
worker_processes auto; | |
error_log /app/nginx/logs/error.log warn; | |
pid /tmp/nginx.pid; | |
events { | |
worker_connections 1024; | |
} | |
http { | |
include /etc/nginx/mime.types; | |
default_type application/octet-stream; | |
sendfile on; | |
keepalive_timeout 65; | |
client_max_body_size 20M; | |
client_body_temp_path /app/nginx/client_body; | |
proxy_temp_path /app/nginx/tmp; | |
fastcgi_temp_path /app/nginx/tmp; | |
uwsgi_temp_path /app/nginx/tmp; | |
scgi_temp_path /app/nginx/tmp; | |
server { | |
listen 7860; | |
server_name localhost; | |
root /app; | |
# LibreChat API | |
location / { | |
proxy_pass http://localhost:3080; | |
proxy_set_header Host \$host; | |
proxy_set_header X-Real-IP \$remote_addr; | |
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; | |
} | |
# 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; | |
expires 30d; | |
access_log off; | |
} | |
access_log /app/nginx/logs/access.log; | |
} | |
} | |
EOF | |
RUN chmod 777 /app/nginx/nginx.conf | |
# Startup script | |
COPY <<"EOF" /start.sh | |
#!/bin/sh | |
# Fix permissions | |
#chown -R nginx:nginx /var/lib/nginx | |
#chmod -R 770 /var/lib/nginx | |
# Start LibreChat (using the correct command) | |
cd /app && npm run backend & | |
# Start Admin Panel | |
cd /app/admin && python3 app.py & | |
# Start NGINX | |
nginx -c /app/nginx/nginx.conf -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" \ | |
NODE_ENV=production | |
EXPOSE 7860 | |
CMD ["/start.sh"] |