Skip to content

Mutable data outside the repo (CAIRN_DATA_DIR)

Source: cairn/ts/src/data.ts · commit 1fc6292 Category: Pattern — deployment

CAIRN_DATA_DIR — a small convention for decoupling code from runtime-mutable content in git-deployed Node apps.

Runtime-mutable files live at a path controlled by an environment variable (CAIRN_DATA_DIR), not inside the repo. The repo ships only seed templates — copied to the target path on first boot if it’s empty.

The problem: Cairn’s admin CMS writes ts/data.json and ts/posts/ on the live VPS. Those files were tracked in git. On the next git pull, one of two things happens:

  1. The VPS’s admin-edited file is silently overwritten by what’s in the repo.
  2. The pull aborts with a conflict and blocks every deploy.

The fix: split code from content. Code lives in the repo. Content lives on the box at $CAIRN_DATA_DIR. First-boot seeds bridge the gap for fresh installs.

ts/src/data.ts
const REPO_TS_DIR = join(__dirname, '..');
const DATA_DIR = process.env.CAIRN_DATA_DIR || REPO_TS_DIR;
const DATA_PATH = join(DATA_DIR, 'data.json');
const SEED_PATH = join(REPO_TS_DIR, 'data.example.json');
// First-boot seed: if data.json is missing, copy from the committed template.
if (!existsSync(DATA_PATH)) {
if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true });
if (existsSync(SEED_PATH)) {
copyFileSync(SEED_PATH, DATA_PATH);
} else {
throw new Error(`No data.json at ${DATA_PATH} and no seed at ${SEED_PATH}`);
}
}

Env var is set on the VPS via a systemd drop-in:

/etc/systemd/system/portfolio-web.service.d/cairn-data-dir.conf
[Service]
Environment="CAIRN_DATA_DIR=/var/lib/cairn"

ts/data.json and ts/posts/ are gitignored. Committed seeds are ts/data.example.json and ts/posts.example/.

For an existing deployment that already has admin-edited data, run before the next git pull:

Terminal window
sudo mkdir -p /var/lib/cairn
sudo cp /opt/portfolio/ts/data.json /var/lib/cairn/
sudo cp -r /opt/portfolio/ts/posts /var/lib/cairn/
sudo chown -R <service-user>:<service-user> /var/lib/cairn

Then add the env var to the systemd drop-in and systemctl daemon-reload.

  • Cairndata.json and blog posts/ are admin-editable surfaces
  • Pattern generalizes to any app with admin UI + git-based deploys
  • Migration order is load-bearing. Back up live data to $CAIRN_DATA_DIR before the first git pull after adopting this. Otherwise the pull deletes the tracked files, the service boots, finds no data, seeds from the older template, and admin edits are gone.
  • Host keys don’t fit this pattern. host_key is written on first run and read forever. Gitignored, yes — but no seed template. A seeded host key would mean every fresh install shares the same SSH identity.
  • Logs, uploads, temp files belong in $CAIRN_DATA_DIR too. Dropping them back in the repo directory re-creates the problem with a different file.
  • Documentation is part of the pattern. The README deploy section has to say “run the migration before the first git pull” or a future reader will delete their own data.