Portal Community
Backup Overview InfraHub Storage uses restic for encrypted incremental backups stored in Backblaze B2. Backups run daily at 02:00 UTC, retain 7 daily / 4 weekly / 12 monthly / 2 annual snapshots, and are AES-256 encrypted with a key you control. No data is ever stored in plaintext outside your MinIO volume.

Why restic + Backblaze B2?

The combination of restic and Backblaze B2 provides enterprise-grade data protection at a fraction of the cost of native S3 backup solutions. Both components are production-proven, open-standard, and interoperable with any S3-compatible toolchain.

Component Why We Chose It Key Properties
restic Open-source, actively maintained, zero trust model — encryption happens on the client before any data leaves your host. Block-level deduplication — only changed blocks are uploaded
AES-256-CTR encryption with Poly1305 authentication
Git-style snapshot model with tagged retention policies
Fast incremental backups — typically completes in under 2 minutes for <50 GB datasets
Cross-platform: runs in any Docker container
Backblaze B2 S3-compatible API with significantly lower cost than AWS S3, free egress to Cloudflare and Fastly CDN partners, and a generous free tier. ~$6/TB/month storage (vs. AWS S3 ~$23/TB/month)
S3-compatible API — no vendor-specific SDK required
Free egress to Cloudflare, Fastly, and other CDN partners
10 GB free tier (sufficient for most development backups)
99.999999% durability SLA

Architecture

The backup pipeline runs as a separate Docker container. It mounts the MinIO data volume read-only, so it cannot modify or corrupt production data. Encrypted chunks are pushed directly to Backblaze B2 via the S3-compatible API. The production MinIO service is never paused or disrupted during backup.

MinIO Volume minio-data (read-only mount)
restic Container bizfirst-backup
AES-256 Encrypt on-host, pre-upload
Backblaze B2 Encrypted chunks

The backup schedule is driven by the ofelia job scheduler (see Setting Up the Cron Schedule):

EventScheduleDetails
Daily backup run 02:00 UTC Full incremental backup of MinIO data volume; applies retention policy; samples 5% of packs for integrity check
Retention prune After each backup Automatically removes snapshots outside the retention window; reclaims B2 storage
Integrity check After each backup restic check --read-data-subset=5% — verifies 5% of encrypted packs are readable and uncorrupted

Docker Compose — Backup Service

Add the following service to your existing docker-compose.yml alongside the minio service. The backup container mounts the MinIO data volume as read-only and the backup scripts directory.

# docker-compose.yml — append to existing services block

  backup:
    image: restic/restic:latest
    container_name: bizfirst-backup
    environment:
      RESTIC_REPOSITORY: s3:https://s3.us-west-004.backblazeb2.com/bizfirst-backup
      RESTIC_PASSWORD: ${BACKUP_ENCRYPTION_KEY}
      AWS_ACCESS_KEY_ID: ${B2_APPLICATION_KEY_ID}
      AWS_SECRET_ACCESS_KEY: ${B2_APPLICATION_KEY}
    volumes:
      - minio-data:/data:ro
      - ./backup-scripts:/scripts
    entrypoint: /bin/sh
    command: /scripts/backup.sh
    depends_on:
      - minio

The container does not run continuously. It is invoked on demand by the cron scheduler (see Setting Up the Cron Schedule). When triggered, it runs backup.sh, completes, and exits. No long-running backup daemon is required.

Volume Mount: Read-Only The :ro flag on the minio-data mount ensures the backup container has no write access to the production data volume. Even a compromised backup container cannot modify or delete production files.

Backup Script

Create the script at ./backup-scripts/backup.sh relative to your docker-compose.yml. Ensure it is executable (chmod +x backup.sh). The script handles first-run initialization, backup, retention enforcement, and integrity verification in a single execution.

#!/bin/sh
# backup.sh — restic backup script for InfraHub Storage
# Runs inside the bizfirst-backup container

# Initialize repository if it does not yet exist
# (idempotent — safe to run on every invocation)
restic snapshots > /dev/null 2>&1 || restic init

# Run incremental backup of the MinIO data volume
restic backup /data \
  --tag bizfirst-storage \
  --hostname bizfirst-minio

# Apply retention policy and prune orphaned pack files
restic forget \
  --keep-daily   7  \
  --keep-weekly  4  \
  --keep-monthly 12 \
  --keep-annual  2  \
  --prune

# Verify latest snapshot integrity (samples 5% of pack files)
# Full --read-data check can be run manually on a schedule (e.g., monthly)
restic check --read-data-subset=5%

echo "Backup completed: $(date)"

The --tag bizfirst-storage flag labels every snapshot, making it easy to filter snapshots in restic snapshots --tag bizfirst-storage. The --hostname flag identifies the source host in multi-host restic repositories.

Retention Policy

The retention policy balances recovery granularity against storage cost. Recent backups are kept daily for maximum recovery precision; older backups are consolidated to weekly, monthly, and annual checkpoints.

