Skip to content

Latest commit

 

History

History

remote

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 

README.md

Remote Quickstart (Obsidian Sync)

Run Vault Cortex on a VPS with Obsidian Sync for remote access from any device. Your vault stays in sync; MCP tools work from Claude Desktop, Claude Code, claude.ai, or any MCP client — anywhere. (Vault on this machine and no Obsidian Sync? Use the local quickstart instead.)

Everything runs in one container: the vault-cortex:remote image bundles the Obsidian Sync process and the MCP server, supervised together so both restart automatically (how the container is put together →). The CLI setup below manages that container with plain docker run; the manual setup offers a Docker Compose file that declares the same configuration. Both produce an identical container — restart policy, log rotation, and health check included — and Podman or any OCI-compatible container runtime works in place of Docker.

Contents — Prerequisites · Setup · HTTPS access · Sign in · Connect · Verify · Monitoring · Updating · Restart · Stop · More than one vault · Memory · File Tools · Read-only · Deleting notes · Daily Notes · Config · Hardening · Troubleshooting

Prerequisites

  • A VPS or cloud server with Docker installed (Ubuntu/Debian: curl -fsSL https://get.docker.com | sh)
  • An Obsidian Sync subscription
  • A domain name or public IP address for your server

Setup

npx vault-cortex@latest init --mode remote

The CLI walks through your public URL, Obsidian Sync token (it can sign in to your Obsidian account and capture the token for you), and auth config, then starts the server and prints the connection details for your MCP client (CLI reference →).

Don't have Node.js installed?

The CLI needs Node.js >= 22.12 (the server itself runs in Docker). On Ubuntu/Debian:

curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt-get install -y nodejs
Manual setup (no Node.js needed)

1. SSH into your server and create a working directory:

mkdir -p /opt/vault-cortex && cd /opt/vault-cortex

2. Download the quickstart files:

curl -O https://raw.githubusercontent.com/aliasunder/vault-cortex/main/deploy/remote/docker-compose.yml
curl -O https://raw.githubusercontent.com/aliasunder/vault-cortex/main/deploy/remote/.env.example

Or clone the repo and cd deploy/remote.

3. Generate your Obsidian Sync auth token (one-time):

If you have Node.js >= 22.12 on this machine, the CLI signs in to your Obsidian account and captures the token:

npx vault-cortex@latest get-sync-token

Otherwise, run the helper in a throwaway container — it prints the token and exits; nothing stays running:

docker run --rm -it --entrypoint get-sync-token \
  ghcr.io/aliasunder/vault-cortex:remote

Or leave OBSIDIAN_AUTH_TOKEN blank in .env and sign in through the /setup page after starting the container — you sign in with Obsidian directly; the server keeps only the Sync token from that sign-in (Sign in to Obsidian Sync walks through it). Configure HTTPS access first, since the sign-in form sends your Obsidian password to the server.

4. Create your .env file:

cp .env.example .env

5. Fill in the required values:

Variable Value
MCP_AUTH_TOKEN Generate with openssl rand -hex 32
PUBLIC_URL Your server's public base URL, e.g. https://vault.example.com — no /mcp at the end; it's appended automatically, and clients connect at <PUBLIC_URL>/mcp (see HTTPS access below)
OBSIDIAN_AUTH_TOKEN Output from step 3. Or leave empty to sign in through the /setup page after starting. To sign in with a different account, set a new token and re-create the container — it always overrides the saved login on the volume
VAULT_NAME Your exact Obsidian vault name (case-sensitive)

6. Start the server:

docker compose up -d

No Compose? The docker run (no Compose) block below starts the same server directly.

If you set OBSIDIAN_AUTH_TOKEN, first start pulls the image, logs in to Obsidian Sync, and syncs your vault from Obsidian's servers — 30–120 seconds, depending on vault size. The MCP server waits for that sync to finish before starting, so it begins with your full vault, memory files included. If you left the token empty, the container starts in setup mode and serves the /setup sign-in page instead (Sign in to Obsidian Sync); after you sign in there it restarts and performs this first sync.

If the sync can't complete on a brand-new setup, the server stops instead of starting with an incomplete vault, and Docker retries automatically. On later boots the server starts with the vault it already has while sync catches up in the background.

docker run (no Compose): The command the CLI runs for you, spelled out — for when you have neither Node.js nor Compose, invoke a different runtime directly (swap docker for podman or nerdctl), or want the raw invocation for your own automation. The same .env file works, and the flags mirror the Compose file, volume names included, so your data carries over if you ever switch between methods:

docker run -d --name vault-cortex \
  --hostname vault-cortex \
  --env-file .env \
  -v vault-cortex_vault_data:/vault \
  -v vault-cortex_mcp_data:/data \
  -v vault-cortex_obsidian_config:/home/obsidian/.config \
  -p 8000:8000 \
  --restart unless-stopped \
  --health-cmd "node -e \"fetch('http://127.0.0.1:8000/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\"" \
  --health-interval 15s --health-timeout 5s --health-retries 5 \
  --health-start-period 180s \
  --log-driver json-file --log-opt max-size=10m --log-opt max-file=3 \
  ghcr.io/aliasunder/vault-cortex:remote
Single persistent volume (Railway, Render)

Container hosting platforms that allow one volume per service can't attach the three mounts the Compose file and docker run command use. Mount the one volume anywhere and set STORAGE_ROOT to the same path — the container keeps the vault, the search index, Obsidian Sync state, and log files under that directory (how the paths are laid out →).

The CLI's init always creates the three-volume layout, so single-volume mode is set up with docker run or through a hosting platform. The same .env file works; the command sets LOG_DIR explicitly because the .env.example default, /data/logs, falls outside the volume. On a plain Docker host the complete command is:

docker run -d --name vault-cortex \
  --hostname vault-cortex \
  --env-file .env \
  -e STORAGE_ROOT=/persist \
  -e LOG_DIR=/persist/data/logs \
  -v vault-cortex_storage:/persist \
  -p 8000:8000 \
  --restart unless-stopped \
  --health-cmd "node -e \"fetch('http://127.0.0.1:8000/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\"" \
  --health-interval 15s --health-timeout 5s --health-retries 5 \
  --health-start-period 180s \
  --log-driver json-file --log-opt max-size=10m --log-opt max-file=3 \
  ghcr.io/aliasunder/vault-cortex:remote

Render and Railway have ready-made setups that use this mode: deploy/render/ and deploy/railway/.

On another hosting platform, set the same variables in the service's environment settings. Restarts, health checks, and log rotation come from the platform. Three things to know:

  • PUBLIC_URL — on Render and Railway it can stay unset; the container fills it in from the platform's own address variable (RENDER_EXTERNAL_URL or RAILWAY_PUBLIC_DOMAIN). Elsewhere, set it.
  • LOG_DIR — left unset, the container keeps 90 days of log files on the volume; the platform's own log viewer usually keeps only the last 7 days on hobby plans (Railway and Render both do). Set LOG_DIR=none to keep logs in the platform viewer only.
  • PID 1 — the image's own entrypoint must be the container's first process. If the platform adds its own init process or lets you override the start command, leave the override empty; otherwise the container exits at startup.

HTTPS access

MCP clients need to reach your server over HTTPS. Pick one option — they can also be combined (e.g. API Gateway adding an independent auth check in front of a Cloudflare Tunnel that keeps every port closed):

Option Cost Domain needed Ports open on your server
Cloudflare Tunnel Free Yes (on Cloudflare) None
API Gateway Free tier covers personal use No 8000
Reverse proxy Free (Caddy/nginx) Yes 443
Direct access Free No 8000 (testing only)

Just trying it out? Start with direct access to confirm everything works, then come back and set up HTTPS.

Cloudflare Tunnel (no open ports — free)

Lets clients reach your server without opening any ports — the tunnel is an encrypted connection your server makes outward to Cloudflare, and traffic flows back through it instead of arriving directly. Requires a Cloudflare account (free) with a domain using Cloudflare's nameservers.

Install cloudflared on your server, create a tunnel in the Cloudflare dashboard (Zero Trust → Networks → Tunnels), and point it at http://localhost:8000. Set PUBLIC_URL to the tunnel URL (e.g. https://vault.yourdomain.com) and TRUST_PROXY_HOPS=1 in .env — with a proxy in front, that's what lets OAuth rate limiting tell clients apart (otherwise they all share the tunnel's budget).

Once the tunnel is working, close port 8000 on your server's firewall — all traffic flows through the tunnel, so the direct port is no longer needed. See Hardening below.

API Gateway (AWS — no domain needed)

AWS API Gateway gives you an HTTPS address immediately — no domain, no certificate to manage (it handles TLS in front of your server). You get an HTTPS URL like:

https://<id>.execute-api.<region>.amazonaws.com

HTTP API pricing is $1.00 USD per million requests with a free tier of 1M requests/month for 12 months — effectively free for personal use.

Create an HTTP API in API Gateway with a route that proxies to http://<your-server-ip>:8000/{proxy+}. Set PUBLIC_URL to the API Gateway URL, TRUST_FORWARDED_HOPS=1, and TRUST_PROXY_HOPS=1 in .env — the gateway reports clients through the RFC 7239 Forwarded header, and this tells the server to read it for OAuth rate limiting and request logs. See the project's full cloud deployment for the SST IaC approach, which adds a Lambda authorizer for an extra auth layer.

Need a VPS? Any provider works — AWS Lightsail, DigitalOcean, Hetzner, etc. A 2 GiB instance handles semantic search fine for a typical vault; 4 GiB adds headroom for concurrent search and larger vaults. Add $4 USD/mo for Obsidian Sync. For a fully automated AWS setup, Vault Cortex also includes an SST IaC deployment that provisions Lightsail, API Gateway, and a Lambda authorizer in one command.

Reverse proxy (requires a domain)

Use Caddy or nginx with a domain and TLS certificate. Caddy handles TLS automatically. Port 443 stays open on your server's firewall — the proxy receives traffic directly, so your server stays reachable from the internet, whereas a tunnel needs no inbound ports at all once port 8000 is closed:

vault.yourdomain.com {
    reverse_proxy localhost:8000
}

Set PUBLIC_URL to https://vault.yourdomain.com and TRUST_PROXY_HOPS=1 in .env — with a proxy in front, that's what lets OAuth rate limiting tell clients apart (otherwise they all share the proxy's budget).

Direct access (testing only)

Access the server directly at http://<your-ip>:8000. No TLS — suitable for testing on a private network, not for production. Set PUBLIC_URL to http://<your-ip>:8000.

Claude's "Add custom connector" dialog (claude.ai and Claude Desktop) only accepts https URLs, so with an http PUBLIC_URL connect via Claude Code (claude mcp add --transport http — see below) or the mcp-remote stdio bridge with its --allow-http flag.

Note: Clients connect at <PUBLIC_URL>/mcp, but PUBLIC_URL itself is the base URL only — the server serves the endpoint at /mcp and appends it for you, so a PUBLIC_URL ending in /mcp becomes …/mcp/mcp and won't connect.

The base must be exactly the one clients reach the server through (scheme and host, with no path — the server's routes live at the root) — OAuth discovery metadata uses this URL, so a mismatch causes authentication failures.

Sign in to Obsidian Sync

Skip this section if you set OBSIDIAN_AUTH_TOKEN — the container signed in with it at first start.

If you left it empty, the container is running in setup mode and serves a sign-in page until you connect it to Obsidian Sync. Do this over the HTTPS access you just set up — the sign-in form sends your Obsidian password to the server:

The Connect Obsidian Sync setup page with MCP token, email, and password fields

  1. Open the setup page: visit your PUBLIC_URL in a browser — the server sends your browser straight to the sign-in page. (Adding /setup to the address goes to the same place.)

  2. Paste your MCP_AUTH_TOKEN — the value you set in .env — in the token field.

  3. Enter your Obsidian account email and password. If you use two-factor authentication, the page asks for the code on the next step.

    Two-factor code page with a single code field and Verify button

  4. Wait for "Your vault is ready." The server signs in, validates your vault settings, writes the token, and restarts to download your vault and build the search index. The page follows the progress automatically. A vault of a few thousand notes takes two to three minutes; a large vault can take longer.

Once the page shows your MCP URL, the server is live and ready to connect.

Connect your MCP client

OAuth (Claude Desktop, Claude Code, Claude Mobile, claude.ai)

Add a remote MCP server with URL <PUBLIC_URL>/mcp. Leave OAuth Client ID and Secret empty — dynamic registration handles it. A consent page opens in your browser; enter your MCP_AUTH_TOKEN to approve. The client receives a JWT access token (6h) with automatic refresh (60-day sliding window).

Claude Code also accepts http URLs directly:

claude mcp add --scope user --transport http vault-cortex <PUBLIC_URL>/mcp

--scope user registers the server for every project; omit it to scope it to the current directory only.

Static bearer token (CLI tools, curl)

curl -H "Authorization: Bearer <your-MCP_AUTH_TOKEN>" <PUBLIC_URL>/mcp

Verify

Run these on your server:

# Health check (no auth):
curl http://localhost:8000/healthz
# → {"ok":true}

# One log stream for both processes — MCP server lines are structured
# JSON; init-chain lines carry an [obsidian-sync] or [vault-cortex] prefix
# and the ongoing sync output is plain text:
npx vault-cortex@latest logs   # set up with the CLI (run from your init directory)
docker logs vault-cortex       # Compose or docker run
# Sync output only: docker logs vault-cortex 2>&1 | grep -v '^{'

Once HTTPS access is set up, the same health check works from any device: curl <PUBLIC_URL>/healthz.

Monitoring

# Follow the container logs (sync + MCP server) until ctrl-C:
npx vault-cortex@latest logs --follow   # set up with the CLI
docker logs -f vault-cortex             # Compose or docker run

# Container status — "healthy" tracks the MCP server's /healthz:
docker ps

Updating

Update with the same method you set up with — each one manages the container independently.

Set up with the CLI?

npx vault-cortex@latest upgrade

Run it from the same directory where you ran init — it pulls the new image and re-creates the container for you. Nothing is deleted — see upgrade in the CLI reference for what's preserved and the --dir flag.

Set up with Docker Compose? Compose does not pull new images on up, so pull explicitly:

docker compose pull && docker compose up -d

Set up with docker run (no Compose)? Do manually what upgrade does — pull the new image, remove the container, then re-run the docker run command from Setup:

docker pull ghcr.io/aliasunder/vault-cortex:remote
docker rm -f vault-cortex

Named volumes persist across updates — no re-sync, no device re-registration, and unchanged notes are not re-embedded.

Restart

On every boot, a catch-up sync brings the vault current before the server starts. The server then runs these startup tasks:

  1. Create memory template files if the memory folder doesn't exist (skipped when MEMORY_ENABLED=false or READONLY_MODE=true).
  2. Start the file watcher, which holds the changes it sees until step 4.
  3. Rebuild the search index, including any new memory template files.
  4. Apply the changes the watcher held, so search reflects notes added, edited or deleted while the server started.

Restarting the container re-runs this flow (useful when testing bootstrap behavior). The command is the same for every setup method; an install with its own INSTANCE_NAME uses that name in place of vault-cortex:

# Sync and the MCP server both restart; the startup steps re-run cleanly:
docker restart vault-cortex

Changed .env? A restart does not re-read .env — the container has to be re-created. Set up with the CLI? Run npx vault-cortex@latest restart — unlike docker restart, it re-creates the container, so your edits take effect (upgrade also applies .env edits and pulls the latest image on the way). Using Compose? Run docker compose up -d — it re-creates services whose configuration changed. Using docker run? Remove the container (docker rm -f vault-cortex) and re-run the docker run command from Setup.

The container also restarts automatically on crash (restart: unless-stopped policy), Docker daemon restart, or system reboot.

Stop

# Stop and remove the container (data persists in Docker volumes):
npx vault-cortex@latest down   # set up with the CLI
docker compose down            # Compose

# Stop and delete all volumes (vault re-syncs on next start; index rebuilds):
docker compose down -v                                                   # Compose
docker rm -f vault-cortex && docker volume rm vault-cortex_vault_data \
  vault-cortex_mcp_data vault-cortex_obsidian_config                     # docker run / CLI
docker rm -f vault-cortex && docker volume rm vault-cortex_storage       # single volume

# Stop without removing (any setup method; docker start resumes):
docker stop vault-cortex

Never delete just the vault volume while keeping obsidian_config (or, with a single volume, just the vault directory while keeping config). The container refuses to start to prevent the sync engine from pushing mass deletions to every connected device. Restore the vault, or delete everything together to start fresh.

Set up with the CLI? Start again any time with npx vault-cortex@latest start — your saved settings are reused (down · start in the CLI reference). Otherwise resume with docker compose up -d or docker start vault-cortex.

Run more than one vault

Each vault gets its own install, with its own folder, .env, container, volumes, port, Obsidian Sync device and connector. Give every install after the first its own INSTANCE_NAME before its first start. Docker uses that name for the install's container and volumes (<name>_vault_data, <name>_mcp_data and <name>_obsidian_config); the first install keeps the default, vault-cortex. A second vault must never use the first install's volumes: the container refuses to start rather than merge the two vaults (see Another vault's Sync setup).

Set up with the CLI? Start Docker, so init can check which names and ports are in use. Then, from the folder you ran init in the first time, run it again with a new --dir:

npx vault-cortex@latest init --mode remote --dir ./vault-cortex-work

When this server already has a vault-cortex container or volumes, init asks for the new install's name and port, for example work and 8001. It suggests the first port no other container uses. Then pass the same --dir to every command for that install:

npx vault-cortex@latest upgrade --dir ./vault-cortex-work
npx vault-cortex@latest logs --dir ./vault-cortex-work

Set up with Docker Compose? Set up a new folder as in Manual setup under Setup, with its own MCP_AUTH_TOKEN, VAULT_NAME and PUBLIC_URL. In its .env, also set a new INSTANCE_NAME (see Configuration) and a free PORT. Run each install's docker compose commands from its own folder.

Set up with docker run (no Compose)? Set up a new folder with its own .env as above, but keep PORT at 8000. With docker run, PORT is the port the server listens on inside the container, and the command's -p mapping and health check both expect 8000. Then run the command from Setup with vault-cortex replaced by the new name in --name, --hostname and the volume names, and with the new host port on the left of -p (-p 8001:8000).

For each install:

  • Give it its own address. In your HTTPS access setup, route a separate address to its port, and use that address as its PUBLIC_URL:
    • Cloudflare Tunnel: add a public hostname that points at http://localhost:8001.
    • API Gateway: create another HTTP API that proxies to http://<your-server-ip>:8001/{proxy+}.
    • Reverse proxy: add a site block for another domain with reverse_proxy localhost:8001.
    • Direct access: http://<your-ip>:8001.
  • Connect it as its own server in your MCP client, under its own name. For Claude Code: claude mcp add --scope user --transport http work <PUBLIC_URL>/mcp.
  • Update and restart it on its own:
    • With the CLI: npx vault-cortex@latest upgrade --dir ./vault-cortex-work to update, or npx vault-cortex@latest restart --dir ./vault-cortex-work to restart.
    • With Compose: docker compose pull && docker compose up -d in its folder.
    • With docker run: the steps under Updating, using its name.
  • Use its name in docker commands. The commands in this guide that name the container or its volumes (docker logs vault-cortex, docker restart vault-cortex, docker rm -f vault-cortex, vault-cortex_vault_data) take the install's name instead, for example docker logs work.

Another vault's Sync setup

The container keeps an Obsidian Sync setup in its obsidian_config volume (the config folder with a single volume): the record of which vault in your account it syncs. When that setup is for a different vault than VAULT_NAME, the container stops before it syncs or changes anything, because starting would merge the two vaults' notes through Obsidian Sync. docker logs then shows:

[obsidian-sync] ERROR: This install is set up to sync a different vault than VAULT_NAME.
[obsidian-sync] VAULT_NAME is 'Work' (<vault ID>), but its Sync setup is for 'Personal' (<vault ID>).

It usually means VAULT_NAME was changed, or a second install is using the first one's volumes. Fix it one of these ways:

  • VAULT_NAME is a mistake: set it back and re-create the container. The Changed .env? note under Restart gives the command for each setup method.

  • You want to sync both vaults: set this install's VAULT_NAME back, and give the second vault its own install, as described above. On Render or Railway, deploy it as a separate service.

  • You want this install to sync the new vault instead: delete all of its volumes together (the Stop and delete all volumes commands under Stop), then start it again. The container downloads the new vault like a first start, and the old vault stays in Obsidian Sync. Deleting the volumes also deletes the server's record of signed-in clients, so remove and re-add the server in each MCP client. Deleting only obsidian_config would upload the old vault's notes into the new one. On Render or Railway, follow the entry in that platform's guide.

  • You re-created the same vault in Obsidian, the second log line ends with "which is no longer in your account", and the notes on the server are that vault's notes: remove only the old setup folder the log names, then start the container:

    docker run --rm -v vault-cortex_obsidian_config:/home/obsidian/.config \
      --entrypoint rm ghcr.io/aliasunder/vault-cortex:remote \
      -r /home/obsidian/.config/obsidian-headless/sync/<old vault ID>

    With a single volume (STORAGE_ROOT), mount vault-cortex_storage:/persist instead, and remove the full path the log names, which starts with /persist/config.

A WARNING that the install "also holds Sync setups for other vaults" means the container started, because it uses the right vault, but the Sync client would use one of those setups if this vault's setup were removed. Remove each one's folder with the same command, using the vault ID from the warning.

An ERROR that "a Sync setup file here is not a valid setup" means the Sync client can't read the file the log names and would ignore it. That file may be another vault's setup, whose notes are in this install's vault. If the notes on the server are VAULT_NAME's notes, remove the folder that holds the file with the command above, using that folder's name in place of <old vault ID>. Otherwise delete all of the install's volumes, as in the fix for syncing the new vault instead.

An ERROR that "the Sync client could not read its own setup" means the Sync client's answer about which vault it syncs failed or named no vault, so the container stopped before syncing any notes. Restart it. If the error comes back and the lines just above it hold a message from the Sync client, that message says why.

Memory

The memory layer is enabled by default. Set MEMORY_ENABLED=false in your .env to disable it — memory tools are hidden, no files are created, and the server runs without it.

When enabled, the server creates a memory folder (default: About Me/) with starter template files on first startup, and agents grow it from there. See Memory in the main README for how the layer works, and templates/memory for the file format.

File Tools

File tools (vault_read_file, vault_list_files) are enabled by default. Set FILE_TOOLS_ENABLED=false in your .env to hide them — useful when Obsidian Sync has attachment syncing disabled and no files exist on disk. The server holds only the attachment types Obsidian Sync delivers, so if a CSV or other text file you expect is missing, check SYNC_FILE_TYPES under Configuration.

Read-only mode

Set READONLY_MODE=true in your .env to hide every tool that edits, creates, moves, or deletes notes. The memory folder is not auto-created. Connected clients can read and search but never edit.

Need finer control? DISABLED_TOOLS hides exactly the tools you name, comma-separated — e.g. DISABLED_TOOLS=vault_delete_note,vault_move_note keeps writes on but removes deleting and moving. Names match the Tool column in the README tools table. It only ever takes tools away: it can't bring back one that read-only mode or another setting has already hidden.

Deleting notes

Deletes are permanent, and they sync to your other devices like a delete made in Obsidian. To recover a deleted note, use Obsidian Sync's version history (Settings → Sync → Deleted files) — kept for 1 month on Sync Standard and 12 months on Sync Plus.

Daily notes

vault_get_daily_note finds your daily notes using the folder and date format set in Obsidian's Daily notes options. Obsidian stores those settings in .obsidian/daily-notes.json, and in remote mode the server only receives that file when settings syncing is turned on in two places:

  • Server side — SYNC_CONFIGS defaults to core-plugin-data,community-plugin-data,app: daily notes settings (Obsidian's Core plugin settings toggle), community plugin settings such as the Tasks plugin's format (Installed community plugins), and the new-note location (Main settings). It takes a comma-separated list of Obsidian's settings categories (app, appearance, appearance-data, hotkey, core-plugin, core-plugin-data, community-plugin, community-plugin-data); set SYNC_CONFIGS=none in .env to disable.
  • Desktop side — Obsidian Settings → Sync → Vault configuration sync, enabled per device. Obsidian only pushes categories you enable there.

Some community plugins keep API keys in their settings. Server tools never read .obsidian/, and the server itself opens only daily-notes.json, the Tasks plugin's settings, and app.json (for the new-note location, when the daily notes folder is blank) — other synced settings just sit in the vault's .obsidian/ folder.

If you turn on Core plugin settings syncing after the server is already running, daily-notes.json is picked up automatically once it lands; no restart needed.

If the folder box in Obsidian's Daily notes options is empty, Obsidian puts daily notes where Default location for new notes says, a setting stored in .obsidian/app.json. The server reads that file once Main settings is turned on under Vault configuration sync; until then it looks in the vault root. Same folder as current file also means the vault root, as it does in Obsidian for daily notes. If you can't sync Main settings, set DAILY_NOTES_FOLDER, as described below.

Daily notes in the vault root, or in the new-note folder because the folder box is empty, aren't protected from delete and move and can appear in orphan results; the main README's Daily notes section explains why.

If the settings can't sync — or you use the Periodic Notes plugin, whose settings the core config file doesn't track — set DAILY_NOTES_FOLDER (any vault-relative path: Journal, Planner/Daily, or / for the vault root) and DAILY_NOTES_FORMAT (same tokens as Obsidian's date format setting) in .env. You can set one or both — a set value always wins over the config file. A value you leave unset while daily-notes.json hasn't reached the server comes from the fallbacks: the Daily Notes folder and YYYY-MM-DD.

.env edits take effect when the container is re-created: set up with the CLI, run npx vault-cortex@latest restart; with Compose, docker compose up -d; with docker run, re-create the container as described in the Changed .env? note under Restart.

Configuration

Only MCP_AUTH_TOKEN, PUBLIC_URL, and VAULT_NAME are required. OBSIDIAN_AUTH_TOKEN can be filled via the /setup page when left blank. These optional settings are worth knowing about:

Setting Default What it does
TZ UTC Your IANA timezone (e.g. America/New_York) — affects daily note dates and timestamps
VAULT_PASSWORD — Set this if your vault has end-to-end encryption enabled
EMBEDDING_ENABLED true Set false to skip AI models (~45MB) and use keyword search only — saves memory on smaller instances
RERANK_MODE blended Set none to skip reranking for lower latency
MEMORY_ENABLED true Set false to disable the structured memory layer
FILE_TOOLS_ENABLED true Set false to hide file tools when Obsidian Sync has attachment syncing disabled
READONLY_MODE false Set true to hide every tool that changes the vault and skip memory folder auto-creation — read and search only
DISABLED_TOOLS none hidden Hide individual tools by name, comma-separated; names match the README tools table
SYNC_CONFIGS daily notes, plugins, and app Obsidian settings categories synced to the server (see Daily notes); none disables
INSTANCE_NAME vault-cortex Names this install's container, Compose project and volumes. Set it before the first start to run another vault on the same server (see Run more than one vault)
DEVICE_NAME vault-cortex The device name that labels this container's changes in Obsidian's sync log
SYNC_MODE bidirectional Sync direction: bidirectional, pull-only (edits made on the server stay on the server and are never uploaded), or mirror-remote (Obsidian Sync overwrites edits made on the server, so the server always matches your vault). An invalid value stops the container at boot.
CONFLICT_STRATEGY merge Obsidian Sync conflict resolution: merge integrates changes automatically; conflict writes a separate conflict file.
SYNC_EXCLUDED_FOLDERS (empty) Folders to leave out of sync, comma-separated — the same list as Obsidian's Sync → Excluded folders. Empty excludes nothing.
SYNC_FILE_TYPES (empty) Attachment types to sync, comma-separated — the same toggles as Obsidian's Sync → Selective sync. Valid values: image, audio, video, pdf, unsupported. Empty (default): image,audio,video,pdf. The text files Vault Cortex reads (CSV, JSON, TXT, XML, LOG, YAML) fall under unsupported, Obsidian Sync's name for its Sync all other types category; SVG syncs as image, and notes, canvases, and bases always sync. To read those text files, set it to image,audio,video,pdf,unsupported (the value replaces the default list, so keep the four defaults) and turn on Sync all other types in Obsidian's Sync settings on the device that has them. An invalid value is skipped with a warning in the logs, and the default types sync instead.
DAILY_NOTES_FOLDER from vault config Sets the daily notes folder (see Daily notes)
DAILY_NOTES_FORMAT from vault config Sets the daily note filename format (see Daily notes)
TRUST_PROXY_HOPS 0 Set 1 when a tunnel or reverse proxy fronts the server — OAuth rate limiting then buckets by the real client IP, not the proxy's
TRUST_FORWARDED_HOPS 0 How many trailing for= entries in the RFC 7239 Forwarded header belong to proxies you control. 0 ignores the header; 1 when the proxy in front writes it (e.g. AWS API Gateway); 2 when a CDN fronts that proxy and is the only way to reach it
STORAGE_ROOT — One directory for everything that must persist, for hosting platforms that allow a single volume (see Single persistent volume under Setup). Must not contain *, ?, or [.

All settings are documented in .env.example and in the Configuration section of the main README.

Hardening (recommended)

The setup above is authenticated — every request requires your token or an OAuth session. These optional measures add defense-in-depth:

  • Close port 8000 — once a tunnel or reverse proxy handles HTTPS, close direct access to port 8000, and to each other install's PORT, wherever you manage your server's firewall (provider panel, AWS security groups, infrastructure-as-code). All traffic then flows through the encrypted path.

  • Restrict SSH access — limit SSH to a VPN or trusted IPs instead of the open internet. Tailscale (free for personal use) creates a private network between your devices: install it on the VPS and your laptop, verify SSH works through the Tailscale hostname, then close port 22 in your firewall.

  • Add a second auth layer — the reference AWS deployment validates tokens once at the network edge (API Gateway + Lambda authorizer) and again at the server. The principle applies anywhere: an auth-aware proxy in front means a misconfigured server alone can't expose your vault.

These measures stack. Start with whichever is easiest for your setup — even one makes a meaningful difference.

Troubleshooting

"VAULT_NAME is not set" in docker logs, and the container stops. The container needs your vault name to know which vault to sync. This guide's docker-compose.yml refuses to start without it, but docker run and hosting-platform settings pages don't check. Add VAULT_NAME=<your exact Obsidian vault name, case-sensitive> to .env (or pass -e VAULT_NAME=...) and re-create the container, because docker start keeps the old settings. The Changed .env? note under Restart gives the command for each setup method.

"First sync failed with N file(s) still waiting to download" in docker logs, and the container stops. The container stopped on purpose. Some files in your Obsidian Sync vault haven't downloaded to the server yet, and starting two-way sync without them would delete them from your other devices. This usually happens right after you change SYNC_FILE_TYPES or SYNC_EXCLUDED_FOLDERS, when many files need to download at once.

  1. Wait for the restart policy to retry, or run docker start vault-cortex. If the failure was a dropped connection, the next start picks up where this one stopped.
  2. If it stops again with the same message, set SYNC_MODE=pull-only in .env and re-create the container, because docker start keeps the old settings. The Changed .env? note under Restart gives the command for each setup method. The server starts and keeps retrying the downloads, and pull-only never deletes anything from Obsidian Sync. Edits made on the server in the meantime stay on the server.
  3. Later, set SYNC_MODE=bidirectional and re-create the container again. If it stops with this message again, the downloads haven't finished yet, so switch back to pull-only and try again later.

If it still stops after several hours, one file is probably failing to download every time. In docker logs, the Downloading line just before Sync error: names it. Then do one of these:

  • Fix the file in Obsidian on another device.
  • Delete it in Obsidian on another device. The deletion reaches every device. Obsidian Sync's version history (Settings → Sync → Deleted files) keeps a deleted note for 1 month on Sync Standard and 12 months on Sync Plus, and a deleted attachment for two weeks on either plan.
  • Add its folder to SYNC_EXCLUDED_FOLDERS. That keeps every file in the folder off the server, not just the failing one.

N can be higher than the number of files you expect. It includes files that your SYNC_FILE_TYPES, SYNC_EXCLUDED_FOLDERS, or SYNC_CONFIGS settings leave out, and a sync that completes skips those.

"ob sync-config --mode '…' failed" in docker logs, and the container stops. SYNC_MODE isn't one of bidirectional, pull-only, or mirror-remote. Fix it in .env (or -e SYNC_MODE=...) and re-create the container, because docker start keeps the old settings. The Changed .env? note under Restart gives the command for each setup method.

"This install is set up to sync a different vault than VAULT_NAME" in docker logs, and the container stops. The container's volumes hold the Obsidian Sync setup for another vault, and starting would merge the two. Another vault's Sync setup gives the fixes.

"container name vault-cortex already in use" on start or upgrade. A container from a different management method is still running. The CLI (npx vault-cortex@latest upgrade) and Docker Compose (docker compose up -d) manage the container independently — stop the existing one first with docker rm -f vault-cortex, then retry with your preferred method. Setting up a second vault? Give it its own name instead — see Run more than one vault.

Vault re-syncs from scratch (or the search index is empty) after switching between the CLI, Compose, and docker run. Nothing is lost — your data is still in the old volumes. All three methods use the same named volumes for the same INSTANCE_NAME, so data normally carries over — but only when the names match exactly, and a mismatch produces no error. Docker silently creates fresh, empty volumes and the container starts with a clean slate: Obsidian Sync registers a new device and re-syncs the vault, and the search index rebuilds.

Confirm with docker volume ls: unprefixed names (vault_data) alongside prefixed ones (vault-cortex_vault_data) mean an earlier setup used different volumes. The same thing happens when switching from the single-volume setup (vault-cortex_storage) to the CLI or Compose, which create the three-volume layout: vault-cortex_storage listed alongside the three is the tell, and the way back is the single-volume docker run command under Setup. Two ways to recover from a three-volume mismatch:

  • Let the re-sync finish (simplest). Your vault's source of truth is Obsidian Sync, so the fresh volumes repopulate on their own — wait for the sync and index rebuild to complete, then delete the old volumes (docker volume rm vault_data mcp_data obsidian_config).

  • Carry your existing data over — keeps the built search index (no re-embedding) and the already-registered Sync device. Remove the container and the freshly created volumes, copy each old volume into its prefixed counterpart, then start the server again with your preferred method:

    docker rm -f vault-cortex
    docker volume rm vault-cortex_vault_data vault-cortex_mcp_data vault-cortex_obsidian_config
    
    docker run --rm -v vault_data:/from -v vault-cortex_vault_data:/to \
      --entrypoint sh ghcr.io/aliasunder/vault-cortex:remote -c "cp -a /from/. /to/"
    docker run --rm -v mcp_data:/from -v vault-cortex_mcp_data:/to \
      --entrypoint sh ghcr.io/aliasunder/vault-cortex:remote -c "cp -a /from/. /to/"
    docker run --rm -v obsidian_config:/from -v vault-cortex_obsidian_config:/to \
      --entrypoint sh ghcr.io/aliasunder/vault-cortex:remote -c "cp -a /from/. /to/"