Docker Container Migration: Complete Guide to Moving Containers Between Servers

Docker container migration is a fundamental skill for modern DevOps professionals. Whether you're consolidating infrastructure, upgrading servers, scaling horizontally, or moving to cloud environments, the ability to seamlessly migrate Docker containers between hosts ensures business continuity and operational flexibility. This comprehensive guide covers multiple migration strategies, from simple container exports to advanced orchestration-based approaches.

Understanding Docker Container Migration

Docker container migration involves transferring containerized applications from one host to another while preserving data, configurations, and state. Unlike traditional application migration, Docker's containerization provides unique advantages:

  • Portability: Containers run consistently across different environments
  • Isolation: Dependencies are packaged within containers
  • Reproducibility: Images ensure identical deployments
  • Efficiency: Layered filesystem reduces transfer sizes
  • State Management: Volumes separate data from application logic

However, successful migration requires understanding Docker's architecture, proper volume management, network configuration, and orchestration principles.

Docker Migration Scenarios

Common Migration Use Cases

  1. Server Consolidation: Moving multiple containers to fewer hosts
  2. Infrastructure Upgrade: Migrating to newer hardware or OS versions
  3. Cloud Migration: Transitioning from on-premises to cloud
  4. Disaster Recovery: Restoring services on backup infrastructure
  5. Load Balancing: Distributing containers across multiple hosts
  6. Development to Production: Promoting validated containers
  7. Multi-Region Deployment: Replicating services geographically

Pre-Migration Planning

Environment Assessment

Document your current Docker environment:

# List all running containers
docker ps -a --format "table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}"

# Check Docker version
docker version

# List all images
docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"

# List all volumes
docker volume ls

# List all networks
docker network ls

# Check disk usage
docker system df -v

# Inspect specific container
docker inspect container_name > /tmp/container_config.json

# Export running container list with details
docker ps -a --format "{{json .}}" > /tmp/containers_inventory.json

Identify Dependencies

# Check container dependencies
docker inspect container_name | jq '.[0].HostConfig.Links'

# Check volume mounts
docker inspect container_name | jq '.[0].Mounts'

# Check network connections
docker inspect container_name | jq '.[0].NetworkSettings.Networks'

# Check environment variables
docker inspect container_name | jq '.[0].Config.Env'

# Export docker-compose configuration if available
docker-compose config > /tmp/docker-compose-current.yml

Pre-Migration Checklist

Complete these tasks before migration:

  • Document all running containers and their configurations
  • Identify persistent data volumes and their locations
  • Export all custom Docker networks configuration
  • List all container interdependencies
  • Backup Docker images to registry or tar files
  • Test image pulls on destination server
  • Verify Docker version compatibility
  • Ensure sufficient disk space on destination
  • Configure firewall rules on new server
  • Test network connectivity between servers
  • Prepare rollback procedures
  • Set up monitoring on both servers
  • Schedule migration window
  • Notify stakeholders

Migration Method 1: Docker Export/Import

Best for: Single containers, simple migrations, no registry access

Exporting Containers

# Export running container to tar file
docker export container_name > /tmp/container_name.tar

# Or compress during export
docker export container_name | gzip > /tmp/container_name.tar.gz

# Export multiple containers
for container in $(docker ps -q); do
  name=$(docker inspect --format='{{.Name}}' $container | sed 's/\///')
  echo "Exporting $name..."
  docker export $container | gzip > /tmp/${name}.tar.gz
done

# Transfer to destination server
scp /tmp/container_name.tar.gz user@new-server:/tmp/

Importing Containers

# On destination server: Import container
docker import /tmp/container_name.tar.gz container_name:migrated

# Import with specific configurations
cat /tmp/container_name.tar.gz | docker import \
  --change "ENV DEBUG=true" \
  --change 'CMD ["nginx", "-g", "daemon off;"]' \
  - container_name:migrated

# Create and start container from imported image
docker run -d --name container_name container_name:migrated

Limitations: This method doesn't preserve volumes, networks, or metadata. Use for stateless containers or as part of a broader strategy.

