Forked from dawid-czarnecki/firefly-iii-backuper.sh
Last active
July 31, 2025 20:42
-
-
Save matthiasseghers/7003aab2d272d773bab00a62f90ac3b3 to your computer and use it in GitHub Desktop.
Script to backup Firefly III database, uploads and config files installed with docker-compose
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| files_to_backup=(.*.env .env docker-compose.yml ) | |
| info() { echo -e "\\033[1;36m[INFO]\\033[0m \\033[36m$*\\033[0m" >&2; } | |
| warn() { echo -e "\\033[1;33m[WARNING]\\033[0m \\033[33m$*\\033[0m" >&2; } | |
| fatal() { echo -e "\\033[1;31m[FATAL]\\033[0m \\033[31m$*\\033[0m" >&2; exit 1; } | |
| intro () { | |
| echo " =====================================================" | |
| echo " Backup & Restore docker based FireFly III v1.6 " | |
| echo " =====================================================" | |
| echo " It automatically detects db & upload volumes based on the name matching the following regex: firefly[_-](iii|)[_-]?" | |
| echo " Requirements:" | |
| echo " - Place the script in the same directory where your docker-compose.yml and .env files are saved" | |
| echo " Warning: The destination directory is created if it does not exist" | |
| } | |
| usage () { | |
| echo "Usage: $0 backup|restore /tmp/backup/destination/dir [no_files]" | |
| echo "- backup|restore - Action you want to execute" | |
| echo "- destination path of your backup file including file name" | |
| echo "- optionally backup or restore volumns only when no_files parameter is passed" | |
| echo "Example backup: $0 backup /home/backup/firefly-2022-01-01.tar.gz" | |
| echo "Example restore: $0 restore /home/backup/firefly-2022-01-01.tar.gz" | |
| echo "To backup once per day you can add something like this to your cron:" | |
| echo "1 01 * * * bash /home/myname/backuper.sh backup /home/backup/\$(date '+%F').tar.gz" | |
| echo "To restore a database of a specific Firefly version follow the steps below" | |
| echo "- Look for the Firefly version from your backup. It's in <backup>.tar.gz/version.txt" | |
| echo "- Use the Firefly docker tag (https://hub.docker.com/r/fireflyiii/core/tags) corresponding to your version" | |
| echo "- Change the firefly image to a tag from your version. Example:" | |
| echo " image: fireflyiii/core:version-6.1.10" | |
| echo "- Run docker compose up" | |
| echo "- Run this script to restore the backup" | |
| } | |
| backup () { | |
| script_path="$1" | |
| # Ensure destination directory exists | |
| dest_dir="$(dirname "$2")" | |
| if [ ! -d "$dest_dir" ]; then | |
| info "Creating destination directory: $dest_dir" | |
| mkdir -p "$dest_dir" | |
| fi | |
| # Set full_path directly, do not use realpath (file may not exist yet) | |
| full_path="$2" | |
| dest_path="$(dirname "$full_path")" | |
| dest_file="$(basename "$full_path")" | |
| upload_volume="$3" | |
| no_files=$4 | |
| to_backup=() | |
| if [ -f "$full_path" ]; then | |
| warn "Provided file path already exists: $full_path. Overwriting" | |
| fi | |
| # Create temporary directory | |
| tmp_dir="$dest_path/tmp" | |
| if [ ! -d "$tmp_dir" ]; then | |
| mkdir "$tmp_dir" | |
| fi | |
| # Files backup | |
| if [ "$no_files" = "false" ]; then | |
| not_found=() | |
| for pattern in "${files_to_backup[@]}"; do | |
| for file in ${script_path}/${pattern}; do | |
| if [[ ! -f $file ]]; then | |
| not_found+=("$file") | |
| else | |
| cp "$file" "$tmp_dir/" | |
| to_backup+=($(basename "$file")) | |
| fi | |
| done | |
| done | |
| if ((${#not_found[@]})); then | |
| warn "The following files were not found in $script_path: ${not_found[@]}. Skipping." | |
| fi | |
| if ((${#to_backup[@]})); then | |
| info "Backing up the following files in $script_path: ${to_backup[@]}" | |
| fi | |
| fi | |
| # Version | |
| app_container=$(docker ps --format '{{.Names}}' | grep firefly_iii_core) | |
| app_version=$(docker exec -it $app_container grep -F "'version'" /var/www/html/config/firefly.php | tr -s ' ' | cut -d "'" -f 4) | |
| db_version=$(docker exec -it $app_container grep -F "'db_version'" /var/www/html/config/firefly.php | tr -s ' ' | tr -d ',' | cut -d " " -f 4) | |
| info 'Backing up App & database version numbers.' | |
| echo -e "Application: $app_version\nDatabase: $db_version" > "$tmp_dir/version.txt" | |
| to_backup+=(version.txt) | |
| # DB container | |
| db_container=$(docker ps --format '{{.Names}}' | grep firefly_iii_db) | |
| if [ -z "$db_container" ]; then | |
| warn "db container is not running. Not backing up." | |
| else | |
| info 'Backing up database' | |
| docker exec $db_container bash -c '/usr/bin/mariadb-dump -u $MYSQL_USER --password="$MYSQL_PASSWORD" "$MYSQL_DATABASE"' > "$tmp_dir/firefly_db.sql" | |
| to_backup+=("firefly_db.sql") | |
| fi | |
| # Upload Volume | |
| if [ -z "$upload_volume" ]; then | |
| warn "upload volume does NOT exist. Not backing up." | |
| else | |
| info 'Backing up upload volume' | |
| # Correct tar command: archive contents of /tmp inside the container, not root | |
| docker run --rm -v "$upload_volume:/tmp_upload" -v "$tmp_dir:/backup" alpine tar -czf "/backup/firefly_upload.tar.gz" -C /tmp_upload . | |
| to_backup+=("firefly_upload.tar.gz") | |
| fi | |
| # Compress | |
| tar -C "$tmp_dir" -czf "$dest_path/$dest_file" --files-from <(printf "%s\n" "${to_backup[@]}") | |
| # Clean up | |
| for file in "${to_backup[@]}"; do | |
| rm -f "$tmp_dir/$file" | |
| done | |
| rmdir "$tmp_dir" | |
| } | |
| restore () { | |
| script_path="$1" | |
| full_path=$(realpath $2) | |
| src_path="$(dirname $full_path)" | |
| backup_file="$(basename $full_path)" | |
| upload_volume="$3" | |
| no_files=$4 | |
| if [ ! -f "$src_path/$backup_file" ]; then | |
| fatal "Provided backup file does not exist: $path" | |
| fi | |
| # Create temporary directory | |
| tmp_dir="$src_path/tmp" | |
| if [ ! -d "$tmp_dir" ]; then | |
| mkdir "$tmp_dir" | |
| fi | |
| # Files restore | |
| if [ "$no_files" = "false" ]; then | |
| tar -C "$tmp_dir" -xf "$src_path/$backup_file" | |
| # Remove readarray, not needed | |
| not_found=() | |
| restored=() | |
| for f in "${files_to_backup[@]}"; do | |
| if [ ! -f "$tmp_dir/$f" ]; then | |
| not_found+=("$f") | |
| else | |
| cp "$tmp_dir/$f" "$script_path/" | |
| restored+=("$f") | |
| fi | |
| done | |
| if ((${#not_found[@]})); then | |
| warn "The following files were not found in backup: ${not_found[@]}. Skipping." | |
| fi | |
| if ((${#restored[@]})); then | |
| info "Restoring the following files: ${restored[@]}" | |
| fi | |
| else | |
| tar -C "$tmp_dir" -xf "$src_path/$backup_file" firefly_db.sql firefly_upload.tar.gz | |
| restored=(firefly_db.sql firefly_upload.tar.gz) | |
| fi | |
| if [ ! -z "$upload_volume" ]; then | |
| warn "The upload volume exists. Overwriting." | |
| fi | |
| docker run --rm -v "$upload_volume:/recover" -v "$tmp_dir:/backup" alpine tar -xf /backup/firefly_upload.tar.gz -C /recover --strip 1 | |
| restored+=(firefly_upload.tar.gz) | |
| # Improved db_container detection | |
| docker_ps_names=$(docker ps --format '{{.Names}}') | |
| if [ -n "$docker_ps_names" ]; then | |
| db_container=$(docker ps --format '{{.Names}}' | grep firefly_iii_db) | |
| else | |
| db_container="" | |
| fi | |
| if [ -z "$db_container" ]; then | |
| warn "The db container is not running. Not restoring." | |
| else | |
| info 'Restoring database' | |
| cat "$tmp_dir/firefly_db.sql" | docker exec -i "$db_container" bash -c '/usr/bin/mariadb -u $MYSQL_USER --password="$MYSQL_PASSWORD" "$MYSQL_DATABASE"' | |
| restored+=(firefly_db.sql) | |
| fi | |
| restored+=(version.txt) | |
| # Clean up | |
| rm -rf "$tmp_dir"/* "$tmp_dir"/.[!.]* "$tmp_dir"/..?* | |
| rmdir "$tmp_dir" | |
| } | |
| main () { | |
| intro | |
| if [ $# -lt 2 ]; then | |
| fatal "Not enough parameters.\n$(usage)" | |
| fi | |
| backuper_dir="$(dirname $0)" | |
| action=$1 | |
| path="$2" | |
| if [ -z "$3" ]; then | |
| no_files=false | |
| else | |
| no_files=true | |
| fi | |
| if [ -d "$path" ]; then | |
| fatal "Path is an existing directory. It has to be a file path" | |
| fi | |
| upload_volume="$(docker volume ls | grep -F "firefly_iii_upload" | tr -s ' ' | cut -d ' ' -f 2)" | |
| if [ "$action" == 'backup' ]; then | |
| backup "$backuper_dir" "$path" "$upload_volume" $no_files | |
| elif [ "$action" == 'restore' ]; then | |
| restore "$backuper_dir" "$path" "$upload_volume" $no_files | |
| else | |
| fatal "Unrecognized action $action\n$(usage)" | |
| fi | |
| } | |
| main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment