From Docker to Podman: full migration to rootless
Introduction
There are many posts about migrating from Docker to Podman, but most of them are focused on the basics… docker export
/ podman import
. This post is about a full migration, including volumes data, container’s metadata, and more. Everything, into a rootless Podman environment.
I have created a tool called fly-to-podman that automates the migration process from what I’m going to explain here, please use that tool instead of the manual process because the post will not be updated with the latest changes.
Prerequisites
Start with the obvious, you need to have Docker and Podman installed, and Docker needs to be running.
Appart from that, you need to have jq
, rsync
, and bash
.
Migration
For rootless Podman, all the data is stored in ~/.local/share/containers/
and the configuration can be changed in ~/.config/containers/
. They will change if you aren’t using rootless Podman, so adjust the paths in the post accordingly.
Images
First, let’s migrate the images. We will use docker save
and podman load
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
migrate_images() {
echo "Migrating Docker images to Podman..."
# Get a list of all Docker images (name:tag)
docker images --format "{{.Repository}}:{{.Tag}}" | while read -r image; do
# Skip <none>:<none> images
if [[ "$image" == "<none>:<none>" ]]; then
continue
fi
# Replace slashes in repository names with underscores for filenames
filename=$(echo "$image" | tr '/' '_').tar
echo "Exporting $image..."
docker save -o "$filename" "$image" &&
podman load -i "$filename" &&
echo "Image $image migrated to Podman" || echo "Failed to migrate image $image"
# Remove temporary file
rm -f "$filename"
done
}
This small script will export all Docker images to a tarball, then import them into Podman. It will skip <none>:<none>
images.
Volumes data
Volumes data is stored in /var/lib/docker/volumes/
for Docker and in ~/.local/share/containers/storage/volumes/
for Podman. We can create the same volumes names in Podman so that they are tracked in the database and then copy the data, and chown it accordingly.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
migrate_volumes() {
echo "Migrating Docker volumes to Podman..."
# Get the path to the Podman volumes directory
PODMAN_VOLUMES_PATH=$(podman info --format json | jq -r '.store.volumePath')
DOCKER_VOLUMES_PATH="/var/lib/docker/volumes"
RSYNC_OPTS=""
if [[ "$UID" -ne 0 ]]; then
# If not running as root, make sure to chown the files to the current user
RSYNC_OPTS+=" --chown=$(id -u):$(id -g)"
fi
for volume in $(docker volume ls --format json | jq -r '.Name'); do
echo "Migrating volume: $volume"
podman volume create "$volume" &&
sudo rsync -a "$RSYNC_OPTS" "$DOCKER_VOLUMES_PATH/$volume/_data/" "$PODMAN_VOLUMES_PATH/$volume/_data"
done
}
Networks
We are going to migrate the networks while preserving:
- The network name
- The network driver
- The subnet
- The gateway
- The IP range
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
migrate_networks() {
echo "Migrating Docker networks to Podman..."
# Get all Docker networks
for network in $(docker network ls --format '{{json . }}' | jq -r '.Name'); do
echo "Processing network: $network"
# Skip default Docker networks
if [[ "$network" == "host" || "$network" == "none" ]]; then
echo "Skipping network: $network (Podman does not need it)"
continue
fi
# Extract network details
NETWORK_JSON=$(docker network inspect "$network" | jq '.[0]')
DRIVER=$(echo "$NETWORK_JSON" | jq -r '.Driver')
SUBNET=$(echo "$NETWORK_JSON" | jq -r '.IPAM.Config[0].Subnet // empty')
GATEWAY=$(echo "$NETWORK_JSON" | jq -r '.IPAM.Config[0].Gateway // empty')
IP_RANGE=$(echo "$NETWORK_JSON" | jq -r '.IPAM.Config[0].IPRange // empty')
# Check if the network already exists in Podman
if podman network exists "$network"; then
echo "Network $network already exists in Podman. Skipping creation."
continue
fi
# Build the Podman network create command
PODMAN_NET_CMD="podman network create"
# Add driver (Podman supports `bridge`, `ipvlan` and `macvlan`)
case "$DRIVER" in
"bridge")
PODMAN_NET_CMD+=" --driver bridge"
;;
"macvlan")
PODMAN_NET_CMD+=" --driver macvlan"
;;
"ipvlan")
PODMAN_NET_CMD+=" --driver ipvlan"
;;
*)
echo "Warning: Unsupported network driver '$DRIVER' in Podman. Using default bridge."
PODMAN_NET_CMD+=" --driver bridge"
;;
esac
# Add subnet configuration if available
if [[ -n "$SUBNET" ]]; then
PODMAN_NET_CMD+=" --subnet $SUBNET"
fi
# Add gateway if available
if [[ -n "$GATEWAY" ]]; then
PODMAN_NET_CMD+=" --gateway $GATEWAY"
fi
# Add IP range if available
if [[ -n "$IP_RANGE" ]]; then
PODMAN_NET_CMD+=" --ip-range $IP_RANGE"
fi
# Finalize the command with network name
PODMAN_NET_CMD+=" $network"
# Create the Podman network
echo "Creating Podman network: $network"
eval "$PODMAN_NET_CMD" && echo "Network $network migrated successfully." ||
echo "Failed to migrate network: $network"
done
}
Containers
Containers data is stored in /var/lib/docker/containers/
for Docker and in ~/.local/share/containers/storage/
for Podman. We require a more complex script to migrate the containers data, including metadata like mounts, restart policy, etc.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
migrate_containters() {
echo "Migrating Docker containers to Podman..."
for container in $(docker container ls -a --format json | jq -r '.Names'); do
# Convert container name to lowercase
container_lc=$(echo "$container" | tr '[:upper:]' '[:lower:]')
# Tag for the image to be created from the container
MIGRATION_CONTAINER_TAG="podman.local/${container_lc}-to-podman:latest"
# Get Running status from Docker
WAS_RUNNING=$(docker container inspect -f '{{.State.Running}}' "$container")
# Get RestartPolicy from Docker
RESTART_POLICY=$(docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' "$container")
# Pass the restart policy to Podman
case "$RESTART_POLICY" in
"no") PODMAN_RESTART="" ;;
"always") PODMAN_RESTART="--restart=always" ;;
"unless-stopped") PODMAN_RESTART="--restart=unless-stopped" ;;
"on-failure") PODMAN_RESTART="--restart=on-failure" ;;
*) PODMAN_RESTART="" ;;
esac
echo "Processing container: $container"
# Commit container to an image. It lets us start a new container with the _same_ state and add additional options
docker commit "$container" "$MIGRATION_CONTAINER_TAG" &&
docker save -o "$container_lc".tar "$MIGRATION_CONTAINER_TAG" &&
podman load -i "$container_lc".tar || {
echo "Failed to migrate image for $container"
continue
}
# Extract volume/bind mount information from Docker container
MOUNT_OPTS=""
while read -r mount; do
MOUNT_TYPE=$(echo "$mount" | jq -r '.Type')
SOURCE=$(echo "$mount" | jq -r '.Source')
DESTINATION=$(echo "$mount" | jq -r '.Destination')
READ_WRITE=$(echo "$mount" | jq -r '.RW')
# Pass the RW/RO setting to Podman
if [[ "$READ_WRITE" == "true" ]]; then
MODE="rw"
else
MODE="ro"
fi
if [[ "$MOUNT_TYPE" == "volume" ]]; then
# Use :U to ensure right permissions inside the container.
# It tells Podman to use the correct host UID and GID based on the UID and GID within the <<container|pod>>
MODE+=",U"
# Attach existing named volume
VOLUME_NAME=$(echo "$mount" | jq -r '.Name')
MOUNT_OPTS+=" -v $VOLUME_NAME:$DESTINATION:$MODE"
elif [[ "$MOUNT_TYPE" == "bind" ]]; then
# Use :Z if you're using SELinux to ensure right permissions inside the container
# MODE+=",Z"
# Ensure the source path exists before mounting
[[ -e "$SOURCE" ]] && MOUNT_OPTS+=" -v $SOURCE:$DESTINATION:$MODE"
fi
done < <(docker inspect "$container" | jq -c '.[0].Mounts[]')
# Extract port mappings
PORT_OPTS=""
while read -r port_mapping; do
HOST_IP=$(echo "$port_mapping" | jq -r '.HostIp')
HOST_PORT=$(echo "$port_mapping" | jq -r '.HostPort')
CONTAINER_PORT=$(echo "$port_mapping" | jq -r '.ContainerPort')
PROTOCOL=$(echo "$port_mapping" | jq -r '.Protocol')
# Construct `-p` option (exclude 0.0.0.0 for readability)
if [[ "$HOST_IP" == "0.0.0.0" || -z "$HOST_IP" ]]; then
PORT_OPTS+=" -p $HOST_PORT:$CONTAINER_PORT/$PROTOCOL"
else
PORT_OPTS+=" -p $HOST_IP:$HOST_PORT:$CONTAINER_PORT/$PROTOCOL"
fi
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}')
# Extract network information
NETWORK_OPTS=""
while read -r network; do
NETWORK_NAME=$(echo "$network" | jq -r 'keys[0]')
NETWORK_IP=$(echo "$network" | jq -r ".$NETWORK_NAME.IPAddress")
if [[ -n "$NETWORK_NAME" ]]; then
NETWORK_OPTS+=" --network=$NETWORK_NAME"
fi
if [[ -n "$NETWORK_IP" ]]; then
NETWORK_OPTS+=" --ip=$NETWORK_IP"
fi
done < <(docker inspect "$container" | jq -c '.[0].NetworkSettings.Networks')
# Run the container with the same name and mounts, including RW/RO options
podman run -d --name "$container" $PODMAN_RESTART "$MOUNT_OPTS" "$PORT_OPTS" "$NETWORK_OPTS" "$MIGRATION_CONTAINER_TAG" &&
echo "Container $container migrated successfully" ||
echo "Failed to migrate container: $container"
# Stop the container if this was not running, this allow us to keep the container ready to `podman container start $container`
if [[ "$WAS_RUNNING" == "false" ]]; then
podman stop "$container"
fi
done
}
After running the scripts on that order, you should have all your Docker containers, networks, images, and volumes migrated to rootless Podman with the same state as before.
Thing I like about Podman
- Rootless: Podman is rootless by default, which is a big plus for security.
- Docker compatibility: Podman is mostly compatible with Docker, so you can use the same commands.
- Autoupdates: On Docker, you need to use something like Watchtower to update your containers, but Podman has podman auto-update built-in.
- Podman-compose: Podman has a built in
podman compose
command that is compatible withdocker-compose.yml
files and with thedocker-compose
provider.
Caveats
After migrating from Docker to Podman, you may encounter some issues:
- SELinux: Podman is more strict than Docker with SELinux, so you may need to adjust the labels on your files.
- Networks: Podman does not support all the network drivers that Docker does, so you may need to adjust the network driver in the script.
- Volumes: Podman requires some additional options like
:U
or:Z
to ensure the right permissions inside the container.
Some minor but still annoying issues:
- The system journal will gets spammed with podman-events messages. The only way to disable them is to set
events_logger = "none"
in~/.config/containers/containers.conf
, but then you won’t be able to usepodman container logs
, which is a big downside. Even if you set a default network inThe documentation was not clear, you can actually set a default network in~/.config/containers/containers.conf
, Podman will ignore it.~/.config/containers/containers.conf
using thenetns
option.
I ended up with the following configuration in ~/.config/containers/containers.conf
to workaround the issues:
1
2
3
4
5
6
7
8
9
10
11
12
[engine]
compose_warning_logs = false
healthcheck_events = false
events_logfile_path="/home/edu4rdshl/.local/share/containers/containers.log"
events_logger = "file"
events_logfile_max_size = "1m"
[containers]
default_sysctls = [
"net.ipv4.ping_group_range=0 10000",
]
netns = "podman_rootless" # The default network that you want to use for containers, can be overriden with --network
Tips
- Install podman-docker to continue using
docker
commands with Podman. Or simply create and alias fordocker=podman
. - If you use
docker-compose.yml
files, you can install thedocker-compose
package and then runpodman compose
. See the official documentation for more information.
Conclusion
It’s still too early to say if Podman is a better alternative to Docker, but being rootless by default is nice. There may be some things that I haven’t covered in this post, feel free to open an issue or pull request on the fly-to-podman repository.