Migration Method 2: Docker Save/Load (Images)

Best for: Preserving image layers, offline migrations, custom images

Saving Images

# Save single image
docker save -o /tmp/image_name.tar image_name:tag

# Save multiple images
docker save -o /tmp/all_images.tar image1:tag image2:tag image3:tag

# Save all images (use with caution)
docker save $(docker images -q) -o /tmp/all_docker_images.tar

# Compress saved images
docker save image_name:tag | gzip > /tmp/image_name.tar.gz

# Save images with progress indication
docker save image_name:tag | pv | gzip > /tmp/image_name.tar.gz

# Transfer to destination
rsync -avz --progress /tmp/image_name.tar.gz user@new-server:/tmp/

Loading Images

# On destination server: Load image
docker load -i /tmp/image_name.tar

# Load compressed image
gunzip -c /tmp/image_name.tar.gz | docker load

# Verify loaded images
docker images | grep image_name

# Tag loaded image if needed
docker tag old_name:tag new_name:tag

Migration Method 3: Docker Registry

Best for: Production environments, multiple migrations, distributed teams

Setting Up Private Registry

# On registry server: Run Docker registry
docker run -d \
  -p 5000:5000 \
  --restart=always \
  --name registry \
  -v /mnt/registry:/var/lib/registry \
  registry:2

# With authentication
mkdir -p /mnt/registry/auth
docker run --entrypoint htpasswd registry:2 \
  -Bbn username password > /mnt/registry/auth/htpasswd

docker run -d \
  -p 5000:5000 \
  --restart=always \
  --name registry \
  -v /mnt/registry:/var/lib/registry \
  -v /mnt/registry/auth:/auth \
  -e "REGISTRY_AUTH=htpasswd" \
  -e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm" \
  -e "REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd" \
  registry:2

Pushing and Pulling Images

# On source server: Tag image for registry
docker tag image_name:tag registry.yourdomain.com:5000/image_name:tag

# Push to registry
docker push registry.yourdomain.com:5000/image_name:tag

# Push all images to registry
for image in $(docker images --format "{{.Repository}}:{{.Tag}}" | grep -v "<none>"); do
  registry_image="registry.yourdomain.com:5000/${image}"
  docker tag $image $registry_image
  docker push $registry_image
done

# On destination server: Pull from registry
docker pull registry.yourdomain.com:5000/image_name:tag

# Use pulled image
docker run -d registry.yourdomain.com:5000/image_name:tag

Using Docker Hub or Cloud Registries

# Login to Docker Hub
docker login

# Tag and push to Docker Hub
docker tag local_image:tag username/image_name:tag
docker push username/image_name:tag

# On destination: Pull from Docker Hub
docker pull username/image_name:tag

# For AWS ECR
aws ecr get-login-password --region region | \
  docker login --username AWS --password-stdin account.dkr.ecr.region.amazonaws.com

docker tag image:tag account.dkr.ecr.region.amazonaws.com/repository:tag
docker push account.dkr.ecr.region.amazonaws.com/repository:tag

Migration Method 4: Docker Compose Migration

Best for: Multi-container applications, defined configurations

Exporting Docker Compose Configuration

# On source server: Create docker-compose.yml if needed
docker-compose config > docker-compose-export.yml

# Backup entire compose project
tar czf /tmp/compose-project.tar.gz \
  docker-compose.yml \
  .env \
  volumes/ \
  configs/

# Transfer to destination
scp /tmp/compose-project.tar.gz user@new-server:/tmp/

Complete Compose Migration

# Example docker-compose.yml with named volumes
cat > docker-compose.yml << 'EOF'
version: '3.8'

services:
  webapp:
    image: nginx:latest
    ports:
      - "80:80"
    volumes:
      - webapp_data:/usr/share/nginx/html
      - ./configs:/etc/nginx/conf.d
    networks:
      - frontend
    restart: unless-stopped

  database:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
      MYSQL_DATABASE: ${DB_NAME}
    volumes:
      - db_data:/var/lib/mysql
    networks:
      - backend
    restart: unless-stopped

