tao-shen Claude Opus 4.6 commited on
Commit
08f77cf
·
1 Parent(s): 94a1b4c

feat: git_sync_daemon — gitfs-like auto-sync without FUSE

Browse files

FUSE/gitfs not available in HF Spaces (no CAP_MKNOD).
Implements same behavior via pure git:
- git clone dataset → /data on startup
- rsync / → /data/rootfs/ periodically
- git add/commit/push automatically (like gitfs current/)
- Dataset mirrors filesystem at actual paths
- Configurable sync interval (default 120s)

Simplified start-server.sh: only starts services (sshd, ws-bridge,
ttyd, nginx). All persistence handled by git_sync_daemon.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Dockerfile CHANGED
@@ -5,10 +5,11 @@ FROM ubuntu:24.04
5
 
6
  ENV DEBIAN_FRONTEND=noninteractive
7
 
8
- # System + Python (for HuggingRun persistence sync)
9
  RUN apt-get update && apt-get install -y --no-install-recommends \
10
- ca-certificates curl wget python3 python3-pip python3-venv git \
11
  && pip3 install --no-cache-dir --break-system-packages huggingface_hub websockets \
 
12
  && rm -rf /var/lib/apt/lists/*
13
 
14
  # Server: SSH + nginx + ttyd + tools
@@ -45,7 +46,7 @@ RUN mkdir -p /data
45
  # nginx + bridge + startup scripts
46
  COPY ubuntu-server/nginx.conf /etc/nginx/nginx.conf
47
  COPY ubuntu-server/ws-ssh-bridge.py /opt/ws-ssh-bridge.py
48
- COPY ubuntu-server/upload_sync.py /opt/upload_sync.py
49
  COPY ubuntu-server/start-server.sh /opt/start-server.sh
50
  COPY scripts /scripts
51
  RUN chmod +x /scripts/entrypoint.sh /opt/start-server.sh
 
5
 
6
  ENV DEBIAN_FRONTEND=noninteractive
7
 
8
+ # System + Python + git-lfs (for HuggingRun git-based persistence)
9
  RUN apt-get update && apt-get install -y --no-install-recommends \
10
+ ca-certificates curl wget python3 python3-pip python3-venv git git-lfs \
11
  && pip3 install --no-cache-dir --break-system-packages huggingface_hub websockets \
12
+ && git lfs install \
13
  && rm -rf /var/lib/apt/lists/*
14
 
15
  # Server: SSH + nginx + ttyd + tools
 
46
  # nginx + bridge + startup scripts
47
  COPY ubuntu-server/nginx.conf /etc/nginx/nginx.conf
48
  COPY ubuntu-server/ws-ssh-bridge.py /opt/ws-ssh-bridge.py
49
+ COPY ubuntu-server/git_sync_daemon.py /opt/git_sync_daemon.py
50
  COPY ubuntu-server/start-server.sh /opt/start-server.sh
51
  COPY scripts /scripts
52
  RUN chmod +x /scripts/entrypoint.sh /opt/start-server.sh
scripts/entrypoint.sh CHANGED
@@ -1,95 +1,55 @@
1
  #!/bin/bash
2
  # ─────────────────────────────────────────────────────────────────────
3
  # HuggingRun Entrypoint
4
- # 1. Determine dataset repo, write env to /etc/huggingrun.env
5
- # 2. Download dataset /data (snapshot_download)
6
- # 3. Run user command (start-server.sh sources the env and handles upload)
7
  # ─────────────────────────────────────────────────────────────────────
8
  set -e
9
- echo "[HuggingRun] Entrypoint — persistence + RUN_CMD"
10
-
11
- PERSIST_PATH="${PERSIST_PATH:-/data}"
12
- ENV_FILE="/etc/huggingrun.env"
13
-
14
- # Step 1: Download dataset and write env file
15
- python3 -u << 'PYEOF'
16
- import os, sys, traceback
17
- from pathlib import Path
18
-
19
- PERSIST_PATH = Path(os.environ.get("PERSIST_PATH", "/data"))
20
- HF_TOKEN = os.environ.get("HF_TOKEN", "")
21
- SPACE_ID = os.environ.get("SPACE_ID", "")
22
- HF_DATASET_REPO = os.environ.get("HF_DATASET_REPO", "")
23
- ENV_FILE = "/etc/huggingrun.env"
24
 
25
  # Determine dataset repo
26
- if not HF_DATASET_REPO:
27
- if SPACE_ID:
28
- HF_DATASET_REPO = f"{SPACE_ID}-data"
29
- elif HF_TOKEN:
30
- from huggingface_hub import HfApi
31
- try:
32
- HF_DATASET_REPO = HfApi(token=HF_TOKEN).whoami()["name"] + "/HuggingRun-data"
33
- except:
34
- pass
35
-
36
- # Write env file for other processes
37
- with open(ENV_FILE, "w") as f:
38
- f.write(f'export HF_TOKEN="{HF_TOKEN}"\n')
39
- f.write(f'export HF_DATASET_REPO="{HF_DATASET_REPO}"\n')
40
- f.write(f'export PERSIST_PATH="{PERSIST_PATH}"\n')
41
- print(f"[HuggingRun] Wrote env to {ENV_FILE}")
42
-
43
- if not HF_TOKEN or not HF_DATASET_REPO:
44
- print("[HuggingRun] No HF_TOKEN or dataset. Persistence disabled.")
45
- PERSIST_PATH.mkdir(parents=True, exist_ok=True)
46
- sys.exit(0)
47
-
48
- print(f"[HuggingRun] Dataset repo: {HF_DATASET_REPO}")
49
-
50
- from huggingface_hub import HfApi, snapshot_download
51
-
52
- api = HfApi(token=HF_TOKEN)
53
-
54
- # Ensure dataset exists
55
  try:
56
- api.repo_info(repo_id=HF_DATASET_REPO, repo_type="dataset")
57
- print(f"[HuggingRun] Dataset found: {HF_DATASET_REPO}")
58
  except:
59
- try:
60
- api.create_repo(repo_id=HF_DATASET_REPO, repo_type="dataset", private=True)
61
- print(f"[HuggingRun] Created dataset: {HF_DATASET_REPO}")
62
- except Exception as e:
63
- print(f"[HuggingRun] Cannot create dataset: {e}")
64
- PERSIST_PATH.mkdir(parents=True, exist_ok=True)
65
- sys.exit(0)
66
-
67
- # Check if dataset has files
68
- files = api.list_repo_files(repo_id=HF_DATASET_REPO, repo_type="dataset")
69
- data_files = [f for f in files if not f.startswith(".") and f != "README.md"]
70
 
71
- if not data_files:
72
- print("[HuggingRun] Dataset empty. Starting fresh.")
73
- PERSIST_PATH.mkdir(parents=True, exist_ok=True)
74
- sys.exit(0)
 
 
 
 
 
 
 
 
 
 
 
75
 
76
- print(f"[HuggingRun] Downloading {len(data_files)} files from {HF_DATASET_REPO} ...")
77
- PERSIST_PATH.mkdir(parents=True, exist_ok=True)
78
- snapshot_download(
79
- repo_id=HF_DATASET_REPO,
80
- repo_type="dataset",
81
- local_dir=str(PERSIST_PATH),
82
- token=HF_TOKEN,
83
- ignore_patterns=[".git*", "README.md"],
84
- )
85
- print("[HuggingRun] Download completed.")
86
- PYEOF
87
 
88
- # Step 2: Source env and run user command
89
- if [ -f "$ENV_FILE" ]; then
90
- source "$ENV_FILE"
91
- fi
 
 
92
 
 
93
  CMD="${RUN_CMD:-python3 /app/demo_app.py}"
94
  echo "[HuggingRun] Running: $CMD"
95
  exec $CMD
 
1
  #!/bin/bash
2
  # ─────────────────────────────────────────────────────────────────────
3
  # HuggingRun Entrypoint
4
+ # Uses git_sync_daemon.py (gitfs-like) for dataset filesystem sync
5
+ # Then runs the user command (start-server.sh)
 
6
  # ─────────────────────────────────────────────────────────────────────
7
  set -e
8
+ echo "[HuggingRun] Entrypoint — git-sync persistence + RUN_CMD"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
  # Determine dataset repo
11
+ if [ -z "$HF_DATASET_REPO" ]; then
12
+ if [ -n "$SPACE_ID" ]; then
13
+ export HF_DATASET_REPO="${SPACE_ID}-data"
14
+ elif [ -n "$HF_TOKEN" ]; then
15
+ export HF_DATASET_REPO=$(python3 -c "
16
+ from huggingface_hub import HfApi
17
+ import os
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  try:
19
+ print(HfApi(token=os.environ['HF_TOKEN']).whoami()['name'] + '/HuggingRun-data')
 
20
  except:
21
+ print('')
22
+ " 2>/dev/null)
23
+ fi
24
+ fi
 
 
 
 
 
 
 
25
 
26
+ # Ensure dataset repo exists
27
+ if [ -n "$HF_TOKEN" ] && [ -n "$HF_DATASET_REPO" ]; then
28
+ python3 -c "
29
+ from huggingface_hub import HfApi
30
+ import os
31
+ api = HfApi(token=os.environ['HF_TOKEN'])
32
+ repo = os.environ['HF_DATASET_REPO']
33
+ try:
34
+ api.repo_info(repo_id=repo, repo_type='dataset')
35
+ print(f'[HuggingRun] Dataset: {repo}')
36
+ except:
37
+ api.create_repo(repo_id=repo, repo_type='dataset', private=True)
38
+ print(f'[HuggingRun] Created: {repo}')
39
+ " 2>/dev/null || echo "[HuggingRun] Could not verify dataset"
40
+ fi
41
 
42
+ # Run git-sync daemon (clones repo, restores system, starts background sync)
43
+ python3 -u /opt/git_sync_daemon.py
 
 
 
 
 
 
 
 
 
44
 
45
+ # Write env for other processes (sshd, cron, etc.)
46
+ cat > /etc/huggingrun.env << ENVEOF
47
+ export HF_TOKEN="${HF_TOKEN}"
48
+ export HF_DATASET_REPO="${HF_DATASET_REPO}"
49
+ export PERSIST_PATH="${PERSIST_PATH:-/data}"
50
+ ENVEOF
51
 
52
+ # Run user command
53
  CMD="${RUN_CMD:-python3 /app/demo_app.py}"
54
  echo "[HuggingRun] Running: $CMD"
55
  exec $CMD
ubuntu-server/git_sync_daemon.py ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ git_sync_daemon.py — gitfs-like auto-sync without FUSE
4
+
5
+ Behavior (mirrors gitfs):
6
+ 1. Clones HF dataset repo to /data
7
+ 2. Periodically rsyncs filesystem → /data/rootfs/
8
+ 3. Auto-commits and pushes changes via git
9
+ 4. On restart: git pull → rsync /data/rootfs/ → /
10
+
11
+ The dataset contains the actual filesystem at rootfs/usr/local/bin/..., etc.
12
+ Every change is a git commit. Dataset = disk, always in sync.
13
+ """
14
+
15
+ import os
16
+ import sys
17
+ import time
18
+ import subprocess
19
+ import signal
20
+ import threading
21
+
22
+ # ── Config ─────────────────────────────────────────────────────────
23
+ PERSIST_PATH = os.environ.get("PERSIST_PATH", "/data")
24
+ HF_TOKEN = os.environ.get("HF_TOKEN", "")
25
+ HF_DATASET_REPO = os.environ.get("HF_DATASET_REPO", "")
26
+ ROOTFS = os.path.join(PERSIST_PATH, "rootfs")
27
+
28
+ SYNC_INTERVAL = int(os.environ.get("SYNC_INTERVAL", "120")) # seconds between syncs
29
+ COMMIT_NAME = "HuggingRun"
30
+ COMMIT_EMAIL = "huggingrun@hf.space"
31
+
32
+ RSYNC_EXCLUDES = [
33
+ "/proc", "/sys", "/dev", "/data", "/tmp", "/run",
34
+ "/mnt", "/media", "/snap",
35
+ "/var/cache/apt", "/var/lib/apt/lists",
36
+ "__pycache__", "*.pyc", "*.lock", "*.pid", "*.sock",
37
+ ]
38
+
39
+ stop_event = threading.Event()
40
+
41
+
42
+ def log(msg):
43
+ print(f"[git-sync] {msg}", file=sys.stderr, flush=True)
44
+
45
+
46
+ def run(cmd, cwd=None, check=False):
47
+ """Run a shell command, return (returncode, stdout)."""
48
+ r = subprocess.run(
49
+ cmd, shell=True, cwd=cwd,
50
+ stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
51
+ text=True,
52
+ )
53
+ if check and r.returncode != 0:
54
+ log(f"Command failed: {cmd}\n{r.stdout}")
55
+ return r.returncode, r.stdout.strip()
56
+
57
+
58
+ def git_clone_or_pull():
59
+ """Clone the dataset repo to PERSIST_PATH, or pull if already cloned."""
60
+ repo_url = f"https://user:{HF_TOKEN}@huggingface.co/datasets/{HF_DATASET_REPO}"
61
+
62
+ if os.path.isdir(os.path.join(PERSIST_PATH, ".git")):
63
+ log("Git repo exists, pulling ...")
64
+ run(f"git -C {PERSIST_PATH} fetch origin main", cwd=PERSIST_PATH)
65
+ run(f"git -C {PERSIST_PATH} reset --hard origin/main", cwd=PERSIST_PATH)
66
+ log("Pull completed")
67
+ else:
68
+ log(f"Cloning {HF_DATASET_REPO} to {PERSIST_PATH} ...")
69
+ # Remove any existing non-git content
70
+ if os.path.exists(PERSIST_PATH):
71
+ run(f"rm -rf {PERSIST_PATH}")
72
+ rc, out = run(f"git clone --depth 1 {repo_url} {PERSIST_PATH}")
73
+ if rc != 0:
74
+ log(f"Clone failed: {out}")
75
+ os.makedirs(PERSIST_PATH, exist_ok=True)
76
+ # Init empty repo
77
+ run(f"git init {PERSIST_PATH}")
78
+ run(f"git -C {PERSIST_PATH} remote add origin {repo_url}")
79
+ run(f"git -C {PERSIST_PATH} checkout -b main")
80
+ log("Clone completed")
81
+
82
+ # Configure git
83
+ run(f'git config user.name "{COMMIT_NAME}"', cwd=PERSIST_PATH)
84
+ run(f'git config user.email "{COMMIT_EMAIL}"', cwd=PERSIST_PATH)
85
+ # Enable git-lfs
86
+ run("git lfs install --skip-smudge", cwd=PERSIST_PATH)
87
+
88
+
89
+ def restore_system():
90
+ """Restore filesystem from /data/rootfs/ → /."""
91
+ if not os.path.isdir(ROOTFS) or not os.listdir(ROOTFS):
92
+ log("No rootfs found, starting fresh")
93
+ os.makedirs(ROOTFS, exist_ok=True)
94
+ return
95
+
96
+ log("Restoring filesystem from rootfs ...")
97
+ excludes = " ".join(f"--exclude='{e}'" for e in RSYNC_EXCLUDES)
98
+ run(f"rsync -aAX {excludes} --exclude='/scripts' {ROOTFS}/ /")
99
+ log("Filesystem restored")
100
+
101
+
102
+ def save_and_push():
103
+ """Rsync filesystem → rootfs, git commit + push."""
104
+ log("Saving filesystem ...")
105
+ os.makedirs(ROOTFS, exist_ok=True)
106
+
107
+ # Rsync / → /data/rootfs/
108
+ excludes = " ".join(f"--exclude='{e}'" for e in RSYNC_EXCLUDES)
109
+ run(f"rsync -aAX --delete {excludes} / {ROOTFS}/")
110
+
111
+ # Fix permissions for git
112
+ run(f"chmod -R a+r {ROOTFS}/")
113
+
114
+ # Git add + commit + push
115
+ run("git add -A", cwd=PERSIST_PATH)
116
+
117
+ # Check if there are changes
118
+ rc, _ = run("git diff --cached --quiet", cwd=PERSIST_PATH)
119
+ if rc == 0:
120
+ log("No changes to commit")
121
+ return
122
+
123
+ ts = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime())
124
+ run(f'git commit -m "sync {ts}"', cwd=PERSIST_PATH)
125
+
126
+ log("Pushing to remote ...")
127
+ rc, out = run("git push origin main", cwd=PERSIST_PATH)
128
+ if rc != 0:
129
+ log(f"Push failed, trying force push: {out}")
130
+ rc, out = run("git push --force origin main", cwd=PERSIST_PATH)
131
+ if rc != 0:
132
+ log(f"Force push also failed: {out}")
133
+ else:
134
+ log("Force push succeeded")
135
+ else:
136
+ log("Push completed")
137
+
138
+
139
+ def ensure_passwords():
140
+ """Re-ensure user/root passwords after restore."""
141
+ run("id user >/dev/null 2>&1 || useradd -m -s /bin/bash user")
142
+ run('usermod -p "$(openssl passwd -6 huggingrun)" user')
143
+ run('usermod -p "$(openssl passwd -6 huggingrun)" root')
144
+ run("ldconfig")
145
+ log("Passwords and ldconfig refreshed")
146
+
147
+
148
+ def sync_loop():
149
+ """Background loop: periodically save and push."""
150
+ log(f"Sync loop started (interval={SYNC_INTERVAL}s)")
151
+ # Initial delay to let services start
152
+ if stop_event.wait(timeout=60):
153
+ return
154
+
155
+ while not stop_event.is_set():
156
+ try:
157
+ save_and_push()
158
+ except Exception as e:
159
+ log(f"Sync error: {e}")
160
+
161
+ if stop_event.wait(timeout=SYNC_INTERVAL):
162
+ break
163
+
164
+ # Final save on exit
165
+ log("Final save before exit ...")
166
+ try:
167
+ save_and_push()
168
+ except Exception as e:
169
+ log(f"Final save error: {e}")
170
+
171
+
172
+ def main():
173
+ if not HF_TOKEN or not HF_DATASET_REPO:
174
+ log("No HF_TOKEN or HF_DATASET_REPO. Sync disabled.")
175
+ os.makedirs(PERSIST_PATH, exist_ok=True)
176
+ return
177
+
178
+ log(f"Starting git-sync daemon")
179
+ log(f" Dataset: {HF_DATASET_REPO}")
180
+ log(f" Persist: {PERSIST_PATH}")
181
+ log(f" Interval: {SYNC_INTERVAL}s")
182
+
183
+ # Clone or pull
184
+ git_clone_or_pull()
185
+
186
+ # Restore system
187
+ restore_system()
188
+
189
+ # Re-set passwords
190
+ ensure_passwords()
191
+
192
+ # Start sync loop in background thread
193
+ sync_thread = threading.Thread(target=sync_loop, daemon=True)
194
+ sync_thread.start()
195
+
196
+ # Handle signals
197
+ def on_signal(sig, frame):
198
+ log(f"Signal {sig} received, stopping ...")
199
+ stop_event.set()
200
+ sync_thread.join(timeout=30)
201
+ sys.exit(0)
202
+
203
+ signal.signal(signal.SIGTERM, on_signal)
204
+ signal.signal(signal.SIGINT, on_signal)
205
+
206
+ # Keep main thread alive (will be used by start-server.sh exec)
207
+ log("Daemon ready. System restored.")
208
+
209
+
210
+ if __name__ == "__main__":
211
+ main()
ubuntu-server/start-server.sh CHANGED
@@ -1,94 +1,17 @@
1
  #!/bin/bash
2
  # ─────────────────────────────────────────────────────────────────────
3
- # HuggingRun Ubuntu Server: full system persistence
4
- # Port 7860 (nginx): web terminal + SSH-over-WebSocket
5
  #
6
- # Persistence model:
7
- # /data/rootfs/ mirrors entire / filesystem
8
- # upload_folder pushes /data/rootfs/ to HF dataset periodically
9
- # Dataset shows actual filesystem paths: rootfs/usr/local/bin/...
10
  # ─────────────────────────────────────────────────────────────────────
11
- echo "[start-server] Starting ..." >&2
12
- set -e
13
 
14
- # Source HuggingRun env (HF_TOKEN, HF_DATASET_REPO, PERSIST_PATH)
15
- [ -f /etc/huggingrun.env ] && source /etc/huggingrun.env
16
-
17
- export PERSIST_PATH="${PERSIST_PATH:-/data}"
18
  export SSH_PORT="${SSH_PORT:-2222}"
19
  export TTYD_PORT="${TTYD_PORT:-7681}"
20
 
21
- ROOTFS="$PERSIST_PATH/rootfs"
22
-
23
- # Rsync exclude list
24
- RSYNC_EXCLUDE=(
25
- --exclude='/proc' --exclude='/sys' --exclude='/dev'
26
- --exclude='/data' --exclude='/tmp' --exclude='/run'
27
- --exclude='/mnt' --exclude='/media' --exclude='/snap'
28
- --exclude='/var/cache/apt' --exclude='/var/lib/apt/lists'
29
- --exclude='__pycache__' --exclude='*.pyc'
30
- --exclude='.git'
31
- )
32
-
33
- # ── Phase 1: Restore system from dataset ──────────────────────────
34
- echo "[start-server] Restoring persisted system state ..." >&2
35
- if [ -d "$ROOTFS" ] && [ "$(ls -A "$ROOTFS" 2>/dev/null)" ]; then
36
- echo "[persist] Restoring filesystem from $ROOTFS ..." >&2
37
- rsync -aAX \
38
- --exclude='/scripts' \
39
- "${RSYNC_EXCLUDE[@]}" \
40
- "$ROOTFS/" / 2>/dev/null || true
41
- echo "[persist] Filesystem restored" >&2
42
- else
43
- echo "[persist] No rootfs found, starting fresh" >&2
44
- mkdir -p "$ROOTFS"
45
- fi
46
-
47
- # Always re-ensure passwords and user
48
- id user &>/dev/null || useradd -m -s /bin/bash user 2>/dev/null || true
49
- usermod -p "$(openssl passwd -6 huggingrun)" user 2>/dev/null || true
50
- usermod -p "$(openssl passwd -6 huggingrun)" root 2>/dev/null || true
51
- ldconfig 2>/dev/null || true
52
- echo "[persist] System ready" >&2
53
-
54
- # ── Phase 2: Save system (rsync + upload_folder) ─────────────────
55
- save_system() {
56
- echo "[persist] Saving filesystem ..." >&2
57
- mkdir -p "$ROOTFS"
58
-
59
- # Rsync entire filesystem to /data/rootfs (exclude lock files etc.)
60
- rsync -aAX --delete \
61
- "${RSYNC_EXCLUDE[@]}" \
62
- --exclude='*.lock' --exclude='*.pid' --exclude='*.sock' \
63
- / "$ROOTFS/" 2>/dev/null || true
64
-
65
- # Fix permissions so upload can read all files
66
- chmod -R a+r "$ROOTFS/" 2>/dev/null || true
67
-
68
- # Source env for credentials
69
- [ -f /etc/huggingrun.env ] && source /etc/huggingrun.env
70
-
71
- # Upload via huggingface_hub
72
- python3 -u /opt/upload_sync.py 2>&1 || true
73
-
74
- echo "[persist] Save completed at $(date -u +%H:%M:%S)" >&2
75
- }
76
-
77
- # Background: save every 300 seconds
78
- (
79
- sleep 180 # initial delay (3 min, let system settle)
80
- while true; do
81
- save_system
82
- sleep 300
83
- done
84
- ) &
85
- PERSIST_PID=$!
86
- echo "[start-server] Persistence background PID=$PERSIST_PID" >&2
87
-
88
- # Save on exit
89
- trap 'echo "[start-server] Saving before exit..." >&2; save_system; exit 0' SIGTERM SIGINT
90
-
91
- # ── Phase 3: Start sshd ──────────────────────────────────────────
92
  mkdir -p /run/sshd
93
  echo "[start-server] Starting sshd on 127.0.0.1:$SSH_PORT ..." >&2
94
  /usr/sbin/sshd -o "Port=$SSH_PORT" \
@@ -102,20 +25,20 @@ SSHD_PID=$!
102
  sleep 1
103
  echo "[start-server] sshd PID=$SSHD_PID" >&2
104
 
105
- # ── Phase 4: Start WebSocket-to-SSH bridge ────────────────────────
106
  echo "[start-server] Starting WS-SSH bridge on 127.0.0.1:7862 ..." >&2
107
  python3 /opt/ws-ssh-bridge.py &
108
  BRIDGE_PID=$!
109
  sleep 1
110
  echo "[start-server] WS-SSH bridge PID=$BRIDGE_PID" >&2
111
 
112
- # ── Phase 5: Start ttyd (web terminal) ───────────────────────────
113
  echo "[start-server] Starting ttyd on 127.0.0.1:$TTYD_PORT ..." >&2
114
  ttyd --port "$TTYD_PORT" --writable --base-path / bash --login &
115
  TTYD_PID=$!
116
  sleep 1
117
  echo "[start-server] ttyd PID=$TTYD_PID" >&2
118
 
119
- # ── Phase 6: Start nginx (foreground, port 7860) ─────────────────
120
  echo "[start-server] Starting nginx on 0.0.0.0:7860 ..." >&2
121
  exec nginx -g 'daemon off;'
 
1
  #!/bin/bash
2
  # ─────────────────────────────────────────────────────────────────────
3
+ # HuggingRun Ubuntu Server: ttyd + SSH-over-WebSocket
4
+ # Port 7860 (nginx): web terminal + SSH
5
  #
6
+ # Persistence is handled by git_sync_daemon.py (started in entrypoint)
7
+ # This script just starts the services.
 
 
8
  # ─────────────────────────────────────────────────────────────────────
9
+ echo "[start-server] Starting services ..." >&2
 
10
 
 
 
 
 
11
  export SSH_PORT="${SSH_PORT:-2222}"
12
  export TTYD_PORT="${TTYD_PORT:-7681}"
13
 
14
+ # ── sshd ──────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  mkdir -p /run/sshd
16
  echo "[start-server] Starting sshd on 127.0.0.1:$SSH_PORT ..." >&2
17
  /usr/sbin/sshd -o "Port=$SSH_PORT" \
 
25
  sleep 1
26
  echo "[start-server] sshd PID=$SSHD_PID" >&2
27
 
28
+ # ── WebSocket-to-SSH bridge ──────────────────────────────────────
29
  echo "[start-server] Starting WS-SSH bridge on 127.0.0.1:7862 ..." >&2
30
  python3 /opt/ws-ssh-bridge.py &
31
  BRIDGE_PID=$!
32
  sleep 1
33
  echo "[start-server] WS-SSH bridge PID=$BRIDGE_PID" >&2
34
 
35
+ # ── ttyd (web terminal) ─────────────────────────────────────────
36
  echo "[start-server] Starting ttyd on 127.0.0.1:$TTYD_PORT ..." >&2
37
  ttyd --port "$TTYD_PORT" --writable --base-path / bash --login &
38
  TTYD_PID=$!
39
  sleep 1
40
  echo "[start-server] ttyd PID=$TTYD_PID" >&2
41
 
42
+ # ── nginx (foreground, port 7860) ────────────────────────────────
43
  echo "[start-server] Starting nginx on 0.0.0.0:7860 ..." >&2
44
  exec nginx -g 'daemon off;'