Contenedores LXC: de la plantilla a producción

Por: Artiko
lxccontenedorespctunprivilegedcgroupsapparmorpveamidmapproxmox-ve

Contenedores LXC: de la plantilla a producción

Tienes un nodo con veinte servicios pequeños: un DNS interno, un proxy inverso, tres APIs, un Postgres, un par de exportadores de métricas. Si montas cada uno en una máquina virtual como las del capítulo 3, pagas veinte kernels invitados, veinte arranques de sesenta segundos y veinte bloques de RAM reservada que casi nunca se usan del todo. El nodo se queda sin memoria mucho antes de quedarse sin CPU.

Los contenedores LXC resuelven exactamente ese caso. Comparten el kernel 7.0 del host, arrancan en menos de un segundo, sólo ocupan la RAM que consumen sus procesos y se gestionan con la misma interfaz, el mismo storage y el mismo sistema de backup que las VMs. A cambio pierdes tres cosas concretas: sólo corren Linux, no puedes cargar módulos de kernel dentro, y no existe live migration.

Este capítulo recorre el ciclo completo: de dónde salen las plantillas y cómo las verifica pveam, qué significa realmente “unprivileged” a nivel de UIDs, qué activa cada una de las seis features, cómo se comportan los bind mounts y el nuevo idmap por mount point de Proxmox VE 9.2, qué límites de cgroup v2 escribe Proxmox por ti, y qué hace falta —y qué se rompe— cuando metes Docker dentro. Todo verificado contra Proxmox VE 9.2 sobre Debian 13.5 Trixie, con pve-container 6.1.9, lxc-pve 7.0.0-2, lxcfs 7.0.0-pve1 y pve-lxc-syscalld 2.0.2 en el repositorio pve-no-subscription de trixie. La base es LXC 7.0 LTS, publicada el 30 de abril de 2026 con soporte hasta junio de 2031, que entre otras cosas elimina por completo el soporte de cgroup v1.

LXC frente a VM: qué se comparte y qué se pierde

La documentación oficial lo resume en una frase: los contenedores “usan el kernel del sistema anfitrión en el que corren, en vez de emular un sistema operativo completo”. No hay kernel invitado. Cada syscall de un proceso del contenedor lo atiende directamente el kernel del host, filtrado por namespaces, seccomp, AppArmor y cgroup v2.

flowchart TB
    subgraph HOST["Nodo Proxmox VE 9.2 con un unico kernel 7.0"]
        K["Kernel Linux 7.0 del host"]
        subgraph CT["Contenedor LXC 110"]
            CTINIT["init del contenedor"]
            CTAPP["procesos de la aplicacion"]
        end
        subgraph VM["Maquina virtual QEMU 200"]
            QEMU["proceso qemu-system-x86_64"]
            GK["Kernel invitado propio"]
            GAPP["procesos del invitado"]
        end
    end
    CTINIT -->|"syscall directa filtrada"| K
    CTAPP -->|"syscall directa filtrada"| K
    GAPP --> GK
    GK --> QEMU
    QEMU -->|"KVM"| K

La consecuencia se ve en la tabla de decisión:

CriterioLXC (CT)VM (QEMU/KVM)
SO invitadoSolo distribuciones LinuxCualquiera: Linux, Windows, BSD
KernelCompartido con el hostPropio del invitado
Overhead en runtime”low, usually negligible” según la docOverhead de virtualización
RAMSolo lo que consumen los procesos; el límite es un tope de cgroupSe reserva el tamaño configurado
Live migrationNo soportada
Restart migrationSí, con downtime de “some hundreds of milliseconds”No aplica
Módulos de kernelNo se pueden cargar
AislamientoMenor; “full virtual machines provide better isolation”Mayor
PassthroughPuntual con dev[n]PCI(e) completo

Las dos citas literales que fijan el terreno: “Only Linux distributions can be run in Proxmox Containers” y “Running containers cannot live-migrated due to technical limitations”.

Limitaciones que hay que memorizar antes de empezar

Uno. No se cargan módulos de kernel dentro del contenedor. El fichero /usr/share/lxc/config/common.conf del paquete lxc-pve contiene literalmente:

lxc.cap.drop = mac_admin mac_override sys_time sys_module sys_rawio

Con sys_module fuera, modprobe e insmod fallan. Si tu aplicación necesita un módulo, cárgalo en el host y el contenedor lo usará. Si el módulo tiene que compilarse o parametrizarse por instancia, el caso es una VM.

Dos. mount está prohibido salvo lo autorizado explícitamente. AppArmor bloquea el syscall: “Some system calls, i.e. mount, are prohibited from execution”. Se abre por tipo de filesystem con features: mount=<fstype>.

Tres. FUSE dentro del contenedor está desaconsejado con todas las letras, porque “existing issues in the Linux kernel’s freezer subsystem” chocan con la congelación que necesitan los backups en modo snapshot o suspend. La alternativa oficial es montar el FUSE en el host y exponerlo con un bind mount.

Cuatro. Las cuotas de disco por usuario (quota=1) hoy no funcionan. La doc dice que “this currently requires the use of legacy cgroups”, y cgroup v1 fue eliminado por completo en Proxmox VE 9.0. Además sólo aplicaban a storage de tipo imagen ext4 y sólo en contenedores privilegiados. Cinco. pct suspend sigue marcado como experimental en la propia man page.

Seis. Los límites duros: net0 a net9, es decir diez interfaces de red, y mp0 a mp255, es decir 256 mount points adicionales. Los CTIDs menores de 100 están reservados para uso interno y deben ser únicos a nivel de cluster.

Contenedores de sistema y contenedores de aplicación OCI

Lo que llevas leyendo son contenedores de sistema: una distribución Linux completa con su init, sus servicios y su gestor de paquetes. Desde Proxmox VE 9.0, y ampliado en 9.2, existe además soporte de imágenes OCI como technology preview: “when creating a container from an OCI image, the image is automatically converted to the LXC stack that Proxmox VE uses”.

En la interfaz web aparece el botón Pull from OCI registry en la vista de plantillas de un storage, y también puedes subir la imagen a mano. Tres opciones de configuración acompañan a este modo: entrypoint: <cmd>, con default /sbin/init, que es el comando que se ejecuta como init; env: VAR=valor como lista separada por NUL, que reemplaza las entradas lxc.environment.runtime; y net[n]: ...,host-managed=1, para imágenes que no traen ninguna pila de gestión de red interna. Cuando entrypoint es distinto de /sbin/init, Proxmox añade automáticamente una entrada de montaje de /dev/shm como tmpfs para alinearse con los defaults de la OCI Runtime Specification.

La recomendación que acompaña a la preview es explícita y conviene leerla dos veces: “for use cases demanding maximum isolation and the ability to live-migrate, nesting containers inside a Proxmox QEMU VM remains a recommended practice”.

Plantillas: de dónde salen realmente y cómo funciona pveam

Una plantilla LXC no es una imagen de disco: es “un archivo tar que contiene todo lo necesario para ejecutar un contenedor”. Proxmox las descubre a través de dos índices independientes que baja pveam update:

FuenteÍndiceContenido
http://download.proxmox.com/images/aplinfo-pve-9.dat.gzoficial de Proxmoxsecciones system y mail
https://releases.turnkeylinux.org/pve/aplinfo.dat.gzTurnKey Linuxsección turnkeylinux, 111 paquetes

