Unraid runs entirely in RAM from your USB flash drive. If you download a binary straight to /usr/local/bin, it will vanish the moment you reboot your server. [1, 2]

Follow this standard Unraid method to make Restic persist across system reboots:

Step 1: Save the Binary to Your Flash Drive

  1. Go to the official Restic GitHub Releases Page and download the latest stable Linux AMD64 file (e.g., restic_x.x.x_linux_amd64.bz2).
  2. Extract the .bz2 file on your computer to get the raw restic binary file.
  3. Plug your Unraid flash drive into your computer (or access it over the network via the boot SMB share).
  4. Create a folder named extra on the flash drive if it doesn’t exist, and drop the restic file inside it:
mkdir -p /boot/extra && cd /boot/extra
wget https://github.com/restic/restic/releases/download/v0.19.0/restic_0.19.0_linux_amd64.bz2

bunzip2 restic_0.19.0_linux_amd64.bz2 

cp restic_0.19.0_linux_amd64 restic
chmod +x restic

Step 2: Force Unraid to Load it at Boot

To make Unraid load Restic into system memory automatically every time the server starts up, add it to your go script: [1, 2]

  1. Open /boot/config/go on your flash drive using a text editor (like Notepad++ or VS Code).
  2. Append the following lines to the very bottom of the file
# Copy and activate Restic binary on boot
cp /boot/extra/restic /usr/local/bin/restic
chmod 755 /usr/local/bin/restic
  1. Save and close the file.

(To avoid rebooting right now to test this, simply paste those two command lines directly into your current Unraid web terminal to install it instantly).


How to Initialize and Configure Your First Backup Repo

Before Restic can process automated backups, you must initialize an encrypted repository destination where your deduplicated snapshots will be safely stored. [1, 2]

Step 1: Create the Backup Repository

Open your Unraid terminal. We will initialize a local repository targeting your main HDD Array storage pool

mkdir -p /mnt/user/bak_appdata/restic/data
restic init --repo /mnt/user/bak_appdata/restic/data

The terminal will prompt you to type a password. Do not lose this password. Every piece of data sent to this directory is encrypted using AES-256 by default; without the password, your data is completely unrecoverable. [1, 2]

Step 2: Use the Automation Script

Now that the binary is installed globally and the target directory is initialized, you can deploy the automated backup-gitea.sh script provided in the previous step. The script will handle:

  • Running a live, non-destructive dump of the MySQL container databases.
  • Scanning your Gitea appdata folders for structural changes.
  • Injecting only newly created data chunks into the encrypted repository.
  • Trimming away historical versions that fall outside your set retention periods.
#!/bin/bash
#.../dc-gitea/restic-gitea-backup.sh 

set -e

# --- CONFIGURATION ---
BACKUP_TAG="gitea-production"
SRC_DATA_DIR="/mnt/nvme-pool/appdata/dc-gitea/gitea"
TEMP_DB_DIR="/mnt/nvme-pool/appdata/dc-gitea/tmp_db"
RESTIC_REPO="/mnt/user/bak_appdata/restic/data" # Store snapshots on your main HDD Array Share

# Define an encryption key for your backup repository (Keep this safe!)
export RESTIC_PASSWORD="myd0gsk1p" 

# --- START BACKUP PROCESS ---
echo "Creating temporary database staging directory..."
mkdir -p "$TEMP_DB_DIR"

echo "Executing consistent hot-dump of Gitea MySQL database..."
# Pulls db configuration variables dynamically out of your running stack env
docker compose -f /mnt/nvme-pool/appdata/dc-gitea/docker-compose.yml exec -T db \
  mysqldump -u root -p"$(grep DB_PASSWORD /mnt/nvme-pool/appdata/dc-gitea/.env | cut -d'=' -f2)" gitea > "$TEMP_DB_DIR/gitea_db.sql"

# Initialize Restic repository if it doesn't exist yet
if [ ! -d "$RESTIC_REPO" ]; then
    echo "Initializing new Restic backup repository..."
    restic -r "$RESTIC_REPO" init
fi

echo "Running deduplicated incremental snapshot..."
restic -r "$RESTIC_REPO" backup \
    --tag "$BACKUP_TAG" \
    "$SRC_DATA_DIR" \
    "$TEMP_DB_DIR/gitea_db.sql"

echo "Cleaning up local database dump..."
rm -f "$TEMP_DB_DIR/gitea_db.sql"
rmdir "$TEMP_DB_DIR"

echo "Pruning old historical snapshots based on retention policy..."
# Keeps 7 daily snapshots, 4 weekly snapshots, and 12 monthly snapshots
restic -r "$RESTIC_REPO" forget \
    --keep-daily 7 \
    --keep-weekly 4 \
    --keep-monthly 12 \
    --prune

echo "Gitea Incremental Backup Successfully Completed!"

