External SSD enclosures and a NAS unit with drive bays on a wooden shelf, status LEDs glowing
Concept visualization: external SSDs and a NAS forming the local storage tier of the stack. Generated illustration, not a photo of the actual setup.
2025 to 2026 Infrastructure / Storage / Privacy GitHub ↗

Overview

This stack replaces Google Drive and Google Photos with fully owned infrastructure. Nextcloud AIO and Immich run in Docker on a BTRFS pool of external USB 3.2 SSDs, with per-user ACLs enforcing isolation and zstd compression reducing capacity needs. A 3-2-1 backup design keeps local BTRFS snapshots as fast recovery points, replicates encrypted archives to BorgBase, and keeps a secondary copy on rsync.net, aiming for GDPR-aligned retention with monthly restore drills.

Storage Architecture

Backup Flow (3-2-1)

Implementation

1

Create the compressed BTRFS pool [PRIVILEGED]

Warning: mkfs.btrfs destroys all data on the target disks. Use stable /dev/disk/by-id/ names, never /dev/sdX (those can reorder between boots), and double-check each name before running. Note that -d single stripes data with no redundancy: one failed SSD loses the whole pool. That is a deliberate capacity tradeoff here; redundancy comes from the offsite backups, not the pool itself.

# Identify the disks by stable ID first ls -l /dev/disk/by-id/ # DESTRUCTIVE: creates the filesystem (replace with your disks' by-id names) mkfs.btrfs -d single -L data \ /dev/disk/by-id/usb-Samsung_SSD_A-0 \ /dev/disk/by-id/usb-Samsung_SSD_B-0 mount -o compress=zstd:3 /dev/disk/by-id/usb-Samsung_SSD_A-0 /mnt/data
2

Deploy Nextcloud AIO and Immich

Run each stack as its own Compose project. Do not merge unrelated stacks with multiple -f flags; that merges their networks, volumes, and service namespaces into one project.

docker compose -f nextcloud-aio.yml up -d docker compose -f immich.yml up -d
3

Encrypted offsite backup with Borg

Initialize the repository once with encryption, and store the passphrase somewhere safe: losing it loses the backups. Databases need consistent dumps: enable Nextcloud maintenance mode during the backup and pg_dump Immich's Postgres into the data tree first, because backing up open database files can produce unrestorable copies. A BTRFS snapshot is a local recovery point, not a backup; the offsite Borg archives are the backups.

# One-time: initialize the encrypted repository export BORG_REPO="ssh://[email protected]/./repo" export BORG_PASSPHRASE="a-long-passphrase-stored-in-a-password-manager" borg init --encryption=repokey-blake2 "$BORG_REPO" # Quiesce and dump databases before the backup mkdir -p /mnt/data/db-dumps docker exec nextcloud-aio-nextcloud php occ maintenance:mode --on docker exec immich_postgres pg_dump -U postgres immich > /mnt/data/db-dumps/immich.sql # Snapshot, then back up (container names are examples; use yours) btrfs subvolume snapshot -r /mnt/data /mnt/data/.snapshots/daily borg create --stats "$BORG_REPO::data-{now}" /mnt/data docker exec nextcloud-aio-nextcloud php occ maintenance:mode --off # borg prints archive statistics with --stats; the first run takes the longest

Key Results

  • 3-2-1 backup design in place: three copies, two offsite (BorgBase and rsync.net); restore drills run monthly.
  • Replaces recurring cloud storage subscriptions with owned hardware (exact savings depend on capacity and drive prices).
  • External USB 3.2 SSDs form the fast local tier (measure throughput on your own hardware before quoting numbers).
  • Enforced multi-user isolation via filesystem ACLs preserved across container mounts.

How I built it

This project consolidates two goals I cared about: owning my files and photos outright, and surviving hardware failure or a house fire. The architecture separates the fast local tier from two independent encrypted offsite copies.

  1. Phase 1, build the local storage pool: Created a BTRFS pool across external USB 3.2 SSDs with zstd compression, giving transparent checksumming and cheap snapshots on commodity drives.
  2. Phase 2, deploy the services: Brought up Nextcloud AIO for files and Immich for photos as Docker containers on top of the pool.
  3. Phase 3, isolate users: Configured multi-user ACL isolation so each person's data stays in their own namespace.
  4. Phase 4, wire the 3-2-1 backups: Set up encrypted Borg backups to BorgBase and a second copy to rsync.net, keeping three copies with two offsite.
  5. Phase 5, prove restores work: Made restore testing a monthly routine, because a backup you have never restored is only a hope.

What you need

CategoryItemNotes
HardwareExternal USB 3.2 SSDsSized to hold files, photos, and snapshot history with headroom
HardwareLinux host with USB 3 portsRuns the containers and the BTRFS pool
SoftwareDocker and Docker ComposeHosts Nextcloud AIO and Immich
AccountsBorgBase and rsync.net accountsThe two independent offsite destinations
SkillsBTRFS administration and Borg basicsSnapshots, compression options, prune and check commands

Challenges and lessons learned

  • USB-attached storage reliability: external drives can drop off the bus under load, which BTRFS notices immediately. Lesson: use quality enclosures and cables, and monitor dmesg after any disconnect.
  • Backup verification discipline: it is easy to let backups run silently for months. Lesson: schedule the monthly restore test like any other maintenance window and keep a log of each one.
  • Deduplication surprises: Borg's deduplication keeps offsite sizes manageable, but the first full backup is still the long pole. Lesson: seed the initial backup over a fast local link before switching to incremental uploads.

What is next

  • Add a second local mirror on separate hardware for faster recovery from total pool loss.
  • Send BTRFS snapshots offsite directly as an additional recovery path.
  • Add SMART monitoring with alerts so drive degradation is caught before failure.
  • Evaluate an object-storage tier for cold archival of old photo originals.
NextcloudImmichBTRFSDockerBorgBasersync.netBackupPrivacy