Skip to main content
gateway/entrypoint.sh is the container’s entrypoint. It runs on every container start and is fully idempotent — steps that already completed on a prior boot are skipped. On shutdown (SIGTERM/SIGINT), the entrypoint runs gateway_backup.py backup to save gateway state to Supabase Storage before forwarding the signal to child processes. This ensures auth tokens, agent configs, and secrets survive container recreation.

Boot sequence

Step 0 — Resolve Supabase credentials

Three credential paths are tried in order: Path A: environment variablesSUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY already set in env. Used immediately, no waiting. Path B: gateway token exchange — For remote gateways installed via the gateway installer (install.yourhq.ai/gateway). If GATEWAY_TOKEN is set and $OPENCLAW_HOME/.token-consumed does not exist, the entrypoint calls consume_gateway_token() to exchange the one-time token for a gateway slug and UUID.
  • Requires: GATEWAY_TOKEN, SUPABASE_URL, SUPABASE_ANON_KEY
  • On success: writes the assigned slug to .gateway-slug and marks the token consumed with .token-consumed
  • One-shot — the marker file prevents re-exchange on subsequent boots
  • If it fails, the container exits (restart with a fresh token)
Path C: workspace registry — For co-located installs where the UI writes creds to the shared ui-config volume. The entrypoint polls /config/workspaces.json + /config/secrets.json every 5 seconds until creds appear. This lets the gateway start before the user has completed browser onboarding — it will wait indefinitely, logging a status line every 30 seconds. On every subsequent boot, the pinned slug from .gateway-slug (if present) is restored into GATEWAY_ID so the gateway always registers as the same row in Supabase.

Step 0b — Restore from backup (conditional)

If the $OPENCLAW_HOME/agents directory does not exist (indicating a fresh boot or recreated container) and Supabase credentials are available:
  1. Locates gateway_backup.py (from /opt/yourhq/daemons/ or the entrypoint’s sibling daemons/ directory)
  2. Runs gateway_backup.py restore to check Supabase Storage for previous backups
  3. Downloads and extracts the newest available backup (falls back to older backups if the newest is corrupted)
  4. Restores auth tokens, agent configs, secrets, and Telegram pairing data
  5. If a VNC password is found in the restored backup ($OPENCLAW_HOME/.vnc-password), exports it as VNC_PASSWORD so the VNC setup step reuses it instead of generating a random one
If the agents directory already exists (returning container with existing state), this step is skipped entirely. Retention: 3 backups per gateway, 7-day max age. The backup excludes sessions and codex-home directories.

Step 1 — Seed git repo (first boot only)

If $OPENCLAW_HOME/repo.git does not exist:
  1. Creates a bare git repo with default as the HEAD branch
  2. Seeds it with agent template branches:
    • From TEMPLATES_SOURCE (if set, format git+<url>)
    • Otherwise from /opt/templates bundled in the image
  3. Each template directory becomes a git branch: default or template/<name>
This step is skipped on every subsequent boot.

Step 2 — Attach git remote (conditional)

If GIT_REMOTE_URL is set (or synthesized from GITHUB_TOKEN + GITHUB_REPO_OWNER + GITHUB_REPO_NAME):
  1. Adds/updates the origin remote on the bare repo
  2. Installs a post-commit hook that async-pushes every commit to origin
  3. Fetches from origin to fast-forward any branches that moved while the gateway was down
This step runs on every boot so the remote URL stays current (tokens rotate).

Step 3 — OpenClaw onboard (first boot only)

If $OPENCLAW_HOME/openclaw.json does not exist, runs:
Creates openclaw.json with baseline config. If it exits non-zero, the entrypoint logs a warning and continues — the next boot will retry.

Step 4 — Patch openclaw.json