Useful Commands to Manage Your Globally Stored Backups

Once your script starts running daily, you can open your Unraid terminal at any time and run these basic management commands:

  • List all historical backup instances:
restic -r /mnt/user/backups/gitea-restic snapshots
  • Check the real storage footprint and deduplication stats:
restic -r /mnt/user/backups/gitea-restic stats
  • Browse or verify file integrity inside a backup
restic -r /mnt/user/backups/gitea-restic check

Risk of High CPU/RAM Starvation

Restic heavily compresses, hashes (SHA-256), and encrypts data chunks on the fly.

  • The Risk: If you back up large datasets (like your 85GB Gitea stack), a bare-metal Restic process will consume as much CPU and RAM as Unraid will give it. This can cause temporary performance stuttering in your other active Docker containers or plex streams.
  • The Mitigation: Unlike a Docker container where you can easily set a hard RAM limit, on bare metal you must restrict it manually by prepending nice and ionice to your script commands to force Restic to run at the lowest priority:
nice -n 19 ionice -c 3 restic -r /mnt/user/bak_appdata/restic/data backup ...

Part 1: How to Restore Gitea From Your Backup

If your NVMe drive fails or you lose data, the restore process takes under 5 minutes. You spin up your basic container environment, restore the files from Restic, and drop the database backup back into MySQL. [1]

Step 1: Stop the Gitea App Container

Keep your database and Redis containers running, but shut down the Gitea application container so it does not lock files:

docker compose stop server runner

Step 2: Extract Your Files Using Restic

Run the Restic restore command. You can choose to restore everything, or use the --target path to extract files exactly where they belong:

# Get your snapshot ID first
restic -r /mnt/user/bak_appdata/restic/data snapshots

# Restore the files straight back to your NVMe appdata pool
restic -r /mnt/user/bak_appdata/restic/data restore LATEST_SNAPSHOT_ID --target /

(This places your gitea/ folder data and the gitea_db.sql file back onto your disk).

Step 3: Restore the MySQL Database

Inject your text-based database dump back into your running MySQL container instance:

docker compose exec -T db mysql -u root -p"YOUR_DB_PASSWORD" gitea < /mnt/nvme-pool/appdata/dc-gitea/tmp_db/gitea_db.sql

Step 4: Restart the Stack

docker compose start server runner

Pro-Tip for Nextcloud & Rocket.Chat Backups

Unlike Git repositories, which are completely immutable, users can write files to Nextcloud while Restic is mid-scan, causing potential file index warning errors during a snapshot.

To achieve 100% data integrity for your other apps, modify your backup script to put Nextcloud into Maintenance Mode before running the backup, and turn it off immediately after Restic finishes:

# Enable Maintenance Mode (Freezes user web writes instantly)
docker exec -u www-data nextcloud-app-container php occ maintenance:mode --on

# ... [ Run your DB Dump & Restic Backup Here ] ...

# Disable Maintenance Mode
docker exec -u www-data nextcloud-app-container php occ maintenance:mode --off

Part 2: Does This Process Work for Nextcloud and Rocket.Chat?

Yes, the core architectural concept is identical, but the command syntax changes slightly depending on the database engine. You always extract the live database to a text file first, then let Restic snapshot the files and the database dump together.

For Nextcloud (MySQL / MariaDB)

Nextcloud operates exactly like Gitea. You use the exact same mysqldump command structure.

The Backup Script Modification:

# Dump the Nextcloud database
docker compose -f /path/to/dc-nextcloud/docker-compose.yml exec -T db \
  mysqldump -u root -p"NEXTCLOUD_DB_PASS" nextcloud > /tmp_db/nextcloud_db.sql

# Run Restic snapshot
restic -r /mnt/user/backups/nextcloud-restic backup /mnt/user/appdata/nextcloud /tmp_db/nextcloud_db.sql

The Restore Command:

docker compose exec -T db mysql -u root -p"NEXTCLOUD_DB_PASS" nextcloud < nextcloud_db.sql

For Rocket.Chat (PostgreSQL)

PostgreSQL handles hot-backups differently. Instead of mysqldump, you must use pg_dump. [1, 2]

The Backup Script Modification:

# Dump the PostgreSQL database (Notice the username syntax flag)
docker compose -f /path/to/dc-rocketchat/docker-compose.yml exec -T db \
  pg_dump -U rocketchat_user -d rocketchat_db > /tmp_db/rocketchat_db.sql

# Run Restic snapshot
restic -r /mnt/user/backups/rocketchat-restic backup /mnt/user/appdata/rocketchat /tmp_db/rocketchat_db.sql

The Restore Command:
PostgreSQL restores utilize the standard application terminal router:

docker compose exec -T db psql -U rocketchat_user -d rocketchat_db < rocketchat_db.sql

