Skip to content

Instantly share code, notes, and snippets.

@diegofcornejo
Last active January 2, 2026 22:28
Show Gist options
  • Select an option

  • Save diegofcornejo/f5807d43ba5414b955106e41e1c9dcaf to your computer and use it in GitHub Desktop.

Select an option

Save diegofcornejo/f5807d43ba5414b955106e41e1c9dcaf to your computer and use it in GitHub Desktop.
Backup Docker volumes to Cloudflare R2 using AWS CLI

Create backup script

sudo nano /path/to/scripts/backup_docker_volume_to_r2.sh

Create service and timer files

sudo nano /etc/systemd/system/backup-docker-volumes.service
sudo nano /etc/systemd/system/backup-docker-volumes.timer

Reload systemd

sudo systemctl daemon-reexec
sudo systemctl daemon-reload

Enable and start the timer

sudo systemctl enable --now backup-docker-volumes.timer

Execute manually

sudo systemctl start backup-docker-volumes.service

View logs

journalctl -u backup-docker-volumes.service
[Unit]
Description=Backup Docker volumes to Cloudflare R2
After=docker.service network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/bin/bash /path/to/scripts/backup_docker_volume_to_r2.sh
User=diego
Group=diego
# Optional but recommended
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=7
[Install]
WantedBy=multi-user.target
[Unit]
Description=Daily Backup Docker volumes to Cloudflare R2
[Timer]
OnCalendar=*-*-* 09:00:00
Persistent=true
[Install]
WantedBy=timers.target
#!/bin/bash
set -euo pipefail
# ==============================
# Global configuration
# ==============================
S3_BUCKET="<bucket-name>"
HOST_NAME="$(hostname)"
DATE="$(date +%F-%H%M%S)" # YYYY-MM-DD-HHMMSS
AWS_PROFILE="cloudflare-r2"
R2_ACCOUNT_ID="<r2-account-id>"
R2_ENDPOINT="https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com"
# Filter used to select Docker volumes
VOLUME_FILTER="db"
# ==============================
# AWS CLI runner (Cloudflare R2)
# ==============================
aws_cli() {
docker run --rm \
-v "${HOME}/.aws:/root/.aws:ro" \
-v "/tmp:/tmp" \
-e "AWS_PROFILE=${AWS_PROFILE}" \
amazon/aws-cli "$@"
}
# ==============================
# Upload backup and cleanup
# ==============================
upload_and_cleanup() {
local temp_dir="$1"
local volume_name="$2"
local archive_name="$3"
local archive_path="${temp_dir}/${archive_name}"
local s3_key="${HOST_NAME}/docker/${volume_name}/${archive_name}"
echo "Archive path: ${archive_path}"
echo "S3 key: ${s3_key}"
# Ensure archive exists and is not empty
if [[ ! -s "${archive_path}" ]]; then
echo "ERROR: archive is empty or does not exist: ${archive_path}"
return 1
fi
echo "Uploading ${archive_name} to R2..."
aws_cli s3api put-object \
--bucket "${S3_BUCKET}" \
--key "${s3_key}" \
--body "${archive_path}" \
--endpoint-url "${R2_ENDPOINT}" \
--checksum-algorithm CRC32
# Remove temporary directory after successful upload
rm -rf "${temp_dir}"
}
# ==============================
# Main logic
# ==============================
volumes="$(docker volume ls -q | grep -F "${VOLUME_FILTER}" || true)"
if [[ -z "${volumes}" ]]; then
echo "No Docker volumes matched filter: ${VOLUME_FILTER}"
exit 0
fi
for volume in ${volumes}; do
echo "Processing volume: ${volume}"
temp_dir="$(mktemp -d)"
archive_name="${volume}-${DATE}.tar.gz"
# Ensure temp directory is cleaned up if something fails
trap 'rm -rf "${temp_dir}"' RETURN
# Create compressed backup from Docker volume
docker run --rm \
-v "${volume}:/volume:ro" \
-v "${temp_dir}:/backup" \
busybox tar czvf "/backup/${archive_name}" -C /volume ./
# Fix ownership of generated archive
docker run --rm \
-v "${temp_dir}:/backup" \
busybox chown "$(id -u):$(id -g)" "/backup/${archive_name}"
# Upload backup and clean up local files
upload_and_cleanup "${temp_dir}" "${volume}" "${archive_name}"
# Disable trap for next iteration
trap - RETURN
done
echo "Backup process completed."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment