6

De Docker a Podman. Migración completa

Vistas: 1
Tutorial: Tutorial de Podman
De Docker a Podman. Migración completa

Si vienes de Docker, tengo una buena noticia: Podman está diseñado para ser tu reemplazo natural. No solo porque su sintaxis de comandos es prácticamente idéntica, sino porque comparte el mismo estándar subyacente (OCI), entiende tus Dockerfiles y hasta puede leer tus archivos docker-compose.yml. Pero ojo: no es un clon. Detrás de esa familiaridad hay una arquitectura radicalmente distinta que lo hace más seguro, más ligero y más Linux-nativo.

En este capítulo vas a descubrir exactamente qué cambia, qué se queda igual, y cómo migrar tus proyectos de Docker a Podman sin romper nada en el intento. Te llevaré de la mano desde las diferencias filosóficas hasta el script de migración que puedes ejecutar en tu servidor esta misma tarde.

Diferencias arquitectónicas: Docker vs Podman

Para entender por qué Podman no es «otro Docker», tienes que mirar debajo del capó. La diferencia no está en lo que hacen —ambos gestionan contenedores OCI— sino en cómo lo hacen.

Modelo cliente-servidor vs modelo daemonless

Docker sigue una arquitectura clásica cliente-servidor:

CLI (docker) → dockerd (demonio) → containerd → runc
  • docker es solo el cliente. Le envías instrucciones a un demonio (dockerd) que se ejecuta como root.
  • dockerd es monolítico: gestiona imágenes, contenedores, redes, volúmenes, todo.
  • Si dockerd se cuelga, todos tus contenedores quedan huérfanos. No puedes ni listarlos.
  • Es un cuello de botella y un punto único de fallo.

Podman rompe ese esquema por completo con un modelo daemonless (sin demonio):

CLI (podman) → fork/exec → runc/crun (proceso hijo directo)
  • No hay demonio. podman es un binario que habla directamente con el runtime OCI.
  • Cada contenedor es un proceso hijo directo de tu shell o del proceso que lo lanzó.
  • Si algo falla, solo afecta a ese contenedor, no al sistema entero.
  • Puedes listar contenedores aunque estés en un entorno de recuperación sin demonio.

En la práctica esto significa:

# En Docker: el cliente se comunica con dockerd
docker ps

# En Podman: el proceso habla directamente con el runtime OCI
podman ps

# El resultado es el mismo, pero el camino es completamente distinto

Rootless nativo: no necesitas ser root para nada

Esta es, probablemente, la ventaja de seguridad más importante de Podman.

Docker requiere que dockerd se ejecute como root. Siempre. Para ejecutar contenedores rootless necesitas configurar rootless mode de forma explícita, y aún así el demonio debe estar levantado.

Podman, en cambio, es rootless por defecto:

# Sin sudo — funciona directamente
$ podman run -d -p 8080:80 nginx:alpine

# Verifica que el proceso es del usuario, no de root
$ ps aux | grep nginx
lorenzo  12345  ...  nginx: master process nginx -g daemon off;

Esto implica:

  • No necesitas pertenecer al grupo docker (que es esencialmente acceso root sin contraseña).
  • Cada usuario tiene su propio espacio de almacenamiento en ~/.local/share/containers/.
  • Los contenedores de un usuario no pueden interferir con los de otro.
  • Si un contenedor es comprometido, el atacante solo tiene privilegios de usuario no root.

Limitación práctica: Los puertos privilegiados (< 1024) no están disponibles en modo rootless sin configuración adicional.

# Esto FALLA si no eres root
podman run -d -p 80:80 nginx:alpine

# Esto funciona — puerto no privilegiado
podman run -d -p 8080:80 nginx:alpine

Para usar puertos < 1024 rootless, necesitas sysctl net.ipv4.ip_unprivileged_port_start=80 o usar podman-proxy con capacidades adecuadas.

Compatibilidad OCI: el estándar que lo une todo

Tanto Docker como Podman implementan las especificaciones de la Open Container Initiative (OCI):

Especificación OCIDockerPodman
Image SpecificationSí (formato Docker → OCI)Sí (nativo OCI)
Runtime SpecificationSí (via containerd + runc)Sí (runc o crun)
Distribution SpecificationSí (Docker Hub, registros)Sí (mismos registros)

¿Qué significa esto en la práctica? Que las imágenes son intercambiables:

# Una imagen construida con Docker funciona en Podman
docker build -t mi-app .
podman run mi-app       # ✅ Funciona

# Una imagen construida con Podman funciona en Docker
podman build -t mi-app .
docker run mi-app       # ✅ Funciona

El formato de imagen es el mismo. Los registros (Docker Hub, quay.io, GitHub Container Registry) son los mismos. Los Dockerfiles son los mismos. La diferencia está en el motor que las ejecuta.

CaracterísticaDockerPodman
ArquitecturaCliente-servidor (demonio)Sin demonio (fork/exec)
Procesodockerd como rootProceso del usuario
RootlessConfigurable, no por defectoNativo, por defecto
Contenedores como hijosNo (hijos del demonio)Sí (hijos del proceso)
Punto único de falloSí (dockerd)No
Runtime OCIcontainerd + runccrun (por defecto) o runc
Formato de imágenesOCI / DockerOCI nativo
Integración systemdLimitadaNativa
Management de podsNo nativoSí (pods)
Compatibilidad KubernetesVia komposeNativa (pods → k8s)

Runtime OCI: crun vs runc

Aunque ambos usan runtimes OCI, Podman viene con crun por defecto, escrito en C (frente a runc en Go):

