Skip to content

Self-Hosting Glassy

Self-hosting runs Glassy on your own machine as a single-user personal appliance. This is the recommended path if you want live Obsidian vault sync, Ollama, Agent Gateway, or full data sovereignty — all features that require the server to reach 127.0.0.1 on your machine.

The cloud-hosted Glassy (at app.glassy.fyi) cannot reach your localhost. Self-hosting solves it by running the server where those services live.

Quick start

Self-hosting requires an active Glassy membership (Clear or Pro). Sign up at clear.glassy.fyi or glassy.today before continuing. The appliance starts and creates your admin account immediately; membership verification runs every 30 days. If verification fails, the app enters a fail-closed state until restored.

git clone https://github.com/0Reliance/glassy-selfhost.git
cd glassy-selfhost
cp .env.example .env
# Edit .env — fill in four required fields:
#   GLASSY_MEMBER_EMAIL=your@glassy-account-email
#   GLASSY_SELFHOST_TOKEN=<pairing token from Settings → Self-hosting on app.glassy.fyi>
#     # The token prevents anyone who merely knows your email from
#     # spinning up an unlocked instance using your membership.
#   JWT_SECRET=$(openssl rand -hex 32)
#   API_KEY_ENCRYPTION_KEY=$(openssl rand -hex 32)
docker compose up -d

On first boot the appliance verifies your membership against the cloud, creates your local account using your membership email, and prints the initial password once:

docker compose logs glassy | grep -A2 "Default admin created"

Alternatively, the initial password is written to a file inside the container that survives recreation and is deleted after your first password change:

docker exec glassy cat /app/data/.initial_admin_password

Sign in at http://localhost:3000 with your membership email and that password. The password change is forced on first login — you are immediately prompted to set a permanent password before you can use the workspace. Registration is permanently disabled — this is a single-owner personal appliance. All premium features are unlocked automatically.

How your account works (and what the cloud sees)

The self-hosted appliance and the cloud are two separate systems that share only one thing: your email address. No passwords, tokens, or note content ever leave your machine.

At boot (every 30 days): The appliance sends your email and a pairing token you control — and nothing else (no password, no note content, no session data) — to app.glassy.fyi. The cloud checks whether that email has an active membership and replies with a simple yes/no. No password is ever exchanged. The result is cached locally for 30 days, so the appliance works offline within that window.

First boot only: If no account exists yet, the appliance creates a local admin account using your membership email. A random password is generated, printed to the logs once, and stored only as a bcrypt hash — the plaintext is never saved. Sign in with it, then set a permanent password — the password change is forced on first login before you can use the workspace.

Every login after that: 100% local. The appliance authenticates you against its own SQLite database using a JWT signed with your JWT_SECRET. No cloud call happens during login — the cloud only confirmed your membership at boot, and it only ever saw your email and a pairing token you control.

Your email + pairing token → Cloud: "membership active?" → Yes (cached 30 days)
First boot → Create local admin (random password, printed once)
Every login → Local JWT auth → No network, no phone-home

What works differently from cloud

Capability Cloud (app.glassy.fyi) Self-hosted
Notes, AI, capture, companion Yes Yes
Live Obsidian vault sync No (server cannot reach your localhost) Yes
Ollama local AI No Yes
Agent Gateway (OpenClaw, Hermes) No (requires localhost) Yes
MCP server + Second Brain No Yes
All premium features unlocked Pro plan required Yes — owner auto-unlocked
Registration Open / admin-controlled Permanently disabled
Commerce / upgrade funnel Yes Not present
Transactional email Yes Disabled (nothing leaves the machine)
Data location Cloud VM Your machine (glassy-data volume)

Configuration

Required

Variable Description
GLASSY_MEMBER_EMAIL Your Clear or Pro membership email. The appliance verifies this against the cloud on first boot (cached 30 days).
GLASSY_SELFHOST_TOKEN Required. Pairing token from Settings → Self-hosting on app.glassy.fyi. Prevents unauthorized use of your membership by someone who merely knows your email.
JWT_SECRET Session token signing key. Generate with openssl rand -hex 32.
API_KEY_ENCRYPTION_KEY Encrypts stored API keys. Generate with openssl rand -hex 32.

Set in compose (no action needed)

