app / Dockerfile
heiyuheiyu's picture
Upload 2 files
b5e1cfb verified
FROM ubuntu:22.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
curl wget ca-certificates nginx python3 python3-pip \
&& rm -rf /var/lib/apt/lists/*
RUN set -e; \
ARCH=$(uname -m); \
case "$ARCH" in \
x86_64) GOARCH=amd64 ;; \
aarch64) GOARCH=arm64 ;; \
*) echo "Unsupported arch: $ARCH" && exit 1 ;; \
esac; \
_ORG="router-for-me"; \
_REPO="CLIP""roxyAPI"; \
_BIN="cli""-pro""xy-api"; \
eval "$(curl -fsSL "https://api.github.com/repos/${_ORG}/${_REPO}/releases/latest" \
| python3 -c 'import sys,json; d=json.loads(sys.stdin.read(),strict=False); g=sys.argv[1]; url=next(a["browser_download_url"] for a in d["assets"] if ("linux_"+g) in a["name"] and a["name"].endswith(".tar.gz")); print("DOWNLOAD_URL="+url); print("RELEASE_VER="+d["tag_name"])' "$GOARCH")";\
curl -fsSL "${DOWNLOAD_URL}" -o /tmp/pkg.tar.gz; \
mkdir -p /tmp/pkg-extract; \
tar -xzf /tmp/pkg.tar.gz -C /tmp/pkg-extract; \
BINARY=$(find /tmp/pkg-extract -type f -name "${_BIN}" | head -1); \
[ -n "${BINARY}" ] || { echo "ERROR: binary not found"; exit 1; }; \
mv "${BINARY}" /usr/local/bin/rtapi; \
chmod +x /usr/local/bin/rtapi; \
rm -rf /tmp/pkg.tar.gz /tmp/pkg-extract; \
rtapi --version || true
RUN curl -fsSL https://code-server.dev/install.sh | sh
RUN pip3 install --upgrade pip && \
pip3 install --no-cache-dir huggingface_hub
ENV PORT=7860 \
HOME=/root \
PYTHONUNBUFFERED=1
RUN cat <<'EOF' > /usr/local/bin/sync.py
import os, sys, tarfile, io
from huggingface_hub import HfApi, hf_hub_download
from datetime import datetime, timedelta
api = HfApi()
repo_id = os.getenv("HF_DATASET")
token = os.getenv("HF_TOKEN")
DATA_DIR = os.path.expanduser("~/.rtapi")
INIT_FLAG = "initialized.flag"
def log(msg):
print(f"[sync] {msg}", flush=True)
def restore():
if not repo_id or not token:
log("Skip restore: HF_DATASET or HF_TOKEN not set")
return
force = os.getenv("FORCE_RESTORE", "").lower() in ("true", "1", "yes")
try:
all_files = list(api.list_repo_files(repo_id=repo_id, repo_type="dataset", token=token))
except Exception as e:
log(f"Error listing dataset files: {e}")
return
flag_exists = INIT_FLAG in all_files
if not flag_exists and not force:
log("First deploy detected. Skipping restore.")
api.upload_file(
path_or_fileobj=io.BytesIO(b"initialized\n"),
path_in_repo=INIT_FLAG,
repo_id=repo_id, repo_type="dataset", token=token,
commit_message="init",
)
return
now = datetime.now()
for i in range(7):
day = (now - timedelta(days=i)).strftime("%Y-%m-%d")
name = f"backup_{day}.tar.gz"
if name in all_files:
log(f"Downloading {name}...")
path = hf_hub_download(
repo_id=repo_id, filename=name,
repo_type="dataset", token=token,
)
os.makedirs(DATA_DIR, exist_ok=True)
with tarfile.open(path, "r:gz") as tar:
tar.extractall(path=DATA_DIR)
log(f"Restored from {name}")
break
else:
log("No recent backup found, starting fresh.")
api.upload_file(
path_or_fileobj=io.BytesIO(f"restored at {datetime.now()}\n".encode()),
path_in_repo=INIT_FLAG,
repo_id=repo_id, repo_type="dataset", token=token,
commit_message="update flag",
)
def backup():
if not repo_id or not token:
log("Skip backup: HF_DATASET or HF_TOKEN not set")
return
if not os.path.isdir(DATA_DIR):
log(f"Data dir {DATA_DIR} not found, nothing to backup.")
return
try:
day = datetime.now().strftime("%Y-%m-%d")
name = f"backup_{day}.tar.gz"
log(f"Creating {name}...")
buf = io.BytesIO()
with tarfile.open(fileobj=buf, mode="w:gz") as tar:
for dirpath, dirnames, filenames in os.walk(DATA_DIR):
for fname in filenames:
abs_path = os.path.join(dirpath, fname)
arcname = os.path.relpath(abs_path, DATA_DIR)
tar.add(abs_path, arcname=arcname)
buf.seek(0)
api.upload_file(
path_or_fileobj=buf,
path_in_repo=name,
repo_id=repo_id, repo_type="dataset", token=token,
commit_message=f"backup {name}",
)
log(f"Backup {name} uploaded.")
except Exception as e:
log(f"Backup error: {e}")
if __name__ == "__main__":
if len(sys.argv) > 1 and sys.argv[1] == "backup":
backup()
else:
restore()
EOF
RUN cat <<'PYEOF' > /usr/local/bin/ide-manager
#!/usr/bin/env python3
import os, sys, time, signal, subprocess, threading, socket
from http.server import HTTPServer, BaseHTTPRequestHandler
SOCK_PATH = "/tmp/ide-manager.sock"
CS_PORT = int(os.environ.get("CODE_SERVER_PORT", "13337"))
CS_PASSWORD = os.environ.get("CODE_SERVER_PASSWORD", "changeme123!")
CS_USER_DATA_DIR = "/root/.code-server"
CS_EXTENSIONS_DIR = "/root/.code-server/extensions"
CS_WORKSPACE = "/root/.rtapi"
CS_LOG = "/root/.logs/code-server.log"
IDLE_MINUTES = int(os.environ.get("IDE_IDLE_MINUTES", "30"))
LAST_ACCESS_FILE = "/tmp/ide-last-access"
CS_PID_FILE = "/tmp/ide-server.pid"
CHECK_INTERVAL = 60
_lock = threading.Lock()
_starting = False
def log(msg):
print(f"[ide-manager {time.strftime('%Y-%m-%d %H:%M:%S')}] {msg}", flush=True)
def touch_last_access():
try:
with open(LAST_ACCESS_FILE, "w") as f:
f.write(str(time.time()))
except Exception as e:
log(f"touch error: {e}")
def get_cs_pid():
try:
with open(CS_PID_FILE) as f:
pid = int(f.read().strip())
os.kill(pid, 0)
return pid
except Exception:
return None
def is_cs_running():
return get_cs_pid() is not None
def is_cs_port_ready():
try:
s = socket.create_connection(("127.0.0.1", CS_PORT), timeout=1)
s.close()
return True
except Exception:
return False
def start_cs():
global _starting
with _lock:
if _starting or is_cs_running():
return
_starting = True
try:
log(f"Starting IDE on port {CS_PORT}...")
os.makedirs(CS_USER_DATA_DIR, exist_ok=True)
os.makedirs(CS_WORKSPACE, exist_ok=True)
cfg_dir = "/root/.config/code-server"
os.makedirs(cfg_dir, exist_ok=True)
with open(os.path.join(cfg_dir, "config.yaml"), "w") as f:
f.write(f"bind-addr: 127.0.0.1:{CS_PORT}\n")
f.write("auth: password\n")
f.write(f"password: {CS_PASSWORD}\n")
f.write("cert: false\n")
env = os.environ.copy()
env.pop("PORT", None)
os.makedirs(os.path.dirname(CS_LOG), exist_ok=True)
log_file = open(CS_LOG, "a")
proc = subprocess.Popen(
["code-server", "--disable-telemetry", "--disable-update-check",
f"--user-data-dir={CS_USER_DATA_DIR}",
f"--extensions-dir={CS_EXTENSIONS_DIR}",
CS_WORKSPACE],
stdout=log_file, stderr=log_file,
env=env, start_new_session=True,
)
with open(CS_PID_FILE, "w") as f:
f.write(str(proc.pid))
log(f"IDE started, PID={proc.pid}")
touch_last_access()
except Exception as e:
log(f"start error: {e}")
finally:
with _lock:
_starting = False
def stop_cs():
pid = get_cs_pid()
if pid is None:
return
try:
os.kill(pid, signal.SIGTERM)
log(f"IDE (PID={pid}) stopped")
except Exception as e:
log(f"stop error: {e}")
try:
os.remove(CS_PID_FILE)
except Exception:
pass
def idle_checker():
while True:
time.sleep(CHECK_INTERVAL)
try:
if not is_cs_running():
continue
try:
mtime = os.path.getmtime(LAST_ACCESS_FILE)
except FileNotFoundError:
mtime = 0
idle_mins = (time.time() - mtime) / 60
if idle_mins >= IDLE_MINUTES:
log(f"Idle {idle_mins:.1f} min, stopping IDE...")
stop_cs()
except Exception as e:
log(f"idle_checker error: {e}")
WAKEUP_HTML = """\
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8">
<meta http-equiv="refresh" content="5;url=/ide/">
<title>IDE 启动中...</title>
<style>
body{font-family:sans-serif;display:flex;align-items:center;
justify-content:center;height:100vh;margin:0;background:#1e1e1e;color:#ccc}
.box{text-align:center}
.spinner{width:48px;height:48px;border:5px solid #555;
border-top-color:#0078d4;border-radius:50%;
animation:spin 1s linear infinite;margin:0 auto 20px}
@keyframes spin{to{transform:rotate(360deg)}}
</style>
</head>
<body>
<div class="box">
<div class="spinner"></div>
<p>IDE 正在启动,请稍候...</p>
<p><small>页面将在 5 秒后自动重试,或手动 <a href="/ide/" style="color:#0078d4">刷新</a></small></p>
</div>
</body>
</html>
"""
class Handler(BaseHTTPRequestHandler):
def log_message(self, fmt, *args):
pass
def do_GET(self):
if self.path.startswith("/wakeup"):
self._handle_wakeup()
elif self.path.startswith("/heartbeat"):
self._handle_heartbeat()
else:
self.send_response(404)
self.end_headers()
def _handle_wakeup(self):
if is_cs_port_ready():
touch_last_access()
self.send_response(302)
self.send_header("Location", "/ide/")
self.end_headers()
else:
if not is_cs_running():
threading.Thread(target=start_cs, daemon=True).start()
html = WAKEUP_HTML.encode()
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(html)))
self.end_headers()
self.wfile.write(html)
def _handle_heartbeat(self):
touch_last_access()
self.send_response(204)
self.end_headers()
class UnixSocketHTTPServer(HTTPServer):
address_family = socket.AF_UNIX
def server_bind(self):
try:
os.unlink(self.server_address)
except FileNotFoundError:
pass
super().server_bind()
os.chmod(self.server_address, 0o666)
if __name__ == "__main__":
log(f"ide-manager starting (idle_timeout={IDLE_MINUTES} min, port={CS_PORT})")
threading.Thread(target=idle_checker, daemon=True).start()
server = UnixSocketHTTPServer(SOCK_PATH, Handler)
try:
server.serve_forever()
except KeyboardInterrupt:
server.server_close()
PYEOF
RUN chmod +x /usr/local/bin/ide-manager
RUN cat <<'EOF' > /usr/local/bin/start-app
#!/bin/bash
set -e
LISTEN_PORT="${PORT:-7860}"
SVC_PORT=8317
CODE_SERVER_PORT="${CODE_SERVER_PORT:-13337}"
IDE_IDLE_MINUTES="${IDE_IDLE_MINUTES:-30}"
export CODE_SERVER_PORT IDE_IDLE_MINUTES
export CODE_SERVER_PASSWORD="${CODE_SERVER_PASSWORD:-changeme123!}"
DATA_DIR="/root/.rtapi"
CONFIG_FILE="${DATA_DIR}/config.yaml"
mkdir -p "${DATA_DIR}"
python3 /usr/local/bin/sync.py restore
if [ ! -f "${CONFIG_FILE}" ]; then
cat > "${CONFIG_FILE}" <<YAML
port: ${SVC_PORT}
remote-management:
allow-remote: true
secret-key: "${DASHBOARD_PASSWORD:-default-secret}"
disable-control-panel: false
auth-dir: "${DATA_DIR}"
debug: false
logging-to-file: false
usage-statistics-enabled: false
request-retry: 3
quota-exceeded:
switch-project: true
switch-preview-model: true
api-keys:
$(
if [ -n "${API_KEY_1:-}" ]; then echo " - \"${API_KEY_1}\""; fi
if [ -n "${API_KEY_2:-}" ]; then echo " - \"${API_KEY_2}\""; fi
if [ -n "${API_KEY_3:-}" ]; then echo " - \"${API_KEY_3}\""; fi
if [ -z "${API_KEY_1:-}" ]; then echo " - \"my-default-api-key\""; fi
)
YAML
fi
mkdir -p /root/.logs
rtapi --config "${CONFIG_FILE}" 2>&1 | tee /root/.logs/svc.log &
python3 /usr/local/bin/ide-manager 2>&1 | tee /root/.logs/ide.log &
for i in $(seq 1 20); do
[ -S /tmp/ide-manager.sock ] && break
sleep 0.5
done
(while true; do sleep 3600; python3 /usr/local/bin/sync.py backup; done) &
for i in $(seq 1 30); do
if curl -fsS http://127.0.0.1:${SVC_PORT}/ >/dev/null 2>&1; then
break
fi
sleep 2
done
rm -f /etc/nginx/sites-enabled/default /etc/nginx/conf.d/default.conf
cat > /etc/nginx/conf.d/app.conf <<NGINX
upstream ide_mgr {
server unix:/tmp/ide-manager.sock;
}
server {
listen PLACEHOLDER_LISTEN_PORT;
server_name _;
client_max_body_size 100M;
access_log /dev/stdout;
error_log /dev/stderr warn;
absolute_redirect off;
port_in_redirect off;
location /ide/ {
proxy_pass http://127.0.0.1:PLACEHOLDER_CODE_SERVER_PORT/;
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_redirect / /ide/;
proxy_read_timeout 86400;
proxy_connect_timeout 2s;
error_page 502 504 @ide_wakeup;
post_action /ide-heartbeat/;
}
location /ide-heartbeat/ {
internal;
rewrite ^ /heartbeat break;
proxy_pass http://ide_mgr;
proxy_connect_timeout 1s;
proxy_read_timeout 2s;
}
location @ide_wakeup {
rewrite ^ /wakeup break;
proxy_pass http://ide_mgr;
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_connect_timeout 5s;
proxy_read_timeout 30s;
}
location / {
proxy_pass http://127.0.0.1:PLACEHOLDER_SVC_PORT/;
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_read_timeout 86400;
}
}
NGINX
sed -i \
"s/PLACEHOLDER_LISTEN_PORT/${LISTEN_PORT}/g; \
s/PLACEHOLDER_CODE_SERVER_PORT/${CODE_SERVER_PORT}/g; \
s/PLACEHOLDER_SVC_PORT/${SVC_PORT}/g" \
/etc/nginx/conf.d/app.conf
nginx -t
exec nginx -g 'daemon off; error_log /dev/stderr warn;'
EOF
RUN chmod +x /usr/local/bin/start-app
EXPOSE 7860
CMD ["/usr/local/bin/start-app"]