Policy Count Explanation Recovery Scenario
Daily 7 One backup per day for the last 7 days Recover from accidental deletion or corruption discovered within the last week
Weekly 4 One backup per week for the last 4 weeks Recover from issues discovered 1–4 weeks ago (e.g., silent data corruption)
Monthly 12 One backup per month for the last 12 months Regulatory / compliance restore; recover from ransomware discovered weeks later
Annual 2 One backup per year for the last 2 years Long-term archival; legal hold scenarios; year-end document recovery

With this policy, approximately 25 snapshots are retained at any given time. Due to restic's block-level deduplication, the total B2 storage consumed by 25 snapshots of a 50 GB dataset is typically less than 3× the dataset size — restic only stores unique blocks across all snapshots. Snapshots outside the retention window are automatically pruned during each backup run.

How restic Deduplication Works restic splits files into variable-length content-defined chunks using a rolling hash. Identical chunks across different snapshots are stored exactly once in the repository. A daily backup of a 50 GB dataset where only 1 GB changed overnight uploads approximately 1 GB, not 50 GB. Over a year, total B2 usage typically stays well under 3× the dataset size even with 25+ snapshots retained.

Setting Up the Cron Schedule

The backup container is triggered by ofelia, a Docker-native cron scheduler. Ofelia reads scheduling configuration from Docker container labels and triggers the target container at the configured interval without requiring a separate system cron or SSH access.

# docker-compose.yml — add alongside the backup service

  backup-scheduler:
    image: mcuadros/ofelia:latest
    container_name: bizfirst-backup-scheduler
    command: daemon --docker
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    labels:
      ofelia.job-run.backup.schedule: "0 2 * * *"   # 02:00 UTC daily
      ofelia.job-run.backup.container: bizfirst-backup
    restart: unless-stopped

The cron expression 0 2 * * * means: at minute 0, hour 2, every day, every month, every weekday. Adjust the hour to match your lowest-traffic window if 02:00 UTC is not ideal for your deployment region.

FieldValueMeaning
Minute0At the top of the hour
Hour202:00 UTC
Day of month*Every day
Month*Every month
Day of week*Every weekday
Docker Socket Access Ofelia requires access to /var/run/docker.sock to start and stop containers. This grants the ofelia container elevated privileges. Ensure your Docker host is hardened appropriately and that the ofelia image is pinned to a specific version in production environments.

Verifying a Backup

Always verify your backup configuration after initial setup, and perform periodic manual verifications (at minimum monthly). A backup that has never been verified is not a backup — it is an untested assumption.

List All Snapshots

# List all snapshots in the repository
docker exec bizfirst-backup restic snapshots

# Filter by tag
docker exec bizfirst-backup restic snapshots --tag bizfirst-storage

# Show detailed snapshot info
docker exec bizfirst-backup restic snapshots --verbose

Check Repository Integrity

# Fast structural integrity check (no data download)
docker exec bizfirst-backup restic check

# Check with data verification — samples 10% of pack files (recommended monthly)
docker exec bizfirst-backup restic check --read-data-subset=10%

# Full data verification — reads ALL pack files from B2 (run quarterly)
# This will incur B2 API call costs for large repositories
docker exec bizfirst-backup restic check --read-data

Inspect a Specific Snapshot

# List files in the latest snapshot
docker exec bizfirst-backup restic ls latest

# List files in a specific snapshot by ID
docker exec bizfirst-backup restic ls abc12345

# Show snapshot stats (unique data, total size)
docker exec bizfirst-backup restic stats latest
Expected Output from a Healthy Repository A successful restic check ends with: no errors were found. If any errors appear, investigate immediately — do not wait for the next scheduled backup. Contact your infrastructure team and consider restoring from a known-good snapshot.

Environment Variables Required

All sensitive values are passed to the backup container as environment variables from your .env file or secrets manager. Never hard-code these values in docker-compose.yml or the backup script.

Variable Description Example Format
BACKUP_ENCRYPTION_KEY The AES-256 encryption key for the restic repository. This is the only key that can decrypt your backups. If lost, all backups are permanently unreadable. 64+ character random string (use a password manager to generate)
B2_APPLICATION_KEY_ID Backblaze B2 application key ID. Generated in the B2 dashboard under App Keys. Should be scoped to the backup bucket only. 0012345abcdef...
B2_APPLICATION_KEY Backblaze B2 application key secret. Shown only once at creation time — store immediately in your secrets manager. K001AbCdEfGhIj...
RESTIC_REPOSITORY Full restic repository URL pointing to your B2 bucket. Format: s3:https://<endpoint>/<bucket-name> s3:https://s3.us-west-004.backblazeb2.com/bizfirst-backup
Critical: BACKUP_ENCRYPTION_KEY Cannot Be Recovered If you lose BACKUP_ENCRYPTION_KEY, your backups are permanently and irreversibly unreadable. restic uses zero-knowledge encryption — there is no recovery mechanism, no master key, and no support process that can recover your data. Store this key in a password manager (1Password, Bitwarden) AND a hardware-backed secrets manager (HashiCorp Vault, AWS Secrets Manager, Azure Key Vault). Keep at least two independent copies in different locations. Never store it only in the .env file.