Variable Default Description
INSTANCE_ID self_hosted Activates single-user gating — do not change.
DEPLOYMENT_LOCALITY local Tells the app it's running locally. Hides the cloud-limitation banner in Obsidian settings.
OBSIDIAN_HOST_OVERRIDE host.docker.internal Rewrites 127.0.0.1 to the Docker host gateway so the containerized server can reach Obsidian on the host.
OBSIDIAN_NETWORK_ALLOWLIST Additional comma-separated hostnames/IPs allowed for Obsidian connections. Use when Obsidian is on a LAN IP, Tailscale node, or WSL2 bridge address not covered by the defaults. Self-hosted only.
ENABLE_MCP_SERVER true Enables MCP server and Second Brain features.
ENABLE_AGENT_GATEWAY true Enables Agent Gateway (OpenClaw / Hermes).

Optional

Variable Default Description
GLASSY_TAG latest Image tag to pull from GHCR. Pin a version for reproducibility.
APP_PORT 3000 Host port Glassy listens on. Change if port 3000 is in use — then update APP_URL + CORS_ORIGINS to match.
APP_URL http://localhost:3000 Base URL. Set this if you access via Tailscale or a domain.
CORS_ORIGINS http://localhost:3000 Must include every origin you use (Tailscale hostname, LAN IP, domain).
OLLAMA_BASE_URL http://host.docker.internal:11434 Ollama on the host (reachable via Docker host gateway). Use http://ollama:11434 with the bundled sidecar overlay.
OLLAMA_MODEL llama3.2 Default Ollama model when none is selected in-app.
BACKUP_ENCRYPTION_KEY Encrypt on-demand backups (AES-256-GCM). Generate with openssl rand -hex 32; store the key off-machine.
CLUSTER_WORKERS min(2, CPUs−1) Worker processes. Raise on a multi-core host; set to 1 on a small box.

Cloud AI keys (GEMINI_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY) are not read from .env on the appliance. Add your own keys in-app at Settings → API Keys (BYOK) — they are stored encrypted per-account in the database.

Local AI (Ollama)

Ollama on the host is auto-detected via host.docker.internal:11434 — no config needed if Ollama is already running. To use the bundled Ollama sidecar (nothing to install on the host):

docker compose -f docker-compose.yml -f docker-compose.ollama.yml up -d
docker compose -f docker-compose.yml -f docker-compose.ollama.yml \
  exec ollama ollama pull llama3.2

The sidecar supports NVIDIA GPUs — uncomment the deploy block in docker-compose.ollama.yml after installing the NVIDIA Container Toolkit.

Obsidian Vault Sync

Glassy can connect to your Obsidian vault via the Local REST API plugin. There are three connection methods:

On Windows with Docker Desktop (WSL2), the container cannot reach Obsidian on the Windows host's 127.0.0.1. The Glassy Companion browser extension solves this by proxying Obsidian requests from the server through your browser, which runs on the same machine as Obsidian.

Requires extension v2.14.0+ — earlier versions lacked localhost host permissions for the self-host Glassy server URL, silently blocking the SSE connection. v2.14.0 broadens optional_host_permissions to cover any localhost port.

Setup:

  1. Install the Obsidian Local REST API plugin (v4.0+) in Obsidian. Copy the API key.
  2. Install the Glassy Companion extension (Chrome or Firefox) — v2.14.0 or later.
  3. Sign in to the extension with your Glassy account (same email as your self-hosted appliance). Set the extension's Server URL to your self-host Glassy address (e.g. http://localhost:3000).
  4. Open the extension popup → Settings → Obsidian Bridge:
  5. Set Obsidian URL to http://127.0.0.1:27123 (HTTP avoids self-signed cert issues; HTTPS is https://127.0.0.1:27124).
  6. Paste the API Key from the Obsidian plugin.
  7. Toggle Obsidian Bridge on. Chrome will prompt for permission to access localhost — click Allow. This grants the extension host permission to reach both the Obsidian URL and the Glassy server URL (both are localhost). If you deny it, the popup shows a warning banner and the bridge won't connect.
  8. Click Test Connection — this tests the FULL bridge loop (server → extension → Obsidian), not just extension→Obsidian. A green result confirms both legs work.
  9. Click Save.
  10. In the self-hosted web app (Settings → Obsidian), you should see "✓ Extension bridge active — Obsidian connected." The URL/token fields are hidden on self-hosted because the extension manages them.

How it works: The extension maintains a persistent SSE connection to the Glassy server (via the offscreen document, which Chrome does not evict). When the server needs Obsidian data (AI context, vault browsing, search), it pushes a request to the extension, which calls Obsidian on 127.0.0.1:27124 directly and returns the result. The extension holds the API key locally — it is never sent to the server.

