Spaces:
Running
Running
Update Dockerfile
Browse files- Dockerfile +116 -192
Dockerfile
CHANGED
@@ -1,150 +1,32 @@
|
|
1 |
# Use official LibreChat base image
|
2 |
FROM ghcr.io/danny-avila/librechat-dev:latest
|
3 |
|
4 |
-
# Install
|
5 |
USER root
|
6 |
RUN apk update && apk add --no-cache \
|
7 |
-
nginx \
|
8 |
python3 \
|
9 |
py3-pip \
|
10 |
sqlite \
|
11 |
-
&& pip3 install flask werkzeug
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
&& mkdir -p /app/admin/{templates,static} \
|
16 |
-
&& mkdir -p /app/data \
|
17 |
&& chown -R 1000:1000 /app \
|
18 |
-
&&
|
19 |
-
&& chmod -R 777 /app/uploads/temp \
|
20 |
&& chmod -R 777 /app/client/public/images \
|
21 |
-
&& chmod -R 777 /app/api/logs
|
22 |
-
&& mkdir -p /app/{nginx/{logs,tmp,client_body},admin/{templates,static},data,uploads,client/public/images,api/logs} \
|
23 |
-
&& mkdir -p /app/nginx/client_body \
|
24 |
-
&& chmod -R 777 /app/nginx/client_body
|
25 |
-
# 2. Recompile NGINX with custom paths (critical fix)
|
26 |
-
RUN apk add --no-cache --virtual .build-deps \
|
27 |
-
build-base \
|
28 |
-
linux-headers \
|
29 |
-
openssl-dev \
|
30 |
-
pcre-dev \
|
31 |
-
zlib-dev \
|
32 |
-
&& wget http://nginx.org/download/nginx-1.24.0.tar.gz \
|
33 |
-
&& tar xzf nginx-1.24.0.tar.gz \
|
34 |
-
&& cd nginx-1.24.0 \
|
35 |
-
&& ./configure \
|
36 |
-
--prefix=/app/nginx \
|
37 |
-
--sbin-path=/usr/sbin/nginx \
|
38 |
-
--conf-path=/app/nginx/nginx.conf \
|
39 |
-
--error-log-path=/app/nginx/logs/error.log \
|
40 |
-
--pid-path=/app/nginx/nginx.pid \
|
41 |
-
--lock-path=/app/nginx/nginx.lock \
|
42 |
-
--http-log-path=/app/nginx/logs/access.log \
|
43 |
-
--http-client-body-temp-path=/app/nginx/client_body \
|
44 |
-
--http-proxy-temp-path=/app/nginx/tmp \
|
45 |
-
--http-fastcgi-temp-path=/app/nginx/tmp \
|
46 |
-
--user=nginx \
|
47 |
-
--group=nginx \
|
48 |
-
&& make \
|
49 |
-
&& make install \
|
50 |
-
&& apk del .build-deps \
|
51 |
-
&& rm -rf /nginx-1.24.0*
|
52 |
-
# ===== Admin Panel =====
|
53 |
-
COPY <<"EOF" /app/admin/app.py
|
54 |
-
from flask import Flask, request, jsonify, render_template
|
55 |
-
import os
|
56 |
-
import sqlite3
|
57 |
-
from werkzeug.security import generate_password_hash
|
58 |
-
from pathlib import Path
|
59 |
-
|
60 |
-
app = Flask(__name__,
|
61 |
-
template_folder=str(Path(__file__).parent/'templates'),
|
62 |
-
static_folder=str(Path(__file__).parent/'static'))
|
63 |
-
|
64 |
-
# Validate secrets
|
65 |
-
required_secrets = ['FLASK_SECRET', 'SUDO_SECRET']
|
66 |
-
for secret in required_secrets:
|
67 |
-
if not os.getenv(secret):
|
68 |
-
raise RuntimeError(f"Missing required secret: {secret}")
|
69 |
-
|
70 |
-
app.secret_key = os.getenv("FLASK_SECRET")
|
71 |
-
|
72 |
-
# SQLite database
|
73 |
-
DB_PATH = '/app/data/admin.db'
|
74 |
-
|
75 |
-
def get_db():
|
76 |
-
Path(DB_PATH).parent.mkdir(exist_ok=True)
|
77 |
-
db = sqlite3.connect(DB_PATH)
|
78 |
-
db.execute('''CREATE TABLE IF NOT EXISTS users
|
79 |
-
(username TEXT PRIMARY KEY, password TEXT, role TEXT)''')
|
80 |
-
return db
|
81 |
-
|
82 |
-
@app.route('/sudo')
|
83 |
-
def home():
|
84 |
-
return render_template('login.html')
|
85 |
-
|
86 |
-
@app.route('/sudo/login', methods=['POST'])
|
87 |
-
def login():
|
88 |
-
if request.json.get('sudo_secret') == os.getenv("SUDO_SECRET"):
|
89 |
-
return jsonify({"status": "success"})
|
90 |
-
return jsonify({"error": "Invalid secret"}), 403
|
91 |
-
|
92 |
-
@app.route('/sudo/dashboard')
|
93 |
-
def dashboard():
|
94 |
-
return render_template('dashboard.html')
|
95 |
-
|
96 |
-
@app.route('/sudo/add_user', methods=['POST'])
|
97 |
-
def add_user():
|
98 |
-
if request.headers.get('X-Sudo-Secret') != os.getenv("SUDO_SECRET"):
|
99 |
-
return jsonify({"error": "Unauthorized"}), 403
|
100 |
-
|
101 |
-
db = get_db()
|
102 |
-
try:
|
103 |
-
db.execute("INSERT INTO users VALUES (?,?,?)", [
|
104 |
-
request.json["username"],
|
105 |
-
generate_password_hash(request.json["password"]),
|
106 |
-
"user"
|
107 |
-
])
|
108 |
-
db.commit()
|
109 |
-
return jsonify({"status": "User added"})
|
110 |
-
except sqlite3.IntegrityError:
|
111 |
-
return jsonify({"error": "User exists"}), 400
|
112 |
-
|
113 |
-
@app.route('/sudo/list_users', methods=['GET'])
|
114 |
-
def list_users():
|
115 |
-
if request.headers.get('X-Sudo-Secret') != os.getenv("SUDO_SECRET"):
|
116 |
-
return jsonify({"error": "Unauthorized"}), 403
|
117 |
-
|
118 |
-
db = get_db()
|
119 |
-
return jsonify([
|
120 |
-
{"username": row[0]} for row in db.execute("SELECT username FROM users")
|
121 |
-
])
|
122 |
-
|
123 |
-
@app.route('/sudo/remove_user', methods=['POST'])
|
124 |
-
def remove_user():
|
125 |
-
if request.headers.get('X-Sudo-Secret') != os.getenv("SUDO_SECRET"):
|
126 |
-
return jsonify({"error": "Unauthorized"}), 403
|
127 |
-
|
128 |
-
db = get_db()
|
129 |
-
db.execute("DELETE FROM users WHERE username=?", [request.json["username"]])
|
130 |
-
db.commit()
|
131 |
-
return jsonify({"status": "User removed"})
|
132 |
-
|
133 |
-
if __name__ == "__main__":
|
134 |
-
app.run(host="0.0.0.0", port=5000)
|
135 |
-
EOF
|
136 |
|
137 |
-
# Admin
|
138 |
COPY <<"EOF" /app/admin/templates/login.html
|
139 |
<!DOCTYPE html>
|
140 |
<html>
|
141 |
<head>
|
142 |
-
<title>
|
143 |
<link rel="stylesheet" href="/static/admin.css">
|
144 |
</head>
|
145 |
<body>
|
146 |
<div class="container">
|
147 |
-
<h2>
|
148 |
<form id="loginForm">
|
149 |
<input type="password" name="sudo_secret" placeholder="Admin Secret" required>
|
150 |
<button type="submit">Login</button>
|
@@ -183,7 +65,7 @@ COPY <<"EOF" /app/admin/templates/dashboard.html
|
|
183 |
</html>
|
184 |
EOF
|
185 |
|
186 |
-
# Static
|
187 |
COPY <<"EOF" /app/admin/static/admin.css
|
188 |
body {
|
189 |
font-family: Arial, sans-serif;
|
@@ -273,81 +155,123 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
273 |
});
|
274 |
EOF
|
275 |
|
276 |
-
|
277 |
-
|
278 |
-
|
279 |
-
|
280 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
281 |
|
282 |
-
|
283 |
-
|
284 |
-
|
|
|
|
|
285 |
|
286 |
-
|
287 |
-
|
288 |
-
default_type application/octet-stream;
|
289 |
-
sendfile on;
|
290 |
-
keepalive_timeout 65;
|
291 |
-
client_max_body_size 20M;
|
292 |
-
|
293 |
-
client_body_temp_path /app/nginx/client_body;
|
294 |
-
proxy_temp_path /app/nginx/tmp;
|
295 |
-
fastcgi_temp_path /app/nginx/tmp;
|
296 |
-
uwsgi_temp_path /app/nginx/tmp;
|
297 |
-
scgi_temp_path /app/nginx/tmp;
|
298 |
|
299 |
-
|
300 |
-
|
301 |
-
|
302 |
-
|
|
|
|
|
303 |
|
304 |
-
|
305 |
-
|
306 |
-
|
307 |
-
|
308 |
-
proxy_set_header X-Real-IP \$remote_addr;
|
309 |
-
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
310 |
-
}
|
311 |
|
312 |
-
|
313 |
-
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
|
318 |
-
|
319 |
-
|
320 |
-
|
321 |
-
expires 30d;
|
322 |
-
access_log off;
|
323 |
-
}
|
324 |
|
325 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
326 |
|
327 |
-
|
328 |
-
|
329 |
-
|
330 |
-
|
331 |
-
|
332 |
-
|
333 |
-
|
334 |
-
|
335 |
-
|
336 |
-
#chmod -R 770 /var/lib/nginx
|
337 |
|
338 |
-
|
339 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
340 |
|
341 |
-
#
|
342 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
343 |
|
344 |
-
|
345 |
-
|
|
|
|
|
|
|
|
|
|
|
346 |
|
347 |
-
#
|
348 |
-
|
|
|
|
|
|
|
349 |
EOF
|
350 |
-
RUN chmod +x /start.sh
|
351 |
|
352 |
# Environment variables
|
353 |
ENV HOST=0.0.0.0 \
|
@@ -365,4 +289,4 @@ ENV HOST=0.0.0.0 \
|
|
365 |
NODE_ENV=production
|
366 |
|
367 |
EXPOSE 7860
|
368 |
-
CMD ["/start.sh"]
|
|
|
1 |
# Use official LibreChat base image
|
2 |
FROM ghcr.io/danny-avila/librechat-dev:latest
|
3 |
|
4 |
+
# Install Python dependencies
|
5 |
USER root
|
6 |
RUN apk update && apk add --no-cache \
|
|
|
7 |
python3 \
|
8 |
py3-pip \
|
9 |
sqlite \
|
10 |
+
&& pip3 install flask werkzeug waitress requests --break-package-system
|
11 |
+
|
12 |
+
# Setup directory structure
|
13 |
+
RUN mkdir -p /app/{admin/{templates,static},data,uploads,client/public/images,api/logs} \
|
|
|
|
|
14 |
&& chown -R 1000:1000 /app \
|
15 |
+
&& chmod -R 777 /app/uploads \
|
|
|
16 |
&& chmod -R 777 /app/client/public/images \
|
17 |
+
&& chmod -R 777 /app/api/logs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
18 |
|
19 |
+
# ===== Admin Templates =====
|
20 |
COPY <<"EOF" /app/admin/templates/login.html
|
21 |
<!DOCTYPE html>
|
22 |
<html>
|
23 |
<head>
|
24 |
+
<title>Admin Login</title>
|
25 |
<link rel="stylesheet" href="/static/admin.css">
|
26 |
</head>
|
27 |
<body>
|
28 |
<div class="container">
|
29 |
+
<h2>Admin Portal</h2>
|
30 |
<form id="loginForm">
|
31 |
<input type="password" name="sudo_secret" placeholder="Admin Secret" required>
|
32 |
<button type="submit">Login</button>
|
|
|
65 |
</html>
|
66 |
EOF
|
67 |
|
68 |
+
# ===== Admin Static Files =====
|
69 |
COPY <<"EOF" /app/admin/static/admin.css
|
70 |
body {
|
71 |
font-family: Arial, sans-serif;
|
|
|
155 |
});
|
156 |
EOF
|
157 |
|
158 |
+
# ===== Combined Server =====
|
159 |
+
COPY <<"EOF" /app/combined_server.py
|
160 |
+
from flask import Flask, request, jsonify, render_template, make_response
|
161 |
+
from werkzeug.middleware.proxy_fix import ProxyFix
|
162 |
+
from werkzeug.security import generate_password_hash
|
163 |
+
import os
|
164 |
+
import sqlite3
|
165 |
+
from pathlib import Path
|
166 |
+
import subprocess
|
167 |
+
import requests
|
168 |
+
from waitress import serve
|
169 |
|
170 |
+
app = Flask(__name__,
|
171 |
+
template_folder='/app/admin/templates',
|
172 |
+
static_folder='/app/admin/static')
|
173 |
+
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1)
|
174 |
+
app.secret_key = os.getenv("FLASK_SECRET")
|
175 |
|
176 |
+
# Database setup
|
177 |
+
DB_PATH = '/app/data/admin.db'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
178 |
|
179 |
+
def get_db():
|
180 |
+
Path(DB_PATH).parent.mkdir(exist_ok=True)
|
181 |
+
db = sqlite3.connect(DB_PATH)
|
182 |
+
db.execute('''CREATE TABLE IF NOT EXISTS users
|
183 |
+
(username TEXT PRIMARY KEY, password TEXT, role TEXT)''')
|
184 |
+
return db
|
185 |
|
186 |
+
# Admin routes
|
187 |
+
@app.route('/sudo')
|
188 |
+
def admin_home():
|
189 |
+
return render_template('login.html')
|
|
|
|
|
|
|
190 |
|
191 |
+
@app.route('/sudo/login', methods=['POST'])
|
192 |
+
def login():
|
193 |
+
if request.json.get('sudo_secret') == os.getenv("SUDO_SECRET"):
|
194 |
+
return jsonify({"status": "success"})
|
195 |
+
return jsonify({"error": "Invalid secret"}), 403
|
196 |
|
197 |
+
@app.route('/sudo/dashboard')
|
198 |
+
def dashboard():
|
199 |
+
return render_template('dashboard.html')
|
|
|
|
|
|
|
200 |
|
201 |
+
@app.route('/sudo/add_user', methods=['POST'])
|
202 |
+
def add_user():
|
203 |
+
if request.headers.get('X-Sudo-Secret') != os.getenv("SUDO_SECRET"):
|
204 |
+
return jsonify({"error": "Unauthorized"}), 403
|
205 |
+
|
206 |
+
db = get_db()
|
207 |
+
try:
|
208 |
+
db.execute("INSERT INTO users VALUES (?,?,?)", [
|
209 |
+
request.json["username"],
|
210 |
+
generate_password_hash(request.json["password"]),
|
211 |
+
"user"
|
212 |
+
])
|
213 |
+
db.commit()
|
214 |
+
return jsonify({"status": "User added"})
|
215 |
+
except sqlite3.IntegrityError:
|
216 |
+
return jsonify({"error": "User exists"}), 400
|
217 |
|
218 |
+
@app.route('/sudo/list_users', methods=['GET'])
|
219 |
+
def list_users():
|
220 |
+
if request.headers.get('X-Sudo-Secret') != os.getenv("SUDO_SECRET"):
|
221 |
+
return jsonify({"error": "Unauthorized"}), 403
|
222 |
+
|
223 |
+
db = get_db()
|
224 |
+
return jsonify([
|
225 |
+
{"username": row[0]} for row in db.execute("SELECT username FROM users")
|
226 |
+
])
|
|
|
227 |
|
228 |
+
@app.route('/sudo/remove_user', methods=['POST'])
|
229 |
+
def remove_user():
|
230 |
+
if request.headers.get('X-Sudo-Secret') != os.getenv("SUDO_SECRET"):
|
231 |
+
return jsonify({"error": "Unauthorized"}), 403
|
232 |
+
|
233 |
+
db = get_db()
|
234 |
+
db.execute("DELETE FROM users WHERE username=?", [request.json["username"]])
|
235 |
+
db.commit()
|
236 |
+
return jsonify({"status": "User removed"})
|
237 |
|
238 |
+
# LibreChat proxy
|
239 |
+
@app.route('/', defaults={'path': ''})
|
240 |
+
@app.route('/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE'])
|
241 |
+
def proxy(path):
|
242 |
+
if path.startswith('sudo/') or path == 'sudo':
|
243 |
+
return make_response("Not Found", 404)
|
244 |
+
|
245 |
+
resp = requests.request(
|
246 |
+
method=request.method,
|
247 |
+
url=f"http://localhost:3080/{path}",
|
248 |
+
headers={key: value for (key, value) in request.headers if key != 'Host'},
|
249 |
+
data=request.get_data(),
|
250 |
+
cookies=request.cookies,
|
251 |
+
allow_redirects=False
|
252 |
+
)
|
253 |
+
|
254 |
+
response = make_response(resp.content, resp.status_code)
|
255 |
+
for key, value in resp.headers.items():
|
256 |
+
if key.lower() not in ['content-encoding', 'content-length', 'transfer-encoding']:
|
257 |
+
response.headers[key] = value
|
258 |
+
return response
|
259 |
|
260 |
+
if __name__ == "__main__":
|
261 |
+
# Start LibreChat backend
|
262 |
+
subprocess.Popen(["npm", "run", "start:backend"], cwd="/app")
|
263 |
+
|
264 |
+
# Start combined server
|
265 |
+
serve(app, host='0.0.0.0', port=7860, threads=4)
|
266 |
+
EOF
|
267 |
|
268 |
+
# Startup script
|
269 |
+
COPY <<"EOF" /app/start.sh
|
270 |
+
#!/bin/sh
|
271 |
+
# Start the combined server
|
272 |
+
python3 /app/combined_server.py
|
273 |
EOF
|
274 |
+
RUN chmod +x /app/start.sh
|
275 |
|
276 |
# Environment variables
|
277 |
ENV HOST=0.0.0.0 \
|
|
|
289 |
NODE_ENV=production
|
290 |
|
291 |
EXPOSE 7860
|
292 |
+
CMD ["/app/start.sh"]
|