#!/bin/bash # ============================================================ # HF-VPS — One-shot project creator # Run this script and the entire project is created ready to push # Usage: bash setup.sh # ============================================================ set -e PROJECT="hf-vps" echo "============================================" echo " Creating $PROJECT..." echo "============================================" # --- Create structure --- mkdir -p $PROJECT/{config,app/node,scripts,data,app/public} # ============================================================ # Dockerfile # ============================================================ cat > $PROJECT/Dockerfile << 'EOF' FROM python:3.11-slim LABEL maintainer="hf-vps" LABEL description="Universal VPS-like base environment for Hugging Face Spaces" ENV DEBIAN_FRONTEND=noninteractive \ PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 \ NODE_VERSION=20 \ APP_HOME=/app \ DATA_DIR=/app/data \ LOG_DIR=/app/logs \ PORT=7860 WORKDIR $APP_HOME RUN apt-get update && apt-get install -y --no-install-recommends \ curl wget git ca-certificates gnupg \ build-essential gcc g++ make \ supervisor cron \ nginx \ sqlite3 \ redis-server \ htop procps net-tools vim nano \ zip unzip \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ && apt-get install -y nodejs \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* \ && npm install -g pm2 npm@latest COPY requirements.txt . RUN pip install --no-cache-dir --upgrade pip \ && pip install --no-cache-dir -r requirements.txt RUN npm install -g axios dotenv RUN mkdir -p \ $DATA_DIR \ $LOG_DIR \ /app/app \ /app/scripts \ /var/log/supervisor \ /var/log/nginx \ /run/nginx COPY config/nginx.conf /etc/nginx/nginx.conf COPY config/supervisord.conf /etc/supervisor/conf.d/supervisord.conf COPY config/redis.conf /etc/redis/redis.conf COPY app/ /app/app/ COPY scripts/ /app/scripts/ COPY scripts/entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh \ && chmod +x /app/scripts/*.sh 2>/dev/null || true EXPOSE 7860 ENTRYPOINT ["/entrypoint.sh"] EOF # ============================================================ # requirements.txt # ============================================================ cat > $PROJECT/requirements.txt << 'EOF' fastapi>=0.111.0 uvicorn[standard]>=0.30.0 starlette>=0.37.0 httpx>=0.27.0 requests>=2.32.0 aiohttp>=3.9.0 apscheduler>=3.10.0 sqlalchemy>=2.0.0 aiosqlite>=0.20.0 redis>=5.0.0 python-dotenv>=1.0.0 pydantic>=2.7.0 pydantic-settings>=2.2.0 python-jose[cryptography]>=3.3.0 passlib[bcrypt]>=1.7.4 python-multipart>=0.0.9 loguru>=0.7.0 tenacity>=8.3.0 python-dateutil>=2.9.0 EOF # ============================================================ # config/nginx.conf # ============================================================ cat > $PROJECT/config/nginx.conf << 'EOF' worker_processes auto; pid /tmp/nginx.pid; error_log /app/logs/nginx_error.log warn; events { worker_connections 512; multi_accept on; } http { include /etc/nginx/mime.types; default_type application/octet-stream; access_log /app/logs/nginx_access.log; sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; gzip on; gzip_types text/plain application/json application/javascript text/css; proxy_connect_timeout 120s; proxy_send_timeout 120s; proxy_read_timeout 120s; server { listen 7860; server_name _; client_max_body_size 50M; location /health { proxy_pass http://127.0.0.1:8000/health; proxy_set_header Host $host; } location /api/ { proxy_pass http://127.0.0.1:8000/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } location /node/ { proxy_pass http://127.0.0.1:3000/; proxy_set_header Host $host; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } location /static/ { alias /app/public/; expires 1d; add_header Cache-Control "public"; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } } } EOF # ============================================================ # config/supervisord.conf # ============================================================ cat > $PROJECT/config/supervisord.conf << 'EOF' [supervisord] nodaemon=true logfile=/app/logs/supervisord.log logfile_maxbytes=10MB logfile_backups=3 loglevel=info pidfile=/tmp/supervisord.pid user=root [unix_http_server] file=/tmp/supervisor.sock [rpcinterface:supervisor] supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface [supervisorctl] serverurl=unix:///tmp/supervisor.sock [program:redis] command=redis-server /etc/redis/redis.conf autostart=true autorestart=true priority=10 stdout_logfile=/app/logs/redis.log stderr_logfile=/app/logs/redis_error.log [program:fastapi] command=uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 2 --loop asyncio directory=/app autostart=true autorestart=true priority=20 stdout_logfile=/app/logs/fastapi.log stderr_logfile=/app/logs/fastapi_error.log environment=PYTHONPATH="/app" [program:node] command=node /app/app/node/server.js directory=/app/app/node autostart=false autorestart=true priority=30 stdout_logfile=/app/logs/node.log stderr_logfile=/app/logs/node_error.log [program:scheduler] command=python /app/app/scheduler.py directory=/app autostart=true autorestart=true priority=25 stdout_logfile=/app/logs/scheduler.log stderr_logfile=/app/logs/scheduler_error.log environment=PYTHONPATH="/app" [program:nginx] command=nginx -g "daemon off;" autostart=true autorestart=true priority=40 stdout_logfile=/app/logs/nginx.log stderr_logfile=/app/logs/nginx_error.log EOF # ============================================================ # config/redis.conf # ============================================================ cat > $PROJECT/config/redis.conf << 'EOF' bind 127.0.0.1 port 6379 protected-mode yes maxmemory 256mb maxmemory-policy allkeys-lru save 900 1 save 300 10 dbfilename dump.rdb dir /app/data loglevel notice logfile /app/logs/redis.log tcp-keepalive 300 timeout 0 tcp-backlog 128 EOF # ============================================================ # app/main.py # ============================================================ cat > $PROJECT/app/main.py << 'EOF' # ============================================================ # HF-VPS — Main FastAPI Application # Uvicorn: uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 2 # PYTHONPATH=/app — "app.main" resolves to /app/app/main.py # ============================================================ import os import time import platform import socket import json from fastapi import FastAPI from fastapi.staticfiles import StaticFiles from fastapi.responses import HTMLResponse, JSONResponse from fastapi.middleware.cors import CORSMiddleware from loguru import logger from pathlib import Path import psutil import redis as redis_lib app = FastAPI( title="HF-VPS", description="Universal VPS-like environment on Hugging Face Spaces", version="1.0.0", docs_url="/docs", redoc_url="/redoc", ) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) public_dir = Path("/app/public") public_dir.mkdir(exist_ok=True) app.mount("/static", StaticFiles(directory=str(public_dir)), name="static") START_TIME = time.time() # Add your routers here: # from app.routers import myrouter # app.include_router(myrouter.router, prefix="/api/v1") @app.get("/", response_class=HTMLResponse, include_in_schema=False) async def root(): index = public_dir / "index.html" if index.exists(): return HTMLResponse(content=index.read_text()) return HTMLResponse(content="

