Obsidian to VPS Pipeline: Sync, Pull, and Redeploy

Category: Development

Tags: workflow, deployment, obsidian


I write posts in Obsidian, but shipping each one meant opening VS Code, committing Markdown, pushing, and then SSHing into the server to rebuild the container. Four steps, three places, too many opportunities to forget one. I wanted a single command on my laptop to do all of it.

This post documents the pipeline: one local shell script that updates the repo on the VPS, rsyncs the Markdown out of my Obsidian vault, and rebuilds the Astro container in one shot.

The setup

  • Local: macOS, Obsidian vault synced via iCloud Drive.
  • VPS: Debian with git, podman, and podman-compose installed.
  • Site: Astro running in a container managed by podman-compose, source at /opt/websites/www.adrian-altner.de.

The goal

Three operations in order, triggered by one command locally:

  1. Update the repository on the VPS (git pull --ff-only).
  2. Sync the Markdown posts from macOS to the VPS (rsync).
  3. Rebuild and restart the Astro container (podman-compose).

Source and target paths

Local — the Obsidian vault lives inside iCloud’s synced container, which means the path has spaces and tildes and needs quoting every time it appears:

/Users/adrian/Library/Mobile Documents/iCloud~md~obsidian/Documents/Web/adrian-altner-com/posts/

Remote — inside the Astro project:

/opt/websites/www.adrian-altner.de/src/content/posts

Prerequisites

SSH alias

An alias in ~/.ssh/config keeps the script readable — ssh hetzner everywhere instead of a repeated IP plus flags:

Host hetzner
  HostName <your-vps-ip-or-host>
  User root
  IdentityFile ~/.ssh/<your-key>

Tools

  • macOS: rsync, ssh.
  • VPS: git, podman, podman-compose.

Keeping one seed post in Git

Most posts are written in Obsidian and rsync’d, not committed. But I want at least one seed file (hello-world.md) tracked in Git so a fresh clone of the repo still renders something. The .gitignore split handles that:

src/content/posts/*
!src/content/posts/hello-world.md

Commit the seed once and forget about it:

git add src/content/posts/hello-world.md
git commit -m "add hello-world seed post"
git push

The publish script

Saved as ~/bin/publish-posts.sh:

#!/usr/bin/env bash
set -euo pipefail

SRC='/Users/adrian/Library/Mobile Documents/iCloud~md~obsidian/Documents/Web/adrian-altner-com/posts/'
VPS="${1:-hetzner}"
REMOTE_BASE='/opt/websites/www.adrian-altner.de'
REMOTE_POSTS="${REMOTE_BASE}/src/content/posts"

# 1) Update code on VPS
ssh "$VPS" "cd '$REMOTE_BASE' && git pull --ff-only"

# 2) Sync posts from Obsidian to VPS
# The explicit exclude keeps a Git-tracked seed file safe from --delete.
ssh "$VPS" "mkdir -p '$REMOTE_POSTS'"
rsync -az --delete \
  --exclude='hello-world.md' \
  --include='*.md' --exclude='*' \
  "$SRC" "$VPS:$REMOTE_POSTS/"

# 3) Rebuild and replace running container
ssh "$VPS" "cd '$REMOTE_BASE' && podman-compose -f compose.yml up --build -d --force-recreate"

echo "Repo pulled + posts synced + redeploy done via $VPS."

Two details are load-bearing. The --exclude='hello-world.md' on rsync prevents the seed post from being deleted by --delete whenever it isn’t in my Obsidian folder. The --include='*.md' --exclude='*' pair restricts the sync to Markdown — Obsidian drops .trash directories and .obsidian config files alongside notes, and none of that belongs on the server.

Make it executable and run:

chmod +x ~/bin/publish-posts.sh
~/bin/publish-posts.sh

Verification

After a run, two SSH checks tell me the container is up and not screaming:

ssh hetzner "podman ps --filter name=www.adrian-altner.de"
ssh hetzner "podman logs --tail 100 www.adrian-altner.de"

Then the new post URL in the browser — route resolves, title and date render, no startup errors in the logs.

Troubleshooting

Problem: container name conflict. podman-compose sometimes fails with name is already in use when a previous container is stale. The --force-recreate flag in the script usually handles it, but if the error persists:

ssh hetzner "cd /opt/websites/www.adrian-altner.de && podman-compose -f compose.yml up --build -d --force-recreate"

or remove the container explicitly:

ssh hetzner "podman rm -f www.adrian-altner.de"

Problem: git pull blocked by local changes on VPS. If someone edited files directly on the server, fast-forward fails. Check the state:

ssh hetzner "cd /opt/websites/www.adrian-altner.de && git status"

Discipline, not tooling, is the fix here — keep the VPS working tree clean for predictable deploys.

What to take away

  • One command beats four. The pipeline collapses write → commit → push → deploy into a single shell invocation.
  • --ff-only is non-negotiable on a server. A merge commit on the VPS is a deploy-time surprise you never want.
  • Whitelist what rsync sends. --include='*.md' --exclude='*' stops Obsidian’s hidden files from leaking into the site.
  • Keep one tracked seed post. It prevents --delete from stripping the content directory bare and makes a fresh clone build without extra setup.
  • Boring on purpose. Every step of the script is replaceable by cron, launchd, or CI later — the interface stays the same.