Stop containers and tarball Docker volumes for backup.
0
docker-volume-backup.sh
edited
1#!/bin/bash
2
3# Space-separated list of containers to stop before backup.
4# Prefix with context to target a specific Docker context (e.g. "rootless:app-db default:app-web").
5# Uses the $GLOBAL_CONTEXT specified below if no context is provided.
6CONTAINER_NAMES="${CONTAINER_NAMES}"
7
8# Space-separated list of volumes to backup.
9# Prefix with context to target a specific Docker context (e.g. "rootless:app-db_data default:app-uploads").
10# Uses the $GLOBAL_CONTEXT specified below if no context is provided.
11VOLUME_NAMES="${VOLUME_NAMES}"
12
13# Default Docker context to use when entries don't specify one.
14GLOBAL_CONTEXT="${DOCKER_CONTEXT}"
15
16# Directory on the host where backup archives are stored.
17BACKUP_DIR="${BACKUP_DIR:-/data/backups}"
18
19# Set to "true" to preview actions without executing them.
20DRY_RUN="${DRY_RUN:-false}"
21
22IFS=' ' read -r -a container_entries <<< "$CONTAINER_NAMES"
23IFS=' ' read -r -a volume_entries <<< "$VOLUME_NAMES"
24
25stopped=()
26
27if [ "$DRY_RUN" = "true" ]; then
28 echo "=== DRY RUN MODE - No changes will be made ==="
29 echo ""
30fi
31
32parse_entry() {
33 local entry="$1"
34 if [[ "$entry" == *":"* ]]; then
35 local context="${entry%%:*}"
36 local name="${entry#*:}"
37 echo "$context $name"
38 else
39 local context="${GLOBAL_CONTEXT:-default}"
40 local name="$entry"
41 echo "$context $name"
42 fi
43}
44
45ensure_context() {
46 local ctx="$1"
47 if [ "$DRY_RUN" = "true" ]; then
48 echo " [dry-run] Would switch to context '$ctx'"
49 return
50 fi
51 if [ "$ctx" = "default" ]; then
52 docker context use default >/dev/null 2>&1
53 return
54 fi
55 if docker context ls --format '{{.Name}}' | grep -q "^${ctx}$"; then
56 docker context use "$ctx" >/dev/null 2>&1
57 else
58 echo "! Creating Docker Context '$ctx' !"
59 docker context create "$ctx" \
60 --docker host="unix:///var/run/docker_${ctx}.sock"
61 docker context use "$ctx" >/dev/null 2>&1
62 fi
63}
64
65restart_containers() {
66 local -n restarts_ref=$1
67 if [ ${#restarts_ref[@]} -eq 0 ]; then
68 return
69 fi
70 echo ""
71 echo "Re-starting containers..."
72 for entry in "${restarts_ref[@]}"; do
73 read -r ctx name <<< "$(parse_entry "$entry")"
74 ensure_context "$ctx"
75 if [ "$DRY_RUN" = "true" ]; then
76 echo " [dry-run] Would start $name (context: $ctx)"
77 else
78 echo " Starting $name (context: $ctx)"
79 docker start "$name"
80 fi
81 done
82}
83
84current_context=""
85echo "Stopping containers:"
86for entry in "${container_entries[@]}"; do
87 [ -z "$entry" ] && continue
88 read -r ctx name <<< "$(parse_entry "$entry")"
89 if [ "$ctx" != "$current_context" ]; then
90 ensure_context "$ctx"
91 current_context="$ctx"
92 fi
93 if [ "$DRY_RUN" = "true" ]; then
94 echo " [dry-run] Would stop $name"
95 stopped+=("$entry")
96 elif docker ps --format '{{.Names}}' | grep -q "^${name}$"; then
97 echo " Stopping $name..."
98 docker stop -t 30 "$name"
99 exit_code=$?
100 if [[ ${exit_code} -ne 0 ]]; then
101 echo " ⚠ Failed to stop $name"
102 restart_containers stopped
103 exit 1
104 else
105 stopped+=("$entry")
106 echo " ✓ $name stopped"
107 fi
108 else
109 echo " ⚠ $name not running, skipping"
110 fi
111done
112
113echo ""
114echo "Preparing volumes for export..."
115for entry in "${volume_entries[@]}"; do
116 [ -z "$entry" ] && continue
117 read -r ctx name <<< "$(parse_entry "$entry")"
118 if [ "$ctx" != "$current_context" ]; then
119 ensure_context "$ctx"
120 current_context="$ctx"
121 fi
122
123 archive_name="${name}.tar.gz"
124 if [ "$DRY_RUN" = "true" ]; then
125 echo " [dry-run] Would backup $name to ${BACKUP_DIR}/${archive_name}"
126 elif docker volume ls --format "{{.Name}}" | grep -q "^${name}$"; then
127 echo " Backing up $name to ${BACKUP_DIR}/${archive_name}..."
128 docker run --rm \
129 -v "${name}":/source:ro \
130 -v "${BACKUP_DIR}":/backup \
131 alpine:3 tar czf "/backup/${archive_name}" -C /source .
132 echo " ✓ $name exported"
133 else
134 echo " ⚠ Volume $name not found, skipping"
135 fi
136done
137
138restart_containers stopped
139echo ""
140echo "Backup complete."