Final Direction:

  1. unraid-restic-master-backup.sh that calls individual restic-back.sh scripts
#!/bin/bash
#.../myUNRAID/scripts/unraid-restic-master-backup.sh
                                                                                                                                                                          
# --- GLOBAL ORCHESTRATOR ---                                                                                                                                                        
APPDATA_DIR="/mnt/nvme-pool/appdata"                                                                                                                                                 

echo "=== Starting Global Container Backup Sequence ==="

# Find all dc-* directories containing a restic-backup.sh script                                                                                                                     
find "$APPDATA_DIR" -maxdepth 2 -type f -name "restic-backup.sh" | while read -r backup_script; do                                                                                   
    stack_dir=$(dirname "$backup_script")                                                                                                                                            
    stack_name=$(basename "$stack_dir")                                                                                                                                              

    echo "--------------------------------------------------"                                                                                                                        
    echo "Found backup configuration for stack: [$stack_name]"                                                                                                                       
    echo "Executing: $backup_script"                                                                                                                                                 
    echo "--------------------------------------------------"                                                                                                                        

    # Run the script inside its native directory context
    (cd "$stack_dir" && ./restic-backup.sh)                                                                                                                                          
done                                                                                                                                                                                 

echo "=================================================="                                                                                                                            
echo "=== Global Backup Sequence Completed Safely ==="                                                                                                                               
echo "=================================================="
#!/bin/bash
#.../dc-gita/restic-backup.sh
set -e

# --- CONFIGURATION ---
STACK_DIR=$(pwd)
STACK_NAME=$(basename "$STACK_DIR")
RESTIC_REPO="/mnt/user/bak_appdata/restic/data"
TEMP_DB_DIR="/mnt/nvme-pool/appdata/tmp_db_staging"

RESTIC_PASS="${1:-$(grep ^RESTIC_PASS .env | cut -d'=' -f2)}"
DB_USER="${2:-$(grep ^DB_USER .env | cut -d'=' -f2)}"
DB_PASS="${3:-$(grep ^DB_PASS .env | cut -d'=' -f2)}"
DB_NAME="${4:-$(grep ^DB_NAME .env | cut -d'=' -f2)}"
echo "  RESTIC_PASS:$RESTIC_PASS"
echo "  DB_USER:$DB_USER"
echo "  DB_PASS:$DB_PASS"
echo "  DB_NAME:$DB_NAME"
usage() {
    echo ""
    echo "    Usage: $0 RESTIC_PASS DB_USER DB_PASS DB_NAME"

    exit 0
}
if [ "$1" == "-h" ]; then
    usage
fi
if [ -z "$DB_USER" ] || [ -z "$DB_PASS" ] || [ -z "$DB_NAME" ] || [ -z "$RESTIC_PASS" ]; then
    echo "  ERROR: Cannot continue with missing arguments!"
    usage
fi
export RESTIC_PASSWORD="$RESTIC_PASS"


echo "[$STACK_NAME] Exporting consistent snapshot of Gitea MySQL database..."
echo "  docker compose exec -T db mysqldump -u $DB_USER -p$DB_PASS $DB_NAME > $TEMP_DB_DIR/${DB_NAME}_db.sql"
mkdir -p "$TEMP_DB_DIR"
docker compose exec -T db mysqldump -u $DB_USER -p$DB_PASS $DB_NAME > "$TEMP_DB_DIR/${DB_NAME}_db.sql"

echo "[$STACK_NAME] Executing localized deduplicated backup..."
restic -r "$RESTIC_REPO" backup \
    --tag "$STACK_NAME" \
    --exclude "$STACK_DIR/redis" \
    "$STACK_DIR" \
    "$TEMP_DB_DIR/gitea_db.sql"

# Clean up
#rm -rf "$TEMP_DB_DIR"

echo "[$STACK_NAME] Pruning old historical timelines..."
restic -r "$RESTIC_REPO" forget --tag "$STACK_NAME" --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --prune
#!/bin/bash
#.../dc-gitea/restic-restore.sh
set -e

# --- INITIAL CONFIGURATION ---
ORIGINAL_STACK_DIR="/mnt/nvme-pool/appdata/dc-gitea"
STACK_NAME="dc-gitea"
RESTIC_REPO="/mnt/user/bak_appdata/restic/data"
export RESTIC_PASSWORD="YourSuperSecureGlobalBackupPassword"

clear
echo "================================================================="
echo "        GITEA INTERACTIVE BACKUP RESTORATION UTILITY            "
echo "================================================================="
echo ""

# 1. Fetch and list available snapshots for this specific application tag
echo "Fetching available recovery points for tag [$STACK_NAME]..."
restic -r "$RESTIC_REPO" snapshots --tag "$STACK_NAME"
echo ""

