Tag: Minio

S3-compatible object storage you run yourself: buckets, keys and backups in one container.

  • The 3-2-1 Backup Strategy for a Home Server (with restic)

    The 3-2-1 Backup Strategy for a Home Server (with restic)

    Every other guide on this site ends with “back up the volume” — this one is what that actually means. The 3-2-1 rule (three copies, two different media, one off-site) is the floor, not the ceiling, for a self-hosted home, and restic is the tool that makes it boring: incremental, encrypted, deduplicated backups to S3 that run unattended and verify themselves. This guide builds the whole loop — what to back up, the restic setup, the cron job, and the restore test that separates a real backup strategy from a hope.

    Intermediate · 12 min · Docker

    The rule, translated to a home server

    • 3 copies of every file that matters: the live data, a local backup on a second disk, and an off-site copy.
    • 2 different media: your SSD and the backup disk are different devices; the off-site copy is a different medium entirely (S3, another machine, an encrypted drive in a different building).
    • 1 off-site: fire, flood, theft and ransomware that spreads over your LAN all take out everything on-site. Exactly one copy must live somewhere the house cannot reach.

    The version this site runs: restic on the server, three targets — a second local disk (fast restores of yesterday’s data), MinIO on a second machine (the “off-site” for a home network), and an encrypted local drive you take to a friend’s house or a bank box once a month (the real off-site). You do not need all three on day one; you need the off-site one eventually, and restic makes adding targets later a one-line change.

    Why restic (and not the obvious alternatives)

    resticBorgplain rsync to a disk
    Incremental + deduplicatedYesYesNo (full copies)
    Encrypted at restYesYesNo
    Native S3 targetYesYes (via restic/borgbase)No
    Self-check (verify)Built inBuilt inYou build it
    Retention policies (“keep last 7 daily”)One flagOne flagA script

    rsync-to-a-disk is not a backup strategy: it copies what is there, including deletions and ransomware, with no encryption and no version history. restic’s snapshot model — every backup is a named, restorable state of the file set — is what lets you roll back to “before the bad thing happened” instead of “before the last sync”.

    Step 1: What to back up (the inventory)

    Walk your stacks and write down the state directories. For the services on this site, the list is short and stable:

    StackPath to back upNotes
    Navidromethe navidrome_data volumeDB with play counts; the music folder is source data, see below
    Jellyfinconfig volumeMetadata/DB; the media library is source data
    MinifluxPostgres volumeSubscriptions and history
    DokuWikithe /data folderIt is plain-text files — trivial to verify by eye
    Home Assistant./configEntire setup, tens of MB
    Vaultwarden./dataEncrypted already, but back it up anyway
    Gitea./giteaRepositories + DB
    Immich./upload + DB dumpThe photos are the whole point

    Two distinctions keep this list from being a trap:

    • State vs source data. A service’s database and config are state — small, and the thing restic handles beautifully. Your music, photos and videos are source data — large, and often already stored in a library that is itself the archive. Back up the state everywhere; for source data, decide explicitly (photo library → Immich’s upload/ is the archive, so restic it too, or at least the DB dump; music you own on a card → a one-off full copy to the second disk is enough).
    • Named Docker volumes. Everything above that is a named volume (navidrome_data, Postgres data) lives in /var/lib/docker/volumes/<name>/_data. Either back that path directly, or bind-mount the stacks’ state into project folders (the convention used in every guide on this site, which is exactly why the inventory is a flat list of paths).

    Step 2: Install and initialize restic

    sudo apt install restic   # or your package manager's equivalent
    restic -r /mnt/backup-disk/init --password-file ~/.config/restic/pass init
    restic -r s3:http://192.168.1.20:9000/backups \
      --s3-provider minio \
      --s3-access-key BACKUP_USER_KEY --s3-secret-key BACKUP_USER_SECRET \
      --s3-region us-east-1 \
      --password-file ~/.config/restic/pass init

    Three things to get right here:

    • The repository is a URL, and the same URL must be used for every command against it. Write both of yours into a file (e.g. ~/.config/restic/repos.txt) and copy from there. A one-character difference means “repository not found” at 2 a.m.
    • The password file. chmod 600 it. This password encrypts everything in the repository — losing it means losing the backups, so it lives in two places outside the backup targets (a password manager entry, and written paper). restic will not recover it for you; that is a feature.
    • A dedicated S3 user. From the MinIO guide: a non-root user with a policy scoped to the backups bucket. The backup job should be able to do exactly one thing.

    Step 3: The backup command

    Put the paths from the inventory in one file, ~/.config/restic/paths.txt (one per line; Docker volume paths included), and the actual job becomes:

    restic -r /mnt/backup-disk/init --password-file ~/.config/restic/pass \
      backup $(cat ~/.config/restic/paths.txt | tr '\n' ' ') \
      --tag daily

    Run it once by hand and watch it work: the first run is a full backup (every file), every run after is incremental (only what changed). On a typical home stack the first run is a few GB; daily runs afterwards are usually under 100 MB.

    Step 4: Retention (forget, but keep the useful)

    Without retention, the repository grows forever. The pattern that covers every realistic disaster:

    restic -r /mnt/backup-disk/init --password-file ~/.config/restic/pass forget \
      --tag daily --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --prune

    Seven daily snapshots (any failure in the last week is restorable), four weekly (the “I broke the config in June” case), twelve monthly (year-over-year). Run forget after each backup. --prune reclaims the space from dropped snapshots; it is the slow step, which is why it runs once a day, not on every snapshot.

    Step 5: Put it on cron (and make it report)

    # /etc/cron.d/restic-backup  (or crontab -e as the backup user)
    15 3 * * *  backupuser  /home/backupuser/scripts/restic-daily.sh >> /var/log/restic-daily.log 2>&1

    With restic-daily.sh doing backup → forget → a short --files-from verify sample, and emailing you only on failure:

    #!/usr/bin/env bash
    set -euo pipefail
    REPO=/mnt/backup-disk/init
    PASS=~/.config/restic/pass
    PATHS=$(cat ~/.config/restic/paths.txt | tr '\n' ' ')
    
    restic -r "$REPO" --password-file "$PASS" backup $PATHS --tag daily
    restic -r "$REPO" --password-file "$PASS" forget --tag daily --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --prune
    
    # Spot-check: verify a sample of files (full verify is the weekly job)
    if ! restic -r "$REPO" --password-file "$PASS" check --read-data-subset=0.01; then
      echo "restic check failed" | mail -s "BACKUP PROBLEM on $(hostname)" you@example.com
    fi

    Silence on success is the design: you want the one email a month that says something is wrong, not sixty that say all is well. (The weekly job — check --read-data on the full repository, plus the off-site S3 repository — is the same script pointed at the other URL.)

    Step 6: The restore test (the part everyone skips and needs)

    Why this is non-optional

    A backup you have never restored is a theory. The failure modes it catches are the real ones: the password file was “backed up” only to the machine that died, the paths file listed a folder that moved, the S3 policy silently rejects reads, the snapshot exists but the data chunks do not. Twenty minutes a quarter:

    1. Pick a real file you care about (a DokuWiki page, a photo from three weeks ago).
    2. restic -r $REPO --password-file $PASS restore last --target /tmp/restore-test
    3. Open it. The actual file, read by an actual human, on an actual day.
    4. Delete /tmp/restore-test. Done. You now know the strategy works.

    Write the date you last restored in a note next to the paths file. A “last verified” stamp is the difference between a strategy and a ritual.

    Resource usage (measured)

    OperationTypical cost
    First full backup (~10 GB of stacks state + photo lib)20–60 min, disk + network bound, ~200 MiB RAM
    Daily incremental (small changes)1–5 min, <100 MiB RAM
    check --read-data full verify (weekly)Re-reads everything: 1 h per 100 GB, schedule it overnight

    It runs on the same box as the stacks it protects and never notices it. The hardware it saves is the point.

    FAQ

    What about the “one off-site” if I do not have a second machine?

    Order of preference: a second machine running MinIO (even a Pi in a different room, or a cheap VPS) → an encrypted external drive on a rotation schedule (LUKS, take it off-site monthly) → a provider’s object storage (the last resort, because it is the one copy you do not control). The restic repository URL is the only thing that changes between these; the script, the retention, and the verify jobs are identical.

    Does restic protect against ransomware on the server?

    Partially, and honestly: restic snapshots are append-only from the server’s perspective, so files encrypted after the last snapshot are restorable to their pre-encryption state. What it does not protect against is a compromised backup user that deletes snapshots — which is why the off-site repository is the one you verify weekly, and why the S3 user’s policy should be read/write on the repository path only, no admin rights.

    Can I back up Docker containers themselves?

    Back up their state (volumes, per the inventory), not the containers — containers are disposable; docker compose up -d rebuilds them. The compose files are text; keep them in Gitea, which is itself a backup target. You end up with the elegant loop: the backup of your infrastructure is one of the things being backed up.

    What if a backup run fails?

    The email tells you. The usual causes, in order: the target disk is unmounted (the /mnt path is empty), the S3 target is unreachable (the second machine is down), or a path in the file no longer exists (restic exits non-zero and the mailer fires). Fix the cause, re-run the script by hand, and confirm the next snapshot lands. Do not let two consecutive daily runs fail silently — that is when a “backup” stops being one.

    Where does this fit?

    This is the article every other guide on this site points at: each stack’s “back up the volume” step resolves to a line in paths.txt here. The off-site target is MinIO, the compose files live in Gitea, and the whole thing is reachable for checking from anywhere via Cloudflare Tunnel — with the tunnel itself being just one more line in the inventory.

  • MinIO in Docker: Self-Hosted S3 Object Storage (Compose Guide)

    MinIO in Docker: Self-Hosted S3 Object Storage (Compose Guide)

    MinIO is an S3-compatible object store you run in a single container: any app that speaks the S3 protocol — backups, media servers, databases, CI pipelines — can store and fetch files from it without changing a line of configuration. It runs in about 200 MB of RAM at idle, and a single-disk setup is enough for personal and small-team use. This guide covers the Docker Compose setup, first bucket, connecting clients, and the settings that matter.

    Beginner · 8 min · Docker

    Why you would want your own S3

    Three reasons come up constantly in self-hosted setups:

    • Backups need a second location. Restic, Borg, and Duplicacy all write natively to S3. A folder on the same machine is not a backup destination; an S3 bucket is at least a clean abstraction, and you can point it at a second machine later without touching your backup scripts.
    • Apps want S3, not folders. Nextcloud, some monitoring stacks, and a long tail of SaaS-style tools take an S3 endpoint, access key and secret key as configuration. MinIO gives them that endpoint on your own hardware.
    • Portability. Your data lives in a standard protocol. If you outgrow a single disk, you can move the bucket to a MinIO cluster or even to cloud S3 and most clients keep working.

    The honest caveat: single-node MinIO with one volume is not a replicated, self-healing storage system. It is a well-behaved S3 endpoint in front of your disk. Treat it as such: back the volume up like any other data directory.

    Prerequisites

    • Docker + Compose plugin
    • A free TCP port (this guide uses 9000 for the API and 9001 for the web console)
    • A folder for the data — put it on your largest disk; MinIO writes everything to it

    Step 1: The compose file

    mkdir -p ~/stacks/minio && cd ~/stacks/minio

    Create docker-compose.yml:

    services:
      minio:
        image: minio/minio:latest
        container_name: minio
        command: server /data --console-address ":9001"
        environment:
          MINIO_ROOT_USER: chikewa
          MINIO_ROOT_PASSWORD: CHANGE-ME-16-CHARS
        ports:
          - "9000:9000"
          - "9001:9001"
        volumes:
          - ./data:/data
        restart: unless-stopped

    Three notes on that file:

    • command: server /data — the data directory is a single path. MinIO supports multiple drives per node (pass several paths, space-separated) for larger single-node deployments.
    • MINIO_ROOT_USER / MINIO_ROOT_PASSWORD — the root credentials. The password must be at least 8 characters; use a long random one. You can create additional users later with their own keys (see Step 4), which is the right way to hand out access to apps.
    • --console-address ":9001" — the web UI. If you would rather not expose the console at all, remove port 9001 from ports and use mc (the CLI) instead.

    Step 2: Start it and open the console

    docker compose up -d
    docker compose logs -f minio

    Wait for the log line API: http://192.168.x.x:9000, then open http://YOUR_SERVER_IP:9001 and log in with the root credentials. The console shows buckets, usage, and a simple file browser — enough for day-to-day checking.

    Step 3: Create a bucket

    In the console, click Add Bucket, pick a name (lowercase, no spaces, e.g. backups), and leave the defaults. That is it — no ACL gymnastics needed for a private bucket. MinIO buckets are private by default; access is controlled by credentials, not by open listing.

    Step 4: Create a dedicated user for apps

    Do not hand the root keys to your backup job. In the console go to Access Keys → Add User (or use mc), create a user, then create an access key for it. Attach a policy that limits it to the bucket it needs:

    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": ["s3:GetObject", "s3:PutObject", "s3:ListBucket", "s3:DeleteObject"],
          "Resource": [
            "arn:aws:s3:::backups",
            "arn:aws:s3:::backups/*"
          ]
        }
      ]
    }

    Save that as a custom policy and attach it to the user. Now every app on your network uses its own key, and you can revoke one without touching the others.

    Step 5: Connect a client

    Any S3 client works. Three examples you will actually use:

    mc (MinIO CLI) — the official client, one command to configure:

    mc alias set myminio http://YOUR_SERVER_IP:9000 ACCESS_KEY SECRET_KEY
    mc mb myminio/backups
    mc cp big-file.iso myminio/backups/

    restic — backup to your own S3:

    restic init --repository s3:http://YOUR_SERVER_IP:9000/backups \
      --s3-provider minio --s3-access-key ACCESS_KEY --s3-secret-key SECRET_KEY \
      --s3-region us-east-1 --s3-no-verify-ssl
    restic -r s3:http://YOUR_SERVER_IP:9000/backups backup /home

    The us-east-1 region value is a dummy: MinIO accepts any region string, and --s3-no-verify-ssl is only needed while you are on plain HTTP inside your LAN. Once you put TLS in front (see below), drop that flag.

    aws CLI — if an app or script demands it:

    aws --endpoint-url http://YOUR_SERVER_IP:9000 s3 ls
    aws --endpoint-url http://YOUR_SERVER_IP:9000 s3 cp report.pdf s3://backups/

    Step 6: TLS and external access

    MinIO ships with a self-signed certificate automatically; clients can use HTTPS against it with no-verify, which is fine for testing but not for anything you leave on. The two clean paths, in order of preference:

    • Cloudflare Tunnel or Tailscale Funnel in front of MinIO — no open ports, TLS handled for you. We cover Cloudflare Tunnel in the Security & Networking series.
    • Reverse proxy (Caddy / Nginx Proxy Manager) with a real certificate — the same foundation you would use for any other exposed service.

    Do not forward port 9000 directly on your router. An S3 endpoint with a weak key is exactly the thing credential-stuffing bots look for.

    Resource usage (measured)

    From a single-disk, single-bucket setup on a Pi 5-class machine:

    StateRAM
    Idle~180–220 MiB
    Steady single-stream copy (1 GbE)~250 MiB, disk-bound at ~100 MB/s

    Throughput is your disk, not MinIO. A SATA SSD will saturate a gigabit line easily; a mechanical disk will not. If you need more, the single-node multi-drive setup (multiple paths in command) stripes across them.

    Updating

    docker compose pull && docker compose up -d

    MinIO stores its metadata inside the data volume; there is no separate database to migrate. Your buckets and objects are untouched by updates.

    FAQ

    Can I run MinIO on a Raspberry Pi?

    Yes, with the usual caveat that a Pi’s storage is the bottleneck. It is a perfectly good S3 endpoint for backups, metadata-heavy workloads, and small files. For large video libraries, put the data on a NAS-class machine and let the Pi run the apps that consume it.

    What happens to my data if the container breaks?

    Everything is plain files under ./data. You can copy that folder to another machine, point a fresh MinIO at it, and serve the same objects. That is the real safety property of single-node MinIO: the data is not locked in a proprietary format.

    Do I need versioning?

    Turn it on (console → bucket → versioning) if the bucket holds anything you do not want an accidental delete to destroy — backup repositories, irreplaceable originals. It costs you storage equal to whatever you overwrite.

    How is this different from Samba or a plain folder?

    A folder gives you a filesystem; S3 gives you an API. Anything that needs to store objects programmatically — backup software, web apps, pipelines — speaks S3, not CIFS. MinIO is the cheapest way to get that API on hardware you own.

    Where does this fit?

    MinIO is the storage layer for the rest of the stack: Miniflux keeps its database tiny, Jellyfin keeps its media in a folder, and MinIO is where the copies that survive a disk failure live. See the starter guide for the full picture.