Ambos van firmados con un .asc en el mismo directorio, y cada entrada del índice trae su sha512sum. Ese es el motivo por el que descargar una plantilla con pveam es una operación verificada de extremo a extremo, a diferencia de lo que verás más adelante con los scripts de comunidad. Los índices se cachean en /var/lib/pve-manager/apl-info/ (un fichero por host de origen), el log va a /var/log/pveam.log y las plantillas del storage local acaban en /var/lib/vz/template/cache/.

El índice se refresca a diario mediante el timer pve-daily-update, y el volid local:vztmpl/debian-13-standard_13.6-1_amd64.tar.zst corresponde exactamente al fichero /var/lib/vz/template/cache/debian-13-standard_13.6-1_amd64.tar.zst, porque el plugin de storage mapea el content type vztmpl al subdirectorio template/cache.

sequenceDiagram
    participant T as pve-daily-update.timer
    participant A as pveam update
    participant P as download.proxmox.com
    participant K as releases.turnkeylinux.org
    participant C as /var/lib/pve-manager/apl-info/
    participant S as Storage con content vztmpl

    T->>A: dispara a diario
    A->>P: GET aplinfo-pve-9.dat.asc y .gz
    A->>K: GET aplinfo.dat.asc y .gz
    A->>A: verifica firma GPG y sha512
    A->>C: escribe los indices cacheados en /var/lib/pve-manager/apl-info/
    Note over S: pveam download local plantilla
    S->>P: GET system/plantilla.tar.zst
    S->>S: guarda en /var/lib/vz/template/cache/

El catálogo completo de subcomandos es corto y no tiene sorpresas ocultas:

pveam update                                  # refresca los dos indices
pveam available                               # todo el catalogo
pveam available --section system              # system, turnkeylinux o mail
pveam download local debian-13-standard_13.6-1_amd64.tar.zst
pveam list local
pveam remove local:vztmpl/debian-13-standard_13.6-1_amd64.tar.zst

Gotcha: no existe pveam search ni pveam upgrade. Y pveam remove recibe el volid completo (local:vztmpl/debian-13-standard_13.6-1_amd64.tar.zst), no el nombre suelto del fichero. Desde la capa de storage puedes hacer lo mismo con pvesm list local --content vztmpl y pvesm path <volid>.

El catálogo oficial y las appliances de TurnKey Linux

El índice oficial del 9 de agosto de 2026 trae 27 plantillas LXC, incluidas ya variantes arm64 (alpine-3.24-default_20260803_arm64.tar.xz y debian-13-standard_13.6-1_arm64.tar.zst), coherentes con la publicación de Proxmox VE 9.2 para arm64 del 5 de agosto de 2026.

La convención de nombres dice más de lo que parece: el sufijo -standard_ marca las plantillas construidas por Proxmox (Debian, Ubuntu, Devuan, Proxmox Mail Gateway) y el sufijo -default_ las imágenes de linuxcontainers.org reempaquetadas, cuyo campo Infopage: apunta a ese proyecto.

Cada entrada del índice trae Package, Version, Type: lxc, OS, Section, Architecture, Location, md5sum, sha512sum, Infopage y Description. Es exactamente la información que necesitas cuando una descarga falla y quieres saber de dónde venía el fichero.

TurnKey Linux aporta 111 appliances preconfigurados sobre Debian: LAMP, WordPress, GitLab, Nextcloud, Ansible, BookStack y compañía. Dos detalles operativos: su campo Location: es una URL absoluta a mirror.turnkeylinux.org en vez de una ruta relativa, y el campo ManageUrl: http://__IPADDRESS__/ es lo que la interfaz web usa para ofrecerte el enlace de administración tras el primer arranque.

Advertencia: en el índice de TurnKey conviven appliances construidos sobre Debian 10, 11 y 12. Revisa el campo OS: antes de desplegar: un appliance con systemd antiguo en un entorno cgroup v2 puro puede darte problemas de arranque. Y en un cluster, la recomendación oficial es usar un storage compartido con content vztmpl para que todos los nodos vean las mismas plantillas sin duplicarlas.

Crear un contenedor: pct create de principio a fin

La sintaxis mínima es pct create 999 local:vztmpl/debian-13-standard_13.6-1_amd64.tar.zst, y también acepta una ruta absoluta al fichero en vez de un volid. Pero un contenedor real se crea en una sola invocación con todo puesto:

pct create 110 local:vztmpl/debian-13-standard_13.6-1_amd64.tar.zst \
  --hostname web01 --ostype debian --arch amd64 \
  --unprivileged 1 --features nesting=1 \
  --cores 2 --cpulimit 1.5 --cpuunits 100 \
  --memory 2048 --swap 512 \
  --rootfs local-lvm:8 \
  --mp0 local-lvm:20,mp=/var/lib/data,backup=1 \
  --net0 name=eth0,bridge=vmbr0,ip=192.168.10.110/24,gw=192.168.10.1,firewall=1 \
  --nameserver 192.168.10.1 --searchdomain lan.example \
  --timezone Europe/Madrid \
  --onboot 1 --startup order=2,up=30 \
  --tags "produccion;web" \
  --ssh-public-keys /root/.ssh/id_ed25519.pub \
  --start 1

Las opciones que más se usan, con sus defaults reales de la man page 9.2.4:

OpciónValoresDefaultNota
--archamd64 | arm64 | armhf | i386 | riscv32 | riscv64amd64
--cores1 - 8192sin límite”A container can use all available cores by default”
--cpulimit0 - 8192, flotante00 es sin límite
--cpuunits0 - 500000100 en cgroup v2se recorta a [1, 10000]
--memory / --swap16 - N MB / 0 - N MB512 / 512
--cmode / --ttyconsole | shell | tty / 0 - 6tty / 2cómo se conecta pct console; tty escribe lxc.tty.max
--ostypealpine | archlinux | centos | debian | devuan | fedora | gentoo | nixos | opensuse | ubuntu | unmanagedautodetectado
--startuporder=N,up=N,down=Ndown por defecto 60 segundos
--timezonehost o zona de /usr/share/zoneinfo/zone.tab“If option isn’t set, then nothing will be done”
--protectionbooleano0impide borrado y modificación de discos
--ssh-public-keysruta de fichero“one key per line, OpenSSH format”
--uniquebooleanoMAC aleatoria; requiere --restore
--unprivilegedbooleano0 en el esquema, 1 al crearver la sección siguiente

Sobre el orden de arranque: un contenedor con order=1 es el primero en arrancar y el último en apagarse, porque el apagado usa el orden inverso. Los contenedores sin startup definido arrancan siempre después de los que sí lo tienen, y el parámetro sólo tiene sentido entre guests del mismo nodo, no a nivel de cluster. Para engancharte al ciclo de vida del contenedor con un script propio, pct set 100 -hookscript local:snippets/hookscript.pl: el storage necesita content snippets habilitado y tienes un ejemplo en /usr/share/pve-docs/examples/guest-example-hookscript.pl.

Unprivileged frente a privileged: el mapeo 0 a 100000

Esta es la decisión de seguridad más importante del capítulo, y la buena noticia es que Proxmox ya la toma por ti. La definición oficial de unprivileged: “The root UID 0 inside the container is mapped to an unprivileged user outside the container. This means that most security issues (container escape, resource abuse, etc.) in these containers will affect a random unprivileged user, and would be a generic kernel security bug rather than an LXC issue. The LXC team thinks unprivileged containers are safe by design.”

La de privileged es igual de contundente en el otro sentido: “The LXC team considers this kind of container as unsafe, and they will not consider new container escape exploits to be security issues worthy of a CVE and quick fix.”

