My backups in 2026: seafile and restic
| 1564 words | 8 minutes
I’ve been neglecting backing up my data. Reading posts such as “Backing up data like the adult I supposedly am”, I figured it’s finally time to move forward from an external HDD I’ve been making simple copies to one or two times a year.
This is what I’ve started with: one desktop, two servers, and one external HDD. The data moves between these only when manually triggered: either (A) re-arranging files, such as old projects or photos, into “long term storage” folder on a separate drive attached to the same computer, or (B) using rsync to copy relevant data from all places to the external drive.
I’ve documented every time I performed the manual sync to the external drive: 2 times in 2022, 2 in 2023, once in 2024, and once in 2025. The smallest delta is 83 days and largest is 364 days. It never occurred to me before, but now, while reviewing this data, it finally hit me that throughout the last few years I’ve had practically no backups of recently-created files. I’m also not sure what purpose the RAID1 served; /mnt/OnlineBackup wasn’t actively accessed, so even if one of the drives went down, I would probably not have noticed that for months.
After some re-arranging, the latest iteration of the backup system relies on one single server handling everything related to file sharing and backups. The “server” part is important, as simply relying on myself alone is not a viable strategy, so the server MUST be automated.
Right now, the server hosts seafile for general “OneDrive functionality”, such as immediately syncing file changes between different machines and sharing folders between multiple people/machines. Seafile has data retention functionality, which is great for “I changed/deleted a file one hour ago and I want to restore it” situations. However, seafile is slightly complicated with considerations of data and databases as for the long-term storage you’d have to consider all docker containers and whatnot. This is where the restic comes into play – to provide simple snapshotting. Similarly to seafile, restic also splits files into chunks for efficient de-duplication, but I like that all of that is supported by a single executable (compared to seafile’s multi-step recovery process).
I also find some joy in the names of the data drives. For a long time, I’ve named the servers after gods from Norse Mythology, so midgard being the humans’ realm (which I’m from) and bifrost being the bridge between gods’ realm (the servers) and midgard just seems appropriate. Similarly, the air gapped copy being, in some sense, “frozen in time” seems to allude heavily to jotunheim, often represented as an icy realm. Then again, Jotunheim is also associated with “chaotic nature” (ref), so the associations break down quickly, as I definitely don’t want any chaos in my stable data snapshots!
The data moves as follows:
A. Desktop uses seafile-client, which immediately sync changed files to the backup server.
This requires the backup server to always be online. Encrypting your drives is generally considered a good hygiene, so for the server system to “just work”, I’ve created a keyfile for LUKS, and
*.mountand*.servicesystemd files (similarly to these instructions). This way, the server can continue running unattended upgrades with automatic reboots, without me lifting a finger.Seafile has also a separate “drive client”, which acts as a network drive, providing access to files without requiring a full download of a library to a local drive. I’m using this client sometimes to move data to the archives. For example, I prefer not to keep 100 GB+ of photos/videos locally available at all times, so when my phone’s storage becomes full, I move those photos to seafile using the drive client.
B. Normal servers use Seafile’s REST API to upload daily snapshots of their data.
Using Seafile’s APIs is quite easy. I’ve set up a “Servers” library, generated an API token for each client (Library -> Advanced -> API token), and then set up a bash script for each server to collect all relevant data and upload them to the seafile library.
seafile_upload function
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34seafile_upload() { local FILEPATH="$1" if [ ! -f "$FILEPATH" ]; then echo "File $FILEPATH not found!" return 0 fi echo "Uploading $FILEPATH..." # Get upload link UPLOAD_URL=$(curl --connect-timeout 30 \ --max-time 120 \ -sS \ -H "Authorization: Token ${AUTH_TOKEN}" \ "${DOMAIN}/api/v2.1/via-repo-token/upload-link/?path=/${HOST}") # Remove quotes and trailing whitespace/characters UPLOAD_URL="${UPLOAD_URL//\"}" # Upload the file RESPONSE=$(curl --connect-timeout 30 \ --max-time 60 \ -sS \ "$UPLOAD_URL" \ -F "file=@$FILEPATH" \ -F "parent_dir=/${HOST}" \ -F "relative_path=$TIMESTAMP") echo "Uploaded $FILEPATH, server response: $RESPONSE\n" } seafile_upload "serviceFoo.tar.bz2" seafile_upload "serviceBar.tar.bz2"C. Backup server uses restic to take a weekly snapshot of seafile’s libraries.
This relies on “normal” file access to the seafile’s data storage, which it supports well with FUSE extension. The essence of the backup script becomes simply mounting the long-term storage drive (
midgard) and calling restic to create a snapshot from seafile’s short-term storage (bifrost). The restic storage is not used for any other purpose, so I keep it unmounted by default, just in case.Relevant parts of the weekly backup script
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20## Setup systemctl start mnt-midgard.mount docker exec seafile /opt/seafile/seafile-server-latest/seaf-fuse.sh start /shared-fuse ## Act restic \ -r "/mnt/midgard/restic-repo" \ --password-file="/.../keys/{{ midgard_uuid }}" \ backup "/mnt/bifrost/seafile-fuse" restic \ -r "/mnt/midgard/restic-repo" \ --password-file="/.../keys/{{ midgard_uuid }}" \ forget \ --keep-within-daily 7d --keep-within-weekly 1m --keep-within-monthly 1y --keep-within-yearly 75y \ --prune ## Cleanup docker exec seafile /opt/seafile/seafile-server-latest/seaf-fuse.sh stop systemctl stop mnt-midgard.mountD. Final manual step is to copy restic snapshots to air gapped drive.
Finally, the only manual step is for maintaining an air gapped copy of the long-term storage on
bifrost. This is easily setup by callingrestic -r ... init --from-repo ...; the important aspect is to also copy the chunker parameters, so that files are split into identical chunks.The main difference in the script content is the manual mounting of
jotunheim, the air gapped copy. The only purpose of manual operations here is to not have a separate automatic LUKS key on the device, but I’m not sure if that makes much sense, as themidgardanyway has the exact same content and it can be opened automatically.Relevant parts of the manual backup script
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24# Setup systemctl start mnt-midgard.mount mkdir -p /mnt/jotunheim /usr/sbin/cryptsetup luksOpen /dev/disk/by-uuid/{{ jotunheim_uuid }} jotunheim mount /dev/mapper/jotunheim /mnt/jotunheim # Act # Note, repo 1 is source, repo 2 is destination. restic \ -r "/mnt/midgard/restic-repo" \ --password-file="/data/backup-scripts/keys/{{ midgard_uuid }}" \ --repo2="/mnt/jotunheim/restic-repo" \ copy restic \ -r "/mnt/jotunheim/restic-repo" \ forget \ --keep-within-daily 7d --keep-within-weekly 1m --keep-within-monthly 1y --keep-within-yearly 75y \ --prune # Cleanup systemctl stop mnt-midgard.mount umount /mnt/jotunheim /usr/sbin/cryptsetup luksClose jotunheim
As a bit of an “nice to have” extra functionality, the weekly script is also doing two additional things: uploading a copy to AWS S3 and pinging healthchecks.io with the backup logs. Overall, the script completes in less than 3 hours:
- 2 hours for processing 200,000+ files / 200+ GiB into a new restic snapshot.
- 3 minutes for pruning old restic snapshots and garbage collecting seafile’s data.
- 35 minutes for running
restic check. - 10 minutes for uploading ~2 GB of new data to AWS.
Restoration of data is quite straightforward. With seafile’s web UI, for libraries that enabled history preservation, there’s historical view of changes and option to restore/download relevant files. With restic, there’s options to restore full/partial snapshot to a specified folder, or mount a snapshot with FUSE. The former is faster, the latter could be more convenient for browsing files first and selecting only few of them.
The backup system attempts to follow the 3-2-1 strategy: 3 copies, 2 devices, 1 offsite. However, by my count, I actually have 5 copies: one active (on server or on desktop), one in seafile (bifrost), one in attached restic (midgard), one in airgapped restic (jotunheim), one in AWS. More copies is probably not a good thing in itself. I do like the idea of having one airgapped copy, in case automation breaks and all backup scripts get replaced with rm -rf /. I also like the offsite copy, for cases when physical preservation of copies becomes a problem. The main struggle I find is with justifying the extra restic copy on midgard. Seafile does support S3 storage but it’s in Pro Edition only and I’m “fiscally stingy”. Alternatively, I could host the ‘primary restic repo’ in the AWS, but that would mean the manual backup would require me downloading the S3 files back again (which is slow and I’m still “fiscally observant”). So, in some sense, it would be nice to have a program that would merge seafile and restic into one package, which, in turn, would allow me to merge bifrost and midgard into one. Then again, I bought the SSDs in 2025 while they were still relatively cheap, so one extra drive/copy is not the biggest issue in the world.
Overall, the best aspect of this system is that it’s automated, even if it’s a bit over-engineered.