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
dockeres solo el cliente. Le envías instrucciones a un demonio (dockerd) que se ejecuta como root.dockerdes monolítico: gestiona imágenes, contenedores, redes, volúmenes, todo.- Si
dockerdse 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.
podmanes 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 OCI | Docker | Podman |
|---|---|---|
| Image Specification | Sí (formato Docker → OCI) | Sí (nativo OCI) |
| Runtime Specification | Sí (via containerd + runc) | Sí (runc o crun) |
| Distribution Specification | Sí (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ística | Docker | Podman |
|---|---|---|
| Arquitectura | Cliente-servidor (demonio) | Sin demonio (fork/exec) |
| Proceso | dockerd como root | Proceso del usuario |
| Rootless | Configurable, no por defecto | Nativo, por defecto |
| Contenedores como hijos | No (hijos del demonio) | Sí (hijos del proceso) |
| Punto único de fallo | Sí (dockerd) | No |
| Runtime OCI | containerd + runc | crun (por defecto) o runc |
| Formato de imágenes | OCI / Docker | OCI nativo |
| Integración systemd | Limitada | Nativa |
| Management de pods | No nativo | Sí (pods) |
| Compatibilidad Kubernetes | Via kompose | Nativa (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
| Aspecto | podman-compose | Pods nativos |
|---|---|---|
| Curva de aprendizaje | Baja (misma sintaxis) | Media (nuevo concepto) |
| Compatibilidad | Alta con compose.yml | Total con Kubernetes |
| Rendimiento | Sin sobrecarga | Sin sobrecarga |
| Aislamiento | Por contenedor | Namespace compartido |
| Comunicación | Red bridge | localhost |
| Exportar a K8s | Limitado | Directo (generate kube) |
| systemd | Manual | Quadlets 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
| Docker | Podman | Notas |
|---|---|---|
docker run | podman run | Idéntico |
docker ps | podman ps | Idéntico |
docker images | podman images | Idéntico |
docker pull | podman pull | Idéntico |
docker build | podman build | Idéntico |
docker exec | podman exec | Idéntico |
docker logs | podman logs | Idéntico |
docker inspect | podman inspect | Idéntico |
docker network | podman network | Idéntico |
docker volume | podman volume | Idéntico |
docker system prune | podman system prune | Idéntico |
docker compose up | podman-compose up | Requiere podman-compose |
docker swarm | ❌ | Usa Kubernetes |
docker plugin | ❌ | No tiene equivalente |
docker save | podman save | Formato OCI preferido |
docker load | podman load | Acepta formato Docker y OCI |
docker export | podman export | Idéntico |
docker import | podman import | Idéntico |
docker commit | podman commit | Idéntico |
docker cp | podman cp | Idéntico |
docker stats | podman stats | Menos detallado en versiones antiguas |
docker top | podman top | Idéntico |
docker rename | podman rename | Idéntico |
docker wait | podman wait | Idéntico |
docker attach | podman attach | Idéntico |
docker diff | podman diff | Idéntico |
docker history | podman history | Idéntico |
docker info | podman info | Más detallado en Podman |
docker version | podman version | Idéntico |
docker login | podman login | Idéntico |
docker logout | podman logout | Idéntico |
docker tag | podman tag | Idéntico |
docker push | podman push | Idéntico |
docker search | podman search | Idé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
- Documentación de Podman: Migración desde Docker — Guía oficial de compatibilidad entre Docker y Podman, incluyendo diferencias de CLI y API.
- Podman man page: podman-compatibility — Documentación detallada sobre las diferencias de comportamiento entre Docker y Podman.
- Podman socket activation — Cómo activar y usar el socket de compatibilidad Docker en Podman.
- Podman pods — Referencia completa del comando
podman podpara gestionar grupos de contenedores. - Podman Quadlets — Documentación oficial sobre Quadlets y su integración con systemd.
- Open Container Initiative (OCI) — Especificaciones de formato de imágenes y runtime que comparten Docker y Podman.
Herramientas de migración
- Podlet — Generador de Quadlets a partir de archivos docker-compose. Lee
docker-compose.ymly produce archivos.containery.podlistos 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
- Adiós Docker, bienvenido Podman — Episodio del podcast atareao donde se explica por qué migrar de Docker a Podman, analizando arquitectura, logs y pods.
- Adiós a Docker Compose. Cómo usar PODS en Podman — Episodio dedicado a la migración de stacks docker-compose a pods nativos de Podman, con un ejemplo real de WordPress.
- ¿Es Podman la alternativa a Docker? — Análisis de las ventajas y desventajas de Podman frente a Docker en el contexto de 2025-2026.
- Podman vs Docker: Differences and Migration Guide (Linuxize) — Guía en inglés sobre diferencias y migración práctica.
- Docker vs Podman en 2026: guía completa de migración (Pockit) — Comparativa honesta y actualizada que cubre licenciamiento, CI/CD y estrategias reales de migración.
Tutoriales y guías
- Tutorial de Podman en atareao.es — Tutorial completo del que forma parte este capítulo, con contenido en español actualizado periódicamente.
- Podman: Introducción y ventajas (Pledin) — Excelente introducción a Podman en español, cubriendo arquitectura fork/exec y contenedores rootless.
- Podman vs Docker: ¿Cuál elegir? (Geekflare) — Comparativa detallada con tabla de diferencias y casos de uso.
- ¿Qué es Podman? Alternativa segura a Docker (Tecnoloblog) — Guía completa en español con ejemplos prácticos.
- Podman Compose: alternativa sin raíces a Docker Compose (DataCamp) — Tutorial práctico sobre podman-compose con ejemplos de stacks multicontenedor.
- Podman y Systemd (Valebyte) — Guía detallada sobre integración de Podman con systemd, incluyendo Quadlets y generación de unidades.
Artículos técnicos y referencias
- Podman: arquitectura daemonless y fork/exec — Análisis detallado del modelo de contenerización sin demonio.
- Docker vs Podman: guía definitiva para contenedores sin demonio (Introserv) — Comparativa exhaustiva en español, perfecta como referencia rápida.
- Managing Docker compatibility (Podman Desktop) — Cómo configurar un entorno compatible con Docker usando Podman Desktop.
- Podman 5 vs Docker: The Container Runtime War — Análisis en profundidad de Podman 5 frente a Docker en entornos empresariales.
- Red Hat: Creación, ejecución y gestión de contenedores — Guía oficial de Red Hat sobre Podman, Buildah y Skopeo en RHEL 8/9.
- Awesome Podman — Lista curada de recursos, herramientas y proyectos del ecosistema Podman.