# Self-Hosted Raspberry Pi Backups with a Mainframe Aesthetic (COBOL Edition)
=> /raspberry/immich_backup.gmi 🇷🇺 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:

* The black box effect. When you're pulling 25..50..100 GB of data, you're just staring at a hanging terminal. You have no idea if the archive is actually transferring, what your throughput is, or if the pipe just died.
* Hot backups are a gamble. Tarring up live PostgreSQL database files while the containers are actively writing is asking for trouble. Eventually, you'll end up with a corrupted database.

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:

* Graceful shutdown: The script safely spins down the Docker containers on the Pi via SSH. If the stop command fails, it triggers a rollback (starts them back up) and aborts the job (ABEND). The Docker output is intercepted, stripped of ANSI escape codes, and converted to UPPERCASE to keep the mainframe aesthetic.
* Pre-flight sizing: Before piping the tarball, it runs du -sb on the Pi to get the exact payload size in bytes.
* Progress bar & ETA: That size variable is fed into pv (Pipe Viewer) on the receiving end (Fedora). This gives us a beautiful, real-time progress bar with an ETA right in the terminal.
* Retention policy: The script keeps the last 4 archives (roughly a month's worth of weeklies), automatically scratching the old "data sets". It dumps the backups in the exact same directory the script is executed from.

## 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).

```bash
#!/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:

=> https://immich.app/ Official Immich project website
=> https://docs.docker.com/compose/ Docker Compose documentation
=> https://man7.org/linux/man-pages/man1/pv.1.html pv (Pipe Viewer) manual
=> https://www.raspberrypi.com/ Official Raspberry Pi portal
---

=> /raspberry/index.gmi Back to the Raspberry section