Every boot (idempotent via //= defaults in jq):
  • Sets tools.profile = "full"
  • Sets Chrome executable and profile paths
  • Enables Telegram channel with pairing DM policy
  • Enables the hq-bootstrap plugin and adds its path to plugins.load.paths

Step 5 — Install hq-bootstrap plugin

Copies *.json and *.ts files from /opt/openclaw-plugins/hq-bootstrap into $OPENCLAW_HOME/plugins/hq-bootstrap/. Runs every boot — safe to repeat.

Step 6 — VNC password

If $HOME/.vnc/passwd does not exist, generates a VNC password:
  • Uses VNC_PASSWORD env if set (this is automatically populated from a backup restore in Step 0b, so restored gateways keep their original VNC password)
  • Otherwise generates a random 12-character password
  • Saves the vncpasswd-encoded hash to $HOME/.vnc/passwd
  • Saves plaintext to $OPENCLAW_HOME/.vnc-password (readable inside the gateway-state volume)

Step 7 — Start Xtigervnc + XFCE

Starts the virtual display and desktop:
  1. Removes stale X lock files (/tmp/.X1-lock, /tmp/.X11-unix/X1) from a crashed prior run
  2. Starts Xtigervnc on :1 — combined X server + VNC server (RFB on port 5901, localhost-only)
  3. Starts autocutsel twice (CLIPBOARD → PRIMARY sync for noVNC clipboard passthrough)
  4. Starts dbus-daemon session bus with a deterministic socket path
  5. Starts startxfce4
The D-Bus socket is started explicitly (not via dbus-launch) because dbus-launch double-forks in a way that leaves XFCE components unable to find the bus address in container environments.

Step 8 — Start websockify (noVNC)

If NOVNC_BIND != "off", starts:
Inside the container websockify always binds 0.0.0.0:6901. Port 6901 is not mapped to the host — the UI proxies noVNC through its own /api/novnc route over Docker’s internal network (gateway:6901). Set NOVNC_BIND=off to skip noVNC entirely (e.g. on a headless host where you never need remote desktop).

Step 9 — Register in Supabase

If Supabase creds are available, upserts a row in the gateways table with:
  • slug = GATEWAY_ID
  • label = GATEWAY_LABEL
  • status = ready
  • last_seen_at = now
  • meta.reachable_urls = URLs built from HOST_REACHABLE_URL + port config
  • meta.networking_mode = NETWORKING_MODE
  • tenant_id = TENANT_ID
Conflict key is (tenant_id, slug), so re-runs update the existing row. Also resolves GATEWAY_DB_ID (the row’s UUID) and exports it so the hq-bootstrap plugin can tag agent_usage rows with the gateway that ran them.

Step 10 — Start files-API (conditional)

If GATEWAY_AUTH_TOKEN is available (from env or /config/gateway-auth-token) AND FILES_API_BIND != "off":
Serves the agent worktrees to the HQ UI file browser over HTTP with bearer-token auth. If no token is available, the step is skipped silently — the token file is created by the UI the first time someone opens an agent’s Files tab. The next gateway restart picks it up.

Step 11 — Clear Chrome Singleton locks

Sweeps $HOME/.openclaw/browser/ for Singleton* files left behind by a crashed browser session. Without this, Chrome aborts launch on next boot reporting it’s already running.

Step 12 — Source gateway secrets

If the secrets_sync daemon (running in the runner container) has written gateway-level secrets to disk, the entrypoint sources them into the environment before starting OpenClaw. This makes gateway-scoped credentials (e.g. shared API keys) available to the main process and all agents. The secrets .env files are generated by secrets_sync.py, which runs inside the runner container on the shared gateway-state volume. The file at ~/.openclaw/secrets/gateway.env contains gateway-scoped keys; per-agent files at ~/.openclaw/secrets/agents/<slug>.env contain merged (gateway + agent-specific) keys. All files are chmod 0600.

Step 13 — Exec OpenClaw gateway

Replaces the entrypoint shell process with the OpenClaw v2026.6.6 gateway as the container’s foreground process. All X11/XDG/DBUS env vars are exported first so OpenClaw’s browser launcher can find the Xtigervnc display.

State directory layout

$OPENCLAW_HOME defaults to ~/.openclaw and is persisted in the gateway-state Docker volume. $HOME/.vnc/passwd is the vncpasswd-encoded password hash read by Xtigervnc.