File size: 9,076 Bytes
2be62d0 a0c86f6 b1363c6 9649929 b1363c6 a0c86f6 b1363c6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 | FROM diegosouzapw/omniroute:latest
# 1. Switch to root to install system packages
USER root
# 2. Install Python for the backup script (Debian-based)
RUN apt-get update && \
apt-get install -y --no-install-recommends python3 python3-pip tar openssl && \
pip3 install --break-system-packages --no-cache-dir huggingface_hub && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
# 3. Create the Python sync script (FIXED: tarball mirrors real FS layout
# + marker-based legacy healer + file-level verify)
RUN cat > /app/sync.py <<'PY'
import os
import shutil
import tarfile
import tempfile
import time
import sys
from pathlib import Path
from huggingface_hub import HfApi, hf_hub_download
REPO_ID = os.environ.get("DATASET_REPO")
TOKEN = os.environ.get("HF_TOKEN")
BACKUP_FILE = "/tmp/omniroute_backup.tar.gz"
DATA_DIR = Path("/app/data")
CONFIG_DIR = Path("/root/.omniroute")
def safe_extract(tar, path):
"""Extract `tar` into `path`, rejecting members that would escape it."""
base = Path(path).resolve()
for member in tar.getmembers():
target = (base / member.name).resolve()
if not str(target).startswith(str(base)):
raise RuntimeError(f"Unsafe tar member: {member.name}")
# Python 3.12+ requires the `filter` argument (and we WANT data filter,
# which strips leading slashes / .. and refuses absolute paths).
if sys.version_info >= (3, 12):
tar.extractall(path, filter="data")
else:
tar.extractall(path)
def heal_legacy_layout():
"""
Older backups were packed as `./data/...` and `./.omniroute/...`, which the
old code extracted to `/data` and `/.omniroute` instead of `/app/data` and
`/root/.omniroute`.
IMPORTANT: run.sh `mkdir -p`s both target dirs BEFORE we run, so we cannot
just check `dst.exists()` — the target is always there (empty). We must
check the actual *file* the app needs:
- /app/data/storage.sqlite (target) vs /data/storage.sqlite (misplaced)
- /root/.omniroute/server.env (target) vs /.omniroute/server.env (misplaced)
If the misplaced copy exists and the target marker is missing, we swap.
If the target is already populated, we REFUSE to clobber it.
"""
plans = [
(Path("/data"), "storage.sqlite", Path("/app/data")),
(Path("/.omniroute"), "server.env", Path("/root/.omniroute")),
]
for src, marker_name, dst in plans:
src_marker = src / marker_name
dst_marker = dst / marker_name
if not src_marker.exists():
continue # nothing misplaced for this pair
if dst_marker.exists():
print(
f">>> [Sync] Healer: target {dst_marker} already present, "
f"leaving {src} alone (would not clobber real data)",
flush=True,
)
continue
print(f">>> [Sync] Healer: misplaced legacy content at {src}", flush=True)
# Target dir exists but is missing its marker -> it must be empty.
# Try to remove it so shutil.move produces a clean swap; if it's not
# empty (unexpected), bail out instead of nesting src inside dst.
if dst.exists():
try:
dst.rmdir()
print(f">>> [Sync] Healer: removed empty target {dst}", flush=True)
except OSError:
print(
f">>> [Sync] Healer: ABORT — {dst} is not empty, "
f"refusing to clobber",
flush=True,
)
continue
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.move(str(src), str(dst))
print(f">>> [Sync] Healer: relocated {src} -> {dst}", flush=True)
def upload():
if not REPO_ID or not TOKEN:
print(">>> [Sync] DATASET_REPO or HF_TOKEN not configured. Skipping backup.")
return
with tempfile.TemporaryDirectory() as tmpdir:
tmp_path = Path(tmpdir)
# Mirror the REAL filesystem layout. After upload, the tar contains:
# ./app/data/...
# ./root/.omniroute/...
# so extracting to "/" puts files back where the app expects them.
if DATA_DIR.exists():
target = tmp_path / "app" / "data"
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copytree(DATA_DIR, target)
print(f">>> [Sync] Packed {DATA_DIR} -> app/data", flush=True)
if CONFIG_DIR.exists():
target = tmp_path / "root" / ".omniroute"
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copytree(CONFIG_DIR, target)
print(f">>> [Sync] Packed {CONFIG_DIR} -> root/.omniroute", flush=True)
try:
with tarfile.open(BACKUP_FILE, "w:gz") as tar:
tar.add(str(tmp_path), arcname=".")
HfApi(token=TOKEN).upload_file(
path_or_fileobj=BACKUP_FILE,
path_in_repo="omniroute_backup.tar.gz",
repo_id=REPO_ID,
repo_type="dataset",
)
print(f">>> [Sync] Backup successful: {time.strftime('%Y-%m-%d %H:%M:%S')}", flush=True)
except Exception as e:
print(f">>> [Sync] Backup failed: {e}", flush=True)
def download():
if not REPO_ID or not TOKEN:
print(">>> [Sync] DATASET_REPO or HF_TOKEN not configured. Skipping restore.")
return
try:
print(">>> [Sync] Downloading data from Dataset...", flush=True)
path = hf_hub_download(
repo_id=REPO_ID,
filename="omniroute_backup.tar.gz",
repo_type="dataset",
token=TOKEN,
)
print(f">>> [Sync] Downloaded archive to {path}", flush=True)
with tarfile.open(path, "r:gz") as tar:
members = tar.getmembers()
print(f">>> [Sync] Extracting {len(members)} members to / ...", flush=True)
safe_extract(tar, "/")
# Self-heal: handle backups created before the path fix.
heal_legacy_layout()
# Verify the app can actually see the restored files.
# Check the REAL files, not just the dirs (mkdir -p makes them lie).
critical = [
(DATA_DIR, "storage.sqlite"),
(CONFIG_DIR, "server.env"),
]
for d, fname in critical:
marker = d / fname
status = "OK" if marker.exists() else "MISSING"
print(f">>> [Sync] Verify {marker}: {status}", flush=True)
print(">>> [Sync] Data restore successful.", flush=True)
except Exception as e:
print(f">>> [Sync] Skipping restore (might be first run): {e}", flush=True)
if __name__ == "__main__":
if len(sys.argv) > 1 and sys.argv[1] == "upload":
upload()
elif len(sys.argv) > 1 and sys.argv[1] == "download":
download()
PY
# 4. Create the startup shell script (FIXED: don't clobber restored server.env)
RUN cat > /app/run.sh <<'SH'
#!/bin/sh
set -e
mkdir -p /app/data
mkdir -p /root/.omniroute
echo '=== 1. Restoring Data & Config ==='
python3 /app/sync.py download
echo '=== 2. Starting Scheduled Backup (Every 10 minutes) ==='
(while true; do sleep 600; python3 /app/sync.py upload; done) &
echo '=== 3. Generating & Persisting Secrets ==='
# Only mint fresh secrets if restore didn't bring a server.env with us.
# Otherwise we'd rotate JWT_SECRET etc. on every container restart and
# invalidate every active session / API key / initial-password.
if [ ! -f /root/.omniroute/server.env ]; then
echo '>>> No restored server.env found, generating fresh secrets'
export JWT_SECRET="${JWT_SECRET:-$(openssl rand -base64 48 | tr -d '\n')}"
export API_KEY_SECRET="${API_KEY_SECRET:-$(openssl rand -hex 32 | tr -d '\n')}"
export STORAGE_ENCRYPTION_KEY="${STORAGE_ENCRYPTION_KEY:-$(openssl rand -hex 32 | tr -d '\n')}"
export OMNIROUTE_WS_BRIDGE_SECRET="${OMNIROUTE_WS_BRIDGE_SECRET:-$(openssl rand -base64 32 | tr -d '\n')}"
export INITIAL_PASSWORD="${INITIAL_PASSWORD:-$(openssl rand -hex 16 | tr -d '\n')}"
cat > /root/.omniroute/server.env <<EOF
JWT_SECRET=${JWT_SECRET}
API_KEY_SECRET=${API_KEY_SECRET}
STORAGE_ENCRYPTION_KEY=${STORAGE_ENCRYPTION_KEY}
OMNIROUTE_WS_BRIDGE_SECRET=${OMNIROUTE_WS_BRIDGE_SECRET}
INITIAL_PASSWORD=${INITIAL_PASSWORD}
DATA_DIR=/app/data
PORT=7860
NODE_ENV=production
NEXT_PUBLIC_BASE_URL=https://${SPACE_HOST:-localhost:7860}
BASE_URL=http://localhost:7860
AUTH_COOKIE_SECURE=true
REQUIRE_API_KEY=true
CORS_ORIGIN=*
OMNIROUTE_MEMORY_MB=14745
PRICING_SYNC_ENABLED=true
OMNIROUTE_AUTO_FREE_FALLBACK_TO_FULL_POOL=true
OMNIROUTE_SHOW_LOG=0
APP_LOG_LEVEL=error
EOF
else
echo '>>> Restored server.env found, keeping restored secrets'
fi
# Source whichever server.env we ended up with (restored or freshly minted).
set -a
. "/root/.omniroute/server.env"
set +a
export NODE_OPTIONS="--max-old-space-size=${OMNIROUTE_MEMORY_MB}"
echo '=== 4. Starting OmniRoute ==='
exec node server.js
SH
RUN chmod +x /app/run.sh
EXPOSE 7860
CMD ["/bin/sh", "/app/run.sh"] |