Obsidian to VPS Pipeline: Sync, Pull, and Redeploy
A complete one-command publishing pipeline from Obsidian on macOS to a live Astro site on a VPS.
Category: Development
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, andpodman-composeinstalled. - 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:
- Update the repository on the VPS (
git pull --ff-only). - Sync the Markdown posts from macOS to the VPS (
rsync). - 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-onlyis 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
--deletefrom 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.