Migrating an entire stack of Docker containers that includes critical services like Supabase, n8n, Baserow, MinIO, Redis, and PostgreSQL can be terrifying.
Iโve done it. The first time, it took me hours, with lots of data loss, broken volumes, and missing .env values. But now, Iโve built a battle-tested system that makes the move safe, fast, and complete.
In this guide, Iโll walk you through how I:
- ๐ Backed up containers, volumes, Compose files, and environment variables
- ๐น Migrated everything including Portainer stacks
- โ๏ธ Recreated all my services:
supabase,baserow,n8n,redis,minio,postgresql, and more - โ Wrote 2 easy shell scripts to make this process automatic
Letโs dive in.
๐ My Stack: What I Needed to Migrate
Hereโs a snapshot of my Docker environment:
- Supabase (with Postgres + Auth)
- n8n (workflow automation)
- Baserow (no-code database)
- MinIO (S3-compatible storage)
- Redis (session caching)
- PostgreSQL (independent instances)
- Portainer (for managing stacks)
These are all running as Docker Compose stacks, mostly deployed through Portainer. Data is stored in named volumes. Environment variables are defined either inline or in .env files.
๐ Folder Structure I Use
All backups and scripts live under a single directory:
~/docker-backup/
โโโ backup.sh # Backup script
โโโ restore.sh # Restore script
โโโ volumes/ # Docker volume backups (tar.gz)
โโโ compose/ # docker-compose.yml & .env files
โโโ images.tar # All Docker images
โโโ logs/ # Backup/restore logs
๐พ The Backup Script (backup.sh)
#!/bin/bash
set -e
BASE_DIR="$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)"
VOLUME_BACKUP_DIR="$BASE_DIR/volumes"
COMPOSE_BACKUP_DIR="$BASE_DIR/compose"
LOG_DIR="$BASE_DIR/logs"
PROJECTS_DIR=~/docker-projects
mkdir -p "$VOLUME_BACKUP_DIR" "$COMPOSE_BACKUP_DIR" "$LOG_DIR"
NOW=$(date +"%Y-%m-%d_%H-%M-%S")
LOG_FILE="$LOG_DIR/backup_$NOW.log"
echo "๐ฆ Backing up Docker volumes..." | tee -a "$LOG_FILE"
for vol in $(docker volume ls -q); do
echo "โ $vol" | tee -a "$LOG_FILE"
docker run --rm -v "$vol":/volume -v "$VOLUME_BACKUP_DIR":/backup alpine \
tar czf "/backup/${vol}.tar.gz" -C /volume . >> "$LOG_FILE" 2>&1
done
echo "๐ธ Saving Docker images..." | tee -a "$LOG_FILE"
docker save $(docker images -q | uniq) -o "$BASE_DIR/images.tar"
echo "๐ Backing up Compose & Env files..." | tee -a "$LOG_FILE"
rsync -av --include='*/' --include='docker-compose.yml' --include='.env' --exclude='*' "$PROJECTS_DIR/" "$COMPOSE_BACKUP_DIR/" >> "$LOG_FILE"
echo "โ
Backup complete!" | tee -a "$LOG_FILE"
๐ช The Restore Script (restore.sh)
#!/bin/bash
set -e
BASE_DIR="$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)"
VOLUME_ARCHIVES_DIR="$BASE_DIR/volumes"
COMPOSE_DIR="$BASE_DIR/compose"
LOG_DIR="$BASE_DIR/logs"
mkdir -p "$LOG_DIR"
NOW=$(date +"%Y-%m-%d_%H-%M-%S")
LOG_FILE="$LOG_DIR/restore_$NOW.log"
echo "๐ฆ Restoring Docker volumes..." | tee -a "$LOG_FILE"
for archive in "$VOLUME_ARCHIVES_DIR"/*.tar.gz; do
vol_name=$(basename "$archive" .tar.gz)
echo "โ Volume: $vol_name" | tee -a "$LOG_FILE"
docker volume create "$vol_name"
docker run --rm -v "$vol_name":/volume -v "$VOLUME_ARCHIVES_DIR":/backup alpine \
tar xzf "/backup/${vol_name}.tar.gz" -C /volume >> "$LOG_FILE" 2>&1
done
echo "๐ธ Loading Docker images..." | tee -a "$LOG_FILE"
docker load -i "$BASE_DIR/images.tar" >> "$LOG_FILE" 2>&1
echo "๐ Starting services..." | tee -a "$LOG_FILE"
find "$COMPOSE_DIR" -name 'docker-compose.yml' | while read -r compose_file; do
stack_dir=$(dirname "$compose_file")
echo "โถ๏ธ $stack_dir" | tee -a "$LOG_FILE"
(cd "$stack_dir" && docker-compose up -d >> "$LOG_FILE" 2>&1)
done
echo "โ
Restore complete!" | tee -a "$LOG_FILE"
๐ Step-by-Step Workflow
On Old Server:
cd ~/docker-backup
bash backup.sh
Transfer to New Server:
scp -r ~/docker-backup user@new-server:~/
On New Server:
cd ~/docker-backup
bash restore.sh
๐ Notes for Supabase, Baserow, and Others
- Supabase: Ensure that your Postgres and storage volumes are named and backed up.
- MinIO: Its
/datavolume must be persistent. - Redis/PostgreSQL: All data lives in volumes (e.g.,
/data,/var/lib/postgresql). - n8n: Store its
.envfile carefully. Webhook URLs may change post-migration. - Baserow: Needs
BASEROW_PUBLIC_URLand plugin paths preserved. - Portainer: Reinstall and re-import stacks using the restored Compose files.
โจ Pro Tips
- Add cronjob to run backup every night
- Use
docker contextto work remotely if needed - Store
.envfiles next to eachdocker-compose.ymlfor auto-loading
๐ Future Improvements (Optional)
- Sync backups to Google Drive or S3
- Notify via n8n workflow after backup
- Add encryption for volume tarballs
๐ Final Thoughts
With this setup, I can migrate my entire Docker infrastructure with confidence. Whether Iโm switching hosts, rebuilding from scratch, or doing DR testing โ it just works.
If you're using Docker with Supabase, Redis, Baserow, and others, try this out. Youโll save yourself hours of manual work.