Generating a Strong Encryption Key

# Linux / macOS — generate a 64-character random key
openssl rand -base64 48

# Windows PowerShell
[Convert]::ToBase64String([System.Security.Cryptography.RandomNumberGenerator]::GetBytes(48))

# Or use your password manager's secure password generator (64+ chars, full character set)

Monitoring Backup Status

The backup container writes all output to stdout, which Docker captures as container logs. In a Grafana + Loki setup, these logs are automatically indexed and queryable.

Viewing Backup Logs

# View the last backup run logs
docker logs bizfirst-backup

# Follow logs of the next backup run (useful during testing)
docker logs bizfirst-backup --follow

# View logs with timestamps
docker logs bizfirst-backup --timestamps

Grafana + Loki Alert: Missing Daily Backup

In your Grafana instance, create a Loki-based alert rule that fires when the expected daily "Backup completed" log line has not appeared within 26 hours. The 26-hour window gives a 2-hour grace period beyond the 24-hour schedule interval, accommodating minor delays without false positives.

# Loki query — counts "Backup completed" lines in last 26 hours
# Alert fires when count = 0
count_over_time(
  {container="bizfirst-backup"} |= "Backup completed"
  [26h]
)

Configure the alert rule in Grafana as follows:

SettingValue
Data sourceLoki
Querycount_over_time({container="bizfirst-backup"} |= "Backup completed" [26h])
ConditionIS BELOW 1
Evaluate every1h
For30m (wait 30 min before firing to avoid transient gaps)
Alert messageInfraHub daily backup has not completed in the last 26 hours
SeverityCritical
Backup Failure Is a Critical Event A missed backup means your recovery point objective (RPO) has slipped. If the backup alert fires, investigate immediately: check container logs, verify B2 credentials are valid, confirm the MinIO volume is mounted, and check available disk space on the Docker host. Do not dismiss the alert until the next successful backup has run.

Prometheus Metric (Optional)

For teams running Prometheus, the backup script can emit a metric via the Pushgateway at the end of each run. Add the following to the end of backup.sh:

# Push backup timestamp metric to Prometheus Pushgateway
BACKUP_TIMESTAMP=$(date +%s)
echo "bizfirst_backup_last_success_timestamp $BACKUP_TIMESTAMP" | \
  curl --data-binary @- \
  http://pushgateway:9091/metrics/job/restic_backup/instance/bizfirst-minio

Then create a Prometheus alert on time() - bizfirst_backup_last_success_timestamp > 90000 (25 hours) to catch missed backups.

Backup Storage Cost Estimate

Backblaze B2 charges $6.00/TB/month for storage (as of 2026). With restic's block-level deduplication and incremental backups, the total backup repository size is typically 1.5× to 3× the current dataset size, depending on how frequently files change.

Egress fees are $0.00 when restoring to a Cloudflare-protected host (Cloudflare is a B2 bandwidth alliance partner). For non-Cloudflare egress, B2 charges $0.01/GB after the first 1 GB/day free.

MinIO Dataset Size Estimated Repo Size (2×) Monthly B2 Cost Annual B2 Cost
10 GB ~20 GB ~$0.12 ~$1.44
50 GB ~100 GB ~$0.60 ~$7.20
250 GB ~500 GB ~$3.00 ~$36.00
1 TB ~2 TB ~$12.00 ~$144.00
5 TB ~10 TB ~$60.00 ~$720.00

For comparison, storing the same backup repository in AWS S3 Standard would cost approximately 3.8× more at $23/TB/month. For most BizFirst AI deployments (under 250 GB of production file data), the total annual backup cost on B2 is under $40.

B2 Free Tier The first 10 GB of B2 storage is permanently free, with 1 GB/day of free download. For development and staging environments with small datasets, backup storage costs nothing. Use separate B2 buckets and application keys for production vs. non-production environments.

Estimating Your Actual Deduplication Ratio

After your first week of backups, run restic stats to see the actual repository size vs. total data size. This gives you a precise deduplication ratio for your workload:

# Show repository statistics including dedup ratio
docker exec bizfirst-backup restic stats --mode raw-data

# Example output:
# Stats for all snapshots in restore-size mode:
# Total File Count:   142,847
# Total Size:         48.231 GiB   (your MinIO data size)
# Stats for all snapshots in raw-data mode:
# Total Blob Count:   89,234
# Total Size:         12.441 GiB   (actual B2 usage after dedup)

In the example above, 7 daily snapshots of 48 GB of data occupy only 12 GB in B2 — a 3.9× deduplication ratio. For datasets where files rarely change (archived invoices, completed reports), deduplication ratios above 10× are common.