---
title: "Running the real Obsidian client headless in a container"
description: "My agents needed Obsidian's plugin APIs, so I run the real desktop client headless as a service on my home server."
url: "https://michael.zazon.us/notes/running-the-real-obsidian-client-headless-in-a-container"
published: "2026-10-11T12:00:00-04:00"
kind: "build-log"
subject: "tools"
tags: ["obsidian","docker"]
---
I found that the official Obsidian headless sync client ([Obsidian Headless](https://obsidian.md/help/sync/headless)) does most of what I need but to interact with some of obsidian's plugins APIs from my AI agents, I needed a long-lived obsidian as a service and running inside the container on my Linux server was a fleeting idea that turned into something I lean on daily.

The sync client keeps the files in sync, but the plugin APIs only exist inside a running app, so if you want an agent to be able to query a plugin, it needs a live, responding, real Obsidian. The vault is the system described in [My Obsidian setup and how I use it](https://michael.zazon.us/notes/my-obsidian-setup-and-how-i-use-it) and this piece gives agents access to the vault.

## One container instead of two services

I started with this setup: two host services, one for the desktop app running on a virtual display, and one for the headless sync client running alongside it. I set up a watchdog to run every 5 minutes and a reconcile to run hourly. But then I wondered: do I really need both? Could I just run one real desktop client and have it do everything, including sync?

I can. I now run a single container based on the [linuxserver Obsidian image](https://docs.linuxserver.io/images/docker-obsidian/) as a real desktop client (headless). It runs first-party Sync, community plugins, and exposes the Local REST API and TaskNotes API from the same container. I got rid of the watchdog and hourly reconcile, and my Ansible playbook for deployment shrank to a third of its previous size, making everything easier to maintain.

## The cutover that deleted nothing

The riskiest moment was the first sync against my live vault (a misconfigured client can delete real notes!). I was concerned about how to know if the new client had seen everything before trusting it. I did a pure download of the entire remote vault into a scratch folder using my production config, then did an rsync checksum diff against the live vault. Only then did I point the container at the real vault. As a result, the first real sync didn't delete anything.

That staging pull exposed a mistake I'd made: 7,495 files were silently skipped because I'd taken a shortcut and added some file types directly to the sync filter instead of using the setter, stranding existing remote files of those types. Toggling those types off and back on through the proper route brought them down. It was a good lesson in how the filter can look right but leave thousands of files behind. (No harm done here, of course.. it was in the scratch folder.)

> [!tip] Stage a sync cutover
> Pull the remote vault into an empty folder with your production config and compare checksums against the live copy before the new client touches it.

I added a sidecar that forwards the plugin APIs, so agents and scripts keep using the same address as before.

## The daily workbench

I use the obsidian CLI and REST API for TaskNotes the most, but I have also introduced some other custom plugins in development, and I can test these on this container image easily and take screenshots and iterate quickly (using the Chrome DevTools Protocol).

The container also doubles as a workbench for me: I can open the full Obsidian GUI in the browser, and with the Lean Terminal plugin, I get a terminal inside the GUI, where I have Claude Code installed in the container. So the very same place where I serve my agents is also where I work alongside them. (The vault search is a separate project, see [niko hybrid search over my Obsidian vault](https://michael.zazon.us/notes/niko-hybrid-search-over-my-obsidian-vault))

[Image: Obsidian in a browser with a note open beside a Lean Terminal pane](https://michael.zazon.us/notes/running-the-real-obsidian-client-headless-in-a-container)

> [!note] Terminal binaries stay put
> Terminal binaries don't sync between devices.. what's installed in the container stays in the container.

## A bug only an always-on Obsidian would find

And when I used the GUI, I discovered a mystery: the Tab key was landing in the wrong pane. It was a global key handler in the Tasks plugin. I wrote my own 25-line plugin to fix it locally, then filed an issue upstream with my tested-and-working fix branch, so the fix can make its way to others too.

Next up: Auto-restart, which I want to enable once the container has a long healthy record. After that, I'll build more agent tools on top of the REST and TaskNotes APIs, since I always have a real app available to call.