Skip to content

aliasunder/obsidian-headless-sync-docker

 
 

Repository files navigation

obsidian-headless-sync-docker

A minimal, rootless Docker image for continuously syncing an Obsidian vault via obsidian-headless — the official headless client for Obsidian Sync released February 2026.

Built on s6-overlay for proper process supervision, signal handling, and ordered service startup. The container starts as root to perform one-time user/group and ownership setup, then runs the main services as a non-root user.

Requirements: An active Obsidian Sync subscription.

Fork of Belphemur/obsidian-headless-sync-docker, maintained for vault-cortex. Images: ghcr.io/aliasunder/obsidian-headless-sync-docker.


Quick Start

Step 1 — Get your auth token (one-time)

Pull the image and run the interactive login helper. It will prompt for your Obsidian email, password, and MFA code (if enabled), then print your token.

# Docker
docker run --rm -it --entrypoint get-token ghcr.io/aliasunder/obsidian-headless-sync-docker:latest

# Podman
podman run --rm -it --entrypoint get-token ghcr.io/aliasunder/obsidian-headless-sync-docker:latest

Copy the printed OBSIDIAN_AUTH_TOKEN value — you'll need it in step 3.

Note: The token persists until you explicitly log out or revoke it from your Obsidian account. You only need to run this once per machine (or per token rotation).


Step 2 — Find your remote vault name (one-time)

List the vaults available on your Obsidian Sync account:

# Docker
docker run --rm \
  -e OBSIDIAN_AUTH_TOKEN=your-token-here \
  --entrypoint ob \
  ghcr.io/aliasunder/obsidian-headless-sync-docker:latest \
  sync-list-remote

# Podman
podman run --rm \
  -e OBSIDIAN_AUTH_TOKEN=your-token-here \
  --entrypoint ob \
  ghcr.io/aliasunder/obsidian-headless-sync-docker:latest \
  sync-list-remote

⚠️ Note: Using --entrypoint ob bypasses s6-overlay and runs the command as root. This is fine for read-only operations like sync-list-remote, but avoid mounting config volumes with this method as it may create root-owned files that conflict with the unprivileged service.

Note the exact vault name — you'll use it in VAULT_NAME.


Step 3 — Configure your environment

cp .env.example .env

Edit .env and fill in at minimum:

OBSIDIAN_AUTH_TOKEN=<token from step 1>
VAULT_NAME=My Vault
VAULT_HOST_PATH=./vault
CONFIG_HOST_PATH=./config

See Environment Variables for all options.


Step 4 — Start continuous sync

docker compose up -d

On first run the container performs a one-time ob sync-setup to link the local directory to your remote vault, then enters continuous sync mode. Subsequent restarts skip the setup and go straight to syncing.

Watch logs:

docker compose logs -f

The sync daemon's own output is not timestamped in the message text — add -t (docker compose logs -f -t) to have Docker prepend a timestamp to each line. On startup the service logs a context banner (device, conflict strategy, vault) so you can confirm which configuration the sync is running under.


Architecture

This image uses s6-overlay v3 as its init system. See docs/s6-overlay-design.md for the full design documentation.

The startup sequence runs through ordered s6-rc services:

  1. init-setup-user — adjusts UID/GID to match PUID/PGID
  2. init-check-auth — validates OBSIDIAN_AUTH_TOKEN is set
  3. init-obsidian-login — runs ob login to authenticate
  4. init-setup-vault — runs ob sync-setup and applies optional config
  5. svc-obsidian-sync — starts ob sync --continuous under s6 supervision

If any init step fails, the container exits immediately (S6_BEHAVIOUR_IF_STAGE2_FAILS=2).

Supported platforms: linux/amd64, linux/arm64.


Environment Variables

