Skip to content

Instantly share code, notes, and snippets.

@pirate
Last active December 6, 2024 09:54
Show Gist options
  • Save pirate/265e19a8a768a48cf12834ec87fb0eed to your computer and use it in GitHub Desktop.
Save pirate/265e19a8a768a48cf12834ec87fb0eed to your computer and use it in GitHub Desktop.
Backup a docker-compose project, including all images, named and unnamed volumes, container filesystems, config, logs, and databases.
#!/usr/bin/env bash
### Bash Environment Setup
# http://redsymbol.net/articles/unofficial-bash-strict-mode/
# https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html
# set -o xtrace
set -o errexit
set -o errtrace
set -o nounset
set -o pipefail
IFS=$'\n'
# Fully backup a docker-compose project, including all images, named and unnamed volumes, container filesystems, config, logs, and databases.
project_dir="${1:-$PWD}"
if [ -f "$project_dir/docker-compose.yml" ]; then
echo "[i] Found docker-compose config at $project_dir/docker-compose.yml"
else
echo "[X] Could not find a docker-compose.yml file in $project_dir"
exit 1
fi
project_name=$(basename "$project_dir")
backup_time=$(date +"%Y-%m-%d_%H-%M")
backup_dir="$project_dir/data/backups/$backup_time"
# Source any needed environment variables
[ -f "$project_dir/docker-compose.env" ] && source "$project_dir/docker-compose.env"
[ -f "$project_dir/.env" ] && source "$project_dir/.env"
echo "[+] Backing up $project_name project to $backup_dir"
mkdir -p "$backup_dir"
echo " - Saving docker-compose.yml config"
cp "$project_dir/docker-compose.yml" "$backup_dir/docker-compose.yml"
# Optional: pause the containers before backing up to ensure consistency
# docker compose pause
# Optional: run a command inside the contianer to dump your application's state/database to a stable file
echo " - Saving application state to ./dumps"
mkdir -p "$backup_dir/dumps"
# your database/stateful service export commands to run inside docker go here, e.g.
# docker compose exec postgres env PGPASSWORD="$POSTGRES_PASSWORD" pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB" | gzip -9 > "$backup_dir/dumps/$POSTGRES_DB.sql.gz"
# docker compose exec redis redis-cli SAVE
# docker compose exec redis cat /data/dump.rdb | gzip -9 > "$backup_dir/dumps/redis.rdb.gz"
for service_name in $(docker compose config --services); do
image_id=$(docker compose images -q "$service_name")
image_name=$(docker image inspect --format '{{json .RepoTags}}' "$image_id" | jq -r '.[0]')
container_id=$(docker compose ps -q "$service_name")
service_dir="$backup_dir/$service_name"
echo "[*] Backing up ${project_name}__${service_name} to ./$service_name..."
mkdir -p "$service_dir"
# save image
echo " - Saving $image_name image to ./$service_name/image.tar"
docker save --output "$service_dir/image.tar" "$image_id"
if [[ -z "$container_id" ]]; then
echo " - Warning: $service_name has no container yet."
echo " (has it been started at least once?)"
continue
fi
# save config
echo " - Saving container config to ./$service_name/config.json"
docker inspect "$container_id" > "$service_dir/config.json"
# save logs
echo " - Saving stdout/stderr logs to ./$service_name/docker.{out,err}"
docker logs "$container_id" > "$service_dir/docker.out" 2> "$service_dir/docker.err"
# save data volumes
mkdir -p "$service_dir/volumes"
for source in $(docker inspect -f '{{range .Mounts}}{{println .Source}}{{end}}' "$container_id"); do
volume_dir="$service_dir/volumes$source"
echo " - Saving $source volume to ./$service_name/volumes$source"
mkdir -p $(dirname "$volume_dir")
cp -a -r "$source" "$volume_dir"
done
# save container filesystem
echo " - Saving container filesystem to ./$service_name/container.tar"
docker export --output "$service_dir/container.tar" "$container_id"
# save entire container root dir
echo " - Saving container root to $service_dir/root"
cp -a -r "/var/lib/docker/containers/$container_id" "$service_dir/root"
done
echo "[*] Compressing backup folder to $backup_dir.tar.gz"
tar -zcf "$backup_dir.tar.gz" --totals "$backup_dir" && rm -Rf "$backup_dir"
echo "[√] Finished Backing up $project_name to $backup_dir.tar.gz."
# Resume the containers if paused above
# docker compose unpause
@GeoHolz
Copy link

GeoHolz commented Nov 3, 2023

Awsome script, could you edit it so you can add environment variables for containers in the docker compose file so you can specify if you want to make backups of the attached volumes or not? For Example when backing-up Plex i dont want to backup the volume that contains my music and movies.

I add this feature : https://gist.github.com/GeoHolz/ee9362c82ee13f8a5690d86d6ec7bb0c
Thanks pirate !

@zirkuswurstikus
Copy link

bak.sh: 8: set: Illegal option -o errtrace
I assume you invoke the script with sh bak.sh. Be sure to use bash bak.sh

@JohnLines
Copy link

I have backed up other docker containers, but found Bookwyrm exposed some issues with others may also encounter - I am not fully there yet, so I will post in individual comments for each issue.

First a neat snippet from the Bookwyrm bw-dev script

if docker compose &> /dev/null ; then DOCKER_COMPOSE="docker compose" else DOCKER_COMPOSE="docker-compose" fi
and then use DOCKER_COMPOSE in the rest of the script, unifying for Debian based and other systems.

@JohnLines
Copy link

My .env had a variable (a passkey) which contained a $ (dollar) sign, which caused sourcing it to fail the shell could not find the variable which was the random string following the $

Worked around it for now, simply by commenting out that line in .env - to move on past it, but not an ideal situation - I know (as of very recently - not really a docker person) that secrets would be a better way, but this would be a big upstream change,

@JohnLines
Copy link

Bind volumes - attempting to back up Bind Volumes caused the script to fail -
adapted the save data volumes part to

# save data volumes
mkdir -p "$service_dir/volumes"
for typesource in $(docker inspect -f '{{range .Mounts}}{{println .Type .Source}}{{end}}' "$container_id"); do
type=$(echo $typesource | awk '{print $1}')
if [[ $type == "volume" ]]; then 
  source=$(echo $typesource | awk '{print $2}')
      volume_dir="$service_dir/volumes$source"
      echo "    - Saving $source volume to ./$service_name/volumes$source"
      mkdir -p $(dirname "$volume_dir")
      cp -a -r "$source" "$volume_dir"
 else
	  echo "    - not backing up volume $source with type $type"
fi
 done

@JohnLines
Copy link

One of the services did not have an image - protect against this case with

for service_name in $(docker-compose config --services); do
image_id=$(docker-compose images -q "$service_name")
if [[ -z "$image_id" ]]; then
echo " - Note: $service_name has no image"
else
image_name=$(docker image inspect --format '{{json .RepoTags}}' "$image_id" | jq -r '.[0]')
fi
container_id=$(docker-compose ps -q "$service_name")

and
# save image
if [[ ! -z "$image_id" ]]; then
echo " - Saving $image_name image to ./$service_name/image.tar"
docker save --output "$service_dir/image.tar" "$image_id"
fi

This enabled my docker-compose backup to complete - I will now copy it its migration target host and try a restore on there. Although I
take your point at https://gist.github.com/pirate/265e19a8a768a48cf12834ec87fb0eed#gistcomment-3525603, from experience I know that while backups are nice to have, what people really want is restores - and for that a backup is a required starting point.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment