librechat / Dockerfile
martynka's picture
Update Dockerfile
35599bf verified
raw
history blame
10.2 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 \
&& 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"]