Variable Required Default Description
OBSIDIAN_AUTH_TOKEN Yes Auth token from get-token
VAULT_NAME Yes (first run) Exact name of the remote Obsidian Sync vault
VAULT_HOST_PATH Yes ./vault Host path where vault files will be written
CONFIG_HOST_PATH No ./config Host path for persistent config (login state, etc.)
VAULT_PASSWORD If E2E enabled Vault end-to-end encryption password (see below)
PUID No 1000 UID that will own synced files (see below)
PGID No 1000 GID that will own synced files (see below)
VAULT_PATH No /vault In-container mount path (advanced)
DEVICE_NAME No obsidian-docker Label shown in Obsidian Sync history
CONFLICT_STRATEGY No conflict merge or conflictconflict writes a separate conflict file instead of silently merging
EXCLUDED_FOLDERS No Comma-separated vault folders to skip
FILE_TYPES No Extra types to sync: image,audio,video,pdf,unsupported
SYNC_MODE No bidirectional Sync mode: bidirectional, pull-only, or mirror-remote
SYNC_CONFIGS No Comma-separated config categories to sync (see below)
CONFIG_DIR_NAME No .obsidian Name of the Obsidian config directory inside the vault (advanced)
GHCR_REPO No Override image repository when self-building

File Ownership (PUID / PGID)

At startup the container adjusts its internal obsidian user to match the PUID/PGID you provide, then drops privileges via s6-setuidgid before running any Obsidian commands. This means vault files on the host are owned by the UID/GID you choose.

Regular Docker (daemon runs as root):

# Find your UID and GID
id
# uid=1000(you) gid=1000(you) ...
PUID=1000
PGID=1000

Rootless Docker / Podman (daemon runs as your user):

In rootless mode, container UID 0 already maps to your host user. Set both to 0:

PUID=0
PGID=0

End-to-End Encryption (VAULT_PASSWORD)

Obsidian Sync supports optional end-to-end encryption with a separate vault password. If your vault has this enabled, ob sync-setup will fail to authenticate until the password is provided.

To check: In the Obsidian desktop app, go to Settings → Sync and look for an "Encryption password" field — if it's present and set, E2E is active.

Add the password to your .env:

VAULT_PASSWORD=your-vault-encryption-password

Note: VAULT_PASSWORD is the vault encryption password you chose in Obsidian, not your Obsidian account password. They are separate credentials.

⚠️ Passwords containing $: Docker Compose interpolates .env files, so a bare $ in the value (e.g. pa$sword) is treated as the start of a variable reference and silently stripped, truncating your password. If your password contains a $, wrap it in single quotes so Compose treats it literally:

VAULT_PASSWORD='pa$sword'

Sync Configuration (SYNC_MODE / SYNC_CONFIGS)

These variables map directly to ob sync-config options and are applied every time the container starts.

SYNC_MODE

Controls how local and remote changes are reconciled.

Value Behaviour
bidirectional Upload local changes and download remote changes (default)
pull-only Download remote changes only — local changes are ignored
mirror-remote Download remote changes only — local changes are reverted
SYNC_MODE=pull-only

SYNC_CONFIGS

Comma-separated list of Obsidian config categories to sync alongside vault notes. Leave blank to keep the vault's existing setting (all categories synced by default).

Value Syncs
app Core app settings
appearance Theme and appearance settings
appearance-data Theme assets (CSS snippets, etc.)
hotkey Keyboard shortcuts
core-plugin Core plugin toggle states
core-plugin-data Core plugin configuration data
community-plugin Community plugin list and toggle states
community-plugin-data Community plugin configuration data
# Sync only app settings and hotkeys
SYNC_CONFIGS=app,hotkey

For the full reference see the obsidian-headless ob sync-config documentation.

CONFIG_DIR_NAME

Overrides the name of the Obsidian config directory inside the vault (default: .obsidian). Only needed if your vault uses a non-standard config directory name.

CONFIG_DIR_NAME=.obsidian

Using a Pre-Built Image vs. Building Locally

Pre-built (recommended)

Multi-arch images (linux/amd64, linux/arm64) are published to the GitHub Container Registry on demand via the Manual Release workflow (.github/workflows/manual_release.yml), with SLSA build provenance attached.

# compose.yml already points to:
image: ghcr.io/aliasunder/obsidian-headless-sync-docker:latest

Build locally