Gotcha del esquema: la man page dice unprivileged: <boolean> (default = 0) y a continuación aclara “For creation, the default is 1. For restore, the default is the value from the backup.” Es decir, el 0 es el default del esquema de la API, pero pct create fuerza 1. Y desde Proxmox VE 9.0 hay un cambio que rompe automatizaciones antiguas: crear un contenedor privilegiado requiere el privilegio Sys.Modify. Restaurar en el sitio un contenedor que ya era privilegiado no lo exige.

El mapeo por defecto está escrito en el código de pve-container y es exactamente lxc.idmap = u 0 100000 65536 más su equivalente con g para los grupos.

flowchart LR
    subgraph CTNS["Namespace del contenedor unprivileged"]
        C0["uid 0 = root del CT"]
        C1000["uid 1000"]
        C65535["uid 65535"]
        COUT["uid fuera del rango 0 a 65535"]
    end
    subgraph HOSTNS["Namespace del host"]
        H100000["uid 100000"]
        H101000["uid 101000"]
        H165535["uid 165535"]
        HNOB["nobody = 65534"]
    end
    C0 -->|"lxc.idmap u 0 100000 65536"| H100000
    C1000 --> H101000
    C65535 --> H165535
    COUT --> HNOB

Comprobarlo cuesta dos comandos, uno desde dentro y otro desde el host:

pct exec 110 -- cat /proc/self/uid_map
#          0     100000      65536

ls -ln /rpool/data/subvol-110-disk-0/etc/
# los ficheros de root del CT se ven como 100000:100000

/etc/subuid, /etc/subgid y la conversión entre tipos

/etc/subuid y /etc/subgid declaran qué rangos de sub-UIDs puede usar cada usuario del host al crear user namespaces. El mapeo por defecto de Proxmox usa el rango 100000..165535 porque root ya lo tiene concedido en una instalación estándar; compruébalo en tu nodo con cat /etc/subuid /etc/subgid antes de tocar nada.

Advertencia: si añades un lxc.idmap personalizado que salga de los rangos declarados en esos dos ficheros, el contenedor no arranca. El error que verás es del estilo newuidmap: write to uid_map failed: Operation not permitted.

No existe un pct convert. Cambiar de privilegiado a unprivileged o al revés se hace con backup y restore:

vzdump 1234 --mode stop --compress zstd --storage local
pct destroy 1234 --purge
pct restore 1234 /var/lib/vz/dump/vzdump-lxc-1234-2026_08_09-02_31_03.tar.zst \
  --unprivileged 1 --ignore-unpack-errors 1

--ignore-unpack-errors 1 es prácticamente obligatorio en la conversión de privilegiado a unprivileged, porque el tar contiene xattrs, capabilities y nodos de dispositivo que un usuario no privilegiado no puede recrear. Sin esa bandera el restore aborta. En el sentido inverso el procedimiento es el mismo con --unprivileged 0, requiere Sys.Modify y suele romper permisos si el rootfs tiene ficheros con UIDs altos.

Cuándo elegir cada opción:

EscenarioElección
Caso general, un servicio Linux normalunprivileged
Multi-tenant o cargas poco confiablesVM, no contenedor
Bind mount con UIDs concretos que hay que respetarunprivileged con idmap por mount point
Necesitas mknod libre o cuotas ext4privilegiado en entorno de confianza, o VM
Docker o Podman dentrounprivileged con nesting=1 y keyctl=1, o mejor VM
Cargar módulos de kernelVM obligatoriamente

Las seis features y lo que activan por dentro

La sintaxis completa, verbatim de la man page:

features: [force_rw_sys=<1|0>] [,fuse=<1|0>] [,keyctl=<1|0>] [,mknod=<1|0>]
          [,mount=<fstype;fstype;...>] [,nesting=<1|0>]

Todas vienen desactivadas por defecto. Lo que hace cada una:

nesting. Permite anidar. Por dentro escribe lxc.apparmor.allow_nesting = 1 y lxc.apparmor.raw = allow userns,, y monta procfs y sysfs adicionales bajo /dev/.lxc/. Sin nesting, Proxmox inyecta lo contrario: deny mount -> /proc/, y deny mount -> /sys/,. La doc avisa de que “this will expose procfs and sysfs contents of the host to the guest” y de que “this is also required by systemd to isolate services”.

El síntoma cuando falta es inconfundible: servicios systemd que fallan con 226/NAMESPACE o, en Debian 13 y superiores y Ubuntu 24.04 y superiores, con 243/CREDENTIALS. Si un contenedor recién creado arranca pero con la mitad de los servicios en rojo, empieza por aquí.

keyctl. Sólo para unprivileged. Sin esta feature, Proxmox aplica el perfil seccomp /usr/share/lxc/config/pve-userns.seccomp, que además de rechazar kexec_load, open_by_handle_at, init_module, finit_module y delete_module incluye la línea keyctl errno 38. errno 38 es ENOSYS, o sea que el syscall aparenta no existir. Con keyctl=1 desaparece esa última línea. La doc lo dice sin rodeos: “This is required to use docker inside a container”, y añade el trade-off, también literal: “Essentially, you can choose between running systemd-networkd or docker.”

fuse. Hace bind de /dev/fuse dentro del contenedor y añade mount fstype=fuse, al perfil AppArmor. El nodo de dispositivo (char 10:229) ya está permitido en common.conf. Recuerda el aviso del principio: la interacción entre FUSE y el cgroup freezer “can potentially cause I/O deadlocks” durante los backups.

mknod. Permite a un contenedor unprivileged crear ciertos nodos de dispositivo. Está marcada como experimental y requiere kernel 5.3 o superior, algo trivial en Proxmox VE 9.2 con kernel 7.0. Se implementa delegando el syscall al demonio pve-lxc-syscalld mediante lxc.seccomp.notify.proxy = unix:/run/pve-lxc-syscalld/socket.

mount=<fstype;...>. Añade una línea AppArmor por cada tipo de filesystem que declares. El aviso de la doc es serio: “this can have negative effects on the container’s security. With access to a loop device, mounting a file can circumvent the mknod permission of the devices cgroup, mounting an NFS file system can block the host’s I/O completely and prevent it from rebooting”.

force_rw_sys. Proxmox monta /sys como sys:mixed en unprivileged aunque el default de LXC sería sys:rw; esta feature revierte a rw y “can break networking under newer (>= v245) systemd-network use”. Resumen operativo:

FeatureAplica aEfecto técnicoRiesgo principal
nestingprivilegiado y unprivilegedallow_nesting y allow userns en AppArmorexpone procfs y sysfs del host
keyctlsolo unprivilegedquita keyctl errno 38 del perfil seccompincompatible con systemd-networkd
fuseprivilegiado y unprivilegedbind de /dev/fuse más regla AppArmordeadlocks de I/O con el freezer
mknodunprivilegedseccomp notify hacia pve-lxc-syscalldexperimental
mountprivilegiado y unprivilegeduna regla AppArmor por fstypeNFS puede bloquear la I/O del host
force_rw_sysunprivileged/sys en rw en vez de mixedrompe systemd-networkd 245 y superior

Aplicar features en caliente no basta: hay que reiniciar el contenedor con pct set 110 -features nesting=1,keyctl=1 seguido de pct reboot 110.

Root disk, mount points y bind mounts

La sintaxis completa de los volúmenes, donde rootfs acepta todo salvo mp=, backup= y keepattrs=:

