Problema
Al ejecutar varios servicios basados en Docker (por ejemplo, Komga, Calibre‑Web, LazyLibrarian, Suwayomi) dentro de un contenedor LXC sin privilegios, es frecuente encontrarse con dos patrones de fallo:
- Bloqueos de SQLite (
SQLITE_BUSYoOperationalError: database is locked) que impiden que la aplicación arranque o que realice operaciones de escritura. - Errores al crear symlinks (
ln: failed to create symbolic link … Operation not supported) que aparecen en aplicaciones que dependen de KCEF, Chromium Embedded Framework u otras bibliotecas que usan enlaces simbólicos.
Ambos síntomas aparecen cuando los directorios de datos y de medios están montados desde el host mediante bind mount o SMB/CIFS y el contenedor LXC está configurado como unprivileged (UID/GID mapeados). La combinación de AppArmor, la falta de soporte de O_SYMLINK en sistemas de archivos remotos y el modo de bloqueo de SQLite (WAL) genera un entorno donde las llamadas POSIX esperadas son denegadas o se comportan de forma no atómica.
Causa
1. Mapeo de IDs y permisos en LXC unprivileged
En un LXC sin privilegios, el UID 0 del contenedor se traduce a un UID arbitrario en el host (por ejemplo, 100107). Cuando el bind mount apunta a un recurso SMB, el servidor de archivos interpreta esos IDs como usuarios remotos. Si el servidor no reconoce el UID/GID mapeado, los archivos aparecen como propiedad de nobody y, lo que es peor, el kernel marca el punto de montaje con la opción nosuid y nodev. Estas restricciones impiden que procesos dentro del contenedor creen enlaces simbólicos o cambien atributos de archivo.
2. Falta de soporte de symlinks en SMB/CIFS
SMB 2/3 sí permite symlinks, pero la mayoría de los clientes Linux los tratan como reparse points y requieren la opción mfsymlinks. Sin ella, ln -s devuelve Operation not supported. Incluso con mfsymlinks, el servidor debe estar configurado para aceptarlos; de lo contrario el kernel devuelve EOPNOTSUPP.
3. SQLite en modo WAL sobre red
SQLite usa archivos de registro WAL (-wal y -shm) que requieren bloqueos de fcntl a nivel de archivo. En sistemas de archivos remotos que no implementan correctamente los bloqueos POSIX (SMB sin noperm o noserverino), los procesos pueden recibir SQLITE_BUSY incluso cuando no hay otra conexión activa. Además, la latencia de la red amplifica la ventana de bloqueo, provocando colisiones al iniciar varios contenedores simultáneamente.
4. AppArmor y perfil LXC
El perfil predeterminado de LXC restringe operaciones de mknod, mount y link. Cuando una aplicación intenta crear un symlink dentro de un directorio que está bajo un bind mount, AppArmor puede denegar la llamada antes de que el kernel evalúe la capacidad del sistema de archivos remoto.
Solución
Una solución robusta combina tres capas:
-
Re‑ubicación de los directorios críticos a un storage local
- Mantener los archivos de configuración y bases SQLite en un disco local del nodo Proxmox (por ejemplo,
/var/lib/lxc/107/rootfs/opt/data). - Usar bind mounts solo para los volúmenes de medios (imágenes, PDFs, mangas) que no requieren escritura concurrente de bases.
- Mantener los archivos de configuración y bases SQLite en un disco local del nodo Proxmox (por ejemplo,
-
Ajuste de la capa de montaje SMB
- Añadir
mfsymlinks,nobrl,vers=3.0,uid=100107,gid=100110,forceuid,forcegida/etc/fstabo al comandomount. - Verificar que el servidor Samba tenga
unix extensions = noywide links = yes. - Si el servidor no permite symlinks, crear un link farm local que apunte a la ruta SMB mediante
mount --bindy usarln -sdentro del contenedor solo sobre el punto de montaje local.
- Añadir
-
Configuración de SQLite en modo journal (no WAL)
- Añadir la variable de entorno
SQLITE_JOURNAL_MODE=DELETEo montar los volúmenes con la opciónnoatime,nodiratime. - Alternativamente, habilitar
PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;en la inicialización de la base, pero solo si el backend SMB garantiza bloqueos POSIX (poco fiable).
- Añadir la variable de entorno
-
Modificación del perfil AppArmor del LXC
- Crear un archivo de perfil local
/etc/apparmor.d/lxc/107con:#include <tunables/global> profile lxc-107 flags=(attach_disconnected,mediate_deleted) { #allow symlink creation in /config /config/** wl, #allow fcntl locking on SQLite files /config/**/*.db rw, /config/**/*.db-wal rw, /config/**/*.db-shm rw, } - Recargar con
apparmor_parser -r /etc/apparmor.d/lxc/107y reiniciar el contenedor.
- Crear un archivo de perfil local
-
Ajuste de Docker Compose
- Declarar
user: "100107:100110"para que los procesos dentro del contenedor usen los IDs mapeados. - Añadir
tmpfs: /tmp:execpara evitar que los contenedores intenten crear symlinks en rutas montadas remotamente. - Usar
environment:para forzar el modo de journal de SQLite.
- Declarar
Cuándo aplicar esta solución
- Síntomas: contenedores que reinician constantemente con mensajes de
Operation not supportedal crear symlinks, o logs que muestranSQLITE_BUSYal iniciar. - Entorno: LXC sin privilegios, bind mounts a SMB/CIFS, múltiples servicios Docker que comparten la misma carpeta de datos.
- No aplicar: si todas las rutas están en un disco local ext4 o ZFS y el LXC está en modo privilegiado; en esos casos el problema suele estar en la configuración de la propia aplicación, no en la capa de almacenamiento.
Código
# 1. Añadir opciones al fstab (ejemplo para /mnt/media)
UUID=xxxx-xxxx /mnt/media cifs credentials=/etc/samba/creds,iocharset=utf8,vers=3.0,mfsymlinks,nobrl,uid=100107,gid=100110,forceuid,forcegid 0 0
# 2. Montar inmediatamente
mount -a
# 3. Crear perfil AppArmor para LXC 107
cat > /etc/apparmor.d/lxc/107 <<'EOF'
#include <tunables/global>
profile lxc-107 flags=(attach_disconnected,mediate_deleted) {
/config/** wl,
/config/**/*.db rw,
/config/**/*.db-wal rw,
/config/**/*.db-shm rw,
}
EOF
apparmor_parser -r /etc/apparmor.d/lxc/107
# 4. Docker‑compose snippet (relevante)
cat > docker-compose.yml <<'EOF'
services:
suwayomi:
image: ghcr.io/suwayomi/tachidesk:latest
user: "100107:100110"
environment:
- SQLITE_JOURNAL_MODE=DELETE
volumes:
- /mnt/configs/suwayomi:/home/suwayomi/.local/share/Tachidesk
- /mnt/manga:/mnt/mangas
tmpfs:
- /tmp:exec
restart: unless-stopped
EOF
docker compose up -d
Verificación
-
Symlink
- Dentro del contenedor ejecuta
ln -s /tmp /home/suwayomi/.local/share/Tachidesk/testlink. - El comando debe completarse sin error.
- Dentro del contenedor ejecuta
-
SQLite
- Inicia el contenedor y revisa
docker logs <container>. - No debe aparecer
SQLITE_BUSY. - Opcional: abrir la base con
sqlite3 /config/app.dby ejecutarPRAGMA journal_mode;para confirmar que el modo esDELETE.
- Inicia el contenedor y revisa
-
AppArmor
- Ejecuta
aa-status | grep lxc-107y verifica que el perfil está cargado y en modo enforced.
- Ejecuta
-
Rendimiento
- Copia 1 GB de datos al directorio de medios y mide I/O con
dd if=/dev/zero of=/mnt/media/testfile bs=1M count=1024 oflag=direct. - La velocidad debe estar dentro del rango esperado para el disco local; si sigue siendo muy baja, reconsidera mover los volúmenes de datos a almacenamiento local.
- Copia 1 GB de datos al directorio de medios y mide I/O con
Notas adicionales
- Si el servidor Samba no permite
mfsymlinks, la única alternativa fiable es no usar SMB para directorios que requieran symlinks. - En entornos donde la latencia de red es alta, considera usar NFS con
nolockyno_root_squashcomo alternativa a SMB; NFS maneja mejor los bloqueos de fcntl. - Cuando se usan varios contenedores que comparten la misma base SQLite, es recomendable aislar cada aplicación en su propio archivo de base o usar una base de datos cliente‑servidor (PostgreSQL, MariaDB) para evitar colisiones.
- Siempre revisa los logs de AppArmor (
/var/log/syslogojournalctl -k) después de aplicar un nuevo perfil; una regla faltante suele aparecer comoapparmor="DENIED"en los mensajes.