docker build -t obsidian-headless-sync-docker .

Then update compose.yml to use image: obsidian-headless-sync-docker.


Podman Quadlet (systemd)

A ready-made quadlet unit file (obsidian-sync.container) is included for running the container as a systemd service under rootless Podman.

Install

# Copy the quadlet into the user systemd search path
mkdir -p ~/.config/containers/systemd
cp obsidian-sync.container ~/.config/containers/systemd/

# Create a secrets file (mode 600 keeps your token private)
mkdir -p ~/.config/obsidian-sync
install -m 600 /dev/null ~/.config/obsidian-sync/obsidian-sync.env

Populate ~/.config/obsidian-sync/obsidian-sync.env with at minimum:

OBSIDIAN_AUTH_TOKEN=<token from get-token>
VAULT_NAME=My Vault

Optional keys (defaults are set in the unit file):

VAULT_PASSWORD=
DEVICE_NAME=obsidian-podman
CONFLICT_STRATEGY=conflict
EXCLUDED_FOLDERS=
FILE_TYPES=
SYNC_MODE=
SYNC_CONFIGS=

Start

systemctl --user daemon-reload
systemctl --user start obsidian-sync
systemctl --user status obsidian-sync

Watch logs:

journalctl --user -u obsidian-sync -f

Automatic image updates

Enable the built-in Podman auto-update timer to pull new images from ghcr on a schedule:

systemctl --user enable --now podman-auto-update.timer

The unit also sets Pull=newer, so it will fetch a newer image from ghcr.io each time the service restarts.

Vault location

By default the vault is stored at ~/obsidian-vault. To use a different path, edit the Volume= line in the unit file before copying it:

Volume=/path/to/your/vault:/vault:z

Updating the Image

docker compose pull
docker compose up -d

Stopping

docker compose down

Your vault files remain on disk at VAULT_HOST_PATH.


Troubleshooting

Container exits immediately

  • Check that OBSIDIAN_AUTH_TOKEN and VAULT_NAME are set: docker compose config
  • Check init logs: the container stops on any init failure (S6_BEHAVIOUR_IF_STAGE2_FAILS=2)

"Vault not found" error on setup

  • Confirm the vault name matches exactly (case-sensitive): run ob sync-list-remote as shown in Step 2.

"Failed to validate password" on setup

  • Your vault has end-to-end encryption enabled. Set VAULT_PASSWORD in .env to the encryption password from Obsidian → Settings → Sync. This is distinct from your Obsidian account password.
  • If the password contains a $ character, make sure it's wrapped in single quotes in .env (e.g. VAULT_PASSWORD='pa$sword'), otherwise Compose's variable interpolation will silently truncate it. Run docker compose config and check the resolved VAULT_PASSWORD value matches what you expect.

Sync stops after a while

  • The restart: unless-stopped policy in compose.yml will restart the container automatically. Within the container, s6 supervises the sync process and restarts it if it exits.

Token expired / login required

  • Re-run the get-token step, update OBSIDIAN_AUTH_TOKEN in .env, and restart: docker compose up -d

Permission denied on vault files

  • The container adjusts its internal user to match PUID/PGID (default 1000:1000). Set these in .env to match the host user who should own the files (id shows your values).
  • For rootless Docker/Podman, set both to 0.

Contributing

Contributions are welcome — see CONTRIBUTING.md for the build loop and PR process, and the Code of Conduct. Please report security issues privately per the Security Policy.

License

The packaging in this repository (Dockerfile, s6-overlay scripts, compose/quadlet files, workflows, docs) is MIT — see LICENSE. It builds on Belphemur/obsidian-headless-sync-docker (also MIT).

The published image bundles obsidian-headless (the ob CLI), which is proprietary — its package.json declares "license": "UNLICENSED" (© Dynalist Inc. / Obsidian). It is installed from public npm at build time; the MIT license here does not cover it, and using it requires an active Obsidian Sync subscription.

About

Dockerized version of obsidian-headless-sync

Resources

License

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages

  • Dockerfile 76.1%
  • Shell 23.9%