HF-SaaS / setup.sh
luizcireno-crypto
feat: add status dashboard + keep-alive state tracking
e6dace5
raw
history blame
20.2 kB
#!/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="<h1>HF-VPS running</h1>")
@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 ""