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
- 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
npx vault-cortex@latest init --mode remoteThe 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 nodejsManual setup (no Node.js needed)
1. SSH into your server and create a working directory:
mkdir -p /opt/vault-cortex && cd /opt/vault-cortex2. 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.exampleOr 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-tokenOtherwise, 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:remoteOr 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 .env5. 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 -dNo 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:remoteSingle 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:remoteRender 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_URLorRAILWAY_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). SetLOG_DIR=noneto 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.
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.
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.
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.
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).
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, butPUBLIC_URLitself is the base URL only — the server serves the endpoint at/mcpand appends it for you, so aPUBLIC_URLending in/mcpbecomes…/mcp/mcpand 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.
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:
-
Open the setup page: visit your
PUBLIC_URLin a browser — the server sends your browser straight to the sign-in page. (Adding/setupto the address goes to the same place.) -
Paste your
MCP_AUTH_TOKEN— the value you set in.env— in the token field. -
Enter your Obsidian account email and password. If you use two-factor authentication, the page asks for the code on the next step.
-
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.
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.
curl -H "Authorization: Bearer <your-MCP_AUTH_TOKEN>" <PUBLIC_URL>/mcpRun 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.
# 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 psUpdate with the same method you set up with — each one manages the container independently.
Set up with the CLI?
npx vault-cortex@latest upgradeRun 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 -dSet 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-cortexNamed volumes persist across updates — no re-sync, no device re-registration, and unchanged notes are not re-embedded.
On every boot, a catch-up sync brings the vault current before the server starts. The server then runs these startup tasks:
- Create memory template files if the memory folder doesn't exist (skipped
when
MEMORY_ENABLED=falseorREADONLY_MODE=true). - Start the file watcher, which holds the changes it sees until step 4.
- Rebuild the search index, including any new memory template files.
- 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-cortexChanged
.env? A restart does not re-read.env— the container has to be re-created. Set up with the CLI? Runnpx vault-cortex@latest restart— unlikedocker restart, it re-creates the container, so your edits take effect (upgradealso applies.envedits and pulls the latest image on the way). Using Compose? Rundocker compose up -d— it re-creates services whose configuration changed. Usingdocker run? Remove the container (docker rm -f vault-cortex) and re-run thedocker runcommand from Setup.
The container also restarts automatically on crash (restart: unless-stopped
policy), Docker daemon restart, or system reboot.
# 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-cortexNever delete just the vault volume while keeping
obsidian_config(or, with a single volume, just thevaultdirectory while keepingconfig). 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.
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-workWhen 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-workSet 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.
- Cloudflare Tunnel: add a public hostname that points at
- 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-workto update, ornpx vault-cortex@latest restart --dir ./vault-cortex-workto restart. - With Compose:
docker compose pull && docker compose up -din its folder. - With
docker run: the steps under Updating, using its name.
- With the CLI:
- Use its name in
dockercommands. 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 exampledocker logs work.
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_NAMEis 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_NAMEback, 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_configwould 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), mountvault-cortex_storage:/persistinstead, 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.
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 (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.
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.
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.
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_CONFIGSdefaults tocore-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); setSYNC_CONFIGS=nonein.envto 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.
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.
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.
"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.
- 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. - If it stops again with the same message, set
SYNC_MODE=pull-onlyin.envand re-create the container, becausedocker startkeeps the old settings. The Changed.env? note under Restart gives the command for each setup method. The server starts and keeps retrying the downloads, andpull-onlynever deletes anything from Obsidian Sync. Edits made on the server in the meantime stay on the server. - Later, set
SYNC_MODE=bidirectionaland re-create the container again. If it stops with this message again, the downloads haven't finished yet, so switch back topull-onlyand 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/"

