Backups and Data Management¶
Glassy provides automatic and on-demand backup options so your second brain is always safe.
Automatic daily backups¶
Glassy creates a plain SQLite backup of your database every day at 02:00 (server time). Backups are stored in /app/data/backups inside the glassy-data Docker volume, and approximately 7 days of backups are retained automatically. No configuration is required. On-demand dumps are written to the same directory (fully timestamped filenames) — you can trigger one yourself with the CLI below.
The automatic backups share the volume with the database.
/app/data/backupslives in the sameglassy-datavolume asnotes.db, so a volume-level failure (damaged volume, host disk loss) takes the database and every automatic backup together. The daily schedule covers "I overwrote something yesterday", not "the volume died". Keep an off-volume copy of the newest artifact (the full volume snapshot below, or a scheduleddocker cp glassy:/app/data/backups/notes-<latest>.dbpull) — and drill it (see Verify that a restore works).A missed backup also fails silently by default: the schedule's log carries no alarm you would read day to day, and a file-level drift check on the host stays green while a broken off-volume job dies on every fire (a systemd unit whose
ExecStartpath was retired exits203/EXECforever, for example). If your off-volume copy is driven by a timer of your own, verify the UNIT runs — check the artifact timestamps, not just the file bytes.
Freshness has a witness (v2.53.0+)¶
Two checks run without you:
scripts/check-backup-freshness.jsages the newest nightlynotes-*backup in each backup directory with the fleet's0/1/2exit contract. An event backup (pre-restore, pre-migration) cannot fake nightly freshness. A missing directory is skipped, unless you pass--strict./api/healthreports abackupblock —{newest, ageHours, stale}— so an appliance can see its own backup state instead of assuming it.
If you drive an off-volume copy with your own timer, assert the artifact timestamps, not just that the
unit is enabled. A retired ExecStart path exits 203/EXEC on every fire and changes nothing.
A recovery point before every migration¶
Any migration pass with pending work begins with one consistent VACUUM INTO snapshot taken from the live
connection (WAL-complete). It is named for the last pending migration, encrypted when
BACKUP_ENCRYPTION_KEY is set, and retained to three. A snapshot failure is loud and non-blocking —
transactions still cover table rebuilds — and a routine boot with nothing pending takes no snapshot.
Verify that a restore works (drill)¶
"Backups run" and "backups can be restored" are different claims. When you need the second one is the wrong time to learn that they differ, so run a drill that keeps your live instance untouched:
- Pull the newest artifact out of the volume onto the host:
- Check it opens and its content is real (SQLite integrity + row counts, run on the host where
sqlite3is installed):
sqlite3 restore-drill.db 'PRAGMA integrity_check;' # → ok
sqlite3 restore-drill.db 'SELECT COUNT(*) FROM notes;' # → a plausible, non-zero count
- For the encrypted (
.db.enc) artifacts, the drill needs the backup key — decrypt withBACKUP_ENCRYPTION_KEYin reach (the restore command in Restore below handles.db.encdirectly and transparently, so the drill can equally be "restore + read-back into a scratch container"). - Repeat on a schedule that matches your risk — a monthly drill is the difference between "backups run" and "backups can be trusted". A backup that has never been restored is a hypothesis, not a recovery plan.
On-demand backups (CLI)¶
For encrypted off-machine copies, use the backup CLI. The runtime image has no npm, so call node directly:
# Create a backup
docker compose exec glassy node server/utils/backup.js
# List available backups
docker compose exec glassy ls -1 /app/data/backups
# Restore from a backup
docker compose exec glassy node server/utils/backup.js --restore <backup-file>
Encryption¶
Set BACKUP_ENCRYPTION_KEY in .env to encrypt on-demand CLI backups (AES-256-GCM). Backups are saved as .db.enc files; the plain .db is removed after encryption.
When BACKUP_ENCRYPTION_KEY is set, the automatic daily backup scheduler also encrypts its output using the same key and format.
Important: Store
BACKUP_ENCRYPTION_KEYsomewhere safe outside the machine. Without it, encrypted backups cannot be restored.
Full volume snapshot¶
To capture everything — the database, uploads, logs, and generated backups — in a single archive:
docker run --rm -v glassy-data:/data -v $(pwd):/backup alpine \
tar czf /backup/glassy-backup.tar.gz -C /data .
Restore¶
The restore command auto-detects .db.enc (encrypted) and .db (plain) files, takes a pre-restore safety copy, and waits 5 seconds before overwriting the live database (press Ctrl+C to abort):
For a volume restore from a snapshot:
docker compose down
docker volume rm glassy-selfhost_glassy-data
docker volume create glassy-selfhost_glassy-data
docker run --rm \
-v glassy-selfhost_glassy-data:/data \
-v $(pwd):/backup \
alpine tar xzf /backup/glassy-backup.tar.gz -C /data
docker compose up -d
Database migrations run automatically on start after a restore.
Storage caps¶
Storage limits are configurable per tier via environment variables:
| Variable | Default |
|---|---|
FREE_STORAGE_MB |
10 |
PRO_STORAGE_MB |
200 |
CLEAR_STORAGE_MB |
200 |
ENTERPRISE_STORAGE_MB |
51200 |