February 2Feb 2 Hi guys, I finally managed to set up duplicati to backup my docker containers and also docker compose stacks.Mounting /mnt/user/ as source did not work for me. Seems this is a ZFS issue. I needed to mount the appdata and other forders directly via template.My main purpose for duplicati is to create backups of my immich stack +library, opencloud and also audiobookshelf.All of them are using a database. So creating a backup without stopping the containers is not a good idea.Therefore I´ve mounted docker and also the docker-compose plugin to the container.Now, Duplicati is able to talk to docker and also docker-compose.To start and stop containers, I´ve created a "script-generator". for each Duplicati Backup, it can generates a pre- and post-run script (two scripts) to stop and start defined containers (docker or docker-compose)The sequence of starting and stopping containers can be defined and also the wait time for each container.The generator saves the two scripts in the duplicati "script" folder with name pre-YourDefinedName.sh and post-YourDefinedName.sh It also makes the files executable.Here is the generator script, which can be used in plugin "user scripts" as well.Generator Script#!/bin/bash#name=Generate Duplicati Pre/Post Backup Scripts#description=Creates pre/post backup scripts for Duplicati. Configure settings in USER CONFIGURATION section.#clearLog=true######################################################################################## USER CONFIGURATION SECTION ######################################################################################### Output directory for generated scripts (must match Duplicati's /scripts mount)SCRIPT_OUTPUT_DIR="/mnt/user/appdata/duplicati/scripts"# ---------------------------------------------------------------------------------# MODE SELECTION: Set ONE of these to "true", the other to "false"# ---------------------------------------------------------------------------------USE_DOCKER="true"USE_DOCKER_COMPOSE="false"# ---------------------------------------------------------------------------------# SCRIPT FILENAME: Base name for generated scripts# Will create: pre-<FILENAME>.sh and post-<FILENAME>.sh# ---------------------------------------------------------------------------------SCRIPT_FILENAME="opencloud-backup"# ---------------------------------------------------------------------------------# DOCKER MODE CONFIGURATION (only used if USE_DOCKER="true")# ---------------------------------------------------------------------------------# Define containers and their START wait times (in seconds)# Format: "container_name:wait_seconds"# - Containers are STOPPED in the order listed (top to bottom)# - Containers are STARTED in REVERSE order (bottom to top)# - wait_seconds = time to wait AFTER starting this container before starting the next## Example for OpenCloud stack:# 1. Stop order: Radicale -> Collaboration -> Collabora -> OpenCloud# 2. Start order: OpenCloud -> Collabora -> Collaboration -> Radicale#DOCKER_CONTAINERS=("Radicale:5""Collaboration:8""Collabora:5""OpenCloud:5")# ---------------------------------------------------------------------------------# DOCKER COMPOSE MODE CONFIGURATION (only used if USE_DOCKER_COMPOSE="true")# ---------------------------------------------------------------------------------# Path to compose directory INSIDE the Duplicati container# Host path: /mnt/user/appdata/immich# Container path: /source/appdata/immichCOMPOSE_DIR="/source/appdata/immich"# Docker Compose project name (stack name in Unraid)COMPOSE_PROJECT_NAME="immich"# ---------------------------------------------------------------------------------# TIMING SETTINGS# ---------------------------------------------------------------------------------# Time to wait after ALL containers are stopped (database flush time)STOP_WAIT_SECONDS="15"# Time to wait after ALL containers are started (final initialization)START_WAIT_SECONDS="10"# Timeout for individual docker/compose commandsOPERATION_TIMEOUT="120"# ---------------------------------------------------------------------------------# LOGGING# ---------------------------------------------------------------------------------# Log file location INSIDE the Duplicati container (/config is always writable)CONTAINER_LOG_FILE="/config/backup-scripts.log"######################################################################################## DO NOT EDIT BELOW THIS LINE ######################################################################################### Colors for outputRED='\033[0;31m'GREEN='\033[0;32m'YELLOW='\033[1;33m'BLUE='\033[0;34m'CYAN='\033[0;36m'NC='\033[0m'log() {local message="[$(date '+%Y-%m-%d %H:%M:%S')] $1"echo -e "$message"}# Display configurationdisplay_config() {echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"echo -e "${BLUE} Duplicati Pre/Post Script Generator v3.0 ${NC}"echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"echo ""echo -e "${YELLOW}Configuration:${NC}"echo -e " Script Filename: ${GREEN}${SCRIPT_FILENAME}${NC}"echo -e " Output Dir: ${GREEN}${SCRIPT_OUTPUT_DIR}${NC}"echo -e " Log File: ${GREEN}${CONTAINER_LOG_FILE}${NC}"echo ""if [[ "$USE_DOCKER" == "true" ]]; thenecho -e " Mode: ${GREEN}Docker (individual containers)${NC}"echo ""echo -e "${YELLOW}Container Configuration:${NC}"echo -e " ${CYAN}STOP ORDER (top to bottom):${NC}"local i=1for entry in "${DOCKER_CONTAINERS[@]}"; dolocal container="${entry%%:*}"local wait="${entry##*:}"echo -e " $i. ${GREEN}${container}${NC}"((i++))doneecho ""echo -e " ${CYAN}START ORDER (bottom to top, with wait times):${NC}"local total=${#DOCKER_CONTAINERS[@]}for ((i=total-1; i>=0; i--)); dolocal entry="${DOCKER_CONTAINERS[$i]}"local container="${entry%%:*}"local wait="${entry##*:}"echo -e " $((total-i)). ${GREEN}${container}${NC} (wait ${YELLOW}${wait}s${NC} after start)"doneelseecho -e " Mode: ${GREEN}Docker Compose${NC}"echo -e " Compose Dir: ${GREEN}${COMPOSE_DIR}${NC}"echo -e " Project Name: ${GREEN}${COMPOSE_PROJECT_NAME}${NC}"fiecho ""echo -e "${YELLOW}Timing:${NC}"echo -e " Post-stop wait: ${GREEN}${STOP_WAIT_SECONDS}s${NC} (database flush)"echo -e " Post-start wait: ${GREEN}${START_WAIT_SECONDS}s${NC} (final init)"echo -e " Command timeout: ${GREEN}${OPERATION_TIMEOUT}s${NC}"echo ""}# Validationvalidate_config() {local errors=0log "${YELLOW}Validating configuration...${NC}"if [[ -z "$SCRIPT_FILENAME" ]]; thenlog "${RED}ERROR: SCRIPT_FILENAME cannot be empty${NC}"((errors++))fiif [[ "$USE_DOCKER" "true" && "$USE_DOCKER_COMPOSE" "true" ]]; thenlog "${RED}ERROR: Cannot enable both USE_DOCKER and USE_DOCKER_COMPOSE${NC}"((errors++))fiif [[ "$USE_DOCKER" != "true" && "$USE_DOCKER_COMPOSE" != "true" ]]; thenlog "${RED}ERROR: Must enable either USE_DOCKER or USE_DOCKER_COMPOSE${NC}"((errors++))fiif [[ "$USE_DOCKER" == "true" && ${#DOCKER_CONTAINERS[@]} -eq 0 ]]; thenlog "${RED}ERROR: DOCKER_CONTAINERS array is empty${NC}"((errors++))fiif [[ "$USE_DOCKER_COMPOSE" == "true" && -z "$COMPOSE_DIR" ]]; thenlog "${RED}ERROR: COMPOSE_DIR cannot be empty for compose mode${NC}"((errors++))fireturn $errors}# Create output directorycreate_output_dir() {if [[ ! -d "$SCRIPT_OUTPUT_DIR" ]]; thenlog "Creating output directory: $SCRIPT_OUTPUT_DIR"mkdir -p "$SCRIPT_OUTPUT_DIR"fi}# Generate Docker pre-backup scriptgenerate_docker_pre_script() {local script_path="${SCRIPT_OUTPUT_DIR}/pre-${SCRIPT_FILENAME}.sh"# Build container list for the script (stop order = as defined)local container_list=""for entry in "${DOCKER_CONTAINERS[@]}"; dolocal container="${entry%%:*}"container_list="${container_list} \"${container}\""$'\n'donecat > "$script_path" << 'SCRIPT_HEADER'#!/bin/bash############################################################################## Duplicati Pre-Backup Script (Docker Mode)SCRIPT_HEADERcat >> "$script_path" << SCRIPT_INFO# Auto-generated: $(date '+%Y-%m-%d %H:%M:%S')# Stop Order: $(for e in "${DOCKER_CONTAINERS[@]}"; do echo -n "${e%%:*} "; done)#############################################################################SCRIPT_INFOcat >> "$script_path" << SCRIPT_CONFIG# Containers to stop (in order)CONTAINERS=(${container_list})STOP_WAIT_SECONDS="${STOP_WAIT_SECONDS}"OPERATION_TIMEOUT="${OPERATION_TIMEOUT}"LOG_FILE="${CONTAINER_LOG_FILE}"SCRIPT_CONFIGcat >> "$script_path" << 'SCRIPT_BODY'log() {local msg="[$(date '+%Y-%m-%d %H:%M:%S')] [PRE-BACKUP] $1"echo "$msg"{ echo "$msg" >> "$LOG_FILE"; } 2>/dev/null || true}log "========== Starting Pre-Backup Script =========="log "Containers to stop: ${CONTAINERS[*]}"# Stop containers in orderfor container in "${CONTAINERS[@]}"; doif docker inspect "$container" &>/dev/null; thenlog "Stopping: $container"if timeout "$OPERATION_TIMEOUT" docker stop "$container"; thenlog "Stopped: $container"elselog "WARNING: Failed to stop $container (timeout or error)"fielselog "WARNING: Container '$container' not found, skipping"fidonelog "Waiting ${STOP_WAIT_SECONDS}s for databases to flush..."sleep "$STOP_WAIT_SECONDS"log "Pre-backup completed successfully"exit 0SCRIPT_BODYchmod +x "$script_path"log "${GREEN}Created: $script_path${NC}"}# Generate Docker post-backup scriptgenerate_docker_post_script() {local script_path="${SCRIPT_OUTPUT_DIR}/post-${SCRIPT_FILENAME}.sh"# Build container list with wait times (start order = reverse)local container_entries=""local total=${#DOCKER_CONTAINERS[@]}for ((i=total-1; i>=0; i--)); dolocal entry="${DOCKER_CONTAINERS[$i]}"local container="${entry%%:*}"local wait="${entry##*:}"container_entries="${container_entries} \"${container}:${wait}\""$'\n'donecat > "$script_path" << 'SCRIPT_HEADER'#!/bin/bash############################################################################## Duplicati Post-Backup Script (Docker Mode)SCRIPT_HEADER# Build start order string for commentlocal start_order=""for ((i=total-1; i>=0; i--)); dolocal entry="${DOCKER_CONTAINERS[$i]}"start_order="${start_order}${entry%%:*} "donecat >> "$script_path" << SCRIPT_INFO# Auto-generated: $(date '+%Y-%m-%d %H:%M:%S')# Start Order: ${start_order}#############################################################################SCRIPT_INFOcat >> "$script_path" << SCRIPT_CONFIG# Containers to start with wait times (format: "container:wait_seconds")# Started in this order (reverse of stop order)CONTAINER_ENTRIES=(${container_entries})START_WAIT_SECONDS="${START_WAIT_SECONDS}"OPERATION_TIMEOUT="${OPERATION_TIMEOUT}"LOG_FILE="${CONTAINER_LOG_FILE}"SCRIPT_CONFIGcat >> "$script_path" << 'SCRIPT_BODY'log() {local msg="[$(date '+%Y-%m-%d %H:%M:%S')] [POST-BACKUP] $1"echo "$msg"{ echo "$msg" >> "$LOG_FILE"; } 2>/dev/null || true}log "========== Starting Post-Backup Script =========="# Start containers in order with individual wait timestotal=${#CONTAINER_ENTRIES[@]}current=0for entry in "${CONTAINER_ENTRIES[@]}"; docontainer="${entry%%:*}"wait_time="${entry##*:}"((current++))if docker inspect "$container" &>/dev/null; thenlog "Starting ($current/$total): $container"if timeout "$OPERATION_TIMEOUT" docker start "$container"; thenlog "Started: $container"# Wait after starting (unless it's the last container)if [[ $current -lt $total ]]; thenlog "Waiting ${wait_time}s before starting next container..."sleep "$wait_time"fielselog "WARNING: Failed to start $container (timeout or error)"fielselog "WARNING: Container '$container' not found, skipping"fidonelog "Waiting ${START_WAIT_SECONDS}s for all services to fully initialize..."sleep "$START_WAIT_SECONDS"log "Post-backup completed successfully"exit 0SCRIPT_BODYchmod +x "$script_path"log "${GREEN}Created: $script_path${NC}"}# Generate Docker Compose pre-backup scriptgenerate_compose_pre_script() {local script_path="${SCRIPT_OUTPUT_DIR}/pre-${SCRIPT_FILENAME}.sh"cat > "$script_path" << SCRIPT_CONTENT#!/bin/bash############################################################################## Duplicati Pre-Backup Script (Docker Compose Mode)# Auto-generated: $(date '+%Y-%m-%d %H:%M:%S')# Project: ${COMPOSE_PROJECT_NAME}#############################################################################COMPOSE_DIR="${COMPOSE_DIR}"PROJECT_NAME="${COMPOSE_PROJECT_NAME}"STOP_WAIT_SECONDS="${STOP_WAIT_SECONDS}"OPERATION_TIMEOUT="${OPERATION_TIMEOUT}"LOG_FILE="${CONTAINER_LOG_FILE}"log() {local msg="[\$(date '+%Y-%m-%d %H:%M:%S')] [PRE-BACKUP] \$1"echo "\$msg"{ echo "\$msg" >> "\$LOG_FILE"; } 2>/dev/null || true}log "========== Starting Pre-Backup Script (Compose) =========="# Check for docker compose v2if ! docker compose version &>/dev/null; thenlog "ERROR: 'docker compose' command not available"echo "ERROR: 'docker compose' command not available" >&2exit 1fiCOMPOSE_CMD="docker compose"log "Using: \$COMPOSE_CMD"cd "\$COMPOSE_DIR" || {log "ERROR: Cannot cd to \$COMPOSE_DIR"echo "ERROR: Cannot cd to \$COMPOSE_DIR" >&2exit 1}log "Working directory: \$(pwd)"CMD="\$COMPOSE_CMD -f docker-compose.yml"[[ -n "\$PROJECT_NAME" ]] && CMD="\$CMD -p \$PROJECT_NAME"log "Executing: \$CMD stop"timeout "\$OPERATION_TIMEOUT" \$CMD stop 2>&1log "Waiting \${STOP_WAIT_SECONDS}s for databases to flush..."sleep "\$STOP_WAIT_SECONDS"log "Pre-backup completed successfully"exit 0SCRIPT_CONTENTchmod +x "$script_path"log "${GREEN}Created: $script_path${NC}"}# Generate Docker Compose post-backup scriptgenerate_compose_post_script() {local script_path="${SCRIPT_OUTPUT_DIR}/post-${SCRIPT_FILENAME}.sh"cat > "$script_path" << SCRIPT_CONTENT#!/bin/bash############################################################################## Duplicati Post-Backup Script (Docker Compose Mode)# Auto-generated: $(date '+%Y-%m-%d %H:%M:%S')# Project: ${COMPOSE_PROJECT_NAME}#############################################################################COMPOSE_DIR="${COMPOSE_DIR}"PROJECT_NAME="${COMPOSE_PROJECT_NAME}"START_WAIT_SECONDS="${START_WAIT_SECONDS}"OPERATION_TIMEOUT="${OPERATION_TIMEOUT}"LOG_FILE="${CONTAINER_LOG_FILE}"log() {local msg="[\$(date '+%Y-%m-%d %H:%M:%S')] [POST-BACKUP] \$1"echo "\$msg"{ echo "\$msg" >> "\$LOG_FILE"; } 2>/dev/null || true}log "========== Starting Post-Backup Script (Compose) =========="# Check for docker compose v2if ! docker compose version &>/dev/null; thenlog "ERROR: 'docker compose' command not available"echo "ERROR: 'docker compose' command not available" >&2exit 1fiCOMPOSE_CMD="docker compose"log "Using: \$COMPOSE_CMD"cd "\$COMPOSE_DIR" || {log "ERROR: Cannot cd to \$COMPOSE_DIR"echo "ERROR: Cannot cd to \$COMPOSE_DIR" >&2exit 1}log "Working directory: \$(pwd)"CMD="\$COMPOSE_CMD -f docker-compose.yml"[[ -n "\$PROJECT_NAME" ]] && CMD="\$CMD -p \$PROJECT_NAME"log "Executing: \$CMD start"timeout "\$OPERATION_TIMEOUT" \$CMD start 2>&1log "Waiting \${START_WAIT_SECONDS}s for all services to fully initialize..."sleep "\$START_WAIT_SECONDS"log "Post-backup completed successfully"exit 0SCRIPT_CONTENTchmod +x "$script_path"log "${GREEN}Created: $script_path${NC}"}# Display summarydisplay_summary() {echo ""echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"echo -e "${GREEN} Scripts Generated Successfully! ${NC}"echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"echo ""echo -e " ${BLUE}Pre-backup script:${NC}"echo -e " Host: ${SCRIPT_OUTPUT_DIR}/pre-${SCRIPT_FILENAME}.sh"echo -e " Container: /scripts/pre-${SCRIPT_FILENAME}.sh"echo ""echo -e " ${BLUE}Post-backup script:${NC}"echo -e " Host: ${SCRIPT_OUTPUT_DIR}/post-${SCRIPT_FILENAME}.sh"echo -e " Container: /scripts/post-${SCRIPT_FILENAME}.sh"echo ""echo -e "${YELLOW}━━━━━━━━━━━━ Duplicati Job Configuration ━━━━━━━━━━━━━━━━━━${NC}"echo ""echo -e " In Duplicati WebUI → Edit Job → Options → Advanced options:"echo ""echo -e " ${GREEN}--run-script-before=${NC}/scripts/pre-${SCRIPT_FILENAME}.sh"echo -e " ${GREEN}--run-script-after=${NC}/scripts/post-${SCRIPT_FILENAME}.sh"echo ""echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"echo ""}######################################################################################## MAIN EXECUTION ########################################################################################display_configif ! validate_config; thenecho -e "${RED}Configuration validation failed! Check settings above.${NC}"exit 1filog "${GREEN}Configuration validated${NC}"echo ""create_output_dirif [[ "$USE_DOCKER" == "true" ]]; thenlog "Generating Docker mode scripts..."generate_docker_pre_scriptgenerate_docker_post_scriptelselog "Generating Docker Compose mode scripts..."generate_compose_pre_scriptgenerate_compose_post_scriptfidisplay_summarylog "${GREEN}Done!${NC}"in Duplicati just create/edit a backup, go to options and under advanced options -> add "run-script-before"-> enter path "/scripts/pre-YourDefinedName.sh"-> add "run-script-after"-> enter path "/scripts/post-YourDefinedName.sh"Save and test your backup.I hope this helps some of you guys. Edited February 2Feb 2 by Nemu
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.