Skip to content

Instantly share code, notes, and snippets.

@linux4life798
Created November 19, 2025 23:54
Show Gist options
  • Select an option

  • Save linux4life798/69d4d36dc85280ff54076987275ae374 to your computer and use it in GitHub Desktop.

Select an option

Save linux4life798/69d4d36dc85280ff54076987275ae374 to your computer and use it in GitHub Desktop.
Migrate a /var/lib/docker from one BTRFS filesystem to another encapsulating BTRFS filesystem and subvolumes.
#!/bin/bash
# Migrate a /var/lib/docker from one BTRFS filesystem to another encapsulating
# BTRFS filesystem and subvolume(s).
#
# This script tries to correctly handle transfering the hierarchy of subvolumes
# and snapshots that Docker creates in the /var/lib/docker/btrfs/subvolumes
# directory, when it was using the btrfs storage driver.
#
# This is situation when you start using Docker on a machine using BTRFS as
# the root filesystem, but forgot to create an encapsulating subvolume for
# /var/lib/docker. This is also useful for moving /var/lib/docker from one BTRFS
# filesystem to another.
#
# Requirements:
# - Run as root (or via sudo) with Docker stopped.
# - Source and destination paths must reside on BTRFS filesystems.
# - Destination subvolume (e.g. /mnt/@docker) must not already exist.
#
# Usage:
# sudo systemctl stop docker.service
# # Assume that the new /var/lib/docker location will be on the BTRFS
# # filesystem mounted at /mnt. The new encapsulating subvol will be @docker.
# sudo docker-btrfs-migrate.bash migrate /mnt/@docker
# # Verify the final rsync dry-run reports no changes.
# # Move the original /var/lib/docker to /var/lib/docker.bak.
# # Mount the new @docker subvolume to /var/lib/docker via fstab or another method.
# # /etc/fstab:
# # /dev/sda1 /var/lib/docker btrfs defaults,subvol=@docker 0 0
# # sudo systemctl daemon-reload
# sudo systemctl start docker.service
#
# Actions:
# migrate <dst> Copy ${DKR} to <dst>, including subvolume hierarchy.
# restore-rw Revert ${DKR}/btrfs/subvolumes back to read/write if you abort.
# debug Inspect source tree and preview btrfs send commands (DEBUG=1).
# send Stream subvolumes without receiving (advanced workflows).
#
# Tips:
# - You can run `DEBUG=1 ./docker-btrfs-migrate.bash send` to confirm send order.
# - `btrfs subvolume list -t ${DKR}` is useful for auditing before/after migration.
#
# Craig Hesling <[email protected]>
# Exit on error.
set -e
DKR=/var/lib/docker
# https://github.com/linux4life798/bash-includes/blob/main/bash_include.d/1-msg-print.bash
# Print command in Blue and then run it.
# Usage: msg-run [cmd] [args...]
#
# Example: msg-run confirm
msg-run() {
local f="" redir=""
for f in " <$(readlink /proc/$$/fd/0)" " >$(readlink /proc/$$/fd/1)" " 2>$(readlink /proc/$$/fd/2)"; do
redir+="${f##*/dev/pts/*}"
done
printf "\E[34m%s\E[m\n" "> $*${redir}" >&2
[[ -n "$DEBUG" ]] && return 0
"$@"
local ret="$?"; echo "Returned ${ret}" >&2; return "${ret}"
}
# Usage: btrfs-send <depth> <subvol_parent_path|-> <subvol_path> [<subvol2_path>...]
btrfs-send() {
local depth="$1"
local subvol_parent="$2"
shift 2
local -a subvols=( "${@}" )
local opts=( "--proto=0" "--compressed-data" "-e" )
if [[ "${subvol_parent}" != "-" ]]; then
opts+=( "-p ${subvol_parent}" )
fi
printf "%*s" "${depth}" "" >&2
msg-run btrfs send ${opts[@]} ${subvols[@]}
}
# Lookup a BTRFS subvolume by UUID.
# Usage: btrfs-subvol-lookup-uuid <fs_path> <uuid>
#
# Example: btrfs-subvol-lookup-uuid /var/lib/docker 36c8c3b8-4a38-5f46-9813-d41ddae98fc7
btrfs-subvol-lookup-uuid() {
local fs_path="$1"
local uuid="$2"
btrfs subvolume show --uuid "${uuid}" "${fs_path}"
}
# Lookup a BTRFS subvolume by path.
# Usage: btrfs-subvol-lookup-path <subvol_path>
#
# Example: btrfs-subvol-lookup-path /var/lib/docker/btrfs/subvolumes/f46e053e49384e303d7b10fa686935704a7214a6ba995a58fa445726686a9ef0
btrfs-subvol-lookup-path() {
local subvol_path="$1"
btrfs subvolume show "${subvol_path}"
}
# Extracts the UUID from the output of 'btrfs subvolume show'.
# Usage: btrfs subvolume show <subvol_path> | btrfs-subvol-show-parse-uuid
# Returns: the UUID string
# Example:
# btrfs subvolume show /var/lib/docker/btrfs/subvolumes/xyz | btrfs-subvol-show-parse-uuid
# # Output: 12345678-1234-5678-90ab-cdef12345678
btrfs-subvol-show-parse-uuid() {
awk '/^[[:space:]]*UUID:/{print $2}'
}
# Extracts the Parent UUID from the output of 'btrfs subvolume show'.
# Usage: btrfs subvolume show <subvol_path> | btrfs-subvol-show-parse-parent
# Returns: the Parent UUID string, or '-' if none
# Example:
# btrfs subvolume show /var/lib/docker/btrfs/subvolumes/xyz | btrfs-subvol-show-parse-parent
# # Output: 87654321-4321-ba09-8765-210fedcba987
btrfs-subvol-show-parse-parent() {
awk '/^[[:space:]]*Parent UUID:/{print $3}'
}
# Extracts the path from the output of 'btrfs subvolume show' and prepends the expected base path.
# Usage: btrfs subvolume show <subvol_path> | btrfs-subvol-show-parse-path [<expected_base_path>]
# Returns: the absolute subvolume path
# Example:
# btrfs subvolume show /var/lib/docker/btrfs/subvolumes/xyz | btrfs-subvol-show-parse-path /var/lib/docker
# # Output: /var/lib/docker/btrfs/subvolumes/xyz
btrfs-subvol-show-parse-path() {
local expected_path="${1:-/var/lib/docker}"
read line
echo "${expected_path}${line##*${expected_path}}"
}
# Usage: btrfs-tree-build <associative_array_name>
btrfs-tree-build() {
local -n arr="$1"
local path="${DKR}/btrfs/subvolumes"
local subvol
for subvol in "${path}"/*; do
local info="$(btrfs subvolume show "${subvol}")"
local uuid="$(btrfs-subvol-show-parse-uuid <<<"${info}")"
local parent_uuid="$(btrfs-subvol-show-parse-parent <<<"${info}")"
arr["${uuid}"]="${parent_uuid}"
done
}
# Find all children of a given parent UUID.
# If no parent UUID is provided, find all roots.
# Usage: btrfs-tree-find-children <associative_array_name> [<parent_uuid>]
#
# Example:
# # Find all roots.
# btrfs-tree-find-children src_uuid_tree
# # Find all children of a given parent UUID.
# btrfs-tree-find-children src_uuid_tree 36c8c3b8-4a38-5f46-9813-d41ddae98fc7
btrfs-tree-find-children() {
local -n arr="$1"
local parent_uuid="${2:--}"
local child
for child in "${!arr[@]}"; do
if [[ "${arr["${child}"]}" == "${parent_uuid}" ]]; then
echo "${child}"
fi
done
}
# Usage: btrfs-tree-depth-first <associative_array_name> <function> [<parent_uuid>] [<depth>]
#
# function: function <depth> <subvol_parent_path|-> <subvol_path>
#
# Example: btrfs-tree-depth-first src_uuid_tree btrfs-send
btrfs-tree-depth-first() {
local -n arr="$1"
local func="$2" # func <depth> <subvol_parent_path|-> <subvol_path>
local parent_uuid="${3:--}"
local depth="${4:-0}"
local path="${DKR}/btrfs/subvolumes"
local -a child_uuids
mapfile -t child_uuids < <(btrfs-tree-find-children "${!arr}" "${parent_uuid}")
local parent_path="-"
if [[ "${parent_uuid}" != "-" ]]; then
local info="$(btrfs-subvol-lookup-uuid "${path}" "${parent_uuid}")"
parent_path="$(btrfs-subvol-show-parse-path "${path}" <<<"${info}")"
fi
local subvol_uuid
for subvol_uuid in "${child_uuids[@]}"; do
local info="$(btrfs-subvol-lookup-uuid "${path}" "${subvol_uuid}")"
local subvol_path="$(btrfs-subvol-show-parse-path "${path}" <<<"${info}")"
"${func}" "${depth}" "${parent_path}" "${subvol_path}"
btrfs-tree-depth-first "${!arr}" "${func}" "${subvol_uuid}" "$(( depth + 1 ))"
done
}
# Usage: btrfs-tree-breath-first <associative_array_name> <function> [<parent_uuid>] [<depth>]
#
# function: function <depth> <subvol_parent_path|-> <subvol_child_path> [<subvol_child2_path>...]
#
# Example: btrfs-tree-breath-first src_uuid_tree btrfs-send
btrfs-tree-breath-first() {
local -n arr="$1"
local func="$2" # func <depth> <subvol_parent_path|-> <subvol_child_path> [<subvol_child2_path>...]
local parent_uuid="${3:--}"
local depth="${4:-0}"
local path="${DKR}/btrfs/subvolumes"
local -a child_uuids
mapfile -t child_uuids < <(btrfs-tree-find-children "${!arr}" "${parent_uuid}")
if [[ ${#child_uuids[@]} -eq 0 ]]; then
return
fi
local parent_path="-"
if [[ "${parent_uuid}" != "-" ]]; then
local info="$(btrfs-subvol-lookup-uuid "${path}" "${parent_uuid}")"
parent_path="$(btrfs-subvol-show-parse-path "${path}" <<<"${info}")"
fi
local -a child_paths
local child_uuid
for child_uuid in "${child_uuids[@]}"; do
local info="$(btrfs-subvol-lookup-uuid "${path}" "${child_uuid}")"
child_paths+=( "$(btrfs-subvol-show-parse-path "${path}" <<<"${info}")" )
done
"${func}" "${depth}" "${parent_path}" "${child_paths[@]}"
local subvol_uuid
for subvol_uuid in "${child_uuids[@]}"; do
btrfs-tree-breath-first "${!arr}" "${func}" "${subvol_uuid}" "$(( depth + 1 ))"
done
}
################################################################################
# Set the BTRFS read-only property (ro) for subvolumes.
#
# Usage: btrfs-set-ro [-f] <true|false> <subvol_paths...>
# Example: btrfs-set-ro true /var/lib/docker/btrfs/subvolumes/*
btrfs-set-ro() {
local opts=( )
if [[ "$1" == "-f" ]]; then
opts+=( "-f" )
shift
fi
local ro_boolean="$1"
shift
local paths=( "${@}" )
local path
for path in "${paths[@]}"; do
msg-run btrfs property set "${opts[@]}" "${path}" ro "${ro_boolean}"
done
}
docker-migrate() {
local dst_path="$1"
if [[ -z "${dst_path}" ]]; then
echo "Error: Destination path is required"
exit 1
fi
if systemctl is-active docker.service >/dev/null 2>&1; then
echo "Error: Docker service is running, please stop it"
exit 1
fi
# DEBUG=1
echo "# Create Destination Subvolume:" >&2
msg-run btrfs -v subvolume create "${dst_path}"
echo >&2
echo "# Copy Docker Root Files:" >&2
msg-run rsync -avHAXi --exclude='btrfs/subvolumes/*' "${DKR}/" "${dst_path}/"
echo >&2
echo "# Set source subvolumes to ro:" >&2
btrfs-set-ro true "${DKR}"/btrfs/subvolumes/*
echo >&2
echo "# Build BTRFS Subvolume Parent Tree:" >&2
local -A src_uuid_tree=( )
btrfs-tree-build src_uuid_tree
echo "# Send:" >&2
btrfs-tree-breath-first src_uuid_tree btrfs-send | msg-run btrfs receive "${dst_path}/btrfs/subvolumes/"
echo >&2
echo "# Set destination subvolumes to rw:" >&2
btrfs-set-ro -f false "${dst_path}"/btrfs/subvolumes/*
echo >&2
echo "# Update timestamps for subvolume root directories to match source:" >&2
# Update permission and timestamp only of subvolumes.
# If a previous part of this script failed to create subvoumes in the
# destination, this rsync will incorrectly create the missing dirs here,
# but the final sync check should catch these issue (sorta).
msg-run rsync -avHAXi --exclude='btrfs/subvolumes/*/*' "${DKR}/" "${dst_path}/"
echo >&2
echo "# DRYRUN: Check that all files are identical from source to destination:" >&2
# -a: archive mode
# -v: verbose
# -n: dry run
# --checksum: use checksum to verify files
# --delete: delete files that are not in the source
local lines
lines="$(msg-run rsync -avni --checksum --delete "${DKR}/" "${dst_path}/" | tee /dev/stderr | wc -l)"
if [[ "${lines}" -ne 4 ]]; then
echo >&2
echo "Error: rsync output is not as expected: ${lines}" >&2
echo "This means that we FAILED to create a perfect replica of the source docker directory." >&2
echo "Ideally, rsync should report 0 changes to an file or dir." >&2
exit 1
fi
# --omit-dir-times: ignore directory timestamps (the subvolumes dirs may have modified timestamps)
echo >&2
echo "# Successfully migrated /var/lib/docker to ${dst_path}." >&2
}
main() {
local action="$1"
shift
local -A src_uuid_tree=( )
case "${action}" in
migrate)
docker-migrate "$@"
;;
restore-rw)
# If you do not end up using the new destination docker lib, then you
# can use this command to restore the rw permission of the original
# docker lib subvolumes.
echo "# Restoring original ${DKR} subvolumes to rw:" >&2
btrfs-set-ro false "${DKR}"/btrfs/subvolumes/*
;;
debug)
echo "# Build BTRFS Subvolume Parent Tree:" >&2
btrfs-tree-build src_uuid_tree
declare -pA src_uuid_tree
echo >&2
echo "# Find Roots:" >&2
local -a roots
mapfile -t roots< <(btrfs-tree-find-children src_uuid_tree)
local root
for root in "${roots[@]}"; do
btrfs-subvol-lookup-uuid $DKR "${root}"
btrfs-tree-find-children src_uuid_tree "${root}"
done
echo >&2
echo "# Dry-run Send:" >&2
DEBUG=1
# btrfs-tree-depth-first src_uuid_tree btrfs-send
btrfs-tree-breath-first src_uuid_tree btrfs-send
;;
send)
echo "# Build BTRFS Subvolume Parent Tree:" >&2
btrfs-tree-build src_uuid_tree
echo "# Send:" >&2
btrfs-tree-breath-first src_uuid_tree btrfs-send
;;
-h|--help|*)
echo "Usage: docker-btrfs-migrate.bash <action>"
echo "Actions:"
echo " migrate <new_destination_path>"
echo " restore-rw: restore ${DRK} subvolumes to rw"
echo " debug"
echo " send"
exit 1
;;
esac
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment