Skip to content
View in the app

A better way to browse. Learn more.

Unraid

A full-screen app on your home screen with push notifications, badges and more.

To install this app on iOS and iPadOS
  1. Tap the Share icon in Safari
  2. Scroll the menu and tap Add to Home Screen.
  3. Tap Add in the top-right corner.
To install this app on Android
  1. Tap the 3-dot menu (⋮) in the top-right corner of the browser.
  2. Tap Add to Home screen or Install app.
  3. Confirm by tapping Install.

[Support] Linuxserver.io - Duplicati

Featured Replies

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.

image.png

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.

image.png

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.

image.png

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/immich

COMPOSE_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 commands

OPERATION_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 output

RED='\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 configuration

display_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" ]]; then

echo -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=1

for entry in "${DOCKER_CONTAINERS[@]}"; do

local container="${entry%%:*}"

local wait="${entry##*:}"

echo -e " $i. ${GREEN}${container}${NC}"

((i++))

done

echo ""

echo -e " ${CYAN}START ORDER (bottom to top, with wait times):${NC}"

local total=${#DOCKER_CONTAINERS[@]}

for ((i=total-1; i>=0; i--)); do

local entry="${DOCKER_CONTAINERS[$i]}"

local container="${entry%%:*}"

local wait="${entry##*:}"

echo -e " $((total-i)). ${GREEN}${container}${NC} (wait ${YELLOW}${wait}s${NC} after start)"

done

else

echo -e " Mode: ${GREEN}Docker Compose${NC}"

echo -e " Compose Dir: ${GREEN}${COMPOSE_DIR}${NC}"

echo -e " Project Name: ${GREEN}${COMPOSE_PROJECT_NAME}${NC}"

fi

echo ""

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 ""

}

# Validation

validate_config() {

local errors=0

log "${YELLOW}Validating configuration...${NC}"

if [[ -z "$SCRIPT_FILENAME" ]]; then

log "${RED}ERROR: SCRIPT_FILENAME cannot be empty${NC}"

((errors++))

fi

if [[ "$USE_DOCKER" "true" && "$USE_DOCKER_COMPOSE" "true" ]]; then

log "${RED}ERROR: Cannot enable both USE_DOCKER and USE_DOCKER_COMPOSE${NC}"

((errors++))

fi

if [[ "$USE_DOCKER" != "true" && "$USE_DOCKER_COMPOSE" != "true" ]]; then

log "${RED}ERROR: Must enable either USE_DOCKER or USE_DOCKER_COMPOSE${NC}"

((errors++))

fi

if [[ "$USE_DOCKER" == "true" && ${#DOCKER_CONTAINERS[@]} -eq 0 ]]; then

log "${RED}ERROR: DOCKER_CONTAINERS array is empty${NC}"

((errors++))

fi

if [[ "$USE_DOCKER_COMPOSE" == "true" && -z "$COMPOSE_DIR" ]]; then

log "${RED}ERROR: COMPOSE_DIR cannot be empty for compose mode${NC}"

((errors++))

fi

return $errors

}

# Create output directory

create_output_dir() {

if [[ ! -d "$SCRIPT_OUTPUT_DIR" ]]; then

log "Creating output directory: $SCRIPT_OUTPUT_DIR"

mkdir -p "$SCRIPT_OUTPUT_DIR"

fi

}

# Generate Docker pre-backup script

generate_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[@]}"; do

local container="${entry%%:*}"

container_list="${container_list} \"${container}\""$'\n'

done

cat > "$script_path" << 'SCRIPT_HEADER'

#!/bin/bash

#############################################################################

# Duplicati Pre-Backup Script (Docker Mode)

SCRIPT_HEADER

cat >> "$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_INFO

cat >> "$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_CONFIG

cat >> "$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 order

for container in "${CONTAINERS[@]}"; do

if docker inspect "$container" &>/dev/null; then

log "Stopping: $container"

if timeout "$OPERATION_TIMEOUT" docker stop "$container"; then

log "Stopped: $container"

else

log "WARNING: Failed to stop $container (timeout or error)"

fi

else

log "WARNING: Container '$container' not found, skipping"

fi

done

log "Waiting ${STOP_WAIT_SECONDS}s for databases to flush..."

sleep "$STOP_WAIT_SECONDS"

log "Pre-backup completed successfully"

exit 0

SCRIPT_BODY

chmod +x "$script_path"

log "${GREEN}Created: $script_path${NC}"

}

# Generate Docker post-backup script

generate_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--)); do

local entry="${DOCKER_CONTAINERS[$i]}"

local container="${entry%%:*}"

local wait="${entry##*:}"

container_entries="${container_entries} \"${container}:${wait}\""$'\n'

done

cat > "$script_path" << 'SCRIPT_HEADER'

#!/bin/bash

#############################################################################

# Duplicati Post-Backup Script (Docker Mode)

