Skip to content
Consultation

That Morning .gitignore Commit Made Me Rethink My Backup Setup

Adityo Guni Waluyo

A two-line .gitignore whitelist commit turned into a full backup redesign: Git repo, restic, immutable cloud, 3-2-1-1-0 without a NAS.

TL;DR

A two-line .gitignore change led me to audit my backups, and I realized my NAS setup was untested and ransomware-vulnerable. I replaced it with a 3-2-1-1-0 approach: git bundles, restic snapshots, and immutable cloud storage. Monthly restore drills now verify the whole thing actually works.

7:51 in the morning, September 2. I'm staring at a two-line diff in my-agent: adding whitelist patterns for data/asset directories to .gitignore. Thinking it's just a housekeeping commit. Reverse the ignore so DR assets actually get tracked. Push, move on.

Except that two-line diff sent me down a rabbit hole that ended with me ripping out the NAS-based backup approach I'd been using since the old office and replacing it with something I should've set up years ago.

The Commit That Wasn't Just a Chore

The .gitignore change looked trivial. Certain data directories under the repo were being excluded by a blanket ignore rule. I flipped them to explicit whitelist patterns so git would track them.

That's when I caught myself mid-push and thought: wait, which copies of these files actually matter? The repo has one copy. But what happens when the disk dies? What happens when ransomware hits?

I'd always assumed "backup" meant my NAS mirror plus an external disk sitting in the drawer. Two copies, two media types, textbook 3-2-1. Except I'd never actually tested restoring from either. And I hadn't considered what happens when the attack specifically targets backup files, which per Veeam now happens in 93% of attacks [6].

Git Bundle Limitations Changed the Math

First rabbit hole: git bundle. I'd read somewhere that bundling your repo is the recommended way to back up git data. Turns out that's true but incomplete. The docs are clear: a bundle captures committed history, branches, and tags. It does not save your index, working tree, stash, local config, or hooks [4]. If you've got uncommitted work, a bundle won't help.

That matters because .gitignore itself only affects untracked files [5]. The whitelist patterns I'd just committed? They only take effect on files git hasn't seen yet. Already-tracked files ignore .gitignore rules entirely. So the commit was really about new asset files going forward, a deliberate declaration about what gets version-controlled from this point on.

Second rabbit hole: the ransomware angle. CISA's DBIR-derived data shows 44% of investigated breaches involved ransomware [1]. And the trend is specifically toward targeting backups: Veeam puts it at 93% of attacks explicitly going after backups [6]. My NAS sitting on the same network? Not exactly a fortress.

That's when I found Veeam's extension of the classic rule: 3-2-1-1-0. Three copies of data, on two different types of media, one offsite, one immutable or air-gapped, zero errors on recovery verification [2]. The critical addition is that last two, the immutable copy and the verified restore. Having backups doesn't matter if you can't actually recover from them.

Backblaze makes a good point that in the cloud era, the "two media types" requirement relaxes to "two devices or locations" [3]. You don't need tape and disk anymore. You need separation and independence.

What I Actually Set Up

The new pattern has three legs:

Copy one: the Git repo itself. Every project I work on lives in a git repo. With the DR asset whitelist in place, even data files get committed. A git bundle captures the committed state (acknowledging the stash/index limitation [4]). Bundles get pushed to a remote. This isn't a backup solution, it's the first copy that happens to also have version history.

Copy two: restic snapshots. Restic is encrypted, deduplicated, and has built-in integrity verification [7]. A daily cron job snapshots the working directories, including uncommitted files, index state, stash, the things git bundle deliberately skips. This is the "two different devices" leg.

Copy three: immutable cloud storage. An offsite copy that can't be modified by ransomware or a compromised local account. This satisfies both the offsite requirement and the immutable/air-gapped requirement of 3-2-1-1-0 [2].

The piece that ties it together: restore tests. CISA explicitly recommends testing your recovery process [1]. Not just verifying that backup files exist, but actually restoring and running them. I schedule a monthly restore drill to a clean directory. If it fails, the backup is worthless regardless of how many copies I have.

The repo-side habit that made all of this work: a bootstrap script that persists across container recreates. The entrypoint setup survives docker compose down && docker compose up -d without manual intervention, which means the restic cron jobs survive too. I wrote about that pattern entrypoint bootstrap that survives container recreates.

That two-line .gitignore diff turned out to be the commit that replaced my entire disaster recovery strategy. Sometimes the smallest diffs open the biggest rabbit holes.

Sources: