Headless Obsidian container with a working CLI and interactive TUI accessible via docker exec.
No GUI required. No Catalyst license required at runtime — only the obsidian-1.12.6.asar
file must be supplied at build time (see below).
- Xvfb provides a virtual framebuffer so Electron starts without a real display.
- dbus (system + session bus) is started inside the container — Electron requires it.
- Obsidian 1.12.4 is installed from the official
.debrelease. obsidian-1.12.6.asar(insider build) is placed in the config directory and auto-discovered by the launcher. Version 1.12.6 introduced a fixed Unix socket protocol (~/.obsidian-cli.sock) that makes CLI reliable in a container.obsidian(/usr/local/bin/obsidian) — Python script that serialises argv viajson.dumps, sends it to the socket, and reads the response until EOF. No second Electron process, no lock contention.obsidian-tui(/usr/local/bin/obsidian-tui) — Python script that opens the socket in interactive mode (argv=[], tty=true), switches stdin to raw mode, and proxies bytes in both directions so Tab, arrow keys, and Ctrl+C all work correctly.
.
├── Dockerfile
├── docker-compose.yaml
├── justfile
├── CLI.md # Full command reference
├── scripts/
│ ├── entrypoint.sh # Starts dbus, Xvfb, seeds config dir, launches Obsidian
│ ├── obsidian-cli # One-shot CLI wrapper (Python)
│ └── obsidian-tui # Interactive TUI wrapper (Python, raw TTY)
└── src/
├── obsidian-1.12.6.asar # Insider renderer (not included in repo — see below)
└── obsidian.json # Vault registration, cli: true, frame: hidden
-
Docker + Compose
-
just(optional, forjustfileshortcuts) -
src/obsidian-1.12.6.asar— must be copied from another machine running Obsidian with an active Insider Catalyst license. The file is not distributed in this repo.Typical locations:
- Linux:
~/.config/obsidian/obsidian-1.12.6.asar - Windows:
%APPDATA%\obsidian\obsidian-1.12.6.asar - macOS:
~/Library/Application Support/obsidian/obsidian-1.12.6.asar
Place it at
src/obsidian-1.12.6.asarbefore building. - Linux:
# 1. Copy obsidian-1.12.6.asar from an Insider-licensed instance (see Requirements)
# cp /path/to/obsidian-1.12.6.asar src/
# 2. Set your vault path in docker-compose.yaml:
# volumes:
# - /your/vault/path:/vault
# 3. Build and start
docker compose up -d --build
# or: just up
# 4. Wait ~10 seconds for Obsidian to fully load
docker compose logs -f
# or: just logs
# 5. Run CLI commands
docker exec obsidian-headless obsidian version
docker exec obsidian-headless obsidian vault list
docker exec obsidian-headless obsidian files total
# 6. Open the interactive TUI
just tuidocker exec obsidian-headless obsidian version
docker exec obsidian-headless obsidian vault list
docker exec obsidian-headless obsidian files total
docker exec obsidian-headless obsidian files list
docker exec obsidian-headless obsidian read file="Daily Note"
docker exec obsidian-headless obsidian search query="TODO"
docker exec obsidian-headless obsidian tags counts sort=count
docker exec obsidian-headless obsidian tasks todoFull command reference: see CLI.md.
# Add to ~/.zshrc or ~/.bashrc
alias ob='docker exec obsidian-headless obsidian'
ob version
ob files list
ob tags counts sort=count
ob search query="TODO" format=json | jq '.[].file'just up # build and start
just down # stop
just restart # restart container
just logs # follow logs
just shell # bash shell inside container
just tui # interactive TUI
just version # obsidian version
just vault # vault summary
just files # total file count
just ls # list all files
just recents # recently opened files
just search "q" # full-text search
just grep "q" # search with line context
just tags # tags with counts
just tasks # pending tasks
just tasks-done # completed tasks
just dead-links # unresolved links
just dead-ends # notes with no outgoing links
just aliases # all aliases
just properties # all properties with counts
just read "Note" # read a note by name
just read-path "p" # read a note by path
just backlinks "n" # backlinks for a note
just note-tags "n" # tags for a note
just wc "n" # word count for a note
just create "n" # create a new note
just append "n" "t" # append to a note
just prepend "n" "t"# prepend to a note
just delete "n" # delete a note
just rename "o" "n" # rename a note
just reload # reload the vault
just tabs # show open tabs
just screenshot # take a screenshot
Run just with no arguments to see the full list.
just tui
# or directly:
docker exec -it obsidian-headless /usr/local/bin/obsidian-tuiThe TUI requires a real TTY. It opens Obsidian's built-in interactive REPL with:
- Tab — autocomplete commands and flags
- ↑/↓ — command history
- Ctrl+C — quit
just tui passes the current terminal dimensions ($COLUMNS/$LINES) so the
autocomplete dropdown renders correctly.
| File | Purpose |
|---|---|
src/obsidian.json |
Vault registration, cli: true, frame: hidden, updateDisabled: true |
src/obsidian-1.12.6.asar |
Insider renderer — fixed Unix socket CLI protocol (not in repo) |
scripts/obsidian-cli |
One-shot CLI wrapper (Python) |
scripts/obsidian-tui |
Interactive TUI wrapper (Python, raw TTY) |
scripts/entrypoint.sh |
Starts dbus, Xvfb, seeds config dir, launches Obsidian |
docs/CLI.md |
Full command reference |
justfile |
Convenience recipes |
{
"vaults": {
"<16-char hex id>": {
"path": "/vault",
"ts": 1773743087717,
"open": true
}
},
"cli": true,
"frame": "hidden",
"updateDisabled": true
}The vault id is an arbitrary 16-character hex string. ts is any valid millisecond
timestamp. open: true tells Obsidian to open the vault on startup.
obsidian-config is a named volume mounted at /root/.config/obsidian/. It persists:
- The Unix socket (
~/.obsidian-cli.sock) used for CLI/TUI communication. - Chromium profile data (cache, IndexedDB, session storage).
- The
obsidian-1.12.6.asarfile (copied from the image on first start).
On a fresh volume the entrypoint seeds obsidian.json and copies the .asar file
from /opt/ before starting Obsidian.
--no-sandboxis required — Electron cannot use user namespaces inside a container.SYS_ADMIN+seccomp=unconfinedare needed for Chromium's internal sandboxing.--disable-gpuand--disable-software-rasterizerprevent GPU process crashes.tty: true+stdin_open: trueindocker-compose.yamlare required fordocker exec -it.- The CLI wrapper prepends
vault=<id>automatically by readingobsidian.json, so commands resolve the correct vault without manual configuration. - The TUI wrapper translates bare
\nto\r\nin raw mode — Node.js emits\nonly, but a raw-mode terminal requires CRLF for correct line rendering. - Obsidian closes the socket after writing the full response; the CLI wrapper reads until EOF with a 30-second timeout.
Change OBSIDIAN_VERSION in docker-compose.yaml and rebuild:
just up # or: docker compose up -d --buildLatest releases: https://github.com/obsidianmd/obsidian-releases/releases
The .asar file is only available on machines with an active Insider Catalyst license.
Copy the new obsidian-X.Y.Z.asar from another instance that has Insider access,
place it at src/obsidian-X.Y.Z.asar, update the COPY line in Dockerfile, and
rebuild. The launcher picks the highest version automatically.