SCRIPT_HEADER

# Build start order string for comment

local start_order=""

for ((i=total-1; i>=0; i--)); do

local entry="${DOCKER_CONTAINERS[$i]}"

start_order="${start_order}${entry%%:*} "

done

cat >> "$script_path" << SCRIPT_INFO

# Auto-generated: $(date '+%Y-%m-%d %H:%M:%S')

# Start Order: ${start_order}

#############################################################################

SCRIPT_INFO

cat >> "$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_CONFIG

cat >> "$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 times

total=${#CONTAINER_ENTRIES[@]}

current=0

for entry in "${CONTAINER_ENTRIES[@]}"; do

container="${entry%%:*}"

wait_time="${entry##*:}"

((current++))

if docker inspect "$container" &>/dev/null; then

log "Starting ($current/$total): $container"

if timeout "$OPERATION_TIMEOUT" docker start "$container"; then

log "Started: $container"

# Wait after starting (unless it's the last container)

if [[ $current -lt $total ]]; then

log "Waiting ${wait_time}s before starting next container..."

sleep "$wait_time"

fi

else

log "WARNING: Failed to start $container (timeout or error)"

fi

else

log "WARNING: Container '$container' not found, skipping"

fi

done

log "Waiting ${START_WAIT_SECONDS}s for all services to fully initialize..."

sleep "$START_WAIT_SECONDS"

log "Post-backup completed successfully"

exit 0

SCRIPT_BODY

chmod +x "$script_path"

log "${GREEN}Created: $script_path${NC}"

}

# Generate Docker Compose pre-backup script

generate_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 v2

if ! docker compose version &>/dev/null; then

log "ERROR: 'docker compose' command not available"

echo "ERROR: 'docker compose' command not available" >&2

exit 1

fi

COMPOSE_CMD="docker compose"

log "Using: \$COMPOSE_CMD"

cd "\$COMPOSE_DIR" || {

log "ERROR: Cannot cd to \$COMPOSE_DIR"

echo "ERROR: Cannot cd to \$COMPOSE_DIR" >&2

exit 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>&1

log "Waiting \${STOP_WAIT_SECONDS}s for databases to flush..."

sleep "\$STOP_WAIT_SECONDS"

log "Pre-backup completed successfully"

exit 0

SCRIPT_CONTENT

chmod +x "$script_path"

log "${GREEN}Created: $script_path${NC}"

}

# Generate Docker Compose post-backup script

generate_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 v2

if ! docker compose version &>/dev/null; then

log "ERROR: 'docker compose' command not available"

echo "ERROR: 'docker compose' command not available" >&2

exit 1

fi

COMPOSE_CMD="docker compose"

log "Using: \$COMPOSE_CMD"

cd "\$COMPOSE_DIR" || {

log "ERROR: Cannot cd to \$COMPOSE_DIR"

echo "ERROR: Cannot cd to \$COMPOSE_DIR" >&2

exit 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>&1

log "Waiting \${START_WAIT_SECONDS}s for all services to fully initialize..."

sleep "\$START_WAIT_SECONDS"

log "Post-backup completed successfully"

exit 0

SCRIPT_CONTENT

chmod +x "$script_path"

log "${GREEN}Created: $script_path${NC}"

}

# Display summary

display_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_config

if ! validate_config; then

echo -e "${RED}Configuration validation failed! Check settings above.${NC}"

exit 1

fi

log "${GREEN}Configuration validated${NC}"

echo ""

create_output_dir

if [[ "$USE_DOCKER" == "true" ]]; then

log "Generating Docker mode scripts..."

generate_docker_pre_script

generate_docker_post_script

else

log "Generating Docker Compose mode scripts..."

generate_compose_pre_script

generate_compose_post_script

fi

display_summary

log "${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 by Nemu

  • Replies 425
  • Views 138.2k
  • Created
  • Last Reply

Top Posters In This Topic

Most Popular Posts

  • SpaceInvaderOne
    SpaceInvaderOne

    Hi Guys. I have made a video about setting up and configuring Duplicati on unRAID for cloud and network backups.  

  • Same problem here. Temporary solution that I came up with is to edit the docker settings and change repository to an older version: "linuxserver/duplicati:v2.0.4.23-2.0.4.23_beta_2019-07-14-ls27"

  • Not sure if this is the issue you're having but it has to do with existing session data at least in the x64 version of Duplicati.   When in the web UI, for Chrome, right click "inspect" go t

Posted Images

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.

Guest
Reply to this topic...

Account

Navigation

Search

Search

Configure browser push notifications

Chrome (Android)
  1. Tap the lock icon next to the address bar.
  2. Tap Permissions → Notifications.
  3. Adjust your preference.
Chrome (Desktop)
  1. Click the padlock icon in the address bar.
  2. Select Site settings.
  3. Find Notifications and adjust your preference.