When you run services in Docker, the annoying part is rarely starting containers again. The real headache starts when a disk dies, a machine gets rebuilt, or a panel setup gets wiped and you realize the data inside the volumes was never backed up in a way you can actually restore.

That is why I ended up with a fairly plain backup model for my 1Panel server: archive Docker volumes every day, keep the latest seven days locally, and sync the compressed files to a private GitHub repository through Git LFS. It is not the most advanced Docker backup system you could build, but it is one of the simplest setups that still gives you a real recovery path.

If you are searching for things like Docker volume backup to GitHub, Docker automatic backup, or a practical 1Panel backup workflow, this is the exact problem space here.

Why I kept the backup model deliberately small

The temptation is always to jump straight into a more elaborate stack: object storage, retention policies, off-site replication, maybe even a dedicated backup product.

That can be the right move later. But on a personal server or a smaller team machine, the first real win is usually much simpler:

  • make backup happen every day
  • keep recent copies on the box for fast recovery
  • keep another copy somewhere off the machine

That is the whole idea behind this workflow:

Docker volumes / 1Panel config
          ↓
     docker-sync
          ↓
Create backup_<timestamp>
          ↓
Delete local backups older than 7 days
          ↓
Copy archives into Git working tree
          ↓
Track .tar.gz with Git LFS
          ↓
Push to a private GitHub repository

It is lightweight enough to keep running, which matters more than having a theoretically perfect system that never gets finished.

Get GitHub SSH trust working first

I would not even touch the automation part until the server can push to GitHub cleanly.

Run this on the server:

ssh-keygen -t ed25519 -C "[email protected]"
cat ~/.ssh/id_ed25519.pub
ssh -T [email protected]

The first command creates the key, the second lets you copy the public part into GitHub SSH keys, and the third is the one that matters most: a manual connection test.

This step saves time later. A lot of scheduled backup jobs do not fail because the backup logic is wrong. They fail because SSH was never fully set up in the first place.

I also prefer using a dedicated private repository for backups instead of mixing them into an application repo:

[email protected]:kuangyongjie/my-docker-backup.git

Private is the obvious default here.

I wrapped everything into one docker-sync command

One thing I try to avoid is leaving important ops logic buried in some random script folder.

If this backup workflow is going to stay around for months, maybe years, the entry point needs to be stable. So I turned it into a system command:

/usr/local/bin/docker-sync

That way the same command works for manual runs and for 1Panel scheduled execution.

Here is the core script:

cat << 'EOF' > /usr/local/bin/docker-sync
#!/bin/bash

GITHUB_REPO="[email protected]:kuangyongjie/my-docker-backup.git"
GIT_USER_NAME="kuangyongjie"
GIT_USER_EMAIL="[email protected]"
LOCAL_SYNC_PATH="/root/github_sync_space"

if ! command -v git-lfs &> /dev/null; then
    apt-get update && apt-get install -y git git-lfs && git lfs install
fi

if [ ! -d "$LOCAL_SYNC_PATH/.git" ]; then
    echo "--- 第一次运行:初始化同步目录 ---"
    rm -rf "$LOCAL_SYNC_PATH"
    GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=no" git clone "$GITHUB_REPO" "$LOCAL_SYNC_PATH"
    cd "$LOCAL_SYNC_PATH"
    git lfs track "*.tar.gz" && git add .gitattributes
    git config user.name "$GIT_USER_NAME" && git config user.email "$GIT_USER_EMAIL"
    git commit -m "Init LFS"
else
    cd "$LOCAL_SYNC_PATH" && git pull origin main --rebase
fi

TAG=$(date +%Y%m%d_%H%M%S)
DIR="/root/docker_backups/backup_$TAG"
mkdir -p "$DIR"

echo "--- 正在抓取本地数据 ---"
for vol in $(docker volume ls -q); do
    tar -zcf "$DIR/vol_$vol.tar.gz" -C $(docker volume inspect $vol --format '{{.Mountpoint}}') .
done

tar -zcf "$DIR/1panel_apps_config.tar.gz" /opt/1panel/apps

echo "--- 正在清理本地 7 天前的旧备份 ---"
find /root/docker_backups/ -maxdepth 1 -name "backup_*" -type d -mtime +7 -exec rm -rf {} +

echo "--- 正在同步到 GitHub ---"
cp "$DIR"/*.tar.gz "$LOCAL_SYNC_PATH/"
cd "$LOCAL_SYNC_PATH"
git add .
git commit -m "Auto Backup $TAG"
git push origin main
echo "--- 完美!所有任务已完成 ---"
EOF

chmod +x /usr/local/bin/docker-sync

What the script is really doing

The script looks longer than it is.

In practice, it does six things:

  1. install git-lfs if missing
  2. clone and initialize the backup repository on first run
  3. loop through Docker volumes and archive each one
  4. archive the 1Panel apps config directory
  5. remove local backups older than seven days
  6. copy the new archives into the sync repo, commit, and push

That simplicity is exactly why I like this setup. The moving parts are still easy to understand.

The two places I would keep an eye on are these:

  • git commit will fail if there is nothing new to commit
  • docker volume ls -q backs up every volume, so you should know what is on the machine

Neither issue is hard, but both matter once this starts running unattended.

In 1Panel, keep the scheduled task minimal

I would not paste the full backup script into the 1Panel task itself.

It is cleaner to make the shell command solid first, then let 1Panel do only the scheduling. The setup is basically:

  1. run docker-sync manually once and make sure it works
  2. create a scheduled task in 1Panel
  3. choose Shell Script
  4. run it once per day, for example at 03:00
  5. use this as the full task body:
docker-sync

This makes debugging much easier. If the job fails later, you can SSH into the server and run the same command directly instead of reading a long script inside the panel UI.

Recovery matters more than backup elegance

A lot of backup guides stop at “your files are stored somewhere now.” That is not the part that matters when something actually breaks.

The recovery flow is the real test:

  1. clone the backup repository on a new server
  2. find the archive for the right time
  3. extract the relevant vol_xxx.tar.gz
  4. recreate the target Docker volume
  5. copy the data back into that volume mountpoint
  6. recreate the container and mount the volume again

The exact commands depend on what service you are restoring, but the path stays roughly the same.

The real friction is usually not extraction. It is remembering which volume belongs to which service when you are under time pressure. So if you adopt this workflow, keep your volume naming and service mapping clear.

The small set of commands I actually use

Most of the time, I only care about a few checks:

NeedCommand
Run a backup immediatelydocker-sync
Check local backup disk usagedu -sh /root/docker_backups
Inspect local backup foldersls -lh /root/docker_backups
Review recent sync historycd /root/github_sync_space && git log -n 5
Change local retention daysnano /usr/local/bin/docker-sync

If you want a different retention window, changing the find ... -mtime +7 line is enough. No need to redesign the whole workflow.

Where this setup is most likely to break

In practice, the fragile parts are usually not the tar commands themselves.

They are things like:

  • incomplete SSH trust between the server and GitHub
  • git-lfs not installed or not initialized
  • bad permissions in the local sync directory
  • scheduled runs where nothing changed, so git commit exits non-zero

There is also a scale question you should not ignore. A private GitHub repository works fine for configs, moderate Docker volume data, and common app backups. Once the archive size starts growing fast, object storage or a more dedicated backup target will probably make more sense.

So I would frame this setup correctly: it is not the final form of disaster recovery. It is a practical way to give a Docker and 1Panel server a real automated backup habit without introducing a lot of overhead at once.