# 2. Ask user which snapshot ID they want to recover
read -p "Enter the Snapshot ID you want to restore (or type 'latest'): " SNAPSHOT_ID
if [ -z "$SNAPSHOT_ID" ]; then
    SNAPSHOT_ID="latest"
fi

# 3. Prompt for the extraction target path
echo ""
echo "Where do you want to restore this stack environment?"
echo "Default original location is: $ORIGINAL_STACK_DIR"
read -p "Enter absolute target path (Press Enter for default): " TARGET_DIR

if [ -z "$TARGET_DIR" ]; then
    TARGET_DIR="$ORIGINAL_STACK_DIR"
fi

# 4. SAFETY TRIGGER: Check warnings and path collisions
if [ "$TARGET_DIR" == "$ORIGINAL_STACK_DIR" ]; then
    echo ""
    echo "⚠️  WARNING: You are restoring directly over your LIVE PRODUCTION environment!"
    read -p "Are you absolutely sure? This will overwrite existing active assets! (y/N): " CONFIRM
    if [[ ! "$CONFIRM" =~ ^[Yy]$ ]]; then
        echo "Restoration aborted by user safety control."
        exit 1
    fi
elif [ -d "$TARGET_DIR" ]; then
    echo ""
    echo "⚠️  NOTICE: Target directory [$TARGET_DIR] already exists."
    read -p "Merge/overwrite files within this existing folder? (y/N): " CONFIRM
    if [[ ! "$CONFIRM" =~ ^[Yy]$ ]]; then
        echo "Restoration aborted to prevent data contamination."
        exit 1
    fi
fi

# 5. Execute file layer restoration
echo ""
echo "Extracting files from Restic repository..."
# Using --target / restores files using the structure mapped during backup
restic -r "$RESTIC_REPO" restore "$SNAPSHOT_ID" --tag "$STACK_NAME" --target /

# If the user restored to a custom path, we move the files from the snapshot path to their designated location
if [ "$TARGET_DIR" != "$ORIGINAL_STACK_DIR" ]; then
    echo "Relocating extracted environment files to custom workspace: $TARGET_DIR"
    mkdir -p "$TARGET_DIR"
    cp -r "$ORIGINAL_STACK_DIR"/. "$TARGET_DIR/"
    # Clean up the artifact path left behind by restic's absolute directory restoration matching rules
    # (Optional, based on how pristine you want the host workspace)
fi

# 6. Environmental Configuration Automation Checklist
echo ""
echo "=== Running Post-Restore Optimization Checklist ==="
cd "$TARGET_DIR"

if [ "$TARGET_DIR" != "$ORIGINAL_STACK_DIR" ]; then
    echo "[!] Adjusting .env paths for Isolated Testing Instance..."
    # Update the environment file variables to use the newly selected folder paths
    sed -i "s|GITEA_DATA_DIR=.*|GITEA_DATA_DIR=$TARGET_DIR/gitea|g" .env
    sed -i "s|MYSQL_DATA_DIR=.*|MYSQL_DATA_DIR=$TARGET_DIR/mysql|g" .env
    sed -i "s|REDIS_DATA_DIR=.*|REDIS_DATA_DIR=$TARGET_DIR/redis|g" .env
    
    # Prompt user to change port constraints to bypass runtime container name collisions
    read -p "Enter a non-conflicting HTTP Web Port for this test instance (e.g. 3080): " NEW_PORT
    sed -i "s|GITEA_HTTP_PORT=.*|GITEA_HTTP_PORT=$NEW_PORT|g" .env
    
    # Enforce non-conflicting custom container project identifier names
    sed -i "s|COMPOSE_PROJECT_NAME=.*|COMPOSE_PROJECT_NAME=testrestore|g" .env
fi

echo "[!] Re-establishing container service user privileges (chown 1000:1000)..."
chown -R 1000:1000 "$TARGET_DIR/gitea" || true

echo ""
echo "✅ SUCCESS: Core files extracted successfully to $TARGET_DIR"
echo "================================================================="
echo "👉 FINAL RECOVERY STEP DIRECTIONS:"
echo " 1. Move into the workspace:  cd $TARGET_DIR"
echo " 2. Boot up your database:    docker compose up -d db"
echo " 3. Stream database records back into the running engine:"
echo "    docker compose exec -T db mysql -u root -p\\\$(grep DB_PASSWORD .env | cut -d'=' -f2) gitea < /mnt/nvme-pool/appdata/tmp_db_staging/gitea_db.sql"
echo " 4. Complete stack startup:   docker compose up -d"
echo "================================================================="

Leave a Reply

This website stores cookies on your computer. These cookies are used to provide a more personalized experience and to track your whereabouts around our website in compliance with the European General Data Protection Regulation. If you decide to to opt-out of any future tracking, a cookie will be setup in your browser to remember this choice for one year.

Accept or Deny