Sí. Aquí tienes el recopilatorio exhaustivo de todo lo tratado, organizado para que luego podamos convertirlo en índice, borrador o “signature piece”. La conclusión estratégica ya quedó clara: no es terreno virgen, pero sí una integración rara y valiosa de extremo a extremo: container → rootfs con Chisel → FS inmutable → boot con UKI/LinuxBoot → runtime mínimo sin shell → persistencia estilo volumen. Eso es precisamente lo que lo hace bueno para artículo técnico fuerte, e incluso para paper si después añades benchmark, seguridad o reproducibilidad medible.
0. Idea central que ha ido emergiendo
La conversación empezó con algo muy concreto:
“Quiero saber si un Docker build fue ejecutado con
--outputdesde dentro del Dockerfile”.
Y terminó evolucionando hacia una arquitectura bastante más ambiciosa:
Construir una imagen mínima, inmutable, sin shell, basada en Chisel, exportar licencias fuera de la imagen, y eventualmente convertir ese rootfs en un sistema Linux arrancable tipo appliance, con EROFS/SquashFS, UKI, init mínimo, volumen persistente y posible evolución hacia LinuxBoot/Coreboot.
La idea narrativa final sería algo como:
What if your container WAS the operating system?
O:
Turning a Docker Image into a Bootable Immutable Linux Appliance
1. Punto inicial: detectar --output dentro de un Dockerfile
1.1 Pregunta inicial
Querías saber si dentro del Dockerfile se puede detectar si el build se ejecutó con:
docker build --output ...
Y abortar si no se había usado.
1.2 Conclusión
No se puede detectar directamente.
Dentro del Dockerfile no existe una variable tipo:
DOCKER_BUILD_COMMAND
DOCKER_OUTPUT
BUILD_FLAGS
ni nada equivalente.
Docker no expone al proceso de build:
- el comando completo usado;
- si se pasó
--output; - si se pasó
--load; - si se pasó
--push; - flags como
--progress,--platform, etc.
1.3 Alternativas consideradas
Alternativa A: wrapper externo
Validar antes de invocar Docker:
#!/usr/bin/env bash
set -e
if [[ " $* " != *" --output "* ]]; then
echo "ERROR: debes usar --output"
exit 1
fi
docker build "$@"
Problema: no está dentro del Dockerfile.
Alternativa B: --build-arg
Forzar una señal explícita:
ARG OUTPUT_ENABLED
RUN test "$OUTPUT_ENABLED" = "1" || \
(echo "ERROR: este build requiere --output"; exit 1)
Invocación:
docker build \
--output type=local,dest=out \
--build-arg OUTPUT_ENABLED=1 \
.
Problema: comprueba el ARG, no el --output real.
Alternativa C: comprobar artefacto externo
Tras el build, verificar si el directorio esperado existe.
Problema: post-build, no Dockerfile.
1.4 Conclusión conceptual
El Dockerfile describe el filesystem y la build graph, pero no controla ni conoce el exporter del build.
2. Multi-stage Dockerfile con dos “finales”: app e información legal
2.1 Tu caso real
Querías generar:
- Una imagen final normal con el programa.
- Una salida local con copyrights/licencias usando
--output.
La idea era evitar meter licencias en la imagen final para hacerla más pequeña, pero conservarlas fuera.
2.2 Patrón técnico resultante
Usar dos stages finales:
FROM ... AS app
# imagen final real
FROM ... AS copyrights
# solo material legal
Y construir cada uno con --target.
2.3 Qué pasa si no usas --target
En un Dockerfile multi-stage, si haces:
docker build .
Docker construye el último stage del Dockerfile.
Los stages anteriores solo se construyen si el último los necesita directa o indirectamente, por ejemplo con:
COPY --from=copyrights ...
Por eso decidimos que:
-
licenses/copyrightsdebe ir antes; -
appdebe ir al final; - así
docker build .genera solo la imagen de la app.
2.4 Uso de --target
Para construir solo las licencias:
docker buildx build \
--target licenses \
--output type=local,dest=./legal \
.
Para construir la app:
docker buildx build \
--target app \
-t miapp:latest \
--load \
.
2.5 Qué hace --load
Con docker buildx build, si no usas --load, --push u otro --output, la imagen puede quedarse solo en la caché de BuildKit y no aparecer en:
docker images
--load exporta la imagen al daemon local de Docker, equivalente al flujo clásico de docker build.
Ejemplo:
docker buildx build -t miapp:latest --load .
docker run miapp:latest
3. Licencias, copyrights y cumplimiento
3.1 Motivación
Querías minimizar la imagen final eliminando material legal/documentación, pero sin perderlo.
Duda central:
¿Va contra las licencias quitar los copyrights de la imagen y guardarlos fuera?
3.2 Conclusión matizada
No hay una única regla universal.
Muchas licencias permiten entregar avisos y textos legales como artefacto separado, siempre que acompañen la distribución del binario/imagen.
Pero si publicas una imagen en un registry y no distribuyes junto a ella el material legal, podrías incumplir ciertas licencias.
3.3 Formatos discutidos
Carpeta de licencias
Ventaja:
- más transparente;
- conserva archivos originales;
- mejor para auditoría.
.tgz
Ventaja:
- un solo artefacto;
- fácil de subir a CI;
- fácil de adjuntar en releases.
Markdown
Ventaja:
- legible;
- útil para humanos;
- buen
THIRD_PARTY.md.
Limitación:
- puede no contener todo el texto legal completo si solo lista rutas/resúmenes.
3.4 Decisión final
Generar ambos:
THIRD_PARTY.md
licenses.tgz
files/<package>/copyright
4. Introducción de Canonical Chisel
4.1 Por qué Chisel
Querías usar Canonical Chisel para crear imágenes Ubuntu mínimas tipo distroless.
Chisel permite construir un rootfs mínimo a partir de slices de paquetes Ubuntu.
La conversación corrigió el enfoque anterior:
- no hace falta buscar licencias en
.deb; - los slices de Chisel ya incluyen explícitamente los archivos de licencia;
- el rootfs generado por Chisel puede usarse como base para una imagen
scratch.
4.2 Patrón oficial de Chisel en Dockerfile
La documentación de Ubuntu muestra el patrón:
FROM ubuntu:24.04 AS chiseler
RUN apt-get update && apt-get install -y chisel
RUN chisel cut --release ubuntu-24.04 --root /rootfs libc6
FROM scratch
COPY --from=chiseler /rootfs /
CMD [...]
4.3 Decisiones adoptadas
- Chisel se ejecuta en un stage Ubuntu.
- La imagen final es
scratch. - El rootfs final viene de
/rootfs. - Las licencias se extraen desde ese rootfs.
- No reconstruimos licencias desde
.deb.
5. Reglas que fuiste imponiendo al Dockerfile
Fuiste refinando las restricciones:
5.1 No Alpine, no BusyBox, no Debian
Regla final:
Usar solo imágenes base
scratcho Ubuntu slim cuando sea posible.
Esto descartó soluciones basadas en:
FROM alpine
FROM busybox
FROM debian:bookworm-slim
5.2 No instalar paquetes innecesarios
Las herramientas pueden existir en stages intermedios, pero no deben llegar a la imagen final.
5.3 Imagen final scratch
La final debe ser:
FROM scratch AS app
y copiar únicamente:
- rootfs chiseado;
-
/etc/passwd; -
/etc/group; - home del usuario;
- binario de la app;
- quizá
rochecko init mínimo si aplica.
5.4 Usuario nonroot
Querías usuario similar a distroless:
nonroot
No querías usar solo UID/GID en la declaración final.
Preferencia:
USER nonroot:nonroot
5.5 Shell de usuarios
Inicialmente se propuso /bin/false.
Lo corregiste:
usar
/sbin/nologin.
Por tanto:
root:x:0:0:root:/:/sbin/nologin
nonroot:x:65532:65532:nonroot:/home/nonroot:/sbin/nologin
5.6 Home de root
Querías:
- no copiar
/root; - root con home
/, o a un directorio no existente; - evitar
/rootcomo directorio usable.
Se adoptó:
root:x:0:0:root:/:/sbin/nologin
5.7 Home de nonroot
/home/nonroot
Debe ser el único lugar escribible por defecto.
5.8 Volumen
Declarar:
VOLUME ["/home/nonroot"]
5.9 Permisos
Inicialmente querías eliminar permisos de escritura en todo salvo /home/nonroot.
Después matizamos:
- modificar permisos del rootfs puede romper supuestos;
- no equivale a
--read-only; - puede añadir fragilidad;
- Chisel/Canonical no hace chmod masivo;
- mejor enforcement runtime con
--read-only.
Pero tú aclaraste:
Quiero
chmodsolo como capa adicional, no como sustituto de--read-only.
Finalmente vimos que Docker no permite hacer que una imagen sea read-only por defecto a nivel OCI, así que apareció la solución fail-fast.
6. Dockerfile con Chisel + licencias + app + usuario nonroot
6.1 Estructura final propuesta
Conceptualmente:
# syntax=docker/dockerfile:1.10
FROM ubuntu:24.04 AS chiseler
# install chisel
# chisel cut --root /rootfs ...
FROM ubuntu:24.04 AS licenses
COPY --from=chiseler /rootfs /rootfs
# generate THIRD_PARTY.md
# generate licenses.tgz
FROM ubuntu:24.04 AS build
# build app
FROM ubuntu:24.04 AS etcfiles
# create passwd/group/home
FROM scratch AS app
COPY --from=chiseler /rootfs/ /
COPY --from=etcfiles /etc/passwd /etc/passwd
COPY --from=etcfiles /etc/group /etc/group
COPY --from=etcfiles --chown=nonroot:nonroot /home/nonroot /home/nonroot
COPY --from=build --chown=nonroot:nonroot /out/myapp /usr/local/bin/myapp
ENV HOME=/home/nonroot
WORKDIR /home/nonroot
VOLUME ["/home/nonroot"]
USER nonroot:nonroot
ENTRYPOINT ["/usr/local/bin/myapp"]
6.2 Generación de licencias
Stage conceptual:
FROM ubuntu:24.04 AS licenses
COPY --from=chiseler /rootfs /rootfs
WORKDIR /out
ARG CHISEL_RELEASE=ubuntu-24.04
ARG CHISEL_SLICES
RUN set -e; \
echo "# THIRD_PARTY (Chisel ${CHISEL_RELEASE})" > THIRD_PARTY.md; \
echo "" >> THIRD_PARTY.md; \
echo "Slices utilizados:" >> THIRD_PARTY.md; \
for s in ${CHISEL_SLICES}; do echo "- ${s}" >> THIRD_PARTY.md; done; \
echo "" >> THIRD_PARTY.md; \
echo "## Paquetes y licencias detectadas" >> THIRD_PARTY.md; \
mkdir -p /out/files; \
if [ -d /rootfs/usr/share/doc ]; then \
find /rootfs/usr/share/doc -maxdepth 2 -type f -name copyright \
| sort \
| while read -r f; do \
pkg="$(echo "$f" | sed -E 's#.*/usr/share/doc/([^/]+)/copyright#\1#')"; \
echo "### ${pkg}" >> THIRD_PARTY.md; \
echo "- Ruta: ${f#/rootfs}" >> THIRD_PARTY.md; \
echo "" >> THIRD_PARTY.md; \
mkdir -p "/out/files/${pkg}"; \
cp "$f" "/out/files/${pkg}/"; \
done; \
fi; \
tar -C /out -czf /out/licenses.tgz files THIRD_PARTY.md
6.3 Exportar licencias
docker buildx build \
--target licenses \
--build-arg CHISEL_SLICES="base core ca-certificates tzdata libc6 minimal" \
--output type=local,dest=./legal \
.
Resultado:
./legal/THIRD_PARTY.md
./legal/licenses.tgz
./legal/files/<package>/copyright
7. docker-bake.hcl
7.1 Por qué apareció
Querías generar:
- imagen de app;
- salida de licencias;
potencialmente en un solo comando.
7.2 Qué es HCL
HCL = HashiCorp Configuration Language.
Es un lenguaje declarativo usado por Terraform, Packer y otros.
Docker Buildx Bake lo usa para definir:
- targets;
- args;
- tags;
- outputs;
- grupos de build.
7.3 Ejemplo final
group "default" {
targets = ["app_image", "licenses"]
}
target "app_image" {
target = "app"
tags = ["miapp:latest"]
output = ["type=docker"]
}
target "licenses" {
target = "licenses"
args = {
CHISEL_SLICES = "base core ca-certificates tzdata libc6 minimal"
}
output = ["type=local,dest=./legal"]
}
7.4 Ejecutar
docker buildx bake
Genera:
- imagen local
miapp:latest; - directorio
./legal.
8. Read-only root filesystem en contenedores Docker
8.1 Problema
Querías que la imagen pudiera declarar:
“Este root filesystem debe ser read-only”.
Docker no ofrece un Dockerfile equivalente a:
READONLY_ROOTFS true
8.2 Por qué
OCI separa:
- imagen: qué archivos existen;
- runtime: cómo se montan.
--read-only pertenece al runtime, no a la imagen.
8.3 Opciones disponibles
Docker CLI
docker run --read-only miapp
Docker Compose
services:
app:
image: miapp
read_only: true
volumes:
- app-data:/home/nonroot
Kubernetes
securityContext:
readOnlyRootFilesystem: true
8.4 No hay enforcement desde imagen
No hay label estándar que Docker aplique automáticamente.
Se pueden usar labels solo como documentación/política:
LABEL org.opencontainers.image.readonly-rootfs="true"
Pero no enforcea nada.
9. Fail-fast si el filesystem no es read-only
9.1 Solución conceptual
La imagen no puede imponer --read-only, pero sí puede negarse a arrancar si el rootfs es escribible.
9.2 Primer enfoque con shell
#!/bin/sh
set -e
if touch /.rw-test 2>/dev/null; then
echo "ERROR: root filesystem is writable. Run with --read-only."
rm -f /.rw-test
exit 1
fi
exec /usr/local/bin/myapp
Pero no querías shell.
9.3 Fail-fast sin shell en C
Código propuesto:
#define _GNU_SOURCE
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv) {
int fd = open("/.rw-test", O_WRONLY | O_CREAT, 0600);
if (fd >= 0) {
close(fd);
unlink("/.rw-test");
write(2,
"ERROR: root filesystem is writable.\n"
"Run container with --read-only or "
"readOnlyRootFilesystem: true\n",
98);
_exit(1);
}
if (errno != EROFS) {
perror("ERROR: filesystem check failed");
_exit(1);
}
if (argc < 2) {
write(2, "ERROR: no command specified\n", 28);
_exit(1);
}
execv(argv[1], &argv[1]);
perror("execv failed");
_exit(1);
}
9.4 Compilación
cc -static -Os -s rocheck.c -o rocheck
9.5 Integración en imagen
COPY rocheck /rocheck
COPY myapp /usr/local/bin/myapp
USER nonroot:nonroot
ENTRYPOINT ["/rocheck", "/usr/local/bin/myapp"]
9.6 Comportamiento
docker run miapp
# falla
docker run --read-only miapp
# arranca
9.7 Ventaja
Comprueba el mount real del kernel, no permisos POSIX.
Detecta correctamente EROFS devuelto por open() al intentar escribir en un FS montado read-only.
10. Limitaciones del modelo Docker/OCI respecto a sistemas de archivos reales
10.1 Tu observación
Dijiste que te gustaría usar:
- ISO9660;
- ROMFS;
- SquashFS;
- EROFS;
como root real.
10.2 Conclusión
Una imagen Docker/OCI es básicamente:
capas tar + metadata
El runtime decide cómo montarlas, normalmente con overlayfs.
No eliges desde la imagen el FS final como ISO9660/EROFS/SquashFS.
10.3 Consecuencia
Para tener un FS realmente inmutable:
- no basta con Docker;
- hay que convertir el rootfs en una imagen de disco;
- hay que arrancar Linux contra ese rootfs.
11. Evolución hacia sistema arrancable inmutable
11.1 Idea emergente
Convertir el rootfs generado desde Docker/Chisel en un sistema operativo real:
Docker/Chisel rootfs
↓
EROFS/SquashFS image
↓
Kernel + initramfs / UKI
↓
Bootable immutable appliance
11.2 Motivación
Tener un sistema “tipo contenedor”, pero sobre bare metal:
- rootfs inmutable;
- sin shell;
- sin apt;
- persistencia separada;
- actualización controlada;
- superficie de ataque mínima.
11.3 Casos de uso considerados
- sistema anti-drones TRC;
- SAT Companion Terrestar;
- device guía turístico Tourstream/Viamo;
- edge media;
- gateways WebRTC;
- dispositivos offline;
- IoT industrial;
- defensa/militar;
- cámaras/video analytics;
- Django appliance.
12. ISO9660 vs EROFS vs SquashFS vs ROMFS
12.1 ISO9660
Características:
- diseñado para CD-ROM;
- universalmente soportado;
- muy compatible;
- no ideal para rootfs moderno;
- estructura antigua;
- tamaños/filenames limitados salvo extensiones Rock Ridge/Joliet;
- útil para ISOs bootables/legacy.
12.2 EROFS
Características discutidas:
- moderno;
- read-only;
- diseñado para Linux/Android/embedded;
- compresión eficiente;
- buen acceso aleatorio;
- buen candidato para rootfs inmutable moderno;
- más adecuado que ISO9660 para sistema Linux appliance.
12.3 SquashFS
Características:
- muy usado en LiveCDs, routers, embedded;
- read-only;
- comprimido;
- más clásico;
- muy soportado por bootloaders;
- GRUB lo soporta como
squash4.
12.4 ROMFS
Mencionado como alternativa histórica read-only.
Más simple, pero menos atractivo que EROFS/SquashFS.
12.5 Decisión conceptual
- Para compatibilidad bootloader: SquashFS puede ser más fácil.
- Para rootfs moderno montado por Linux: EROFS parece mejor candidato.
- Para legacy/boot universal: ISO9660.
- Para artículo: comparar los tres.
13. GRUB y sistemas de archivos
13.1 Pregunta
Preguntaste qué FS soporta GRUB y si había alguno similar a EROFS aparte de ISO9660.
13.2 Lista discutida
GRUB soporta muchos FS mediante módulos:
- FAT12/16/32;
- ext2/ext3/ext4;
- XFS;
- JFS;
- Btrfs;
- ReiserFS;
- ISO9660;
- UDF;
- NTFS;
- HFS+;
- ZFS;
- SquashFS/squash4;
- ROMFS;
- soporte emergente/parcial para EROFS según versión.
13.3 Implicación
Si GRUB puede leer el FS donde están kernel/initramfs, puedes usar GRUB clásico.
Pero si usas UKI, puedes evitar GRUB.
14. Kernel, bootloader, UKI y linux-image-generic
14.1 Pregunta
Preguntaste si basta con instalar linux-image-generic.
14.2 Conclusión
No basta por sí solo.
linux-image-generic instala:
-
vmlinuz-*; - módulos;
- initrd generado normalmente por hooks;
- archivos en
/boot.
Pero necesitas algo que lo arranque:
- GRUB;
- systemd-boot;
- UKI;
- U-Boot;
- firmware UEFI directo;
- LinuxBoot/kexec.
14.3 El kernel es pasivo
El kernel no se autoejecuta.
Hace falta:
- firmware;
- bootloader;
- o UKI ejecutable por UEFI.
14.4 Si EROFS está como módulo
Problema huevo-gallina:
- rootfs está en EROFS;
- driver EROFS está en
/lib/modulesdentro del rootfs; - el kernel no puede leer EROFS hasta cargar el módulo;
- no puede cargar el módulo hasta leer EROFS.
Soluciones:
- EROFS built-in en kernel;
- módulo EROFS dentro del initramfs.
15. UKI — Unified Kernel Image
15.1 Qué es
UKI combina en un único binario EFI:
- kernel;
- initramfs;
- cmdline;
- os-release/metadatos;
- opcionalmente firmas/mediciones.
15.2 Ventajas
- evita GRUB;
- un solo archivo
.efi; - se puede firmar;
- más fácil de razonar;
- kernel, initramfs y cmdline están unidos;
- útil para Secure Boot;
- más “appliance-like”.
15.3 Ubicación
En partición EFI FAT32:
/EFI/BOOT/BOOTX64.EFI
o ruta registrada con efibootmgr.
15.4 Partición EFI
No hace falta que sea 512 MB.
Opciones:
- tamaño mínimo ajustado al UKI;
- 64–128 MB si solo un UKI;
- 256 MB si quieres fallback;
- 512 MB como opción conservadora.
15.5 Seguridad
Mejor no montar la ESP en el sistema normal.
Solo montarla temporalmente durante actualización.
15.6 Comando efibootmgr
Ejemplo:
efibootmgr --create \
--disk /dev/sda \
--part 1 \
--label "MiSistemaInmutable" \
--loader /EFI/BOOT/BOOTX64.EFI
Para NVMe:
efibootmgr --create \
--disk /dev/nvme0n1 \
--part 1 \
--label "MiSistemaInmutable" \
--loader /EFI/BOOT/BOOTX64.EFI
16. Initramfs
16.1 Por qué aparece
Si el kernel no tiene todo built-in, necesita initramfs para:
- cargar drivers de disco;
- cargar FS de root;
- montar rootfs real;
- hacer
switch_root.
16.2 Contenido mínimo
Para EROFS root:
- driver de bloque: NVMe, AHCI, USB storage, etc.;
- EROFS;
- compresión usada: LZ4/LZMA/Zstd según caso;
- script/binario de initramfs;
- herramienta o syscall para montar;
-
switch_rooto equivalente.
16.3 Dracut
Se mencionó como herramienta para generar initramfs mínimo.
Ejemplo conceptual:
dracut --force \
--kvm \
--modules "bash rootfs-block" \
--add-drivers "erofs lz4 lz4_rom" \
--filesystems "erofs" \
--strip \
initrd-minimal.img
Pero después se criticó porque incluía shell/bash, no deseado para tu objetivo.
16.4 Initramfs con shell vs sin shell
Querías evitar:
-
/bin/sh; -
mountexterno; - scripts.
Preferías un binario estático que haga syscalls directamente.
17. ¿El initramfs permanece en RAM?
17.1 Pregunta
Te preocupaba que meter cosas en initramfs aumente consumo de RAM permanente.
17.2 Conclusión matizada
Initramfs se carga en RAM.
Tras switch_root, normalmente se puede liberar gran parte, pero no conviene meter cosas innecesarias.
17.3 Decisión
Mantener initramfs mínimo.
Poner el init real, tini y aplicación en EROFS, no en initramfs, salvo que sea estrictamente necesario.
18. Módulos del kernel
18.1 Preguntas tratadas
Preguntaste:
- ¿meter todos los módulos de
linux-image-generic? - ¿solo los usados?
- ¿initramfs o EROFS?
- ¿kernel built-in?
- ¿quién carga módulos sin udev?
- ¿diferencia si conocemos hardware exacto?
- ¿diferencia para máxima compatibilidad?
18.2 Conclusión general
No tiene sentido meter todos los módulos si buscas minimalismo y seguridad.
18.3 Clasificación
Necesarios para montar rootfs
Deben ir:
- built-in en kernel; o
- en initramfs.
Ejemplos:
- EROFS;
- compresión EROFS;
- NVMe/AHCI/USB storage;
- particiones;
- ext4/xfs si
/datase monta muy temprano.
Necesarios tras montar rootfs
Pueden ir en EROFS:
/lib/modules/...
Ejemplos:
- red;
- audio;
- video;
- cámaras;
- GPU;
- periféricos;
- firmware.
No necesarios
No incluir.
18.4 Sin udev
Si no hay udev/systemd, los módulos no se cargan “mágicamente” como en una distro normal.
Opciones:
- built-in;
- cargar manualmente en init propio usando
finit_module; - usar modprobe/kmod, pero eso añade binarios/config;
- usar mdev/udev, pero aumenta complejidad.
18.5 Built-in vs módulos
Built-in (=y)
Ventajas:
- no necesitas cargar nada;
- más simple;
- más seguro;
- no hay
.koque manipular; - arranque más directo.
Desventajas:
- kernel custom;
- mantener parches/updates;
- menos flexible;
- puede crecer el kernel.
Módulos en initramfs
Ventajas:
- puedes usar kernel genérico;
- soportas hardware variable;
- drivers críticos disponibles antes de rootfs.
Desventajas:
- UKI/initramfs crece;
- más RAM inicial;
- más complejidad;
- hay que cargar módulos.
Módulos en EROFS
Ventajas:
- no aumentan UKI;
- no ocupan RAM hasta cargarse;
- puedes conservar flexibilidad.
Desventajas:
- necesitas rootfs montado;
- necesitas mecanismo de carga;
- no sirven para montar el propio rootfs.
18.6 Recomendación por escenario
Hardware exacto
Kernel custom con drivers críticos built-in.
Hardware flexible
Kernel genérico + initramfs host-only.
Máxima compatibilidad
Kernel genérico + más módulos, aceptando más tamaño.
19. Init del sistema final
19.1 Tini
Tini se discutió como opción simple.
Características:
- reaper de zombies;
- forward de señales;
- muy pequeño;
- común en contenedores;
- no monta discos;
- no supervisa múltiples servicios;
- si PID 1 termina y no apaga/reinicia, puede provocar panic en bare metal.
19.2 s6
Se discutió como alternativa.
Características:
- supervisor completo;
- reinicio automático;
- múltiples servicios;
- etapas de init;
- más parecido a docker-compose/system supervisor;
- más complejo;
- más grande.
Tu conclusión:
Prefiero simplicidad; s6 parece más docker-compose.
19.3 Init propio tipo minit
Recordaste NodeOS:
- usabas un script Node.js;
- luego lanzabas un init mínimo parecido a tini;
- no usabas
mount; - usabas paquete npm que hacía syscalls de kernel;
- el sistema cerraba o reiniciaba limpiamente si la app terminaba.
19.4 Solución preferida
Un binario mínimo en C o Rust:
- PID 1;
- monta
/proc,/sys,/dev; - monta
/data; - lanza app/tini;
- espera;
- hace
sync; - llama a
reboot()opoweroff().
19.5 Código C conceptual
#include <sys/mount.h>
#include <sys/wait.h>
#include <sys/reboot.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
mount("proc", "/proc", "proc", 0, NULL);
mount("sysfs", "/sys", "sysfs", 0, NULL);
mount("devtmpfs", "/dev", "devtmpfs", 0, NULL);
if (mount("/dev/sda3", "/data", "ext4", 0, NULL) != 0) {
return 1;
}
pid_t pid = fork();
if (pid == 0) {
char *args[] = {"/usr/bin/tini", "--", "/app/my_binary", NULL};
execv(args[0], args);
_exit(1);
}
if (pid > 0) {
int status;
waitpid(pid, &status, 0);
sync();
reboot(RB_AUTOBOOT);
}
return 1;
}
19.6 Variante sin tini
El init puede lanzar directamente la app.
Pero tini sigue siendo útil si:
- tu app crea hijos;
- quieres reaping estándar;
- quieres señalización limpia.
20. Shell o no shell
20.1 Decisión fuerte
No quieres shell.
Razones:
- seguridad;
- reducir superficie de ataque;
- evitar que un atacante tenga
/bin/sh; - coherencia con sistema appliance;
- coherencia con distroless.
20.2 Consecuencia
Evitar:
- bash;
- dash;
- busybox shell;
- scripts de init;
-
system("mount ...").
Usar:
- C/Rust;
- syscalls directas;
-
mount(2); -
finit_module(2)si hace falta; -
reboot(2).
21. Layout de particiones para sistema bootable
21.1 Layout base con UKI
/dev/sda1 ESP FAT32 UKI: /EFI/BOOT/BOOTX64.EFI
/dev/sda2 EROFS rootfs inmutable
/dev/sda3 EXT4/XFS data persistente
21.2 ESP
- FAT32;
- no montada en runtime normal;
- montada solo para actualizaciones;
- contiene UKI.
21.3 Root
- EROFS o SquashFS;
- read-only real;
- contiene sistema generado desde Docker/Chisel;
- puede incluir app, libs, tini, init, módulos no críticos.
21.4 Data
- EXT4 o XFS;
- equivalente al volumen Docker;
- contiene base de datos, logs, uploads, etc.
21.5 OverlayFS opcional
Se discutió como alternativa:
lowerdir = EROFS
upperdir = data
workdir = data
Ventaja:
- el sistema “parece” escribible.
Desventaja:
- rompe la pureza del modelo;
- más difícil razonar;
- puede ocultar modificaciones;
- para appliance seguro quizá mejor montar rutas explícitas.
22. EXT4 vs XFS para partición de datos
22.1 EXT4
Ventajas:
- muy probado;
- simple;
- fácil de reparar;
- flexible;
- bueno para Raspberry/LuckFox/disco USB;
- permite shrink en ciertos escenarios;
- buena opción por defecto.
22.2 XFS
Ventajas:
- bueno con escrituras concurrentes;
- bueno con archivos grandes;
- bueno para logs/video/datos sostenidos;
- robusto.
Desventajas:
- no se puede reducir fácilmente;
- quizá excesivo para sistemas pequeños.
22.3 Recomendación matizada
Para Django + DB ligera:
- EXT4 probablemente suficiente y prudente.
Para vídeo/logs/escrituras concurrentes sostenidas:
- XFS puede tener sentido.
No vender XFS como “siempre superior”.
23. Actualizaciones
23.1 Root EROFS
Si root es EROFS, no se actualiza “dentro”.
Se reemplaza la imagen completa.
Opciones:
Sin A/B
Sobrescribir partición root:
dd if=new-root.erofs of=/dev/sda2
Problema: si falla, sistema roto.
Con A/B
sda2 rootA
sda3 rootB
sda4 data
Actualizar slot inactivo y cambiar boot target.
Ventaja:
- rollback.
23.2 ESP/UKI
Actualizar UKI implica copiar nuevo .efi.
Proceso seguro:
mount /dev/sda1 /mnt/efi
cp new.efi /mnt/efi/EFI/BOOT/BOOTX64.EFI.tmp
sync
mv /mnt/efi/EFI/BOOT/BOOTX64.EFI.tmp /mnt/efi/EFI/BOOT/BOOTX64.EFI
sync
umount /mnt/efi
Mejor si hay fallback:
BOOTX64.EFI
BOOTX64-previous.EFI
23.3 No actualizar kernel “en caliente”
Tu observación:
Si root es inmutable y hay que reescribir particiones, no tiene sentido pensar en actualización caliente.
Correcto.
Modelo: actualización offline/atómica + reboot.
24. Secure Boot, firmas y verificación
24.1 UKI
UKI permite firmar un único binario:
kernel + initramfs + cmdline
Ventaja:
- más fácil que firmar piezas separadas;
- reduce manipulación de cmdline;
- evita
init=/bin/shsi cmdline está embebida y firmada.
24.2 EROFS/rootfs
Para sistema más serio:
- verificar hash de rootfs;
- dm-verity;
- firma de imagen;
- measured boot;
- attestation.
No lo desarrollamos con comandos, pero quedó como extensión clara.
24.3 Paper potencial
Aquí aparece la línea de paper:
From Docker Image to Verifiable Immutable Appliance
con:
- SBOM;
- attestation;
- Secure Boot;
- dm-verity;
- mediciones;
- benchmark.
25. Coreboot, LinuxBoot y Linux-as-BIOS
25.1 Aclaración conceptual
Tú pensabas que Coreboot era “Linux sustituyendo BIOS/UEFI”.
Matiz:
- Coreboot sustituye firmware/BIOS del fabricante;
- LinuxBoot suele ser un payload Linux ejecutado por Coreboot.
25.2 Capas
Firmware propietario tradicional
↓
UEFI
↓
GRUB/UKI
vs
Coreboot
↓
payload: TianoCore / SeaBIOS / LinuxBoot / Heads
25.3 LinuxBoot
LinuxBoot es usar un kernel Linux como entorno de firmware/boot.
Normalmente:
Coreboot inicializa hardware básico
LinuxBoot payload arranca
LinuxBoot localiza/verifica kernel real
kexec hacia kernel real
25.4 Por qué kexec
Preguntaste:
¿Por qué no usar directamente el kernel de LinuxBoot?
Respuesta:
Se puede, pero kexec permite:
- mantener kernel firmware mínimo;
- actualizar kernel real sin flashear BIOS;
- evitar brick si falla actualización;
- usar kernel real más grande;
- separar firmware de OS;
- tener recuperación.
25.5 Usar LinuxBoot directamente
Tiene sentido si:
- hardware fijo;
- appliance extremo;
- kernel muy pequeño;
- no quieres kernel separado;
- aceptas flashear firmware para actualizar kernel.
25.6 Coreboot + TianoCore
Alternativa intermedia:
Coreboot
↓
TianoCore UEFI
↓
UKI
Mantienes compatibilidad UEFI pero con firmware abierto.
25.7 Conclusión
Para artículo:
- UKI = práctico/portable.
- Coreboot + LinuxBoot = extremo/hardware-specific.
- LinuxBoot directo = appliance muy cerrado.
- kexec = separación limpia firmware/OS.
26. Raspberry Pi, LuckFox, U-Boot y hardware real
26.1 Raspberry Pi
Observaciones:
- no usa UEFI PC estándar por defecto;
- Coreboot no es la vía típica;
- se puede usar UEFI firmware en algunas Pi;
- puede arrancar kernel/initramfs de forma propia;
- buena para PoC;
- limitación de I/O si disco y cámara van por USB.
26.2 LuckFox / Rockchip
Observaciones:
- suelen usar U-Boot;
- más orientados a embedded/cámaras;
- algunos tienen NPU;
- pueden ser interesantes para video analytics;
- U-Boot puede cargar kernel/initramfs/FIT images;
- equivalente conceptual a UKI sería otra cadena: U-Boot + signed FIT.
26.3 Hardware concreto no decidido
La conversación está aún en teoría, para artículo.
Caso mental:
- servidor Django;
- recibe info por red;
- guarda en base de datos;
- dashboard web;
- quizá decodifica/análisis de streams de vídeo;
- baja carga general;
- decenas/cientos registros por segundo.
27. Bootable containers
27.1 Mención
Has visto por ahí “bootable containers”.
Lo conectamos con esta idea.
27.2 Diferencia conceptual
Un contenedor bootable intenta reutilizar artefactos OCI como base de sistemas arrancables.
Tu variante conceptual:
OCI/Docker build
↓
Chisel rootfs
↓
EROFS/SquashFS
↓
UKI / GRUB / U-Boot
↓
Appliance
27.3 Posicionamiento
No es completamente nuevo, pero sí poco explicado de extremo a extremo.
28. Comparativa de arquitecturas de arranque
28.1 Opción A — Docker normal hardened
Docker image
docker run --read-only
volume /home/nonroot
rocheck fail-fast
Ventajas:
- fácil;
- portable;
- compatible con infra actual.
Desventajas:
- depende de runtime;
- no es bare metal;
- rootfs no es FS read-only real;
-
--read-onlyes opt-in externo.
28.2 Opción B — GRUB + kernel/initramfs + EROFS/SquashFS
Ventajas:
- fácil de depurar;
- clásico;
- flexible;
- GRUB soporta muchos FS.
Desventajas:
- más piezas;
- config externa;
- más superficie;
- cmdline editable si no se protege.
28.3 Opción C — UKI + EROFS + data
Ventajas:
- limpio;
- un único
.efi; - firmable;
- sin GRUB;
- ideal appliance.
Desventajas:
- requiere ESP;
- UKI crece con initramfs;
- firmware UEFI requerido.
28.4 Opción D — U-Boot + FIT image
Más adecuada para embedded ARM.
No se desarrolló mucho, pero debe entrar como alternativa para Raspberry/LuckFox/Rockchip.
28.5 Opción E — Coreboot + LinuxBoot + kexec
Ventajas:
- firmware abierto;
- verificación antes del OS;
- recuperación avanzada;
- seguridad extrema.
Desventajas:
- hardware-specific;
- complejo;
- difícil de vender como primer paso;
- riesgo de brick.
28.6 Opción F — Coreboot + LinuxBoot directo
Ventajas:
- extremo minimalismo;
- boot muy rápido;
- kernel en firmware.
Desventajas:
- muy rígido;
- actualizaciones peligrosas;
- límite de flash;
- solo appliances muy cerrados.
29. Paper vs artículo vs PoC
29.1 Evaluación
Preguntaste si esto da para paper o es camino trillado.
Respuesta:
- como idea pura, no es paper fuerte;
- como integración técnica, sí da para artículo muy bueno;
- como paper, necesitaría medición o novedad concreta.
29.2 Para paper necesitarías
Benchmark
Comparar:
Raspbian
Docker normal
Docker --read-only
Chisel container
Chisel + EROFS + UKI
Métricas:
- tamaño;
- boot time;
- RAM;
- I/O;
- superficie de ataque;
- write paths;
- consumo de CPU;
- tiempo de recuperación;
- resistencia a corrupción.
Seguridad
- persistencia tras compromiso;
- paths escribibles;
- ausencia de shell;
- ausencia de package manager;
- manipulación de boot;
- cmdline protegida;
- dm-verity/Secure Boot.
Framework reproducible
Pipeline:
Dockerfile → rootfs → SBOM → EROFS → UKI → signed appliance image
29.3 Recomendación estratégica
Primero artículo.
Después PoC.
Después, si hay datos, paper.
30. Signature piece
30.1 Definición
Un “signature piece” es un artículo que define tu identidad profesional.
No es solo “un post”.
Es algo que hace que alguien diga:
“Esta persona sabe de verdad de esto”.
En tu caso:
“Este tío no solo hace WebRTC; diseña sistemas completos desde hardware, networking, kernel y OS hasta aplicación, optimización e IA”.
30.2 Cómo se cita
No como paper académico con DOI, salvo que lo publiques también en Zenodo/arXiv/etc.
Pero en industria se puede citar como:
Leganés Combarro, J. (2026).
Turning a Docker Image into a Bootable Immutable Linux Appliance.
https://piranna.github.io/...
30.3 Diferenciarlo en tu web
Propuestas:
- sección
Featured Articles; - sección
Signature Work; - tag
signature; - destacar 3–5 piezas en homepage.
30.4 Candidatos existentes
Según lo hablado:
- Types of WebRTC networks;
- AI code generation and the oversight problem;
- npm lifecycle article;
- futuro artículo Chisel/EROFS/UKI;
- potencial artículo de Mafalda SFU / scalable real-time media;
- quizá uno futuro sobre BLE/SAT Companion si se formaliza.
31. Dónde publicar
31.1 WebRTC.ventures
Pros:
- visibilidad;
- SEO;
- marca;
- clientes.
Contras:
- no es claramente WebRTC;
- perderías control;
- tono corporate;
- cola editorial;
- ya tienes otro artículo pendiente;
- podrían suavizarlo.
31.2 Blog personal
Pros:
- control total;
- voz propia;
- ideal como signature piece;
- puedes ser más radical;
- construye tu marca.
Contras:
- menos tráfico inicial.
31.3 Recomendación
Publicar primero en tu blog como versión canonical.
Después, si interesa, derivar una versión corporativa orientada a:
immutable edge systems for real-time media
32. Dominio personal
32.1 Situación
Tu web actual:
https://piranna.github.io
Está ya publicada en muchos sitios.
32.2 Recomendación
Comprar dominio, pero no romper URL.
Usar:
- dominio propio como principal;
- GitHub Pages como backend;
- redirecciones/canonical URLs.
33. Código y comandos aparecidos
33.1 Validar --output con wrapper
if [[ " $* " != *" --output "* ]]; then
echo "ERROR: Debes usar --output"
exit 1
fi
docker build "$@"
33.2 Docker build app
docker buildx build \
--target app \
-t miapp:latest \
--load \
.
33.3 Docker build licenses
docker buildx build \
--target licenses \
--output type=local,dest=./legal \
.
33.4 Bake
docker buildx bake
33.5 Fail-fast C
int fd = open("/.rw-test", O_WRONLY | O_CREAT, 0600);
if (fd >= 0) {
close(fd);
unlink("/.rw-test");
write(2, "ERROR: root filesystem is writable.\n", 36);
_exit(1);
}
if (errno != EROFS) {
perror("ERROR: filesystem check failed");
_exit(1);
}
execv(argv[1], &argv[1]);
33.6 Init mínimo C conceptual
mount("proc", "/proc", "proc", 0, NULL);
mount("sysfs", "/sys", "sysfs", 0, NULL);
mount("devtmpfs", "/dev", "devtmpfs", 0, NULL);
mount("/dev/sda3", "/data", "ext4", 0, NULL);
pid_t pid = fork();
if (pid == 0) {
char *args[] = {"/usr/bin/tini", "--", "/app/my_binary", NULL};
execv(args[0], args);
_exit(1);
}
waitpid(pid, &status, 0);
sync();
reboot(RB_AUTOBOOT);
33.7 efibootmgr
efibootmgr --create \
--disk /dev/sda \
--part 1 \
--label "MiSistemaInmutable" \
--loader /EFI/BOOT/BOOTX64.EFI
33.8 Montar ESP solo durante actualización
mount /dev/sda1 /mnt/efi-temp
cp nuevo_sistema.efi /mnt/efi-temp/EFI/BOOT/BOOTX64.EFI
sync
umount /mnt/efi-temp
34. Posible estructura futura del artículo
34.1 Título posible
What if your container WAS the operating system?
o
Turning a Docker Image into a Bootable Immutable Linux Appliance
34.2 Índice posible
- Containers solved packaging, not booting.
- The missing piece: immutable rootfs.
- Building a minimal Ubuntu rootfs with Chisel.
- Separating application image and legal artifacts.
- Making Docker images read-only: limits of OCI.
- Fail-fast read-only enforcement without shell.
- From OCI rootfs to EROFS/SquashFS.
- Boot options: GRUB, UKI, U-Boot, LinuxBoot.
- Kernel modules: initramfs vs built-in vs EROFS.
- Init without shell: C/Rust PID 1.
- Persistence as a Docker-like volume.
- Updating immutable systems.
- Security model and threat model.
- Use cases: edge, WebRTC, drones, SAT Companion, tourism device.
- What is novel, what is not.
- Future work: benchmarks, SBOM, Secure Boot, dm-verity.
35. Frase de tesis
Una buena tesis del artículo:
Containers gave us reproducible application filesystems. But for edge appliances, drones, satellites, media gateways, and unattended devices, the next step is making that filesystem bootable, immutable, verifiable, and explicitly persistent only where intended.
O en versión más directa:
A Docker image is almost a root filesystem. Chisel, EROFS, and UKI let us close the gap and turn it into a bootable appliance.
36. Conclusión final del resumen
Todo el proceso mental fue:
Detectar --output dentro de Dockerfile
↓
No se puede: usar stages y targets
↓
Separar imagen app y licencias
↓
Usar Chisel para rootfs mínimo
↓
Imagen final scratch, nonroot, nologin, sin shell
↓
Licencias exportadas como Markdown + tgz
↓
Docker no puede imponer read-only desde imagen
↓
Fail-fast sin shell con binario C
↓
Si queremos read-only real: salir de Docker
↓
Convertir rootfs en EROFS/SquashFS
↓
Arrancar con GRUB/UKI/U-Boot/LinuxBoot
↓
Init mínimo en C/Rust
↓
/data como volumen persistente
↓
Modelo appliance robusto para edge/defensa/WebRTC/media
Ahí está el artículo. Y probablemente, con PoC y métricas, también el germen de un paper industrial.