Self-Hosted Raspberry Pi Backups with a Mainframe Aesthetic (COBOL Edition)

🇷🇺 Read in Russian


I use a bare-metal Raspberry Pi as my home server for self-hosting. Among other Docker containers, I run Immich—a fantastic self-hosted alternative to cloud photo galleries. But when your photo library grows past a few dozen gigabytes, backups become a real pain point.


My daily driver is Fedora, and for a while, I just pulled the files over SSH. But classic SSH backups using tar have two major flaws:



To fix this properly, I wrote a bash script. But instead of standard boring output, I decided to style it like an old-school IBM mainframe running a COBOL batch job. Zero emojis—just hardcore UPPERCASE, execution steps (STEP), data sets (DATA SET), and completion codes (CONDITION CODE).


Under the hood


The script fully automates the process and works according to the following algorithm:



The Code Itself (BASH BATCH PROCESSING)


Running it is as simple as: ./immich_backup.sh. It assumes passwordless sudo is configured on the Pi (which is often the default anyway).


#!/bin/bash

# Settings
REMOTE_ALIAS="rpi5"
NODE_NAME="${REMOTE_ALIAS^^}"
SOURCE_PARENT="/home/rachel"
SOURCE_NAME="immich"
TARGET_DIR="$(dirname "$(realpath "$0")")"
FILENAME="immich_week_$(date +%V).tar"
TARGET_PATH="$TARGET_DIR/$FILENAME"

# Strict logging function
log_msg() {
    echo "$(date '+%Y/%m/%d %H:%M:%S') $1"
}

echo "************************************************************************"
echo "* PROGRAM-ID.  IMMICH-BACKUP.                                          *"
echo "* AUTHOR.      SYSTEM ADMIN.                                           *"
echo "* ENVIRONMENT. BASH BATCH PROCESSING.                                  *"
echo "************************************************************************"
echo "IDENTIFICATION DIVISION."
echo "PROCEDURE DIVISION."

log_msg "INFO  - RUN JOB IMMICH-BKP ON NODE $NODE_NAME"

log_msg "STEP 01 - ALLOCATE TARGET DATA SET $TARGET_DIR"
mkdir -p "$TARGET_DIR"

log_msg "STEP 02 - HALT REMOTE DOCKER CONTAINERS"
ssh $REMOTE_ALIAS "cd $SOURCE_PARENT/$SOURCE_NAME && sudo docker compose stop" 2>&1 | while IFS= read -r line; do
    clean_line=$(echo "$line" | sed -r 's/\x1B\[[0-9;]*[a-zA-Z]//g')
    log_msg "DOCKER: ${clean_line^^}"
done

if [ ${PIPESTATUS[0]} -ne 0 ]; then
    log_msg "ABEND - RETURN CODE NOT 00 ON DOCKER STOP"
    log_msg "STEP 02A - ATTEMPT ROLLBACK (RESTART CONTAINERS)"
    ssh $REMOTE_ALIAS "cd $SOURCE_PARENT/$SOURCE_NAME && sudo docker compose start" >/dev/null 2>&1
    log_msg "JOB TERMINATED DUE TO ABEND"
    exit 1
fi
log_msg "STEP 02 - CONDITION CODE 0000"

log_msg "STEP 03 - ESTIMATE REMOTE DATA SET SIZE"
ESTIMATED_SIZE=$(ssh $REMOTE_ALIAS "sudo du -sb $SOURCE_PARENT/$SOURCE_NAME" | cut -f1)

if [[ -n "$ESTIMATED_SIZE" && "$ESTIMATED_SIZE" =~ ^[0-9]+$ ]]; then
    log_msg "INFO  - ESTIMATED SIZE: $ESTIMATED_SIZE BYTES"
    log_msg "STEP 03 - CONDITION CODE 0000"
else
    log_msg "ABEND - FAILED TO ESTIMATE DATA SET SIZE"
    ssh $REMOTE_ALIAS "cd $SOURCE_PARENT/$SOURCE_NAME && sudo docker compose start" >/dev/null 2>&1
    exit 1
fi

log_msg "STEP 04 - EXECUTE REMOTE ARCHIVE AND DATA TRANSFER"
ssh $REMOTE_ALIAS "sudo tar -cf - -C $SOURCE_PARENT $SOURCE_NAME" | pv -ptrbe -s "$ESTIMATED_SIZE" > "$TARGET_PATH"

if [ ${PIPESTATUS[0]} -eq 0 ]; then
    log_msg "STEP 04 - CONDITION CODE 0000, DATA WRITTEN TO $TARGET_PATH"
else
    log_msg "ABEND - DATA TRANSFER FAILED"
    ssh $REMOTE_ALIAS "cd $SOURCE_PARENT/$SOURCE_NAME && sudo docker compose start" >/dev/null 2>&1
    exit 1
fi

log_msg "STEP 05 - COMPUTE DATA SET ALLOCATION SIZE"
if [ -f "$TARGET_PATH" ]; then
    FILE_SIZE=$(stat -c %s "$TARGET_PATH")
    log_msg "INFO  - DATA SET SIZE: $FILE_SIZE BYTES"
    log_msg "STEP 05 - CONDITION CODE 0000"
else
    log_msg "ABEND - DATA SET NOT FOUND"
    exit 1
fi

log_msg "STEP 06 - RESTART REMOTE DOCKER CONTAINERS"
ssh $REMOTE_ALIAS "cd $SOURCE_PARENT/$SOURCE_NAME && sudo docker compose start" 2>&1 | while IFS= read -r line; do
    clean_line=$(echo "$line" | sed -r 's/\x1B\[[0-9;]*[a-zA-Z]//g')
    log_msg "DOCKER: ${clean_line^^}"
done
log_msg "STEP 06 - CONDITION CODE 0000"

log_msg "STEP 07 - EXECUTE ROTATION PROCEDURE (KEEP 4 GENERATIONS)"
cd "$TARGET_DIR" || exit
ls -t immich_week_*.tar 2>/dev/null | tail -n +5 | xargs -I {} bash -c "echo \"\$(date '+%Y/%m/%d %H:%M:%S') INFO  - SCRATCH OLD DATA SET: {}\" | tr '[:lower:]' '[:upper:]' && rm -- \"{}\""
log_msg "STEP 07 - CONDITION CODE 0000"

echo "************************************************************************"
echo "* END OF JOB IMMICH-BKP. MAXIMUM CONDITION CODE 0000                   *"
echo "************************************************************************"

The end result is a rock-solid, standalone backup script that is incredibly satisfying to watch run.


Tools & References:


Official Immich project website

Docker Compose documentation

pv (Pipe Viewer) manual

Official Raspberry Pi portal

---


Back to the Raspberry section



/en/