:::note[Server v2.35.0-beta.9+ recommended] beta.9 fixes a server-side SSE auth bug where the one-time ticket was consumed twice (once by each of two overlapping routers), causing 401 Invalid or expired SSE ticket in logs. The extension masked it by silently falling back to the less-secure ?token=<JWT> URL form — so the bridge still worked, but your JWT was leaking into proxy/access logs. Update the server image to beta.9+. No extension or Obsidian configuration change is required. :::

Verify the connection: curl -H "Authorization: Bearer <jwt>" http://localhost:3000/api/ext/obsidian-bridge/status should return {"connected":true,...}.

:::tip The extension's Test Connection button tests the full bridge loop. If it shows green with "plugin v4.x", both the SSE bridge to the server AND the extension→Obsidian fetch are working. If it shows green but warns "SSE bridge to server is not connected", toggle the bridge off and on to re-establish the SSE connection. :::

See the Obsidian integration guide for full details.

2. Direct Server → localhost (Linux/macOS)

On native Linux/macOS or Docker on Linux, the server reaches Obsidian directly via host.docker.internal:27124. This works out of the box with the default OBSIDIAN_HOST_OVERRIDE=host.docker.internal.

3. Network Allowlist (split-machine setups)

If Obsidian runs on a different machine than Glassy, set OBSIDIAN_NETWORK_ALLOWLIST to the Obsidian host's IP or hostname:

OBSIDIAN_NETWORK_ALLOWLIST=192.168.1.10,glassy.tail-net.ts.net

You'll also need to configure the Obsidian plugin to bind to 0.0.0.0 instead of 127.0.0.1, and use http://<obsidian-host-ip>:27123 as your Obsidian URL in Glassy settings.

:::warning Binding the Obsidian plugin to 0.0.0.0 exposes it to your entire network. Only do this on a trusted network or behind a firewall. The browser extension bridge (option 1) is safer because it doesn't expose Obsidian to the network at all. :::

Multi-device access

To use Glassy from another device (phone, laptop), set APP_URL and CORS_ORIGINS to your network URL and choose one of:

  • Tailscale (recommended) — WireGuard mesh VPN, no port forwarding, no public exposure. APP_URL=http://<machine>.tail-net.ts.net:3000
  • Cloudflare Tunnel — public HTTPS URL through Cloudflare's edge. APP_URL=https://glassy.example.com
  • Caddy HTTPS overlay — if you own a domain, docker compose -f docker-compose.yml -f docker-compose.https.yml up -d gives auto-renewing Let's Encrypt TLS.

See the full instructions in deploy/selfhost/README.md.

Upgrading

docker compose pull
docker compose up -d

Database migrations apply automatically on start. To auto-update daily without manual intervention, use the Watchtower overlay:

docker compose -f docker-compose.yml -f docker-compose.watchtower.yml up -d

Backups

Glassy takes an automatic daily backup at 02:00 into /app/data/backups (inside the glassy-data volume, ~7 days retained). No setup required.

See Backups and Data Management for encryption, CLI backup, restore, and volume snapshot instructions.

Security

  • Change the admin password immediately after first login.
  • Private mode is enforced automatically — anonymous visitors are always redirected to login.
  • Firewall — if exposing beyond localhost, restrict access with UFW or equivalent.
  • Updates — pull regularly to get security patches.

Image source

Images are published to ghcr.io/0reliance/glassy-dash by GitHub Actions on every git tag push. The :latest tag always points to the most recent release:

GLASSY_TAG=v2.35.0-beta.7 docker compose up -d

Troubleshooting

Port 3000 already in use

Set APP_PORT=3001 in .env and update APP_URL + CORS_ORIGINS to match:

APP_PORT=3001
APP_URL=http://localhost:3001
CORS_ORIGINS=http://localhost:3001

Then docker compose up -d. The container always listens on 8080 internally; APP_PORT only changes the host-side mapping.

Checking container health

curl http://localhost:3000/ready
# Returns JSON: {"status":"ready",...} when healthy

If you get HTML instead of JSON, the container may still be starting up — wait a few seconds and retry.

Debugging from inside the container

The runtime image includes curl for network diagnostics:

docker compose exec glassy curl -s http://host.docker.internal:11434/api/tags | head -c 200

Container won't start

docker compose logs glassy

Common causes: missing JWT_SECRET / API_KEY_ENCRYPTION_KEY, or membership verification failed (check for lines in the log).

For the full troubleshooting guide, see SELF_HOSTED_DEPLOYMENT.md.