volumes:
  webapp_data:
  db_data:

networks:
  frontend:
  backend:
EOF

# Backup volumes before migration
docker-compose down
docker run --rm \
  -v compose_webapp_data:/source \
  -v /backup:/backup \
  alpine tar czf /backup/webapp_data.tar.gz -C /source .

docker run --rm \
  -v compose_db_data:/source \
  -v /backup:/backup \
  alpine tar czf /backup/db_data.tar.gz -C /source .

# Transfer backups
scp /backup/*.tar.gz user@new-server:/backup/

# On destination server: Extract project
cd /opt/docker-projects
tar xzf /tmp/compose-project.tar.gz

# Create volumes and restore data
docker volume create compose_webapp_data
docker volume create compose_db_data

docker run --rm \
  -v compose_webapp_data:/target \
  -v /backup:/backup \
  alpine sh -c "cd /target && tar xzf /backup/webapp_data.tar.gz"

docker run --rm \
  -v compose_db_data:/target \
  -v /backup:/backup \
  alpine sh -c "cd /target && tar xzf /backup/db_data.tar.gz"

# Start services
docker-compose up -d

Volume Migration Strategies

Direct Volume Backup and Restore

# Method 1: Backup volume to tar file
docker run --rm \
  -v volume_name:/data \
  -v /backup:/backup \
  alpine tar czf /backup/volume_name.tar.gz -C /data .

# Transfer backup
scp /backup/volume_name.tar.gz user@new-server:/backup/

# On destination: Create volume and restore
docker volume create volume_name
docker run --rm \
  -v volume_name:/data \
  -v /backup:/backup \
  alpine sh -c "cd /data && tar xzf /backup/volume_name.tar.gz"

# Method 2: Using rsync for incremental sync
# Find volume location
docker volume inspect volume_name | jq -r '.[0].Mountpoint'

# Sync volume data
sudo rsync -avz \
  /var/lib/docker/volumes/volume_name/_data/ \
  user@new-server:/var/lib/docker/volumes/volume_name/_data/

Volume Migration with Minimal Downtime

# Create migration script for live sync
cat > /root/sync-docker-volumes.sh << 'EOF'
#!/bin/bash

SOURCE_VOLUME=$1
SOURCE_HOST=$2
DEST_VOLUME=$3

# Get volume mount point
SOURCE_PATH=$(docker volume inspect $SOURCE_VOLUME -f '{{.Mountpoint}}')

# Initial sync (while container running)
rsync -avz --progress \
  $SOURCE_PATH/ \
  user@$SOURCE_HOST:/var/lib/docker/volumes/$DEST_VOLUME/_data/

# Stop container
docker stop container_name

# Final sync (fast, only changes)
rsync -avz --delete \
  $SOURCE_PATH/ \
  user@$SOURCE_HOST:/var/lib/docker/volumes/$DEST_VOLUME/_data/

echo "Volume synced successfully"
EOF

chmod +x /root/sync-docker-volumes.sh

Network Configuration Migration

# Export network configuration
docker network inspect network_name > /tmp/network_config.json

# Create network on destination server
docker network create \
  --driver bridge \
  --subnet 172.20.0.0/16 \
  --gateway 172.20.0.1 \
  network_name

# For custom networks with specific options
docker network create \
  --driver bridge \
  --opt "com.docker.network.bridge.name"="docker1" \
  --opt "com.docker.network.bridge.enable_ip_masquerade"="true" \
  --subnet 192.168.10.0/24 \
  custom_network

Zero-Downtime Migration with Load Balancer

# Setup HAProxy for load balancing during migration
cat > haproxy.cfg << 'EOF'
global
    daemon
    maxconn 256

defaults
    mode http
    timeout connect 5000ms
    timeout client 50000ms
    timeout server 50000ms

frontend http_front
    bind *:80
    default_backend servers

backend servers
    balance roundrobin
    server old_server old-server-ip:80 check
    server new_server new-server-ip:80 check backup
EOF

# Run HAProxy
docker run -d \
  --name haproxy \
  -p 80:80 \
  -v $(pwd)/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro \
  haproxy:latest

# Gradually shift traffic by changing server weights
# Update config to: weight 1 for old, weight 9 for new
# Then: weight 0 for old, weight 10 for new

Docker Swarm Migration

# For Swarm services, use stack deployment
# On source Swarm manager: Export stack
docker stack deploy --compose-file docker-compose.yml stack_name

# Save stack configuration
docker service ls > /tmp/services.txt
docker stack services stack_name > /tmp/stack_services.txt

# On destination: Initialize Swarm
docker swarm init --advertise-addr new-server-ip

# Deploy stack to new Swarm
docker stack deploy --compose-file docker-compose.yml stack_name

# Drain old node and remove from Swarm
docker node update --availability drain old-node
docker node rm old-node

Kubernetes Migration (Docker to K8s)

# Convert Docker Compose to Kubernetes manifests
# Install kompose
curl -L https://github.com/kubernetes/kompose/releases/download/v1.31.2/kompose-linux-amd64 -o kompose
chmod +x kompose
sudo mv kompose /usr/local/bin/

# Convert compose file
kompose convert -f docker-compose.yml

# Apply to Kubernetes cluster
kubectl apply -f .

# Or use Helm chart
helm create myapp
# Edit values.yaml with container configurations
helm install myapp ./myapp

Post-Migration Verification

# Verify containers are running
docker ps -a

# Check container logs
docker logs container_name --tail 100

# Test container connectivity
docker exec container_name ping google.com

# Verify volumes are mounted
docker inspect container_name | jq '.[0].Mounts'

# Check volume data
docker run --rm -v volume_name:/data alpine ls -lah /data

# Test application functionality
curl http://new-server-ip
curl -I https://new-server-ip

# Monitor resource usage
docker stats

# Check container health
docker inspect container_name | jq '.[0].State.Health'

Automated Migration Script

# Complete migration script
cat > /root/migrate-docker-containers.sh << 'EOF'
#!/bin/bash

set -e

SOURCE_HOST=$1
DEST_HOST=$2
CONTAINER_NAME=$3

echo "=== Docker Container Migration Script ==="
echo "Source: $SOURCE_HOST"
echo "Destination: $DEST_HOST"
echo "Container: $CONTAINER_NAME"

# Step 1: Get container configuration
echo "Fetching container configuration..."
ssh $SOURCE_HOST "docker inspect $CONTAINER_NAME" > /tmp/container_config.json

IMAGE=$(jq -r '.[0].Config.Image' /tmp/container_config.json)
VOLUMES=$(jq -r '.[0].Mounts[].Name' /tmp/container_config.json)

echo "Image: $IMAGE"
echo "Volumes: $VOLUMES"

# Step 2: Save and transfer image
echo "Saving image..."
ssh $SOURCE_HOST "docker save $IMAGE | gzip" | \
  ssh $DEST_HOST "gunzip | docker load"

# Step 3: Migrate volumes
for volume in $VOLUMES; do
  echo "Migrating volume: $volume"

  # Create volume on destination
  ssh $DEST_HOST "docker volume create $volume"

  # Sync volume data
  ssh $SOURCE_HOST "docker run --rm -v $volume:/data alpine tar c -C /data ." | \
    ssh $DEST_HOST "docker run --rm -i -v $volume:/data alpine tar x -C /data"
done

# Step 4: Generate run command
echo "Generating docker run command..."
RUN_CMD=$(jq -r '
  "docker run -d " +
  "--name " + .[0].Name[1:] + " " +
  (if .[0].HostConfig.RestartPolicy.Name != "" then
    "--restart=" + .[0].HostConfig.RestartPolicy.Name + " "
  else "" end) +
  (.[0].HostConfig.PortBindings | to_entries | map(
    "-p " + (.value[0].HostPort) + ":" + (.key | split("/")[0]) + " "
  ) | join("")) +
  (.[0].Mounts | map(
    "-v " + .Name + ":" + .Destination + " "
  ) | join("")) +
  (.[0].Config.Env | map(
    "-e \"" + . + "\" "
  ) | join("")) +
  .[0].Config.Image
' /tmp/container_config.json)

echo "Run command: $RUN_CMD"

# Step 5: Stop source container
echo "Stopping source container..."
ssh $SOURCE_HOST "docker stop $CONTAINER_NAME"

# Step 6: Start container on destination
echo "Starting container on destination..."
ssh $DEST_HOST "$RUN_CMD"

# Step 7: Verify
echo "Verifying migration..."
ssh $DEST_HOST "docker ps | grep $CONTAINER_NAME"

echo "=== Migration completed successfully ==="
EOF

chmod +x /root/migrate-docker-containers.sh

# Usage
# ./migrate-docker-containers.sh user@old-server user@new-server container_name

Rollback Procedures

# Emergency rollback script
cat > /root/rollback-docker-migration.sh << 'EOF'
#!/bin/bash

CONTAINER_NAME=$1

echo "Rolling back container: $CONTAINER_NAME"

# Stop container on new server
docker stop $CONTAINER_NAME
docker rm $CONTAINER_NAME

# Start container on old server (if stopped)
ssh user@old-server "docker start $CONTAINER_NAME"

# Update load balancer or DNS if applicable
# Revert application configuration

echo "Rollback completed"
EOF

chmod +x /root/rollback-docker-migration.sh

Monitoring During Migration

# Monitor both servers during migration
cat > /root/monitor-docker-migration.sh << 'EOF'
#!/bin/bash

while true; do
  echo "=== $(date) ==="

  echo "Old Server:"
  ssh old-server "docker ps --format 'table {{.Names}}\t{{.Status}}'"

  echo ""
  echo "New Server:"
  ssh new-server "docker ps --format 'table {{.Names}}\t{{.Status}}'"

  echo ""
  echo "Network Test:"
  curl -o /dev/null -s -w "%{http_code}\n" http://new-server-ip

  sleep 10
done
EOF

chmod +x /root/monitor-docker-migration.sh

Best Practices and Recommendations

Security Considerations

# Use secure registries with TLS
# Generate certificates for private registry
openssl req -newkey rsa:4096 -nodes -sha256 -keyout domain.key \
  -x509 -days 365 -out domain.crt

# Configure Docker to trust registry
sudo mkdir -p /etc/docker/certs.d/registry.domain.com:5000
sudo cp domain.crt /etc/docker/certs.d/registry.domain.com:5000/ca.crt

# Scan images for vulnerabilities before migration
docker scan image_name:tag

# Use secrets management
echo "my_secret_data" | docker secret create my_secret -

Performance Optimization

# Enable Docker build cache for faster rebuilds
docker build --cache-from registry.com/image:latest -t image:new .

# Use multi-stage builds to reduce image size
# Optimize layer ordering in Dockerfiles

# Configure Docker storage driver for performance
sudo nano /etc/docker/daemon.json
{
  "storage-driver": "overlay2",
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}

sudo systemctl restart docker

Conclusion

Docker container migration is a versatile process with multiple approaches depending on your specific requirements. Key takeaways:

  1. Choose the right method: Registry for production, export/import for simple cases
  2. Don't forget volumes: Data persistence is critical for stateful applications
  3. Test thoroughly: Verify functionality before final cutover
  4. Minimize downtime: Use load balancers and parallel operations
  5. Document everything: Record configurations and procedures
  6. Monitor continuously: Watch both old and new environments
  7. Plan for rollback: Always have an escape route

Whether you're moving a single container or an entire microservices architecture, following these strategies ensures successful migration with minimal disruption. Docker's portability makes migration straightforward, but attention to detail in volume management, network configuration, and orchestration separates successful migrations from problematic ones.

By mastering these techniques, you'll be equipped to handle any Docker migration scenario with confidence and efficiency.