diff --git a/heal.sh b/heal.sh
new file mode 100755
index 0000000..0987229
--- /dev/null
+++ b/heal.sh
@@ -0,0 +1,37 @@
+#!/bin/bash
+# Self-heal: pull latest git changes, deploy web content, keep containers up.
+# Runs every 5 minutes via systemd timer.
+set -euo pipefail
+cd /opt/xetup
+
+LOG="[heal $(date '+%H:%M:%S')]"
+
+# Pull latest changes (ff-only: never merge, just update)
+BEFORE=$(git rev-parse HEAD 2>/dev/null || echo "none")
+git fetch --quiet origin main 2>/dev/null || true
+REMOTE=$(git rev-parse origin/main 2>/dev/null || echo "")
+
+if [ -n "$REMOTE" ] && [ "$BEFORE" != "$REMOTE" ]; then
+ git reset --hard origin/main --quiet 2>/dev/null || true
+ AFTER=$(git rev-parse HEAD)
+ echo "$LOG deployed $BEFORE → $AFTER"
+
+ # Write deployed commit info for the web (spec page footer)
+ echo "{\"sha\":\"$(git rev-parse --short HEAD)\",\"ts\":\"$(date -u '+%Y-%m-%dT%H:%M:%SZ')\"}" \
+ > web/data/deploy.json
+
+ # If web content changed, reload nginx config
+ if git diff --name-only "$BEFORE" "$AFTER" 2>/dev/null | grep -q '^web/'; then
+ echo "$LOG web/ changed - reloading nginx"
+ /usr/bin/docker compose exec -T web nginx -s reload 2>/dev/null || true
+ fi
+else
+ # Always keep deploy.json fresh on first run if missing
+ if [ ! -f web/data/deploy.json ]; then
+ echo "{\"sha\":\"$(git rev-parse --short HEAD)\",\"ts\":\"$(date -u '+%Y-%m-%dT%H:%M:%SZ')\"}" \
+ > web/data/deploy.json
+ fi
+fi
+
+# Ensure all containers are running
+/usr/bin/docker compose up -d --remove-orphans 2>&1 | grep -v "up-to-date\|unchanged" || true
diff --git a/web/spec/index.html b/web/spec/index.html
index a4fc03a..797fb7d 100644
--- a/web/spec/index.html
+++ b/web/spec/index.html
@@ -1015,6 +1015,8 @@
Forgejo
·
xetup.x9.cz
+ ·
+ web: ...