Hetzner Storage Boxes provide low-cost offsite backup storage with native snapshot capabilities. This guide covers mounting a Storage Box via CIFS, layering client-side encryption with Gocryptfs, making the encrypted mount persist correctly at boot, and integrating automated rsync pipelines with weekly retention logic backed by Hetzner’s snapshot system.
Prerequisites
The following items are required:
- Linux server with root access
- Hetzner Storage Box credentials and hostname
- cifs-utils package installed
- gocryptfs package installed
- SSH access configured between backup source and storage host
NOTE: The storagebox is available to the entire of Hetzner’s networks. If your intention is to backup or use this storage from outside of Hetzner’s networks, you need to enable that, specifically “External Reachability”.
It is a good idea to have the Storage Box set up in a location other than the data it backs up. For example, if all the servers are based in Germany, get the Storage Box set up in Finland or another distant location to avoid problems with regional internet disruptions or state-wide outages.
Backup Retention Strategy
Hetzner Storage Boxes include automatic daily snapshots with a 10-day rolling retention window. By organizing backup directories using ISO week numbers, the retention model provides two recovery tiers without requiring complex rotation scripts or additional storage overhead:
Daily snapshots provide granular recovery for the last 10 days. Any individual day within this window can be restored from Hetzner’s snapshot interface.
Weekly directories organized by ISO week number create a 52-week archive.
Combined, this structure allows recovery of any single day from the last 10 days and any week from the last year while storing only current active files on disk.
The resulting directory structure on the Storage Box follows this pattern:
/mnt/backups/
└── 30/ <-- ISO Week Number
└── server42/
├── home/
├── root/
├── etc/
├── opt/
└── dumps/ <-- Database exportsMounting the Storage Box with CIFS
Create the mount point directories on the backup storage host. Use /mnt/backups_raw for the raw CIFS share and /mnt/backups for the gocryptfs-decrypted view on top of it:
mkdir -p /mnt/backups_raw
mkdir -p /mnt/backupsCreate a credentials file at /etc/storagebox.creds:
username=u632495
password=YOUR_HETZNER_STORAGEBOX_PASSWORDSecure the credentials file:
chmod 600 /etc/storagebox.credsAdd the CIFS mount to /etc/fstab:
//u632495.your-storagebox.de/backup /mnt/backups_raw cifs credentials=/etc/storagebox.creds,iocharset=utf8,rw,uid=0,gid=0,file_mode=0600,dir_mode=0700,nobrl 0 0Mount the share:
mount /mnt/backups_rawConfiguring Client-Side Encryption with Gocryptfs
Gocryptfs provides FUSE-based transparent filesystem encryption. All encryption and decryption happens on the client before data traverses the network, so the storage provider never sees plaintext.
Install gocryptfs:
apt-get install gocryptfsInitialize the encrypted filesystem on the raw CIFS mount:
gocryptfs -init /mnt/backups_rawSave the generated master key and passphrase in an offline password manager. This key is required to decrypt the volume from any other host, and gocryptfs cannot recover it if it is lost.
Mount it by hand to confirm it works:
gocryptfs /mnt/backups_raw /mnt/backupsData written to /mnt/backups is now transparently encrypted before being stored in /mnt/backups_raw and transmitted over CIFS. Unmount it again before continuing, since the next section replaces this manual step with something that survives a reboot:
fusermount -u /mnt/backupsMaking Gocryptfs Mount Automatically at Boot
A manually-run gocryptfs command does not survive a reboot, and a backup job that fires before the mount exists will happily write plaintext straight into an empty, unencrypted directory instead of failing loudly. Both problems are solved by adding gocryptfs to /etc/fstab alongside the CIFS mount, with an explicit ordering dependency between the two.
Create a password file so gocryptfs does not need to prompt interactively at boot:
mkdir -p /etc/gocryptfs
nano /etc/gocryptfs/backups.pass # paste the gocryptfs password, save
chmod 600 /etc/gocryptfs/backups.passFind the actual gocryptfs binary path, since the fstab entry needs it explicitly rather than just the name:
which gocryptfsAdd the entry to /etc/fstab, directly below the CIFS line:
//u632495.your-storagebox.de/backup /mnt/backups_raw cifs credentials=/etc/storagebox.creds,iocharset=utf8,rw,uid=0,gid=0,file_mode=0600,dir_mode=0700,nobrl 0 0
/mnt/backups_raw /mnt/backups fuse./usr/bin/gocryptfs nofail,passfile=/etc/gocryptfs/backups.pass,x-systemd.requires-mounts-for=/mnt/backups_raw 0 0A few notes on what each piece is doing:
- The filesystem-type field is fuse. followed by the full path to the gocryptfs binary (from which gocryptfs above). This is how gocryptfs itself documents fstab integration, not just fuse.gocryptfs.
- passfile= points at the password file created above, letting the mount happen non-interactively.
- x-systemd.requires-mounts-for=/mnt/backups_raw is the actual fix for mount ordering. Since /etc/fstab entries are converted into systemd mount units, listing them in file order does not guarantee anything about the order they come up in. This option explicitly tells systemd that the gocryptfs mount depends on /mnt/backups_raw already being mounted, so the CIFS share is guaranteed to be in place first.
- nofail means that if the Storage Box is unreachable at boot (network hiccup, credential issue, Hetzner-side maintenance), the server still boots normally instead of hanging or dropping to an emergency shell. You will just be short a mount until it is manually fixed, which is far preferable to an unbootable backup server.
Test the entry without rebooting:
umount /mnt/backups 2>/dev/null
mount -av
mount | grep backupsThen verify the ordering survives an actual reboot, since mount -av alone will not catch systemd dependency-ordering mistakes:
reboot
# after it comes back up:
systemctl status mnt-backups_raw.mount mnt-backups.mountBoth should show as active, with mnt-backups.mount having started after mnt-backups_raw.mount.
If any other user besides root needs to read /mnt/backups, add allow_other to the gocryptfs fstab options and enable it system-wide first:
echo "user_allow_other" >> /etc/fuse.confVerifying Encryption from a Secondary Host
To verify zero-trust operation, mount and decrypt the Storage Box from an entirely separate host.
The secondary host requires the gocryptfs.conf file from the root of the encrypted volume and the master passphrase. Transfer the configuration securely:
scp <storagebox>/gocryptfs.conf /tmp/gocryptfs.confOn the secondary host, mount the raw CIFS share in read-only mode:
mkdir -p /mnt/test_raw /mnt/test_decrypted
mount -t cifs //u632495.your-storagebox.de/backup /mnt/test_raw -o credentials=/etc/storagebox.creds,roInspect the raw directory contents. Filenames should appear as obfuscated ciphertext:
ls -la /mnt/test_rawMount and decrypt the volume using gocryptfs:
gocryptfs /mnt/test_raw /mnt/test_decryptedVerify the decrypted contents match the original backup structure:
ls -la /mnt/test_decryptedUnmounting the decrypted filesystem immediately revokes access to plaintext data:
fusermount -u /mnt/test_decryptedAutomated Backup Script
The following example script executes on any server. It sends messages via discord, performs database dumps, syncs directories to a local staging area, then transfers the payload to the encrypted remote Storage Box mount using rsync over SSH.
Create /opt/backup/backup.sh:
#!/bin/bash
set -euo pipefail
/opt/discord/discord.sh "$(hostname -s)" "/opt/backup/backup.sh is starting..."
# --- CONFIGURATION ---
TARGET_HOST="server.domain.com"
TARGET_USER="root"
TARGET_BASE="/mnt/backups"
EXCLUDE_FILE="/opt/backup/backup_exclude.txt"
DB_DUMPS="/eph3/dumps"
# Directories to back up
FOLDERS=(
"/home"
"/root"
"/etc"
"/opt"
"/eph3/nextcloud"
"$DB_DUMPS"
)
# --- DYNAMIC DESTINATION ---
WEEK=$(date +%V)
HOST=$(hostname -s)
DEST_PARENT="${TARGET_BASE}/${WEEK}/${HOST}"
# --- DATABASE DUMPS ---
/opt/discord/discord.sh "$(hostname -s)" "Generating GitLab database archive..."
docker exec -t gitlab gitlab-backup create SKIP=uploads,builds,lfs,packages,terraform_state
mkdir -p "$DB_DUMPS/gitlab"
mv /opt/docker/gitlab/var/opt/gitlab/backups/*_gitlab_backup.tar "$DB_DUMPS/gitlab"
/opt/discord/discord.sh "$(hostname -s)" "GitLab dump complete."
echo "Preparing remote backup folder on ${TARGET_HOST}..."
ssh "${TARGET_USER}@${TARGET_HOST}" "mkdir -p ${DEST_PARENT}"
# --- RSYNC TO ENCRYPTED STORAGE ---
for src in "${FOLDERS[@]}"; do
folder_name=$(basename "$src")
/opt/discord/discord.sh "$(hostname -s)" "/opt/backup/backup.sh is syncing $src..."
if [ -f "$EXCLUDE_FILE" ]; then
rsync -avzx --delete --exclude-from="$EXCLUDE_FILE" "${src}/" "${TARGET_USER}@${TARGET_HOST}:${DEST_PARENT}/${folder_name}/"
else
rsync -avzx --delete "${src}/" "${TARGET_USER}@${TARGET_HOST}:${DEST_PARENT}/${folder_name}/"
fi
done
/opt/discord/discord.sh "$(hostname -s)" "/opt/backup/backup.sh finished successfully!"Make the script executable:
chmod +x /opt/backup/backup.shThe script can be run directly or scheduled via crontab to any required frequency. Since the script created week number based file system its optimal to have it run daily.
Global Exclusion Rules
Create /opt/backup/backup_exclude.txt to filter temporary files, sockets, and cache directories:
# --- Temp and Cache Files ---
*.tmp
*.temp
*.swp
*.bak
.DS_Store
.cache/
cache/
Cache/
# --- System and Runtime Sockets ---
*.sock
*.pid
/proc/
/sys/
/dev/
/run/
# --- Docker and Development ---
node_modules/
.venv/
venv/
docker/btrfs/
docker/overlay2/
# --- Application Logs ---
/logs/
log/
*.log
jellyfin/cache/
jellyfin/config/data/data/Verification
Execute a manual dry run to verify script operation:
bash -x /opt/backup/backup.shCheck Discord notifications for execution status messages. Verify the backup payload was written to the encrypted mount on the storage host:
ls -la /mnt/backups/$(date +%V)/server42/Inspect file timestamps and directory sizes to confirm rsync completed successfully. Test snapshot recovery through the Hetzner Storage Box web interface by browsing the .zfs/snapshot directory structure.
AI assistance is used on this site for language, formatting, and turning research into a consistent template. It is not used to perform the underlying research or verify technical claims. Every command, configuration, and step in this post is tested by hand before publication.