Files
saiop-skills/skills/saiop-infrastructure/saiop-git-sync-sidecar/SKILL.md
T

52 lines
2.2 KiB
Markdown

---
name: saiop-git-sync-sidecar
description: "Pattern for keeping a container's data volume continuously synced to a Gitea repo via a small sidecar container — used for the Obsidian vault, reusable elsewhere."
version: 1.0.0
author: Claude Code (SAIOP ops session)
license: MIT
platforms: [linux]
prerequisites:
env_vars: []
commands: [docker, git]
metadata:
hermes:
tags: [SAIOP, git, sync, Gitea, pattern, sidecar]
---
# Git-sync sidecar pattern
## When to use
Any time an app's data volume should be continuously backed by a Gitea repo
(used for the Obsidian vault → `atu/saiop-knowledge`). Generalizes to any
app that just reads/writes files in a directory without needing to know
git exists.
## The pattern
A second, tiny container (`alpine/git`) mounts the **same volume** as the
main app and loops: `git pull` → check `git status --porcelain` → if dirty,
`add`/`commit`/`push``sleep`. The main app never needs to know about
git at all.
## Gotchas hit building this
1. **`rm -rf` on the mount point itself fails** ("Resource busy") — you can't
remove a bind-mounted directory, only its contents. Use
`find "$DIR" -mindepth 1 -delete`, or `git clone <url> .` directly into
the already-existing empty directory instead of removing-then-cloning.
2. **UID mismatch silently breaks the main app's write access.** If the
sidecar runs as root (the `alpine/git` default) and the main app runs as
a non-root `PUID`/`PGID` (common for LinuxServer.io images), every file
the sidecar creates is unwritable by the main app. Set `user: "UID:GID"`
on the sidecar to match the main app's user — don't just chown once
after the fact, the mismatch comes back on every commit.
3. **Add a `.gitignore` for the app's own volatile state files** (Obsidian:
`workspace.json`, `cache/`) before the first commit, or every sync cycle
produces a noise commit even with zero real content changes.
4. **Don't hardcode a container's internal IP for inter-container access**
IPs churn (confirmed twice on this stack already). Use the container
**name** if both containers share a network (Docker's embedded DNS
resolves it), which is why the sync sidecar talks to `gitea:3000`
directly rather than an IP.