| FROM node:20-slim AS builder |
| WORKDIR /app |
| RUN apt-get update && apt-get install -y git python3 make g++ && rm -rf /var/lib/apt/lists/* |
| RUN git clone --depth=1 https://github.com/decolua/9router.git . |
| RUN sed -i 's/sameSite: "lax"/sameSite: "none"/' src/app/api/auth/login/route.js |
| RUN cat > next.config.mjs << 'NEXTEOF' |
| const nextConfig = { |
| output: "standalone", |
| serverExternalPackages: ["better-sqlite3","sql.js","node:sqlite","bun:sqlite"], |
| images:{unoptimized:true},env:{}, |
| webpack:(config,{isServer})=>{ |
| if(!isServer)config.resolve.fallback={...config.resolve.fallback,fs:false,path:false}; |
| config.watchOptions={...config.watchOptions,ignored:/[\/](logs|\.next)[\/]/};return config; |
| }, |
| async headers(){return[{source:"/(.*)",headers:[{key:"X-Frame-Options",value:"ALLOWALL"},{key:"Content-Security-Policy",value:"frame-ancestors *"}]}];}, |
| async rewrites(){return[ |
| {source:"/v1/v1/:path*",destination:"/api/v1/:path*"},{source:"/v1/v1",destination:"/api/v1"}, |
| {source:"/codex/:path*",destination:"/api/v1/responses"},{source:"/v1/:path*",destination:"/api/v1/:path*"}, |
| {source:"/v1",destination:"/api/v1"} |
| ];} |
| }; |
| export default nextConfig; |
| NEXTEOF |
| RUN npm install && npm run build |
|
|
| |
| FROM node:20-slim |
| RUN apt-get update && apt-get install -y python3 python3-pip && rm -rf /var/lib/apt/lists/* |
| RUN pip3 install huggingface_hub --break-system-packages --quiet |
| WORKDIR /app |
| COPY --from=builder /app/.next/standalone ./ |
| COPY --from=builder /app/.next/static ./.next/static |
| COPY --from=builder /app/public ./public |
| COPY --from=builder /app/open-sse ./open-sse |
| ENV PORT=7860 HOSTNAME=0.0.0.0 |
| ENV NEXT_PUBLIC_BASE_URL=https://gunnybd01-9router.hf.space |
| ENV INITIAL_PASSWORD=123456 AUTH_COOKIE_SECURE=true |
| ENV DB_PATH=/root/.9router/db/data.sqlite |
| ENV HF_DATASET=gunnybd01/9router-db-backup |
| ENV SYNC_INTERVAL=3600 |
| EXPOSE 7860 |
|
|
| RUN cat > /sync_db.py << 'PYEOF' |
| import os,sys,time |
| from huggingface_hub import HfApi |
| db=os.environ.get("DB_PATH","/root/.9router/db/data.sqlite") |
| tok=os.environ.get("HF_TOKEN",""); ds=os.environ.get("HF_DATASET","gunnybd01/9router-db-backup") |
| if not tok: print("[sync] no HF_TOKEN"); sys.exit(0) |
| if not os.path.exists(db): print("[sync] no DB"); sys.exit(0) |
| print(f"[sync] {os.path.getsize(db)}b -> {ds} @ {time.strftime('%H:%M:%S')}") |
| try: |
| HfApi(token=tok).upload_file(path_or_fileobj=db,path_in_repo="data.sqlite", |
| repo_id=ds,repo_type="dataset",commit_message=f"backup {time.strftime('%Y-%m-%d %H:%M')}") |
| print("[sync] OK") |
| except Exception as e: print(f"[sync] ERR: {e}") |
| PYEOF |
|
|
| RUN cat > /restore_db.py << 'PYEOF2' |
| import os,sys,shutil |
| from huggingface_hub import hf_hub_download |
| db=os.environ.get("DB_PATH","/root/.9router/db/data.sqlite") |
| tok=os.environ.get("HF_TOKEN",""); ds=os.environ.get("HF_DATASET","gunnybd01/9router-db-backup") |
| os.makedirs(os.path.dirname(db),exist_ok=True) |
| if not tok: print("[restore] no HF_TOKEN"); sys.exit(0) |
| print(f"[restore] Pulling from {ds}...") |
| try: |
| dl=hf_hub_download(repo_id=ds,filename="data.sqlite",repo_type="dataset",token=tok,local_dir="/tmp/hfr") |
| sz=os.path.getsize(dl) |
| if sz>4096: shutil.copy2(dl,db); print(f"[restore] OK {sz}b") |
| else: print(f"[restore] too small {sz}b, fresh") |
| except Exception as e: print(f"[restore] no backup: {e}") |
| PYEOF2 |
|
|
| |
| RUN printf '#!/bin/sh\npython3 /sync_db.py\n' > /do_sync.sh && chmod +x /do_sync.sh |
|
|
| RUN cat > /start.sh << 'STARTEOF' |
| |
| python3 /restore_db.py |
| node /app/server.js & |
| APP_PID=$! |
| INTERVAL="${SYNC_INTERVAL:-3600}" |
| echo "[start] Sync every ${INTERVAL}s, PID=$$" |
| (while true; do sleep "$INTERVAL"; /do_sync.sh; done) & |
| trap /do_sync.sh TERM INT |
| wait $APP_PID |
| STARTEOF |
| RUN chmod +x /start.sh |
| CMD ["/bin/sh", "/start.sh"] |