$ podman info | grep -A2 ociRuntime
  ociRuntime:
    name: crun
    version: 1.15

$ podman run --runtime=runc alpine echo "Forzando runc"

crun es más rápido en arranque y consume menos memoria. En servidores con muchos contenedores, la diferencia es notable.

El alias docker=podman: la puerta de entrada

La forma más rápida de empezar con Podman sin cambiar tus hábitos es instalar el paquete podman-docker:

# En Debian/Ubuntu
sudo apt install podman-docker

# En Fedora/RHEL
sudo dnf install podman-docker

# En Arch Linux
sudo pacman -S podman-docker

Este paquete crea un enlace simbólico o script que reemplaza el binario docker con Podman, y expone el socket de Docker (/var/run/docker.sock) apuntando a Podman.

# Después de instalar podman-docker...
$ which docker
/usr/bin/docker

# ...pero en realidad es Podman
$ docker --version
podman version 5.3.0

¿Qué funciona y qué no con el alias?

Funciona sin cambios:

docker ps                   → podman ps ✅
docker run -d -p 8080:80   → podman run ✅
docker build -t mi-app .   → podman build ✅
docker images              → podman images ✅
docker pull alpine         → podman pull ✅
docker exec -it cont bash  → podman exec ✅
docker logs -f cont        → podman logs ✅
docker inspect cont        → podman inspect ✅
docker network ls          → podman network ls ✅
docker volume ls           → podman volume ls ✅

Funciona con diferencias sutiles:

docker stats               → podman stats ✅ (menos detallado)
docker system df           → podman system df ✅
docker system prune        → podman system prune ✅

No funciona o requiere adaptación:

docker swarm               → ❌ No soportado (usa Kubernetes)
docker plugin              → ❌ No soportado
docker-compose up          → ⚠️ Usa podman-compose o pods
docker save/load            → ✅ Pero formato OCI preferido
docker container inspect   → ✅ Pero sintaxis de `--format` puede diferir

Configuración manual del alias

Si prefieres no instalar podman-docker, puedes crear tu propio alias:

# En ~/.bashrc o ~/.zshrc
alias docker=podman

# Para docker-compose
alias docker-compose='podman-compose'

Pero el paquete podman-docker es más completo porque también expone el socket de Docker, necesario para herramientas que hablan la API de Docker (Portainer, Jenkins, etc.).

La capa de compatibilidad Docker

Podman incluye una capa de compatibilidad que emula la API REST de Docker a través de un socket Unix. Esto es clave para herramientas de terceros que solo saben hablar con Docker.

Activar el socket de compatibilidad:

# Como root (system-wide)
sudo systemctl enable --now podman.socket

# Como usuario (rootless)
systemctl --user enable --now podman.socket

Verifica que funciona:

$ curl -s --unix-socket /run/user/$(id -u)/podman/podman.sock http://localhost/_ping
OK

$ curl -s --unix-socket /run/user/$(id -u)/podman/podman.sock http://localhost/version
{"Platform":{"Name":"podman","Version":"5.3.0"},"APIVersion":"4.0.0","...":""}

Usarlo con herramientas Docker:

# Apuntar Docker CLI al socket de Podman
export DOCKER_HOST=unix:///run/user/1000/podman/podman.sock

# Ahora docker-cli funciona contra Podman
docker ps
docker compose up

Con Portainer u otras UIs:

# Portainer puede conectarse al socket de Podman
docker run -d \
  -p 9000:9000 \
  -v /run/user/1000/podman/podman.sock:/var/run/docker.sock \
  portainer/portainer-ce

Migración de docker-compose a pods

Aquí llegamos al punto donde Docker y Podman se separan de verdad. Docker Compose gestiona múltiples contenedores como un stack. Podman introduce el concepto de pods, que son grupos de contenedores que comparten el mismo namespace de red, PID e IPC.

Opción 1: Usar podman-compose (migración directa)

La vía más rápida: podman-compose es un script Python que traduce docker-compose.yml a comandos Podman.

# Instalación
pip3 install podman-compose

# O con el gestor de paquetes
sudo apt install podman-compose  # Debian/Ubuntu
sudo dnf install podman-compose  # Fedora

Uso directo (idéntico a Docker Compose):

# Levantar el stack
podman-compose up -d

# Ver logs
podman-compose logs -f

# Detener
podman-compose down

Modo compatibilidad Docker: Para maximizar compatibilidad con proyectos existentes:

podman-compose --compatibility up -d

Esto activa comportamientos que emulan más fielmente a Docker Compose (nombrado de contenedores, gestión de redes, etc.).

Limitaciones conocidas de podman-compose:

# ─── Funciona sin problemas ───
version: '3'
services:
  web:
    image: nginx:alpine
    ports:
      - "8080:80"
  db:
    image: postgres:15
    environment:
      POSTGRES_PASSWORD: secret
    volumes:
      - pgdata:/var/lib/postgresql/data
volumes:
  pgdata:
# ─── Puede dar problemas ───
version: '3'
services:
  app:
    build: .
    depends_on:
      - db
    networks:
      - custom_net
    # deploy:         # ❌ No soportado (Swarm)
    # dns: 8.8.8.8    # ⚠️ No siempre funciona
    # ulimits:        # ⚠️ Compatibilidad parcial
    # healthcheck:    # ✅ Funciona desde Podman 4+

Opción 2: Migrar a pods de Podman (recomendado)