HF-VPS running

") @app.get("/health", tags=["system"]) async def health(): uptime = int(time.time() - START_TIME) return JSONResponse({ "status": "ok", "uptime_seconds": uptime, "uptime_human": _format_uptime(uptime), "python": platform.python_version(), "platform": platform.system(), }) @app.get("/info", tags=["system"]) async def info(): return JSONResponse({ "name": os.getenv("APP_NAME", "HF-VPS"), "version": "1.0.0", "port": 7860, "environment": os.getenv("ENV", "production"), }) @app.get("/status", tags=["system"]) async def status(): uptime = int(time.time() - START_TIME) return JSONResponse({ "app": { "name": os.getenv("APP_NAME", "HF-VPS"), "version": "1.0.0", "environment": os.getenv("ENV", "production"), }, "uptime_seconds": uptime, "uptime_human": _format_uptime(uptime), "python": platform.python_version(), "platform": platform.system(), "services": { "fastapi": {"status": "ok"}, "redis": {"status": "ok" if _check_redis() else "error"}, "nginx": {"status": "ok" if _check_port(7860) else "error"}, "scheduler": {"status": "ok" if _check_scheduler() else "error"}, }, "keep_alive": _get_scheduler_state(), "system": { "memory": _memory_stats(), "disk": _disk_stats(), }, }) def _format_uptime(seconds: int) -> str: days = seconds // 86400 hours = (seconds % 86400) // 3600 minutes = (seconds % 3600) // 60 return f"{days}d {hours}h {minutes}m" def _check_redis() -> bool: try: r = redis_lib.Redis(host="127.0.0.1", port=6379, socket_timeout=1) return r.ping() except Exception: return False def _check_port(port: int) -> bool: try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(1) result = s.connect_ex(("127.0.0.1", port)) s.close() return result == 0 except Exception: return False def _check_scheduler() -> bool: try: r = redis_lib.Redis(host="127.0.0.1", port=6379, socket_timeout=1) return r.exists("hfvps:scheduler_state") == 1 except Exception: return False def _get_scheduler_state() -> dict: try: r = redis_lib.Redis(host="127.0.0.1", port=6379, decode_responses=True, socket_timeout=1) raw = r.get("hfvps:scheduler_state") if raw: return json.loads(raw) except Exception: pass return { "last_ping_time": None, "last_ping_status": None, "next_ping_time": None, "ping_count": 0, "keep_alive_enabled": os.getenv("ENABLE_KEEP_ALIVE", "true").lower() == "true", "keep_alive_interval_seconds": int(os.getenv("KEEP_ALIVE_INTERVAL", "1800")), } def _memory_stats() -> dict: mem = psutil.virtual_memory() return { "used_mb": round(mem.used / 1024 / 1024), "total_mb": round(mem.total / 1024 / 1024), "percent": round(mem.percent, 1), } def _disk_stats() -> dict: disk = psutil.disk_usage("/app") return { "used_gb": round(disk.used / 1024 / 1024 / 1024, 2), "total_gb": round(disk.total / 1024 / 1024 / 1024, 2), "percent": round(disk.percent, 1), } EOF # ============================================================ # app/scheduler.py # ============================================================ cat > $PROJECT/app/scheduler.py << 'EOF' # ============================================================ # HF-VPS — Background Scheduler # State is persisted to Redis (key: hfvps:scheduler_state) # so the /status endpoint can read it for the dashboard. # ============================================================ import asyncio import json import os from datetime import datetime, timezone, timedelta import httpx import redis as redis_lib from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.triggers.interval import IntervalTrigger from loguru import logger SPACE_URL: str = os.getenv("SPACE_URL", "http://127.0.0.1:8000") KEEP_ALIVE_INTERVAL: int = int(os.getenv("KEEP_ALIVE_INTERVAL", "1800")) ENABLE_KEEP_ALIVE: bool = os.getenv("ENABLE_KEEP_ALIVE", "true").lower() == "true" REDIS_STATE_KEY: str = "hfvps:scheduler_state" _ping_count: int = 0 def _load_state_from_redis() -> int: try: r = redis_lib.Redis(host="127.0.0.1", port=6379, decode_responses=True) raw = r.get(REDIS_STATE_KEY) if raw: return int(json.loads(raw).get("ping_count", 0)) except Exception as e: logger.warning(f"[scheduler] could not load state from Redis: {e}") return 0 def _save_state(last_ping_time, last_ping_status, next_ping_time, ping_count) -> None: state = { "last_ping_time": last_ping_time, "last_ping_status": last_ping_status, "next_ping_time": next_ping_time, "ping_count": ping_count, "keep_alive_enabled": ENABLE_KEEP_ALIVE, "keep_alive_interval_seconds": KEEP_ALIVE_INTERVAL, } try: r = redis_lib.Redis(host="127.0.0.1", port=6379, decode_responses=True) r.set(REDIS_STATE_KEY, json.dumps(state)) except Exception as e: logger.error(f"[scheduler] failed to save state to Redis: {e}") async def keep_alive() -> None: global _ping_count now = datetime.now(timezone.utc) next_ping = (now + timedelta(seconds=KEEP_ALIVE_INTERVAL)).isoformat() try: async with httpx.AsyncClient(timeout=10) as client: r = await client.get(f"{SPACE_URL}/health") if r.status_code == 200: _ping_count += 1 logger.info(f"[keep-alive] OK — uptime: {r.json().get('uptime_human','?')} — count: {_ping_count}") _save_state(now.isoformat(), "ok", next_ping, _ping_count) else: logger.warning(f"[keep-alive] unexpected status: {r.status_code}") _save_state(now.isoformat(), "failed", next_ping, _ping_count) except Exception as e: logger.error(f"[keep-alive] failed: {e}") _save_state(now.isoformat(), "failed", next_ping, _ping_count) async def rotate_tokens() -> None: """Hook for token rotation — add your refresh logic here.""" logger.info("[tokens] rotation tick — add your refresh logic here") async def main() -> None: global _ping_count _ping_count = _load_state_from_redis() _save_state(None, None, None, _ping_count) scheduler = AsyncIOScheduler() if ENABLE_KEEP_ALIVE: scheduler.add_job(keep_alive, IntervalTrigger(seconds=KEEP_ALIVE_INTERVAL), id="keep_alive", next_run_time=None) logger.info(f"[scheduler] keep-alive registered — interval: {KEEP_ALIVE_INTERVAL}s") else: logger.info("[scheduler] keep-alive disabled") scheduler.add_job(rotate_tokens, IntervalTrigger(minutes=40), id="token_rotation") scheduler.start() logger.info("[scheduler] all jobs started") try: while True: await asyncio.sleep(60) except (KeyboardInterrupt, SystemExit): scheduler.shutdown() logger.info("[scheduler] stopped cleanly") if __name__ == "__main__": asyncio.run(main()) EOF # ============================================================ # app/node/server.js # ============================================================ cat > $PROJECT/app/node/server.js << 'EOF' const http = require("http"); const PORT = process.env.NODE_PORT || 3000; const server = http.createServer((req, res) => { if (req.url === "/health") { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ status: "ok", runtime: "node", uptime: process.uptime() })); return; } res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ message: "Node.js layer ready — customize server.js" })); }); server.listen(PORT, "0.0.0.0", () => console.log(`[node] running on port ${PORT}`)); process.on("SIGTERM", () => server.close(() => process.exit(0))); EOF # ============================================================ # scripts/entrypoint.sh # ============================================================ cat > $PROJECT/scripts/entrypoint.sh << 'EOF' #!/bin/bash set -e echo "============================================" echo " HF-VPS Starting..." echo "============================================" mkdir -p /app/logs /app/data /app/public [ ! -f /app/.env ] && touch /app/.env echo "[init] Python: $(python --version)" echo "[init] Node: $(node --version)" echo "[init] Redis: $(redis-server --version | head -1)" nginx -t 2>/dev/null && echo "[init] Nginx config OK" echo "[init] Starting all services..." exec supervisord -c /etc/supervisor/conf.d/supervisord.conf EOF chmod +x $PROJECT/scripts/entrypoint.sh # ============================================================ # .env.example # ============================================================ cat > $PROJECT/.env.example << 'EOF' APP_NAME=HF-VPS ENV=production SPACE_URL=https://your-username-your-space.hf.space ENABLE_KEEP_ALIVE=true KEEP_ALIVE_INTERVAL=1800 # API_KEY= # DATABASE_URL=sqlite:///./data/app.db # TOKEN_1= # TOKEN_2= EOF # ============================================================ # .gitignore # ============================================================ cat > $PROJECT/.gitignore << 'EOF' .env *.env __pycache__/ *.pyc .pytest_cache/ node_modules/ data/*.db data/*.rdb logs/ .DS_Store EOF # ============================================================ # README.md (HF Space header included) # ============================================================ cat > $PROJECT/README.md << 'EOF' --- title: HF-VPS emoji: 🚀 colorFrom: blue colorTo: purple sdk: docker pinned: false --- # 🚀 HF-VPS — Universal Base Environment A solid, flexible Docker environment for Hugging Face Spaces. Built to support the widest variety of projects on free hardware. **2 vCPU · 16GB RAM · 50GB Disk · Free** ## Stack - **Nginx** — reverse proxy, port 7860 - **FastAPI** — Python backend - **Node.js 20** — optional JS layer - **Redis** — cache & queue - **SQLite** — lightweight DB - **APScheduler** — cron + keep-alive - **Supervisord** — keeps everything running ## Quick deploy ```bash git clone https://github.com/your-username/hf-vps cd hf-vps git remote add hf https://huggingface.co/spaces/YOUR_HF_USERNAME/YOUR_SPACE_NAME git push hf main ``` ## Endpoints | URL | Description | |---|---| | `/` | Your app | | `/health` | Health + uptime | | `/docs` | FastAPI docs | | `/static/` | Static files | | `/api/` | API routes | | `/node/` | Node.js (if enabled) | ## Set in HF Secrets ``` SPACE_URL = https://your-username-your-space.hf.space APP_NAME = your-app-name ``` MIT License EOF # ============================================================ # Done # ============================================================ echo "" echo "============================================" echo " ✅ $PROJECT created successfully!" echo "============================================" echo "" echo "Next steps:" echo "" echo " 1. cd $PROJECT" echo " 2. git init" echo " 3. git add ." echo " 4. git commit -m 'initial hf-vps base'" echo " 5. git remote add origin https://github.com/YOUR_USERNAME/hf-vps" echo " 6. git push origin main" echo " 7. git remote add hf https://huggingface.co/spaces/HF_USERNAME/SPACE_NAME" echo " 8. git push hf main" echo "" echo " Then set SPACE_URL in your HF Space Secrets and you're live!" echo ""