mp[n]: [volume=]<volume> ,mp=<Path> [,acl=<1|0>] [,backup=<1|0>] [,idmap=<...>]
       [,keepattrs=<1|0>] [,mountoptions=<opt;opt>] [,quota=<1|0>]
       [,replicate=<1|0>] [,ro=<1|0>] [,shared=<1|0>] [,size=<DiskSize>]

Hay tres tipos de mount point y se comportan de forma muy distinta:

Storage backed. Gestionado por la capa de storage del capítulo 7, en tres sabores: imagen raw con un ext4 dentro, subvolumen ZFS (técnicamente un bind mount, pero con storage gestionado, lo que permite redimensionar y hacer snapshots), o directorio, que se activa pasando size=0. La sintaxis STORAGE:TAMAÑO_EN_GiB reserva el volumen y sustituye el marcador por el volid real:

pct set 100 -mp0 thin1:10,mp=/path/in/container

Bind mount. Un directorio del host expuesto dentro. No lo gestiona la capa de storage, así que no admite snapshots ni cuotas, y en unprivileged tendrás problemas de permisos y no podrás usar ACLs. Dos avisos literales de la documentación que cuestan tiempo y datos: “The contents of bind mount points are not backed up when using vzdump” y “Never bind mount system directories like /, /var or /etc into a container - this poses a great security risk”. La recomendación es reservar una jerarquía específica, por ejemplo bajo /mnt/bindmounts. Además, la ruta de origen no puede contener symlinks.

Device mount. Monta un dispositivo de bloque del host directamente. Honra quota y acl, pero tampoco entra en los backups y “should only be used under special circumstances”. Sub-opciones que conviene conocer: keepattrs (default 0) hereda UID, GID y modo del directorio si ya existe; replicate (default 1) incluye el volumen en los jobs de replicación de storage; ro=1 lo monta de sólo lectura; y shared=1 marca un mount point no gestionado como disponible en todos los nodos, con el matiz importante de que “this option does not share the mount point automatically, it assumes it is shared already”.

Operaciones del día a día:

pct resize 110 rootfs 16G          # ampliar; reducir NO esta soportado
pct resize 110 mp0 +10G            # relativo
pct move-volume 110 mp0 otro-storage --delete 1
pct move-volume 110 mp0 --target-vmid 200 --target-volume mp1
pct fstrim 110                     # salta bind mounts y montajes de solo lectura
pct fsck 110 --device mp0          # con el CT parado
pct df 110
pct rescan --vmid 110 --dryrun 1
pct mount 110                      # emergencia: deja un lock sobre el CT
pct unmount 110

El problema de nobody y el idmap por mount point

Montas /mnt/bindmounts/shared dentro de un contenedor unprivileged y todo aparece como nobody. No es un fallo: es el mapeo funcionando. El wiki oficial lo describe así: “every file and directory will be mapped to ‘nobody’ (uid 65534)”. Cualquier UID del host que no caiga dentro del rango 100000-165535 se ve como 65534 dentro. Y a la inversa: lo que el root del contenedor escriba aparecerá en el host como UID 100000. Hay dos formas de arreglarlo y en Proxmox VE 9.2 una de ellas es claramente preferible.

La forma clásica: lxc.idmap global

Objetivo: que el UID/GID 1005 del host se vea y se escriba como 1005 dentro del contenedor 1234, manteniendo el resto del mapeo estándar. Primero se prepara el directorio y se autoriza el rango extra en el host:

mkdir -p /mnt/bindmounts/shared
chown -R 1005:1005 /mnt/bindmounts/shared
echo 'root:1005:1' >> /etc/subuid
echo 'root:1005:1' >> /etc/subgid

Y en /etc/pve/lxc/1234.conf:

lxc.idmap = u 0 100000 1005
lxc.idmap = g 0 100000 1005
lxc.idmap = u 1005 1005 1
lxc.idmap = g 1005 1005 1
lxc.idmap = u 1006 101006 64530
lxc.idmap = g 1006 101006 64530

El mapa debe cubrir el rango 0-65535 completo, sin huecos ni solapes. La aritmética: el tramo 1 mapea 0-1004 del contenedor a 100000-101004 del host (1005 IDs), el tramo 2 mapea el 1005 sobre sí mismo (1 ID) y el tramo 3 mapea 1006-65535 a 101006-165535 (64530 IDs). Suman exactamente 65536.

Un detalle del código que sorprende: cualquier entrada lxc.idmap en el .conf hace que Proxmox trate el contenedor como unprivileged aunque tenga unprivileged: 0, y en ese caso no añade el mapeo por defecto, respetando el tuyo íntegramente.

La forma nueva de PVE 9.2: idmap por mount point

Proxmox VE 9.2 añade la sub-opción idmap a rootfs y a cada mp[n], “to configure the UID/GID mappings on individual mount points without affecting the global container mapping”. La sintaxis es idmap=<type:container:disk:range-size[;type:container:disk:range-size;...]>, donde type es u para UID o g para GID, container es el primer ID visto dentro del contenedor, disk es el primer ID correspondiente en el filesystem subyacente y range-size el número de IDs consecutivos. Los IDs no mapeados caen de vuelta al lxc.idmap del contenedor. El mismo caso del ejemplo anterior queda en una línea, sin tocar /etc/subuid, y el valor especial passthrough mapea todo en identidad sobre ese volumen:

pct set 1234 -mp0 /mnt/bindmounts/shared,mp=/shared,idmap=u:1005:1005:1;g:1005:1005:1
pct set 1234 -mp1 /mnt/bindmounts/media,mp=/media,idmap=passthrough
lxc.idmap globalidmap= por mount point
Disponible desdesiempreProxmox VE 9.2
Afecta atodo el contenedor, rootfs incluidosólo ese volumen
Requiere /etc/subuid y /etc/subgidNo
Mecanismo de kerneluser namespace del contenedormount_setattr, idmapped mounts
Funciona en privilegiadono aplicano, se ignora con warning
Complejidadalta, hay que cubrir 0-65535baja
Recomendaciónlegado o cuando hay que cambiar el mapeo del rootfspreferente

Seguridad: AppArmor, seccomp, capabilities y devices cgroup

Un proceso dentro de un contenedor atraviesa cinco capas antes de tocar el kernel, y cada una se configura por separado. La doc lo justifica así: “Containers use the kernel of the host system. This exposes an attack surface for malicious users. In general, full virtual machines provide better isolation.”

AppArmor. Proxmox ya no usa un perfil estático sino uno generado: escribe lxc.apparmor.profile = generated y le inyecta líneas con lxc.apparmor.raw según las features activas. El comentario del código lo aclara: el default generado “should be equivalent to the old lxc-container-default-cgns profile”. Los perfiles estáticos siguen presentes en /etc/apparmor.d/lxc/ como referencia.

Un denegado se diagnostica con dmesg | grep apparmor, journalctl -k --since "10 min ago" | grep -i apparmor y aa-status.

Desactivar AppArmor para un contenedor concreto es posible con lxc.apparmor.profile = unconfined en el .conf, pero la doc lo desaconseja explícitamente y remata con “this is not recommended for production use”. Además, si defines el perfil a mano, Proxmox avisa de que estás anulando las features con el mensaje explicitly configured lxc.apparmor.profile overrides the following settings: features:nesting, features:mount.

Capabilities. En privilegiados aplica el lxc.cap.drop de common.conf que ya viste. En unprivileged, /usr/share/lxc/config/userns.conf hace justo lo contrario: deja lxc.cap.drop y lxc.cap.keep vacíos. No es un descuido, porque dentro de un user namespace las capabilities son relativas al namespace y no dan poder real sobre el host.