En lugar de usar una capa de compatibilidad, puedes adoptar el modelo nativo de Podman: los pods. Un pod es un grupo de contenedores que:

  • Comparten la misma IP y namespace de red.
  • Pueden comunicarse entre sí vía localhost.
  • Se gestionan como una unidad (start/stop/kill del pod afecta a todos).
  • Son directamente exportables a Kubernetes (podman generate kube).

Migración paso a paso: WordPress + MariaDB + Redis

Partimos de un docker-compose.yml típico:

version: '3'
services:
  wordpress:
    image: wordpress:6
    ports:
      - "8080:80"
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: wpuser
      WORDPRESS_DB_PASSWORD: secret
      WORDPRESS_DB_NAME: wordpress
    volumes:
      - wp_data:/var/www/html
  db:
    image: mariadb:11
    environment:
      MYSQL_ROOT_PASSWORD: rootsecret
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wpuser
      MYSQL_PASSWORD: secret
    volumes:
      - db_data:/var/lib/mysql
  redis:
    image: redis:7-alpine
volumes:
  wp_data:
  db_data:

Paso 1: Crear el pod

podman pod create \
  --name wordpress-pod \
  --publish 8080:80 \
  --network bridge

Paso 2: Añadir contenedores al pod

# Base de datos
podman create \
  --pod wordpress-pod \
  --name db \
  -e MYSQL_ROOT_PASSWORD=rootsecret \
  -e MYSQL_DATABASE=wordpress \
  -e MYSQL_USER=wpuser \
  -e MYSQL_PASSWORD=secret \
  -v db_data:/var/lib/mysql \
  mariadb:11

# Redis
podman create \
  --pod wordpress-pod \
  --name redis \
  redis:7-alpine

# WordPress (habla con db y redis via localhost)
podman create \
  --pod wordpress-pod \
  --name wordpress \
  -e WORDPRESS_DB_HOST=localhost \
  -e WORDPRESS_DB_USER=wpuser \
  -e WORDPRESS_DB_PASSWORD=secret \
  -e WORDPRESS_DB_NAME=wordpress \
  -v wp_data:/var/www/html \
  wordpress:6

Paso 3: Iniciar todo el pod

# Arranca todos los contenedores del pod
podman pod start wordpress-pod

# Ver estado
podman pod ps
podman ps --pod

Paso 4: Ver logs de todos los contenedores del pod

# Logs de un contenedor específico dentro del pod
podman logs -f wordpress

# Todos los contenedores del pod
for c in $(podman ps --pod wordpress-pod --format "{{.Names}}"); do
  echo "=== $c ==="
  podman logs --tail 10 "$c"
done

Ventajas de usar pods frente a podman-compose

Aspectopodman-composePods nativos
Curva de aprendizajeBaja (misma sintaxis)Media (nuevo concepto)
CompatibilidadAlta con compose.ymlTotal con Kubernetes
RendimientoSin sobrecargaSin sobrecarga
AislamientoPor contenedorNamespace compartido
ComunicaciónRed bridgelocalhost
Exportar a K8sLimitadoDirecto (generate kube)
systemdManualQuadlets nativos

Migración avanzada: Quadlets

Los Quadlets son archivos de unidad systemd que describen pods y contenedores Podman. Es la forma más «Linux-nativa» de gestionar contenedores en producción.

Crea /etc/containers/systemd/wordpress-pod.container (o en ~/.config/containers/systemd/ si eres rootless):

[Unit]
Description=WordPress Pod Container
After=network-online.target

[Container]
Image=wordpress:6
Pod=wordpress-pod.pod
Environment=WORDPRESS_DB_HOST=localhost
Environment=WORDPRESS_DB_USER=wpuser
Environment=WORDPRESS_DB_PASSWORD=secret
Environment=WORDPRESS_DB_NAME=wordpress
Volume=wp_data:/var/www/html

[Install]
WantedBy=default.target

Y el pod /etc/containers/systemd/wordpress-pod.pod:

[Unit]
Description=WordPress Pod
After=network-online.target

[Pod]
PublishPort=8080:80
Network=bridge

[Install]
WantedBy=default.target

Activar con systemd:

systemctl --user daemon-reload
systemctl --user start wordpress-pod
systemctl --user enable wordpress-pod

Compatibilidad de comandos: guía de referencia rápida

DockerPodmanNotas
docker runpodman runIdéntico
docker pspodman psIdéntico
docker imagespodman imagesIdéntico
docker pullpodman pullIdéntico
docker buildpodman buildIdéntico
docker execpodman execIdéntico
docker logspodman logsIdéntico
docker inspectpodman inspectIdéntico
docker networkpodman networkIdéntico
docker volumepodman volumeIdéntico
docker system prunepodman system pruneIdéntico
docker compose uppodman-compose upRequiere podman-compose
docker swarmUsa Kubernetes
docker pluginNo tiene equivalente
docker savepodman saveFormato OCI preferido
docker loadpodman loadAcepta formato Docker y OCI
docker exportpodman exportIdéntico
docker importpodman importIdéntico
docker commitpodman commitIdéntico
docker cppodman cpIdéntico
docker statspodman statsMenos detallado en versiones antiguas
docker toppodman topIdéntico
docker renamepodman renameIdéntico
docker waitpodman waitIdéntico
docker attachpodman attachIdéntico
docker diffpodman diffIdéntico
docker historypodman historyIdéntico
docker infopodman infoMás detallado en Podman
docker versionpodman versionIdéntico
docker loginpodman loginIdéntico
docker logoutpodman logoutIdéntico
docker tagpodman tagIdéntico
docker pushpodman pushIdéntico
docker searchpodman searchIdéntico

Función profesional shell: migrar_docker_a_podman

