├── LICENSE ├── README.md └── fly-to-podman.sh /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Eduard Tolosa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fly-to-podman 2 | Migrate from Docker to Podman. 3 | 4 | fly-to-podman is a small bash script that helps you migrate from Docker to Podman. It will migrate your Docker containers, images, and volumes to Podman, as well as keep your container data and configurations (mounts, ports, etc.) intact. 5 | 6 | Full blog post: [From Docker to Podman: full migration to rootless](https://www.edu4rdshl.dev/posts/from-docker-to-podman-full-migration-to-rootless/) 7 | 8 | # What it does 9 | 10 | - Migrate Docker images to Podman (including tags) 11 | - Migrate Docker volumes to Podman (including all data) 12 | - Migrate Docker networks to Podman (including names, IPs, gateways, IP ranges, etc.) 13 | - Migrate Docker containers to Podman (including names, IDs, and statuses such as restart policy, etc.) 14 | - Keep container data and configurations (mounts, exposed ports, etc.) 15 | 16 | # Requirements 17 | 18 | - Docker 19 | - Podman 20 | - bash 21 | - jq 22 | - rsync 23 | 24 | # Usage 25 | 26 | ```bash 27 | fly-to-podman.sh {images|volumes|containers|full} 28 | images: Migrate Docker images to Podman 29 | volumes: Migrate Docker volumes to Podman 30 | containers: Migrate Docker containers to Podman 31 | networks: Migrate Docker networks to Podman 32 | full: Migrate Docker images, volumes, and containers to Podman 33 | ``` 34 | 35 | # Issues and contributions 36 | 37 | If you find any issues or have any suggestions, please open an issue or a pull request. 38 | -------------------------------------------------------------------------------- /fly-to-podman.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This script is used to migrate from Docker to Podman 3 | 4 | # Migrate images 5 | migrate_images() { 6 | echo "Migrating Docker images to Podman..." 7 | # Get a list of all Docker images (name:tag) 8 | docker images --format "{{.Repository}}:{{.Tag}}" | while read -r image; do 9 | # Skip : images 10 | if [[ "$image" == ":" ]]; then 11 | continue 12 | fi 13 | 14 | # Replace slashes in repository names with underscores for filenames 15 | filename=$(echo "$image" | tr '/' '_').tar 16 | 17 | echo "Exporting $image..." 18 | docker save -o "$filename" "$image" && 19 | podman load -i "$filename" && 20 | echo "Image $image migrated to Podman" || echo "Failed to migrate image $image" 21 | 22 | # Remove temporary file 23 | rm -f "$filename" 24 | done 25 | } 26 | 27 | # Migrate volumes 28 | migrate_volumes() { 29 | echo "Migrating Docker volumes to Podman..." 30 | # Get the path to the Podman volumes directory (and guess at the Docker volumes directory) 31 | PODMAN_VOLUMES_PATH=$(podman info --format json | jq -r '.store.volumePath') 32 | DOCKER_VOLUMES_PATH=$(docker system info -fjson | jq -r '.DockerRootDir')/volumes 33 | 34 | RSYNC_OPTS="" 35 | if [[ "$UID" -ne 0 ]]; then 36 | # If not running as root, make sure to chown the files to the current user 37 | RSYNC_OPTS+=" --chown=$(id -u):$(id -g)" 38 | fi 39 | 40 | for volume in $(docker volume ls --format json | jq -r '.Name'); do 41 | echo "Migrating volume: $volume" 42 | podman volume create "$volume" && 43 | sudo rsync -a "$RSYNC_OPTS" "$DOCKER_VOLUMES_PATH/$volume/_data/" "$PODMAN_VOLUMES_PATH/$volume/_data" 44 | done 45 | } 46 | 47 | # Migrate networks 48 | migrate_networks() { 49 | echo "Migrating Docker networks to Podman..." 50 | 51 | # Get all Docker networks 52 | for network in $(docker network ls --format '{{json . }}' | jq -r '.Name'); do 53 | echo "Processing network: $network" 54 | 55 | # Skip default Docker networks 56 | if [[ "$network" == "host" || "$network" == "none" ]]; then 57 | echo "Skipping network: $network (Podman does not need it)" 58 | continue 59 | fi 60 | 61 | # Extract network details 62 | NETWORK_JSON=$(docker network inspect "$network" | jq '.[0]') 63 | DRIVER=$(echo "$NETWORK_JSON" | jq -r '.Driver') 64 | SUBNET=$(echo "$NETWORK_JSON" | jq -r '.IPAM.Config[0].Subnet // empty') 65 | GATEWAY=$(echo "$NETWORK_JSON" | jq -r '.IPAM.Config[0].Gateway // empty') 66 | IP_RANGE=$(echo "$NETWORK_JSON" | jq -r '.IPAM.Config[0].IPRange // empty') 67 | 68 | # Check if the network already exists in Podman 69 | if podman network exists "$network"; then 70 | echo "Network $network already exists in Podman. Skipping creation." 71 | continue 72 | fi 73 | 74 | # Build the Podman network create command 75 | PODMAN_NET_CMD="podman network create" 76 | 77 | # Add driver (Podman supports `bridge`, `ipvlan` and `macvlan`) 78 | case "$DRIVER" in 79 | "bridge") 80 | PODMAN_NET_CMD+=" --driver bridge" 81 | ;; 82 | "macvlan") 83 | PODMAN_NET_CMD+=" --driver macvlan" 84 | ;; 85 | "ipvlan") 86 | PODMAN_NET_CMD+=" --driver ipvlan" 87 | ;; 88 | *) 89 | echo "Warning: Unsupported network driver '$DRIVER' in Podman. Using default bridge." 90 | PODMAN_NET_CMD+=" --driver bridge" 91 | ;; 92 | esac 93 | 94 | # Add subnet configuration if available 95 | if [[ -n "$SUBNET" ]]; then 96 | PODMAN_NET_CMD+=" --subnet $SUBNET" 97 | fi 98 | 99 | # Add gateway if available 100 | if [[ -n "$GATEWAY" ]]; then 101 | PODMAN_NET_CMD+=" --gateway $GATEWAY" 102 | fi 103 | 104 | # Add IP range if available 105 | if [[ -n "$IP_RANGE" ]]; then 106 | PODMAN_NET_CMD+=" --ip-range $IP_RANGE" 107 | fi 108 | 109 | # Finalize the command with network name 110 | PODMAN_NET_CMD+=" $network" 111 | 112 | # Create the Podman network 113 | echo "Creating Podman network: $network" 114 | eval "$PODMAN_NET_CMD" && echo "Network $network migrated successfully." || 115 | echo "Failed to migrate network: $network" 116 | 117 | done 118 | } 119 | 120 | # Migrate containers 121 | migrate_containters() { 122 | echo "Migrating Docker containers to Podman..." 123 | for container in $(docker container ls -a --format json | jq -r '.Names'); do 124 | # Convert container name to lowercase 125 | container_lc=$(echo "$container" | tr '[:upper:]' '[:lower:]') 126 | # Tag for the image to be created from the container 127 | MIGRATION_CONTAINER_TAG="podman.local/${container_lc}-to-podman:latest" 128 | # Get Running status from Docker 129 | WAS_RUNNING=$(docker container inspect -f '{{.State.Running}}' "$container") 130 | # Get RestartPolicy from Docker 131 | RESTART_POLICY=$(docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' "$container") 132 | 133 | # Pass the restart policy to Podman 134 | case "$RESTART_POLICY" in 135 | "no") PODMAN_RESTART="" ;; 136 | "always") PODMAN_RESTART="--restart=always" ;; 137 | "unless-stopped") PODMAN_RESTART="--restart=unless-stopped" ;; 138 | "on-failure") PODMAN_RESTART="--restart=on-failure" ;; 139 | *) PODMAN_RESTART="" ;; 140 | esac 141 | 142 | echo "Processing container: $container" 143 | 144 | # Commit container to an image. It lets us start a new container with the _same_ state and add additional options 145 | docker commit "$container" "$MIGRATION_CONTAINER_TAG" && 146 | docker save -o "$container_lc".tar "$MIGRATION_CONTAINER_TAG" && 147 | podman load -i "$container_lc".tar || { 148 | echo "Failed to migrate image for $container" 149 | continue 150 | } 151 | 152 | # Extract volume/bind mount information from Docker container 153 | MOUNT_OPTS="" 154 | while read -r mount; do 155 | MOUNT_TYPE=$(echo "$mount" | jq -r '.Type') 156 | SOURCE=$(echo "$mount" | jq -r '.Source') 157 | DESTINATION=$(echo "$mount" | jq -r '.Destination') 158 | READ_WRITE=$(echo "$mount" | jq -r '.RW') 159 | 160 | # Pass the RW/RO setting to Podman 161 | if [[ "$READ_WRITE" == "true" ]]; then 162 | MODE="rw" 163 | else 164 | MODE="ro" 165 | fi 166 | 167 | if [[ "$MOUNT_TYPE" == "volume" ]]; then 168 | # Use :U to ensure right permissions inside the container. 169 | # It tells Podman to use the correct host UID and GID based on the UID and GID within the <> 170 | MODE+=",U" 171 | # Attach existing named volume 172 | VOLUME_NAME=$(echo "$mount" | jq -r '.Name') 173 | MOUNT_OPTS+=" -v $VOLUME_NAME:$DESTINATION:$MODE" 174 | elif [[ "$MOUNT_TYPE" == "bind" ]]; then 175 | # Use :Z if you're using SELinux to ensure right permissions inside the container 176 | # MODE+=",Z" 177 | # Ensure the source path exists before mounting 178 | [[ -e "$SOURCE" ]] && MOUNT_OPTS+=" -v $SOURCE:$DESTINATION:$MODE" 179 | fi 180 | done < <(docker inspect "$container" | jq -c '.[0].Mounts[]') 181 | 182 | # Extract port mappings 183 | PORT_OPTS="" 184 | while read -r port_mapping; do 185 | HOST_IP=$(echo "$port_mapping" | jq -r '.HostIp') 186 | HOST_PORT=$(echo "$port_mapping" | jq -r '.HostPort') 187 | CONTAINER_PORT=$(echo "$port_mapping" | jq -r '.ContainerPort') 188 | PROTOCOL=$(echo "$port_mapping" | jq -r '.Protocol') 189 | 190 | # Construct `-p` option (exclude 0.0.0.0 for readability) 191 | if [[ "$HOST_IP" == "0.0.0.0" || -z "$HOST_IP" ]]; then 192 | PORT_OPTS+=" -p $HOST_PORT:$CONTAINER_PORT/$PROTOCOL" 193 | else 194 | PORT_OPTS+=" -p $HOST_IP:$HOST_PORT:$CONTAINER_PORT/$PROTOCOL" 195 | fi 196 | done < <(docker inspect "$container" | jq -c '.[] | .NetworkSettings.Ports | to_entries[] | {ContainerPort: .key, Protocol: (if .key | contains("udp") then "udp" else "tcp" end), HostMappings: .value} | select(.HostMappings != null) | .HostMappings[] | {HostIp, HostPort, ContainerPort, Protocol}') 197 | 198 | # Extract network information 199 | NETWORK_OPTS="" 200 | while read -r network; do 201 | NETWORK_NAME=$(echo "$network" | jq -r 'keys[0]') 202 | NETWORK_IP=$(echo "$network" | jq -r ".$NETWORK_NAME.IPAddress") 203 | 204 | if [[ -n "$NETWORK_NAME" ]]; then 205 | NETWORK_OPTS+=" --network=$NETWORK_NAME" 206 | fi 207 | 208 | if [[ -n "$NETWORK_IP" ]]; then 209 | NETWORK_OPTS+=" --ip=$NETWORK_IP" 210 | fi 211 | done < <(docker inspect "$container" | jq -c '.[0].NetworkSettings.Networks') 212 | 213 | # Run the container with the same name and mounts, including RW/RO options 214 | podman run -d --name "$container" $PODMAN_RESTART "$MOUNT_OPTS" "$PORT_OPTS" "$NETWORK_OPTS" "$MIGRATION_CONTAINER_TAG" && 215 | echo "Container $container migrated successfully" || 216 | echo "Failed to migrate container: $container" 217 | 218 | # Stop the container if this was not running, this allow us to keep the container ready to `podman container start $container` 219 | if [[ "$WAS_RUNNING" == "false" ]]; then 220 | podman stop "$container" 221 | fi 222 | done 223 | } 224 | 225 | # Process arguments 226 | case "$1" in 227 | images) 228 | migrate_images 229 | ;; 230 | volumes) 231 | migrate_volumes 232 | ;; 233 | containers) 234 | migrate_containters 235 | ;; 236 | networks) 237 | migrate_networks 238 | ;; 239 | full) 240 | migrate_images 241 | migrate_volumes 242 | migrate_networks 243 | migrate_containters 244 | ;; 245 | *) 246 | echo "Usage: $0 {images|volumes|containers|full}" 247 | echo -e "\timages: Migrate Docker images to Podman" 248 | echo -e "\tvolumes: Migrate Docker volumes to Podman" 249 | echo -e "\tcontainers: Migrate Docker containers to Podman" 250 | echo -e "\tnetworks: Migrate Docker networks to Podman" 251 | echo -e "\tfull: Migrate Docker images, volumes, and containers to Podman" 252 | exit 1 253 | ;; 254 | esac 255 | --------------------------------------------------------------------------------