seccomp. El perfil base de privilegiados es /usr/share/lxc/config/common.seccomp y el de unprivileged es el mismo más keyctl errno 38; cuando hacen falta reglas extra, Proxmox genera un perfil propio en /var/lib/lxc/<VMID>/rules.seccomp y lo referencia con lxc.seccomp.profile. Novedad de seguridad de 9.2: Proxmox bloquea con seccomp los sockets AF_ALG dentro de contenedores para prevenir una escalada local de privilegios vía algif_aead (aviso PSA-2026-00018-1, CVE-2026-31431). La regla que aplica en unprivileged es socket => ['errno 97 [0,38,SCMP_CMP_EQ]'], donde 38 es AF_ALG y 97 es EAFNOSUPPORT. Si tienes una carga que necesita AF_ALG de verdad, esta es la razón por la que dejó de funcionar tras actualizar.

Devices cgroup. En privilegiados el modelo es una allow-list estricta que arranca con lxc.cgroup2.devices.deny = a y va abriendo /dev/null, /dev/zero, /dev/full, /dev/tty, /dev/console, /dev/ptmx, /dev/random, /dev/urandom, /dev/pts/* y /dev/fuse. En unprivileged, Proxmox invierte el modelo a deny-list con lxc.cgroup2.devices.allow = a, porque el aislamiento real lo aporta el user namespace y no el controlador de dispositivos.

Los permisos y roles que controlan quién puede tocar todo esto los verás en el capítulo 10, junto con el firewall por interfaz que activa firewall=1 en cada net[n].

cgroup v2: memory.max frente a memory.high, cpu.max y cpu.weight

cgroup v1 fue deprecado en Proxmox VE 7.0 y eliminado por completo en 9.0. Ya no se puede volver atrás: el parámetro de kernel systemd.unified_cgroup_hierarchy=0 que antes lo permitía dejó de tener efecto. La consecuencia práctica es doble: memoria y swap se controlan de forma independiente, y el controlador de dispositivos funciona de otra manera. Y una tercera: los contenedores necesitan systemd 231 o superior, o no usar systemd en absoluto, como Alpine. CentOS 7 y Ubuntu 16.10 son los ejemplos que cita la propia documentación como incompatibles.

Además, Proxmox VE 9.2 empieza a avisar de las entradas lxc.cgroup. (v1) que queden en tu configuración: hay que migrarlas a lxc.cgroup2..

Memoria: dos límites, no uno

Proxmox no escribe un único límite, escribe dos. La función calculate_memory_constraints de pve-container calcula memory.max como memory × 1024 × 1024, el límite duro que dispara el OOM-killer, y memory.high como memory × 1024 × 1016 por debajo de 16 GiB o exactamente 128 MiB menos a partir de ahí, que es el límite blando con el que los procesos del cgroup sufren throttling y presión de reclaim antes de morir.

memory:memory.maxmemory.highMargen
512 MB536 870 912532 676 608~4 MiB
2048 MB2 147 483 6482 130 706 432~16 MiB
16384 MB17 179 869 18417 045 651 456128 MiB exactos
32768 MB34 359 738 36834 225 520 640128 MiB exactos

El swap se escribe aparte en memory.swap.max; con swap: 0 el contenedor no puede usar nada del swap del host. Y lo que hace que free y top dentro del contenedor muestren estos límites y no la RAM total del nodo es lxcfs.

CPU

Tres opciones que se confunden constantemente:

corescpulimitcpuunits
Qué hacecuántas CPUs se ventecho duro de tiempo de CPUpeso relativo bajo contención
Traducción a cgroup v2cpusetcpu.maxcpu.weight
Tipoentero 1-8192flotante 0-8192entero, recortado a [1, 10000]
Defaulttodas0, sin límite100
Efecto sin contenciónlimita el paralelismolimita siempreninguno

cpulimit: 0.5 genera literalmente lxc.cgroup2.cpu.max = 50000 100000, es decir 50 ms de CPU cada 100 ms de periodo. Es perfectamente válido dar dos cores y limitar el consumo total a medio core.

Gotcha de cpuunits: el esquema acepta hasta 500000, pero en cgroup v2 el valor “will be clamped to [1, 10000]”. Poner 500000 no te da más prioridad que poner 10000. Si estás afinando prioridades entre contenedores, trabaja dentro de ese rango.

El reparto de CPUs físicas lo hace una tarea periódica dentro de pvestatd y se consulta con pct cpusets, que imprime qué núcleos tiene asignados cada contenedor. Para ver los valores efectivos en vivo:

cat /sys/fs/cgroup/lxc/110/ns/memory.max
cat /sys/fs/cgroup/lxc/110/ns/memory.high
cat /sys/fs/cgroup/lxc/110/ns/cpu.max
cat /sys/fs/cgroup/lxc/110/ns/cpu.weight
cat /sys/fs/cgroup/lxc/110/ns/cpu.pressure

Desde Proxmox VE 9.0 el panel de resumen del contenedor muestra además el uso de memoria del host atribuible al cgroup del guest, y gráficas de PSI (pressure stall information) de CPU, IO y memoria.

Red, DNS y los ficheros que Proxmox reescribe dentro

La sintaxis de una interfaz, con type=veth como único valor posible:

net[n]: name=<string> [,bridge=<bridge>] [,firewall=<1|0>] [,gw=<GatewayIPv4>]
        [,gw6=<GatewayIPv6>] [,host-managed=<1|0>] [,hwaddr=<MAC>]
        [,ip=<IPv4/CIDR|dhcp|manual>] [,ip6=<IPv6/CIDR|auto|dhcp|manual>]
        [,link_down=<1|0>] [,mtu=<integer>] [,rate=<mbps>] [,tag=<integer>]
        [,trunks=<vlanid;vlanid>] [,type=veth]
pct set 110 -net0 name=eth0,bridge=vmbr0,ip=dhcp
pct set 110 -net0 name=eth0,bridge=vmbr0,ip=192.168.15.147/24,gw=192.168.15.1,tag=20,firewall=1,mtu=1500,rate=100
pct set 110 -net0 name=eth0,bridge=vmbr0,ip=dhcp,link_down=1   # desconectar el cable virtual
pct set 110 -net1 name=eth1,bridge=vmbr1,ip=10.10.0.5/24       # segunda NIC sin gateway

La red se puede añadir y modificar con el contenedor corriendo. Del lado del host, el veth se llama veth<VMID>i<n>, por ejemplo veth110i0. Ese nombre es el que usarás con tcpdump, ip link y las reglas de firewall.

Gotcha: Proxmox aborta el arranque si el MTU del contenedor supera el del bridge, con un mensaje inequívoco: net0: MTU size '9000' is bigger than bridge MTU '1500'. Bridges, VLAN y bonding son el tema del capítulo 9.

Para DNS hay dos opciones, nameserver y searchdomain, con un comportamiento que sorprende: si no defines ninguna de las dos, el contenedor hereda las del host; si defines sólo una, la otra queda vacía. No hay herencia parcial.

pct set 110 -nameserver "192.168.1.1 1.1.1.1"
pct set 110 -searchdomain lan.example
pct set 110 --delete nameserver,searchdomain    # volver a heredar del host

En cada arranque, Proxmox detecta la distribución y reescribe varios ficheros dentro del contenedor: /etc/hostname, /etc/hosts, la configuración de red, el DNS, ajustes del init (por ejemplo el número de procesos getty), la contraseña de root al crearlo, las ssh_host_keys para que cada contenedor tenga las suyas, y el crontab, que aleatoriza para que no arranquen todos los cron a la vez. Los cambios van entre los marcadores # --- BEGIN PVE --- y # --- END PVE ---, y si la sección ya existe se actualiza en el sitio sin moverla.

Si necesitas que deje de tocar un fichero concreto hay un mecanismo por fichero, y ostype: unmanaged lo desactiva todo de golpe:

pct exec 110 -- touch /etc/.pve-ignore.resolv.conf
pct exec 110 -- touch /etc/.pve-ignore.hosts
pct exec 110 -- touch /etc/.pve-ignore.hostname

Advertencia: “Container start fails if the configured ostype differs from the auto detected type”. La detección mira primero /etc/os-release y, si no sirve, ficheros específicos como /etc/debian_version, /etc/alpine-release o /etc/redhat-release. Si conviertes un contenedor de una distro a otra por dentro, actualiza el ostype o no arrancará.

Dos ajustes recientes que evitan confusiones: desde 9.0 el setup de contenedores ya no genera clave SSH host DSA, porque OpenSSH retiró el soporte; y desde 9.2 la zona horaria se inicializa con timedatectl en vez del /etc/timezone deprecado.

Passthrough de dispositivos con dev[n]

La forma moderna de exponer un dispositivo del host, preferible a escribir reglas lxc.cgroup2.devices.allow a mano:

dev[n]: [[path=]<Path>] [,deny-write=<1|0>] [,gid=<integer>] [,mode=<Octal>] [,uid=<integer>]

El caso típico es la iGPU para transcodificación de vídeo, con pct set 110 -dev0 path=/dev/dri/renderD128,gid=104. Por dentro, Proxmox resuelve el major y el minor del dispositivo y genera lxc.cgroup2.devices.allow = c <major>:<minor> rw, más una regla de denegación de escritura si pones deny-write=1. El gid=104 del ejemplo es el grupo que debe poder abrir el nodo dentro del contenedor: compruébalo en tu sistema, porque el GID de render varía entre distribuciones.

Desde Proxmox VE 9.0 se pueden añadir dispositivos en caliente a un contenedor en marcha, pero editar o quitar uno existente en caliente no está implementado y requiere reinicio. Gotcha: si olvidas path=, el arranque muere con Path is not defined for passthrough device dev0.

Docker dentro de LXC: la postura oficial y los riesgos reales

Esta es la pregunta que más se repite, así que va con la fuente delante. El punto 14 del FAQ oficial dice literalmente: “It is not recommended to run docker directly on your Proxmox VE host.” Y la nota que acompaña a los application containers OCI: “doing so is currently a tech preview. For use cases requiring container orchestration or live migration, it is still recommended to run them inside a Proxmox QEMU virtual machine.”

Resumido: no corras Docker en el host, los contenedores de aplicación nativos son preview, para orquestación o live migration usa una VM, y Docker dentro de LXC no está prohibido pero no es el camino probado.

flowchart TD
    START["Quiero correr Docker en Proxmox"]
    START --> Q1{"Necesito orquestacion Swarm o Kubernetes o live migration"}
    Q1 -->|Si| VM["VM QEMU con Docker dentro. Recomendacion oficial"]
    Q1 -->|No| Q2{"Es un homelab o entorno de confianza"}
    Q2 -->|No| VM
    Q2 -->|Si| CT["CT unprivileged con nesting=1 y keyctl=1"]
    CT --> Q3{"El rootfs esta sobre ZFS"}
    Q3 -->|Si| SD["Verificar el storage driver elegido por Docker"]
    Q3 -->|No| OK["overlay2 sobre ext4 o xfs"]
    SD --> ALT["Si overlay2 falla usar un mount point ext4 dedicado para /var/lib/docker"]

Técnicamente hacen falta dos features: nesting=1, sin la cual Docker no puede montar sus propios namespaces y cgroups internos, y keyctl=1, que la doc marca como requisito directo. Receta para un homelab, incluida la comprobación del storage driver y el volumen dedicado para /var/lib/docker si overlay2 da problemas:

pct create 120 local:vztmpl/debian-13-standard_13.6-1_amd64.tar.zst \
  --hostname docker01 --unprivileged 1 \
  --features nesting=1,keyctl=1 \
  --cores 2 --memory 2048 --swap 512 \
  --rootfs local-lvm:16 \
  --net0 name=eth0,bridge=vmbr0,ip=dhcp \
  --onboot 1 --start 1

pct exec 120 -- docker info | grep -A3 'Storage Driver'
pct exec 120 -- docker run --rm hello-world

pct set 120 -mp0 local-lvm:32,mp=/var/lib/docker,backup=0   # backup=0: sin capas en el vzdump
pct reboot 120

Los riesgos concretos, sin dramatismo pero sin maquillaje:

RiesgoDetalle
Aislamiento acumuladoDocker asume estar cerca del kernel; dentro de LXC ya hay seccomp, AppArmor y user namespace encima, y algunos syscalls que espera están filtrados
UID/GID en volúmenesEl contenedor unprivileged mapea root a 100000 y Docker mapea de nuevo; los bind mounts de Docker hacia el host acaban con permisos inesperados
keyctl=1 rompe systemd-networkdTrade-off documentado: eliges uno u otro
Storage driveroverlay2 sobre un rootfs ZFS ha sido históricamente problemático en unprivileged; verifícalo empíricamente con docker info antes de dar nada por hecho
Backups y keyringEl freezer más FUSE puede provocar deadlocks de I/O en modo snapshot o suspend, y con muchos contenedores usando keyctl se agotan kernel.keys.maxkeys y maxbytes del usuario 100000
No probado por ProxmoxUna combinación que funciona hoy puede romperse tras una actualización mayor de kernel o de PVE

Para vigilar el keyring: cat /proc/sys/kernel/keys/maxkeys /proc/sys/kernel/keys/maxbytes y grep 100000: /proc/key-users. Ese 100000: es exactamente el UID al que mapea el root de tus contenedores unprivileged.

Backup, snapshots, restore y migración con reinicio

Los tres modos de backup de un contenedor no son intercambiables:

ModoDowntimeRequisitoEspacio extra
stopalto, todo el backupningunono
suspendmínimo, el segundo rsyncespacio en --tmpdirsí, una copia completa
snapshot (default)mínimo, un freeze brevetodos los volúmenes en storage con snapshotsel del snapshot

El modo suspend copia con rsync a un directorio temporal, suspende el contenedor, hace un segundo rsync de los cambios y lo reanuda. Si el contenedor está en un filesystem local y el destino es NFS o CIFS, pon --tmpdir en un filesystem local: la doc promete “a many fold performance improvement”, y además es obligatorio si respaldas ACLs en modo suspend contra NFS.

Gotcha número uno de los backups de contenedores: por defecto sólo el rootfs entra en el backup. Cada mount point adicional necesita backup=1 explícito. Y los bind mounts y device mounts nunca se respaldan, ni con esa opción.

rootfs: local-lvm:vm-105-disk-0,size=8G
mp0: local-lvm:vm-105-disk-1,mp=/data,size=100G,backup=1
mp1: local-lvm:vm-105-disk-2,mp=/scratch,size=500G,backup=0
vzdump 110 --mode snapshot --compress zstd --storage local --notes-template '{{guestname}}'
vzdump 110 --mode suspend --tmpdir /var/tmp --storage nfs-backup
vzdump 110 --storage pbs --pbs-change-detection-mode metadata

El fichero resultante sigue el patrón vzdump-lxc-<VMID>-<AAAA_MM_DD>-<HH_MM_SS>.tar.zst y vive en /var/lib/vz/dump/ en el storage local. El modo metadata de detección de cambios sólo existe para contenedores y sólo contra Proxmox Backup Server: reutiliza el archivo de metadatos del snapshot anterior para detectar ficheros sin cambios y reaprovechar sus chunks sin leerlos de disco. Todo esto se despliega en el capítulo 13.

Advertencia: el live-restore no existe para contenedores; la documentación lo limita explícitamente a backups de VM en PBS. El restore tiene dos modos: el simple, que se activa cuando no pasas ni rootfs ni ningún mpX y reconstruye el layout tal como estaba en el backup, y el avanzado, que se activa en cuanto pasas uno de esos parámetros e ignora por completo la configuración de volúmenes del archivo.

# ver la config guardada dentro del archivo sin restaurar nada
pvesm extractconfig local:backup/vzdump-lxc-110-2026_08_09-03_00_00.tar.zst

# modo simple: a otro ID y storage, respetando el layout del backup
pct restore 210 /var/lib/vz/dump/vzdump-lxc-110-2026_08_09-03_00_00.tar.zst \
  --storage local-lvm --hostname web01-clon --start 1

# modo avanzado: al pasar rootfs o mpX se ignora el layout del backup
pct restore 210 /var/lib/vz/dump/vzdump-lxc-110-2026_08_09-03_00_00.tar.zst \
  --rootfs local-zfs:16 --mp0 fast-nvme:50,mp=/var/lib/postgresql,backup=1

Los snapshots de contenedor funcionan igual que los de VM del capítulo 5: se guardan como una sección [nombre] dentro del propio /etc/pve/lxc/<CTID>.conf, con parent para la relación padre-hijo y snaptime en epoch Unix. Plantillas y clones también existen: pct template convierte un contenedor parado en plantilla y a partir de ahí pct clone intenta un linked clone por defecto, mientras que con un contenedor normal como origen el clon siempre es completo.

pct snapshot 110 antes-upgrade --description "pre apt full-upgrade"
pct rollback 110 antes-upgrade --start 1
pct delsnapshot 110 antes-upgrade
pct template 110
pct clone 110 120 --hostname web02                                    # linked clone
pct clone 110 121 --full 1 --storage local-lvm --hostname web03       # full clone

Y la migración, que es donde más se nota la diferencia con las VMs. Si el contenedor está parado, pct migrate copia los volúmenes locales al nodo destino y mueve la configuración. Si está corriendo, la única vía es la restart migration:

pct migrate 110 pve2                              # CT parado
pct migrate 110 pve2 --restart --timeout 300      # CT corriendo
pct migrate 110 pve2 --target-storage local-zfs
pct migrate 110 pve2 --target-storage 1           # cada storage a si mismo
pct migrate 110 pve2 --bwlimit 51200

La restart migration apaga el contenedor, lo mata si excede el timeout (180 segundos por defecto), lo migra en frío y lo arranca en el destino. Como los contenedores son ligeros, “this results normally only in a downtime of some hundreds of milliseconds”. Todo esto encaja con el cluster y el HA del capítulo 11.

Dos ficheros de configuración, no uno

Antes de depurar nada conviene tener claro que hay dos configuraciones y sólo una la escribes tú:

FicheroQuién lo escribeQué contiene
/etc/pve/lxc/110.conftú, pct set o la interfaz webconfiguración declarativa de Proxmox, replicada por pmxcfs a todo el cluster
/var/lib/lxc/110/configProxmox, regenerado en cada pct startla configuración LXC real derivada de la anterior

El primero usa un formato de clave y valor separado por dos puntos, y admite además líneas lxc.* de bajo nivel que se pasan directas a LXC. Un ejemplo anotado:

# /etc/pve/lxc/110.conf
arch: amd64
cores: 2
cpulimit: 1.5
cpuunits: 100
features: keyctl=1,nesting=1
hostname: web01
memory: 2048
swap: 512
nameserver: 192.168.10.1
searchdomain: lan.example
net0: name=eth0,bridge=vmbr0,hwaddr=BC:24:11:AA:BB:CC,ip=192.168.10.110/24,gw=192.168.10.1,firewall=1,tag=20,type=veth
onboot: 1
ostype: debian
rootfs: local-lvm:vm-110-disk-0,size=8G
mp0: local-lvm:vm-110-disk-1,mp=/var/lib/data,size=20G,backup=1
mp1: /mnt/bindmounts/media,mp=/media,idmap=passthrough
startup: order=2,up=30
tags: produccion;web
timezone: Europe/Madrid
unprivileged: 1
parent: antes-upgrade

[antes-upgrade]
memory: 2048
rootfs: local-lvm:vm-110-disk-0,size=8G
snaptime: 1786320000

El segundo fichero lo puedes leer para entender qué está aplicando LXC de verdad: verás las líneas lxc.idmap, lxc.cgroup2.memory.max, lxc.cgroup2.cpu.max, lxc.apparmor.profile = generated, lxc.seccomp.profile, lxc.net.0.veth.pair y los includes de /usr/share/lxc/config/<ostype>.common.conf.

Gotcha crítico si depuras con lxc-start directo: ese comando lee /var/lib/lxc/110/config, no el .conf de pmxcfs. La doc lo dice: “If you have changed the container’s configuration since the last start attempt with pct start, you need to run pct start at least once to also update the configuration used by lxc-start.”

La mayoría de los cambios se aplican en caliente. Los que no, quedan como pending y se muestran en rojo en la interfaz. Y para operar dentro del contenedor hay tres puertas distintas que conviene no confundir:

pct pending 110                 # diferencias entre lo aplicado y lo pendiente
pct config 110 --current 1      # valores actualmente en vigor
pct set 110 --revert memory     # cancelar un cambio pendiente
pct reboot 110                  # aplica los pendientes

pct enter 110                   # shell interactivo dentro del namespace, como root
pct exec 110 -- ls -la /        # un comando y salir
pct console 110                 # sesion de login por getty; se sale con Ctrl+a q
pct push 110 /root/app.conf /etc/app/app.conf --user www-data --perms 0640
pct pull 110 /var/log/app/error.log /root/error-110.log
pct unlock 110                  # solo si la operacion que puso el lock ya no corre

Gotcha de --keep-env: la man page de 9.2 documenta default = 1 y a la vez dice que la opción se desactivará por defecto en PVE 9. Pásala explícitamente cuando el entorno importe y verifica con pct exec 110 -- env | sort. Y los valores posibles de lock: son backup, create, destroyed, disk, fstrim, migrate, mounted, rollback, snapshot y snapshot-delete.

El ecosistema community-scripts y la advertencia sobre curl a bash

Tarde o temprano alguien te pasará un one-liner de community-scripts/ProxmoxVE, así que conviene saber exactamente qué es y qué hace. Es la continuación comunitaria de tteck/Proxmox, que quedó archivado con su último push el 2 de noviembre de 2024. El nuevo repositorio se creó el 1 de noviembre de 2024, tiene licencia MIT, unas 29 000 estrellas y su sitio oficial es https://community-scripts.org. Los scripts conservan la autoría original de tteck en sus cabeceras. La función pve_check de misc/core.func valida la versión del host y soporta Proxmox VE 8.0-8.9 y 9.0-9.2, saliendo con código 232 si no estás en un host PVE y 105 si la versión no está soportada.

Lo que hace con el contenedor es razonable: sus defaults en build.func son var_unprivileged=1, var_nesting=1 y var_keyctl=0, con la lógica de forzar keyctl a 1 en contenedores unprivileged, y construye una cadena -features que pasa a un pct create normal. También edita directamente /etc/pve/lxc/<CTID>.conf para cosas que pct no cubre, como el passthrough de /dev/net/tun o de una Coral TPU.

El problema no son los scripts, es el mecanismo de entrega. El patrón bash -c "$(curl -fsSL <url>)" significa que descargas código de internet y lo ejecutas como root en el hipervisor, no dentro de un contenedor aislado, sin revisarlo y sin fijar versión. Y no es un script: build.func tiene 7 312 líneas y a su vez hace source <(curl ...) de core.func, tools.func e install.func. Un solo pegado en la shell acaba ejecutando varios miles de líneas traídas de la red sin firma ni checksum, justo lo contrario de lo que hace pveam, que verifica firma GPG y sha512.

Cómo usarlos sin regalar el hipervisor:

# 1) descargar, leer y solo entonces ejecutar. Sustituye <COMMIT_SHA> por un commit
#    concreto en vez de 'main' para fijar la version que auditaste.
curl -fsSL -o /tmp/debian.sh \
  https://raw.githubusercontent.com/community-scripts/ProxmoxVE/<COMMIT_SHA>/ct/debian.sh
less /tmp/debian.sh
bash /tmp/debian.sh

# 2) desactivar la telemetria de forma permanente
mkdir -p /usr/local/community-scripts
echo 'DIAGNOSTICS=no' > /usr/local/community-scripts/diagnostics

La telemetría viene desactivada por defecto (DIAGNOSTICS="no") y, cuando se activa, envía tamaño de disco, número de cores, RAM, tipo y versión de SO, aplicación, método, versión de PVE, estado y código de salida. Sin IPs, hostnames ni contraseñas según su propia documentación.

El encuadre honesto: son excelentes para un homelab y ahorran horas, pero no son producto de Proxmox Server Solutions GmbH, no tienen soporte comercial y ejecutar curl canalizado a bash como root en un hipervisor es exactamente lo que una política de seguridad seria prohíbe. En producción, lee el script, extrae los pct create y pct set que hace, y ejecútalos tú.

Errores comunes y diagnóstico

Antes de la tabla, la caja de herramientas:

pct start 110 --debug
lxc-start -n 110 -F -l DEBUG -o /tmp/lxc-110.log
journalctl -u [email protected] -n 200 --no-pager
dmesg | grep apparmor
cat /var/lib/lxc/110/config                        # config LXC efectiva
cat /var/lib/lxc/110/rules.seccomp 2>/dev/null     # reglas seccomp generadas
pct exec 110 -- cat /proc/self/uid_map
tcpdump -ni veth110i0
tail -50 /var/log/pveam.log
SíntomaCausa probableDiagnóstico y solución
Servicios systemd fallan con 226/NAMESPACE o 243/CREDENTIALSfalta nesting=1pct set 110 -features nesting=1 y pct reboot 110
Docker no arranca dentro del contenedorfalta keyctl=1, y quizá nesting=1pct set 110 -features nesting=1,keyctl=1
systemd-networkd falla justo tras activar keyctl=1trade-off documentadoelegir entre systemd-networkd y docker; usar ifupdown2 o networking clásico
Todo el bind mount se ve como nobody 65534el mapeo de user namespace funcionando con normalidadidmap= por mount point, o lxc.idmap global
newuidmap: write to uid_map failed: Operation not permittedel rango no está declarado en /etc/subuid o /etc/subgidañadir la línea y reiniciar el contenedor
failed to parse idmap: <valor>, o el mapa personalizado no surte efectosintaxis mala, o huecos en la cobertura de 0 a 65535formato lxc.idmap = u <inicio_ct> <inicio_host> <cantidad>; recalcular los tramos hasta sumar 65536
Container start fails if the configured ostype differs from the auto detected typeostype no coincide con el rootfspct set 110 -ostype <correcto> o -ostype unmanaged
net0: MTU size 'X' is bigger than bridge MTU 'Y'el MTU del net[n] supera el del bridgebajar el del contenedor o subir el del bridge
Path is not defined for passthrough device dev0dev0 sin path=pct set 110 -dev0 path=/dev/dri/renderD128,gid=104
explicitly configured lxc.apparmor.profile overrides the following settings... o el equivalente con lxc.seccomp.profileperfil puesto a mano que anula las featuresquitar la línea manual o asumir el override
El backup en modo snapshot fallaalgún volumen está en un storage sin snapshotsbackup=0 en ese mount point, o --mode suspend
El backup se cuelga con FUSE dentrodeadlock entre el freezer y FUSEmontar el FUSE en el host y hacer bind mount
CT is locked (backup)lock residual tras un corteverificar que nada corre y pct unlock 110
pct resize falla al reducirreducir no está soportadocrear un volumen nuevo y copiar
Las cuotas quota=1 no hacen nadarequieren cgroup v1, eliminado en PVE 9.0limitar a nivel de storage o usar un mount point dedicado
Warning sobre entradas cgroup v1 en el .conflíneas lxc.cgroup. en vez de lxc.cgroup2.migrar las claves
Un contenedor antiguo no arrancasystemd anterior a 231, incompatible con cgroup v2 puroactualizar la distro o mover el servicio a una VM
Key usage is near the limitagotamiento de kernel.keys.maxkeys para el uid 100000subir kernel.keys.maxkeys y maxbytes en /etc/sysctl.d/
pveam download fallaíndice desactualizado, red o proxypveam update y revisar /var/log/pveam.log
La plantilla no aparece en la interfazel storage no tiene el content type vztmplañadir vztmpl al content del storage
pct create falla pidiendo Sys.Modifyintentas crear un privilegiado sin ese privilegio, cambio de PVE 9.0conceder el privilegio o crear unprivileged
Los cambios en /etc/hosts o /etc/resolv.conf se revierten al arrancarProxmox reescribe entre los marcadores PVEtouch /etc/.pve-ignore.<fichero> o ostype: unmanaged
La migración de un contenedor en marcha se rechazano existe live migration para contenedoresusar --restart
lxc-start no refleja los últimos cambios/var/lib/lxc/<VMID>/config desactualizadoejecutar pct start <VMID> al menos una vez

Lo que queda operativo

Con este capítulo tienes contenedores de sistema desplegables de forma repetible: plantillas verificadas por firma con pveam, contenedores unprivileged por defecto con el mapeo 0 a 100000 entendido y no sufrido, features activadas por motivo y no por copiar de un foro, bind mounts con idmap por mount point en vez de aritmética de rangos, límites de cgroup v2 que sabes leer en /sys/fs/cgroup/lxc/<CTID>/ns/, passthrough de dispositivo con dev[n], backup con los mount points correctos incluidos y una postura informada sobre Docker y sobre los scripts de comunidad.

Lo que ha quedado apuntado una y otra vez es el storage: qué backend soporta snapshots, por qué el modo snapshot de vzdump falla sobre un dir, cuándo un subvolumen ZFS se comporta distinto de una imagen raw con ext4 dentro y qué significa realmente el thin provisioning cuando tienes veinte contenedores compartiendo un pool. Ese es el tema del capítulo 7: el modelo de plugins de libpve-storage-perl, storage.cfg línea a línea y la matriz completa de features por backend.