Cuando construyes imágenes con frecuencia, el almacenamiento de los contenedores puede llenar el disco del sistema sin que resulte evidente. En mi entorno de desarrollo de Protos, basado en GraalVM y Truffle, utilizo Podman rootless para varios devcontainers de VS Code y para construir imágenes de distintas versiones de GraalVM. Llegó un momento en que / estaba al 97 %, mientras /datos tenía 386 GB libres.
El objetivo era trasladar los datos de Podman al segundo disco sin perder imágenes, contenedores ni volúmenes. La copia fue sencilla; lo interesante llegó cuando Podman rechazó el nuevo graphroot porque su base de datos seguía recordando la ruta anterior. Este es el procedimiento real, con los errores y las cifras medidas.
1. Diagnóstico: cuánto ocupa realmente Podman
El punto de partida era el siguiente:
Disco del sistema / |
Disco de datos /datos |
|
|---|---|---|
| Dispositivo | /dev/nvme0n1p7 |
/dev/sda1 |
| Sistema de archivos | Btrfs | ext4 |
| Capacidad | 192 GB | 458 GB |
| Usados | 180 GB | 49 GB |
| Libres | 7,5 GB | 386 GB |
| Ocupación | 97 % | 12 % |
Comprobé el estado de Podman y de ambos discos:
podman system df -v
podman info --format '{{.Store.GraphRoot}}'
df -h / /datos
df -T /datos
Podman utilizaba la ruta rootless predeterminada:
/home/guille/.local/share/containers/storage
Había 142 imágenes, 6 contenedores (3 activos) y 2 volúmenes, incluido vscode, que ocupaba aproximadamente 6,3 GB. El tamaño del almacenamiento, una vez estabilizado, fue:
podman unshare du -sh ~/.local/share/containers/storage
# 39G /home/guille/.local/share/containers/storage
No conviene sumar los tamaños aparentes de las imágenes: comparten capas. Tampoco debe confundirse graphRootUsed con los bytes exclusivos de Podman, ni esperar que du y df coincidan en Btrfs, donde pueden existir bloques compartidos y snapshots.
2. Por qué hay que respetar el almacenamiento rootless
En modo rootless, los archivos de las capas pueden pertenecer a identificadores subordinados del espacio de nombres del contenedor. Consulté los mapeos de usuario y grupo:
podman unshare cat /proc/self/uid_map
podman unshare cat /proc/self/gid_map
En ambos casos:
0 1000 1
1 100000 65536
Los rangos también estaban declarados en /etc/subuid y /etc/subgid. Por ese motivo, una copia ordinaria que cambie propietarios, ACL o atributos extendidos puede romper permisos internos. Usé podman unshare y rsync para preservar esos metadatos.
3. Detener las operaciones antes de copiar
Primero guardé el trabajo, cerré las sesiones asociadas a los contenedores y detuve las compilaciones. Desde una terminal del host, no desde un devcontainer:
podman stop --all
systemctl --user stop podman.socket podman.service
podman ps -a
podman unshare podman mount
Los seis contenedores quedaron detenidos y el último comando no mostró capas montadas. También es necesario asegurarse de que no haya otras herramientas, como Buildah, modificando el mismo almacenamiento.
4. Copia conservando permisos y metadatos
El destino elegido fue /datos/podman, en ext4, con espacio suficiente. Con el almacenamiento inactivo:
podman unshare mkdir -p /datos/podman
podman unshare rsync -aHAX \
--numeric-ids \
--info=progress2 \
/home/guille/.local/share/containers/storage/ \
/datos/podman/
echo "RSYNC_EXIT=$?"
-a preserva la estructura y los permisos; -H, los enlaces duros; -A, las ACL; y -X, los atributos extendidos. --numeric-ids evita traducir identidades por nombre. La ejecución terminó con RSYNC_EXIT=0, después de transferir unos 40 GB y aproximadamente 375.900 archivos.
Verifiqué primero el tamaño de ambos directorios y después utilicé el modo simulación de rsync:
podman unshare du -sh /home/guille/.local/share/containers/storage
podman unshare du -sh /datos/podman
podman unshare rsync -aHAXn \
--numeric-ids \
--itemize-changes \
/home/guille/.local/share/containers/storage/ \
/datos/podman/
Los dos tamaños eran de 39 GB. La única diferencia informada fue:
>f..t...... storage.lock
Era una diferencia de fecha de modificación del archivo de bloqueo. La simulación comprueba atributos y tamaños, pero no constituye una comparación criptográfica byte a byte.
5. El error: «database static dir does not match our static dir»
Mi primer intento fue cambiar directamente la configuración rootless, en ~/.config/containers/storage.conf:
[storage]
driver = "overlay"
graphroot = "/datos/podman"
Al ejecutar podman info, Podman respondió:
Error: database static dir
"/home/guille/.local/share/containers/storage/libpod"
does not match our static dir
"/datos/podman/libpod":
database configuration mismatch
La copia no había fallado. El problema era que la base de datos de una instalación existente conservaba referencias a la ruta absoluta anterior. Cambiar graphroot no bastaba para migrar esa instalación sin más.
No quería reinicializar Podman ni recrear los devcontainers. Elegí otra solución: mantener la misma ruta lógica y cambiar únicamente dónde están físicamente los archivos.
6. Solución: un bind mount de Linux
Un bind mount hace visible un directorio en otra ubicación del árbol de archivos. Así, Podman podía seguir utilizando:
/home/guille/.local/share/containers/storage
mientras los datos residían realmente en:
/datos/podman
Desactivé la configuración que acababa de crear:
mv ~/.config/containers/storage.conf \
~/.config/containers/storage.conf.bak
Con los contenedores todavía detenidos, conservé la copia original y preparé el punto de montaje:
mv ~/.local/share/containers/storage \
~/.local/share/containers/storage.before-datos
mkdir -m 700 ~/.local/share/containers/storage
sudo mount --bind \
/datos/podman \
/home/guille/.local/share/containers/storage
Comprobé:
findmnt -T ~/.local/share/containers/storage
podman info --format '{{.Store.GraphRoot}}'
podman ps -a
podman volume ls
findmnt mostraba /dev/sda1[/podman] (ext4) como origen físico. GraphRoot seguía siendo la ruta antigua, y Podman volvió a reconocer los seis contenedores y los dos volúmenes, sin error de base de datos.
La idea clave es que la ruta que utiliza la aplicación y el dispositivo que almacena sus bytes no tienen por qué ser lo mismo.
7. Persistencia del montaje con systemd
El montaje manual desaparece al reiniciar. Guardé una copia de /etc/fstab:
sudo cp -a /etc/fstab /etc/fstab.before-podman
Y añadí esta entrada:
/datos/podman /home/guille/.local/share/containers/storage none bind,x-systemd.requires-mounts-for=/datos 0 0
La opción x-systemd.requires-mounts-for=/datos declara que el montaje depende del sistema de archivos de datos. Recargué systemd y verifiqué la configuración:
sudo systemctl daemon-reload
sudo findmnt --verify --verbose
systemctl status \
home-guille-.local-share-containers-storage.mount
findmnt terminó con «Correcto, no se han detectado errores ni avisos», y la unidad aparecía activa. Esto valida la configuración y el montaje en la sesión actual; la prueba posterior a un reinicio completo seguía pendiente.
Precaución: antes de iniciar Podman tras un reinicio, hay que comprobar que el bind mount apunta a /dev/sda1[/podman]. Si /datos no estuviera montado, no se debe permitir que Podman cree un almacén nuevo en el directorio subyacente del disco del sistema.
8. Comprobación funcional y eliminación del respaldo
Inicié los tres devcontainers que estaban activos antes de empezar:
podman start \
096448da129c \
685fa0652baa \
c2c2f6f903be
podman ps
podman volume inspect vscode \
--format '{{.Mountpoint}}'
Los tres aparecieron en estado Up. El volumen vscode seguía siendo visible bajo la ruta lógica original, que ahora estaba montada sobre /datos.
Antes de recuperar el espacio comprobé que el respaldo no era un montaje y que el punto activo sí lo era:
sudo du -sh \
/home/guille/.local/share/containers/storage.before-datos
findmnt --mountpoint \
/home/guille/.local/share/containers/storage.before-datos
findmnt --mountpoint \
/home/guille/.local/share/containers/storage
Una vez verificado el funcionamiento, eliminé solo el directorio de respaldo:
sudo rm -rf --one-file-system \
/home/guille/.local/share/containers/storage.before-datos
Este último comando es irreversible y no debe ejecutarse antes de validar la copia y el montaje. La opción --one-file-system no evita daños si se selecciona una ruta equivocada.
9. Limpiar las imágenes intermedias
La migración cambiaba el disco utilizado, no eliminaba por sí misma las imágenes sobrantes. Para limpiar únicamente las imágenes dangling ejecuté:
podman image prune
Podman pidió confirmación y eliminó numerosas capas e imágenes intermedias que ya no eran necesarias. No utilicé una limpieza agresiva con --volumes, porque los volúmenes de los devcontainers debían conservarse.
El uso de /datos pasó de unos 87 GB antes de esta fase a 76 GB después, aunque esa diferencia es una medición del disco completo y no una contabilidad exacta por imagen.
10. Resultados medidos
| Métrica | Antes | Después |
|---|---|---|
Espacio libre en / |
7,5 GB | 31 GB |
Ocupación de / |
97 % | 84 % |
Espacio libre en /datos |
386 GB | 359 GB |
Ocupación de /datos |
12 % | 18 % |
| Directorio de almacenamiento de Podman | 39 GB | 35 GB |
| Contenedores conservados | 6 | 6 |
| Volúmenes conservados | 2 | 2 |
| Devcontainers arrancados | 3 | 3 |
El espacio libre en el disco del sistema aumentó 23,5 GB. No se recuperaron 39 GB completos según df: du y df contabilizan conceptos distintos, y Btrfs puede mantener bloques compartidos o snapshots. No hice una auditoría de extents ni de snapshots para atribuir exactamente esa diferencia.
Los 35 GB restantes de Podman ya residen físicamente en /datos, no en /.
11. Lo que aprendí
graphrootno es una operación de migración universal. Una base de datos existente puede conservar rutas absolutas e impedir un cambio directo.- Rootless exige preservar propietarios y metadatos.
podman unsharejunto conrsync -aHAX --numeric-idsfuncionó en este caso. - Los bind mounts permiten separar ruta lógica y ubicación física. Fue posible mantener intactas las referencias de Podman.
- La persistencia debe verificarse. La unidad systemd quedó configurada, pero una prueba real después del reinicio sigue siendo una comprobación importante.
- No borrar antes de probar. Copiar, verificar, montar, reconocer volúmenes, arrancar contenedores y eliminar el respaldo fueron pasos separados.
- Hay que medir el espacio real. Los tamaños de imágenes,
duydfno son intercambiables.
Conclusión
La migración terminó sin recrear los contenedores ni los volúmenes. La copia íntegra de las capas desde Btrfs a ext4 permitió mover los datos; el bind mount resolvió el problema de las rutas guardadas por la base de datos; y systemd quedó configurado para montar /datos antes de exponerlo a Podman.
El resultado: 23,5 GB más libres en el disco del sistema, los tres devcontainers funcionando y el almacenamiento de Podman en el disco de datos. La comprobación después de un reinicio completo queda como último paso operativo.