Añade esta función a tu ~/.bashrc o ~/.zshrc para analizar tu stack Docker actual y generar los comandos Podman equivalentes, incluyendo pods y quadlets.

migrar_docker_a_podman() {
    local compose_file="${1:-docker-compose.yml}"
    local pod_name="${2:-migrated-pod}"
    local output_dir="${3:-./podman-migration}"

    if [ ! -f "$compose_file" ]; then
        echo "❌ Error: No se encuentra $compose_file"
        echo "Uso: migrar_docker_a_podman [compose.yml] [nombre-pod] [dir-salida]"
        return 1
    fi

    mkdir -p "$output_dir"
    echo "=== Migración de Docker a Podman ==="
    echo "Origen:  $compose_file"
    echo "Pod:     $pod_name"
    echo "Salida:  $output_dir"
    echo ""

    # 1. Script para crear el pod y contenedores
    local create_script="${output_dir}/crear-pod.sh"
    cat > "$create_script" << 'SCRIPT'
#!/bin/bash
# Script generado automáticamente para migración Docker → Podman
set -euo pipefail

echo "=== Creando pod y contenedores ==="
SCRIPT

    # 2. Extraer servicios del compose (búsqueda básica con sed)
    local services
    services=$(grep -E "^\s{2}[a-zA-Z0-9_-]+:" "$compose_file" | grep -vE "^\s{2}(version|services|volumes|networks|configs|secrets):" | sed 's/://g' | sed 's/^[[:space:]]*//')

    # Obtener puertos
    local ports
    ports=$(grep -A5 "ports:" "$compose_file" 2>/dev/null | grep -E "^\s*-\s+\"?[0-9]+:" | sed 's/[ "-]//g' | head -5 | paste -sd "," -)

    # 3. Generar comandos podman create para cada servicio
    {
        echo ""
        echo "# Puerto(s) expuesto(s): ${ports:-ninguno}"
        echo ""
        echo "podman pod create \\"
        echo "  --name $pod_name \\"
        echo "  ${ports:+--publish }${ports:-# Sin puertos publicados}"

        while IFS= read -r service; do
            [ -z "$service" ] && continue
            echo ""
            echo "# Servicio: $service"
            local image
            image=$(grep -A2 "^\s\{2\}${service}:" "$compose_file" | grep "image:" | sed 's/.*image:[[:space:]]*//')
            echo "podman create --pod $pod_name --name $service \\"
            if [ -n "$image" ]; then
                echo "  ${image}"
            else
                echo "  # [imagen no detectada - editar manualmente]"
            fi
        done <<< "$services"
    } >> "$create_script"

    chmod +x "$create_script"

    # 4. Generar quadlet para systemd
    local quadlet_dir="${output_dir}/quadlets"
    mkdir -p "$quadlet_dir"

    # Pod quadlet
    local pod_quadlet="${quadlet_dir}/${pod_name}.pod"
    cat > "$pod_quadlet" << QUADLET
[Unit]
Description=Pod migrado: ${pod_name}
After=network-online.target

[Pod]
${ports:+PublishPort=}${ports:-# Sin puertos}
Network=bridge

[Install]
WantedBy=default.target
QUADLET

    # Container quadlets
    while IFS= read -r service; do
        [ -z "$service" ] && continue
        local container_quadlet="${quadlet_dir}/${service}.container"
        local image
        image=$(grep -A2 "^\s\{2\}${service}:" "$compose_file" | grep "image:" | sed 's/.*image:[[:space:]]*//')
        cat > "$container_quadlet" << QUADLET
[Unit]
Description=Contenedor ${service} (migrado)
After=network-online.target
Requires=${pod_name}.pod

[Container]
Image=${image:-IMAGEN_NO_DETECTADA}
Pod=${pod_name}.pod

[Install]
WantedBy=default.target
QUADLET
    done <<< "$services"

    # 5. Generar resumen
    local summary="${output_dir}/LEEME.txt"
    cat > "$summary" << SUMMARY
╔══════════════════════════════════════════════════════╗
║  Migración Docker → Podman                          ║
║  Generado: $(date '+%Y-%m-%d %H:%M')              ║
╚══════════════════════════════════════════════════════╝

Archivos generados:

1. ${create_script}
   Script para crear el pod y contenedores manualmente.
   Ejecutar: bash crear-pod.sh

2. ${quadlet_dir}/
   Quadlets para systemd (gestión nativa como servicio).
   Copiar a ~/.config/containers/systemd/ (rootless)
   o /etc/containers/systemd/ (root).
   Activar: systemctl --user daemon-reload
            systemctl --user start ${pod_name}

Notas:
- Revisa las variables de entorno en cada contenedor.
- Los puertos < 1024 requieren root o configuración adicional.
- Para docker-compose complejo, usa podman-compose como paso intermedio.
SUMMARY

    echo "✅ Migración completada. Revisa: $output_dir/"
    echo "   - Script de creación:  crear-pod.sh"
    echo "   - Quadlets systemd:    quadlets/"
    echo "   - Instrucciones:       LEEME.txt"
    echo ""
    echo "📋 Servicios detectados:"
    while IFS= read -r service; do
        [ -z "$service" ] && continue
        local img
        img=$(grep -A2 "^\s\{2\}${service}:" "$compose_file" | grep "image:" | sed 's/.*image:[[:space:]]*//' || echo "(build local)")
        echo "   • ${service}: ${img}"
    done <<< "$services"
}

Uso:

# Analiza docker-compose.yml y genera migración
cd /ruta/a/mi/proyecto/
migrar_docker_a_podman docker-compose.yml mi-stack ./migracion

# Ejecuta el script generado
bash ./migracion/crear-pod.sh

# O despliega con quadlets
cp -r ./migracion/quadlets/* ~/.config/containers/systemd/
systemctl --user daemon-reload
systemctl --user start mi-stack

Script completo de migración

El siguiente script automatiza la migración completa de un servidor Docker a Podman, incluyendo la transferencia de imágenes, redes y volúmenes. Es seguro ejecutarlo en un servidor en producción: no elimina nada de Docker hasta que tú lo autorices.

#!/bin/bash
# ──────────────────────────────────────────────────────────────────────────────
# Script: migracion-docker-a-podman.sh
# Descripción: Migración completa de Docker a Podman
#              - Detecta imágenes, contenedores, redes y volúmenes Docker
#              - Transfiere imágenes a Podman
#              - Recrea redes y volúmenes
#              - Genera systemd units (quadlets) para contenedores activos
#              - Modo seguro: no elimina nada de Docker sin confirmación
# Versión: 1.0.0
# ──────────────────────────────────────────────────────────────────────────────

set -euo pipefail

# ── Colores para output ───────────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color

info()    { echo -e "${BLUE}[INFO]${NC} $1"; }
success() { echo -e "${GREEN}[OK]${NC} $1"; }
warn()    { echo -e "${YELLOW}[WARN]${NC} $1"; }
error()   { echo -e "${RED}[ERROR]${NC} $1"; }

# ── Variables de configuración ─────────────────────────────────────────────────
BACKUP_DIR="${HOME}/docker-migration-backup-$(date +%Y%m%d-%H%M%S)"
VERBOSE=false
CLEANUP=false
QUADLET=true
DOCKER_CMD="docker"
PODMAN_CMD="podman"

# ── Parseo de argumentos ───────────────────────────────────────────────────────
usage() {
    cat << EOF
Uso: $0 [opciones]

Migración completa de Docker a Podman.

OPCIONES:
  -v, --verbose      Modo detallado (muestra comandos ejecutados)
  --cleanup          Eliminar datos de Docker tras migración (requiere confirmación)
  --no-quadlet       No generar quadlets para systemd
  --docker-cmd CMD   Comando Docker alternativo (defecto: docker)
  --podman-cmd CMD   Comando Podman alternativo (defecto: podman)
  -h, --help         Muestra esta ayuda

SIN OPCIONES: Modo interactivo con confirmaciones paso a paso.
EOF
    exit 0
}

while [[ $# -gt 0 ]]; do
    case $1 in
        -v|--verbose) VERBOSE=true; shift ;;
        --cleanup) CLEANUP=true; shift ;;
        --no-quadlet) QUADLET=false; shift ;;
        --docker-cmd) DOCKER_CMD="$2"; shift 2 ;;
        --podman-cmd) PODMAN_CMD="$2"; shift 2 ;;
        -h|--help) usage ;;
        *) error "Opción desconocida: $1"; usage ;;
    esac
done

# ── Comprobaciones previas ─────────────────────────────────────────────────────
check_prerequisites() {
    info "Comprobando requisitos previos..."

    if ! command -v "$DOCKER_CMD" &>/dev/null; then
        error "Docker ($DOCKER_CMD) no encontrado. ¿Está instalado?"
        exit 1
    fi

    if ! command -v "$PODMAN_CMD" &>/dev/null; then
        error "Podman ($PODMAN_CMD) no encontrado."
        info "Instálalo con: sudo apt install podman"
        exit 1
    fi

    if ! $DOCKER_CMD info &>/dev/null; then
        warn "Docker no está corriendo o no tienes permisos. Intentando con sudo..."
        if sudo docker info &>/dev/null; then
            DOCKER_CMD="sudo docker"
            success "Usando 'sudo docker'"
        else
            error "No se puede conectar con Docker"
            exit 1
        fi
    fi

    success "Requisitos cumplidos"
    info "Docker:   $($DOCKER_CMD --version 2>/dev/null || echo 'N/A')"
    info "Podman:   $($PODMAN_CMD --version 2>/dev/null || echo 'N/A')"
}

# ── Copia de seguridad de la configuración Docker ──────────────────────────────
backup_docker_config() {
    echo ""
    info "📦 Realizando copia de seguridad de la configuración Docker..."
    mkdir -p "$BACKUP_DIR"/{imagenes,contenedores,redes,volumenes,compose}

    # Exportar lista de imágenes
    $DOCKER_CMD images --format "{{.Repository}}:{{.Tag}}" > "$BACKUP_DIR/imagenes/lista.txt" 2>/dev/null || true
    info "  Imágenes detectadas: $(wc -l < "$BACKUP_DIR/imagenes/lista.txt")"

    # Exportar configuración de contenedores
    for c in $($DOCKER_CMD ps -a --format "{{.Names}}" 2>/dev/null); do
        $DOCKER_CMD inspect "$c" > "$BACKUP_DIR/contenedores/${c}.json" 2>/dev/null || true
    done

    # Exportar redes
    for net in $($DOCKER_CMD network ls --format "{{.Name}}" 2>/dev/null | grep -vE "^(bridge|host|none)$"); do
        $DOCKER_CMD network inspect "$net" > "$BACKUP_DIR/redes/${net}.json" 2>/dev/null || true
    done

    # Exportar volúmenes
    for vol in $($DOCKER_CMD volume ls --format "{{.Name}}" 2>/dev/null); do
        $DOCKER_CMD volume inspect "$vol" > "$BACKUP_DIR/volumenes/${vol}.json" 2>/dev/null || true
    done

    success "Backup completado en: $BACKUP_DIR"
}

# ── Migrar imágenes ────────────────────────────────────────────────────────────
migrate_images() {
    echo ""
    info "🖼️  Migrando imágenes Docker a Podman..."

    local image_list="$BACKUP_DIR/imagenes/lista.txt"
    local total=0
    local success_count=0

    while IFS= read -r image; do
        [ -z "$image" ] && continue
        total=$((total + 1))
        info "  [$total] Procesando: $image"

        # Verificar si ya existe en Podman
        if $PODMAN_CMD image exists "$image" 2>/dev/null; then
            info "    Ya existe en Podman, saltando"
            success_count=$((success_count + 1))
            continue
        fi

        # Extraer: pull directa (preferida) o export/import
        if $DOCKER_CMD pull "$image" &>/dev/null; then
            # Re-pull con Podman desde el mismo registro
            if $PODMAN_CMD pull "$image" 2>/dev/null; then
                success "    Migrada: $image"
                success_count=$((success_count + 1))
            else
                warn "    No se pudo migrar $image (pull falló), intentando export..."
                # Fallback: exportar imagen Docker e importar en Podman
                local tmpfile=$(mktemp)
                if $DOCKER_CMD save "$image" -o "$tmpfile" 2>/dev/null; then
                    if $PODMAN_CMD load -i "$tmpfile" 2>/dev/null; then
                        success "    Migrada (export/import): $image"
                        success_count=$((success_count + 1))
                    fi
                fi
                rm -f "$tmpfile"
            fi
        else
            warn "    No se pudo procesar $image"
        fi
    done < "$image_list"

    echo ""
    info "Resumen de migración de imágenes:"
    info "  Total procesadas: $total"
    info "  Migradas:         $success_count"
    info "  Pendientes:       $((total - success_count))"
}

# ── Migrar redes ───────────────────────────────────────────────────────────────
migrate_networks() {
    echo ""
    info "🌐 Migrando redes Docker a Podman..."

    for net in $($DOCKER_CMD network ls --format "{{.Name}}" 2>/dev/null | grep -vE "^(bridge|host|none)$"); do
        if $PODMAN_CMD network exists "$net" 2>/dev/null; then
            info "  Red '$net' ya existe en Podman"
        else
            $PODMAN_CMD network create "$net" 2>/dev/null && \
                success "  Red creada: $net" || \
                warn "  No se pudo crear la red '$net' (puede ser normal en rootless)"
        fi
    done
}

# ── Migrar volúmenes ───────────────────────────────────────────────────────────
migrate_volumes() {
    echo ""
    info "💾 Migrando volúmenes Docker a Podman..."

    for vol in $($DOCKER_CMD volume ls --format "{{.Name}}" 2>/dev/null); do
        if $PODMAN_CMD volume exists "$vol" 2>/dev/null; then
            info "  Volumen '$vol' ya existe en Podman"
        else
            $PODMAN_CMD volume create "$vol" 2>/dev/null && \
                success "  Volumen creado: $vol" || \
                warn "  No se pudo crear el volumen '$vol'"
        fi
    done
}

# ── Generar quadlets para contenedores activos ─────────────────────────────────
generate_quadlets() {
    if [ "$QUADLET" = false ]; then
        info "⏭️  Generación de quadlets omitida (--no-quadlet)"
        return
    fi

    echo ""
    info "⚙️  Generando quadlets para systemd..."
    local quadlet_dir="$BACKUP_DIR/quadlets"
    mkdir -p "$quadlet_dir"

    local containers
    containers=$($DOCKER_CMD ps -a --format "{{.Names}}" 2>/dev/null)
    local count=0

    for c in $containers; do
        [ -z "$c" ] && continue
        local image port_args volumes env_vars pod_name

        image=$($DOCKER_CMD inspect "$c" --format '{{.Config.Image}}' 2>/dev/null || echo "")
        [ -z "$image" ] && continue

        local quadlet_file="${quadlet_dir}/${c}.container"
        {
            echo "# Quadlet generado desde contenedor Docker: $c"
            echo "# Fecha: $(date '+%Y-%m-%d %H:%M')"
            echo ""
            echo "[Unit]"
            echo "Description=Contenedor migrado: $c"
            echo "After=network-online.target"
            echo ""
            echo "[Container]"
            echo "Image=$image"
        } > "$quadlet_file"

        # Puertos
        local ports
        ports=$($DOCKER_CMD inspect "$c" --format '{{range $p, $conf := .NetworkSettings.Ports}}{{$p}} -> {{(index $conf 0).HostPort}}{{"\n"}}{{end}}' 2>/dev/null)
        if [ -n "$ports" ]; then
            echo "# Puertos:" >> "$quadlet_file"
            while IFS= read -r line; do
                local container_port host_port
                container_port=$(echo "$line" | cut -d'/' -f1)
                host_port=$(echo "$line" | grep -oP '(?<=-> )\d+')
                if [ -n "$host_port" ]; then
                    echo "PublishPort=${host_port}:${container_port}" >> "$quadlet_file"
                fi
            done <<< "$ports"
        fi

        # Variables de entorno
        local env_vars
        env_vars=$($DOCKER_CMD inspect "$c" --format '{{range $e := .Config.Env}}{{$e}}{{"\n"}}{{end}}' 2>/dev/null)
        if [ -n "$env_vars" ]; then
            while IFS= read -r env; do
                [ -z "$env" ] && continue
                local key="${env%%=*}"
                local val="${env#*=}"
                if [ "$key" != "$val" ]; then
                    echo "Environment=${key}=${val}" >> "$quadlet_file"
                fi
            done <<< "$env_vars"
        fi

        # Volúmenes
        local volumes
        volumes=$($DOCKER_CMD inspect "$c" --format '{{range .Mounts}}{{if eq .Type "volume"}}{{.Name}}:{{.Destination}}{{"\n"}}{{end}}{{end}}' 2>/dev/null)
        if [ -n "$volumes" ]; then
            while IFS= read -r vol; do
                [ -z "$vol" ] && continue
                echo "Volume=${vol}" >> "$quadlet_file"
            done <<< "$volumes"
        fi

        # Bind mounts
        local binds
        binds=$($DOCKER_CMD inspect "$c" --format '{{range .Mounts}}{{if eq .Type "bind"}}{{.Source}}:{{.Destination}}{{"\n"}}{{end}}{{end}}' 2>/dev/null)
        if [ -n "$binds" ]; then
            while IFS= read -r bind; do
                [ -z "$bind" ] && continue
                echo "Mount=type=bind,source=${bind}" >> "$quadlet_file"
            done <<< "$binds"
        fi

        echo "" >> "$quadlet_file"
        echo "[Install]" >> "$quadlet_file"
        echo "WantedBy=default.target" >> "$quadlet_file"

        count=$((count + 1))
        success "  Quadlet generado: $c"
    done

    info "Quadlets generados: $count en $quadlet_dir"
    info "Para instalarlos:"
    info "  cp -r $quadlet_dir/* ~/.config/containers/systemd/"
    info "  systemctl --user daemon-reload"
    info "  systemctl --user start NOMBRE"
}

# ── Limpieza (opcional) ────────────────────────────────────────────────────────
cleanup_docker() {
    if [ "$CLEANUP" = false ]; then
        info "⏭️  Limpieza omitida (usa --cleanup para eliminar datos de Docker)"
        return
    fi

    echo ""
    warn "⚠️  ATENCIÓN: Se eliminarán los datos de Docker"
    echo "    Esta acción no se puede deshacer."
    echo "    Backup disponible en: $BACKUP_DIR"
    echo ""
    read -p "¿Estás seguro? Escribe 'borrar' para confirmar: " confirm

    if [ "$confirm" != "borrar" ]; then
        info "Limpieza cancelada."
        return
    fi

    info "Deteniendo todos los contenedores Docker..."
    $DOCKER_CMD stop $($DOCKER_CMD ps -q) 2>/dev/null || true

    info "Eliminando contenedores, redes, volúmenes e imágenes..."
    $DOCKER_CMD system prune -a --volumes -f 2>/dev/null || true

    success "Datos de Docker eliminados"
}

# ── Verificación post-migración ────────────────────────────────────────────────
verify_migration() {
    echo ""
    info "🔍 Verificando migración..."

    local errors=0

    # Verificar que Podman funciona
    if $PODMAN_CMD info &>/dev/null; then
        success "Podman funciona correctamente"
    else
        error "Podman no responde"
        errors=$((errors + 1))
    fi

    # Verificar acceso a registros
    if $PODMAN_CMD pull alpine:latest --quiet &>/dev/null; then
        success "Podman puede descargar imágenes de registros"
    else
        warn "No se pudo verificar acceso a registros"
    fi

    # Verificar cantidad de imágenes migradas
    local podman_images podman_containers
    podman_images=$($PODMAN_CMD images --format "{{.Repository}}:{{.Tag}}" 2>/dev/null | wc -l)
    info "  Imágenes en Podman: ${podman_images}"

    echo ""
    if [ "$errors" -eq 0 ]; then
        success "✅ Migración verificada correctamente"
    else
        warn "⚠️  Se detectaron ${errors} errores durante la verificación"
    fi

    echo ""
    info "📋 Resumen final:"
    echo "   Backup:            $BACKUP_DIR"
    echo "   Imágenes:          $podman_images"
    echo "   Docker eliminado:  $CLEANUP"
    echo ""
    info "👉 Próximos pasos:"
    echo "   1. Revisa los quadlets: $BACKUP_DIR/quadlets/"
    echo "   2. Instala podman-docker: sudo apt install podman-docker"
    echo "   3. Prueba tus contenedores con podman ps"
    echo "   4. Para stacks compose: pip3 install podman-compose"
    echo "   5. Lee el tutorial completo en atareao.es/tutorial/podman/"
}

# ── Main ────────────────────────────────────────────────────────────────────────
main() {
    echo ""
    echo "═══════════════════════════════════════════════════════════════"
    echo "  🐳 → 📦  Migración de Docker a Podman"
    echo "═══════════════════════════════════════════════════════════════"
    echo ""

    check_prerequisites
    backup_docker_config
    migrate_images
    migrate_networks
    migrate_volumes
    generate_quadlets
    verify_migration
    cleanup_docker

    echo ""
    success "🎉 Migración completada"
    echo "    Backup disponible en: ${BACKUP_DIR}"
    echo ""
    echo "    Para que el alias docker=podman funcione:"
    echo "      sudo apt install podman-docker"
    echo ""
    echo "    Para migrar stacks docker-compose:"
    echo "      pip3 install podman-compose"
    echo "      podman-compose up -d"
}

main

Uso del script:

# Dar permisos de ejecución
chmod +x migracion-docker-a-podman.sh

# Ejecutar en modo interactivo (recomendado)
./migracion-docker-a-podman.sh

# Ejecución no interactiva (servidores headless)
./migracion-docker-a-podman.sh --verbose

# Con limpieza automática de Docker (¡cuidado!)
./migracion-docker-a-podman.sh --cleanup --verbose

Errores comunes y cómo evitarlos

1. Asumir que docker-compose funciona igual que podman-compose

# Error
docker-compose up -d
# podman: No such file or directory

# Solución
pip3 install podman-compose
# o
alias docker-compose='podman-compose'

Causa: No existe un binario docker-compose en Podman. Necesitas podman-compose o migrar a pods nativos.

2. Olvidar que los puertos < 1024 requieren root

# Error
podman run -d -p 80:80 nginx
# Error: cannot listen on port 80 (permission denied)

# Soluciones
podman run -d -p 8080:80 nginx  # Usar puerto alto
# O como root
sudo podman run -d -p 80:80 nginx

3. Confiar ciegamente en el alias docker=podman

# Error silencioso
docker swarm init
# Error: unknown flag: --init

# Solución
# No uses comandos específicos de Docker Swarm (podman no lo soporta)
# Alternativa: usa Kubernetes o Nomad

4. Usar docker-compose Socket API sin activar el socket de Podman

# Error
export DOCKER_HOST=unix:///var/run/docker.sock
docker ps
# Cannot connect to the Docker daemon

# Solución
systemctl --user enable --now podman.socket
export DOCKER_HOST=unix:///run/user/$(id -u)/podman/podman.sock

5. Esperar que los logs de Podman sean idénticos

# En Docker
docker logs -f contenedor  # Funciona igual

# Pero en Docker
docker logs --tail 100 contenedor | grep error
# En Podman funciona igual, pero --since y --until pueden diferir

Causa: Podman usa el backend de logging propio del runtime OCI, no el logging driver de Docker.

6. No ajustar la propiedad de volúmenes bind-mount

# Error
podman run -v /home/usuario/data:/app alpine
# Permiso denegado (el UID dentro del contenedor no coincide)

# Solución: usar --userns=keep-id o mapear UIDs
podman run --userns=keep-id -v /home/usuario/data:/app alpine

7. Ignorar las diferencias en --format de inspect

# Docker
docker inspect cont --format '{{.State.Status}}'

# Podman (estructura ligeramente diferente)
podman inspect cont --format '{{.State.Status}}'  # Puede funcionar
# Pero para estructuras anidadas complejas puede diferir

# Solución: probar primero y ajustar
podman inspect cont

8. Esperar que docker stats muestre exactamente los mismos campos

# Docker stats muestra CPU, MEM, NET I/O, BLOCK I/O, PIDs
docker stats

# Podman stats muestra CPU, MEM, NET I/O, BLOCK I/O, PIDs
# Pero en versiones antiguas podía faltar NET I/O y BLOCK I/O
podman stats

Solución: En Podman 5+, podman stats es casi idéntico. En versiones antiguas, usa podman top como complemento.

9. Olvidar que docker system prune en Podman también elimina pods

podman system prune -a
# Esto elimina: contenedores, imágenes no usadas, redes, volúmenes Y PODS

Causa: Podman trata los pods como un tipo de recurso gestionado. Si no lo sabes, puedes perder todo un stack de contenedores.

Solución: Usa podman system prune --pod para excluir pods, o verifica antes con podman pod list.

10. Asumir que docker save/load es completamente intercambiable

# Docker exporta en formato Docker o tar
docker save nginx > nginx.tar

# Podman puede cargar ese tar
podman load -i nginx.tar  # ✅ Funciona

# Pero lo óptimo es usar formato OCI
podman save --format oci-archive nginx > nginx-oci.tar

Solución: Para intercambio entre Docker y Podman, usa formato Docker (docker-archive). Para archivo nativo Podman, usa oci-archive.

11. No configurar el almacenamiento rootless correctamente

# Error: espacio insuficiente
podman pull tensorflow/tensorflow:latest
# Error writing blob: no space left on device

# Causa: el almacenamiento rootless está en ~/.local/share/containers/
# que puede tener poco espacio

# Solución
du -sh ~/.local/share/containers/
# Redirigir a otra partición:
export CONTAINERS_STORAGE_DIR=/mnt/grande/containers

12. Esperar que las imágenes multi-arquitectura funcionen igual

# En Docker: pull automático de la arquitectura correcta
docker pull --platform linux/arm64 alpine

# En Podman: también funciona
podman pull --platform linux/arm64 alpine  # ✅

Pero cuidado: Podman no hace el «fat manifest» tan transparentemente como Docker en algunos registros antiguos. Solución: especifica siempre --platform para evitar sorpresas.

Más información

Documentación oficial

Herramientas de migración

  • Podlet — Generador de Quadlets a partir de archivos docker-compose. Lee docker-compose.yml y produce archivos .container y .pod listos para systemd.
  • podman-compose — Capa de compatibilidad para ejecutar archivos docker-compose directamente con Podman. Proyecto oficial del ecosistema containers.
  • Kompose — Convertidor de docker-compose a Kubernetes. Útil si tu destino final es Kubernetes y Podman es solo un paso intermedio.
  • Podman Desktop — GUI para gestionar Podman que incluye asistentes de migración desde Docker Desktop. Extensible con plugins.
  • Skopeo — Herramienta para copiar imágenes entre registros y formatos. Imprescindible para migrar imágenes entre Docker y Podman.
  • Buildah — Construcción de imágenes OCI sin demonio. Complementa a Podman para crear imágenes desde cero.

Podcasts y videos recomendados

Tutoriales y guías

Artículos técnicos y referencias

Deja una respuesta