⏱️ Lectura: 15 min

Un gadget ARM de 10 dólares con solo 512 MB de RAM muestra en la práctica los límites y las posibilidades del self-hosting con poca ram. El desarrollador suizo Bosscher instaló Debian, systemd y Podman sobre un OpenStick y terminó corriendo seis servicios reales con los 450 MB que le quedan libres tras el sistema.

📑 En este artículo
  1. TL;DR
  2. ¿Qué es el self-hosting con poca RAM?
  3. Por qué importa correr servicios en hardware tan limitado
  4. Cómo funciona el stack: Debian, systemd y Podman
  5. Ejemplos prácticos y cómo empezar
  6. Casos de uso reales que entran en 450 MB
  7. Errores comunes y buenas prácticas
  8. Comparativa con alternativas de monitoreo liviano
  9. Profundizando: cgroups, overcommit y por qué cuentan los contenedores
  10. Preguntas frecuentes
    1. ¿Podman reemplaza completamente a Docker en un home server con poca RAM?
    2. ¿Cuánta RAM necesita Debian minimal antes de correr cualquier servicio?
    3. ¿Por qué usar Beszel en vez de Prometheus y Grafana en un dispositivo de 512 MB?
    4. ¿Qué pasa si un contenedor de Podman supera el límite de memoria que le asigné?
    5. ¿Sirve este enfoque de self-hosting con poca ram en una Raspberry Pi Zero?
    6. ¿Hace falta usar swap para que este stack sea estable?
  11. Referencias
    1. 📚 Artículos relacionados

Ese presupuesto tan ajustado sirve como caso de estudio para cualquier placa ARM barata: una Raspberry Pi Zero, un router reflasheado o la VM más económica de un proveedor cloud. La pregunta no es si corre Linux, sino qué combinación de servicios entra sin que el kernel empiece a matar procesos por falta de memoria.

TL;DR

  • Con 450 MB de RAM usable, un ARM Cortex-A53 corre 4 a 6 contenedores Podman livianos a la vez sin swap.
  • El fork Next-Gen de AdGuard Home bloquea DNS con unos 40 MB fijos, sin importar el tamaño de la lista de bloqueo.
  • Beszel vigila CPU y memoria con 20 MB totales: 10 MB de agente y 10 MB de servidor.
  • Rootless Podman suma overhead por contenedor vía slirp4netns, así que la cantidad de servicios pesa más que el tamaño de cada imagen.
  • systemd-oomd y un swap en zram evitan que un pico de memoria tire todo el stack con hardware tan ajustado.

¿Qué es el self-hosting con poca RAM?

El self-hosting con poca ram es la práctica de correr tus propios servicios (DNS, monitoreo, RSS, túneles) en hardware con muy poca memoria disponible, normalmente menos de 512 MB, usando un sistema operativo liviano y contenedores livianos en lugar de una pila completa tipo Kubernetes.

Comparado con un homelab convencional, el límite real es el presupuesto de memoria. Cada servicio compite por los mismos megabytes, y no hay margen para procesos que reservan memoria de más solo «por si acaso». El OpenStick, con sus cuatro núcleos ARM Cortex-A53 a 1 GHz y 4 GB de almacenamiento eMMC, es un ejemplo extremo, pero el mismo razonamiento aplica a cualquier Raspberry Pi Zero 2 W, a un router con OpenWrt o a la VM más barata de un proveedor cloud.

En ese contexto, Debian aporta un sistema base estable y con paquetes probados, systemd se encarga de arrancar y vigilar cada servicio, y Podman reemplaza a Docker como motor de contenedores sin un daemon corriendo todo el tiempo de fondo.

Por qué importa correr servicios en hardware tan limitado

La mayoría de los tutoriales de self-hosting asumen una máquina con 4 u 8 GB de RAM, que sobra para correr media docena de contenedores sin pensar en el costo real de cada uno. Esa comodidad esconde un problema. Cuando migrás a algo más barato (una VM económica, un dispositivo embebido o un servidor reciclado) los mismos contenedores que «funcionaban bien» empiezan a reiniciarse solos.

Entender qué consume memoria y por qué (el motor de contenedores, el proxy de red de cada servicio, los buffers de journald) permite portar un stack de home server a hardware mucho más barato, o meter más servicios en la misma máquina sin subir de plan. El caso del OpenStick lo deja ver con números concretos: con apenas 450 MB usables corren seis servicios completos.

Cómo funciona el stack: Debian, systemd y Podman

Debian minimal, la instalación «netinst» sin entorno gráfico, ocupa entre 80 y 120 MB de RAM en reposo según qué demonios corran además del kernel. Ese consumo base es el primer costo fijo del self-hosting con poca ram: antes de levantar un solo contenedor, el sistema ya reservó una porción nada despreciable del total disponible.

systemd gestiona el arranque y el ciclo de vida de cada servicio con unidades .service. Desde la versión 243 incluye systemd-oomd, un vigilante de memoria que actúa según la presión de cgroups v2, antes de que el OOM killer del kernel mate procesos a ciegas.

Podman, a diferencia de Docker, no necesita un daemon corriendo en segundo plano: cada contenedor arranca como proceso hijo de conmon, supervisado directamente por systemd. En modo rootless, el recomendado en cualquier dispositivo expuesto a internet, cada contenedor con su propia red usa slirp4netns para NAT de usuario. Eso suma entre 5 y 10 MB de RAM por contenedor solo en overhead de red.

Podman corre sin daemon persistente: cada contenedor es un proceso hijo de conmon. Foto de ThisisEngineering en Unsplash
flowchart TD
    A["Debian minimal"] --> B["systemd"]
    B --> C["Podman rootless"]
    C --> D["conmon + contenedor 1"]
    C --> E["conmon + contenedor 2"]
    C --> F["conmon + contenedor N"]
    B --> G["systemd-oomd"]
    G -. "mata bajo presion" .-> D
    G -. "mata bajo presion" .-> E

Ejemplos prácticos y cómo empezar

Antes de instalar nada necesitás una Debian 12 (bookworm) o más nueva, acceso root o sudo, y al menos 2 GB libres de almacenamiento para imágenes de contenedor. Estos comandos están probados para Linux (Debian o Raspberry Pi OS). En macOS o Windows la ruta equivalente es instalar Podman Desktop, que corre una VM Linux liviana de fondo.

sudo apt update
sudo apt install -y podman
podman --version

Ese último comando confirma la instalación. La salida esperada es similar a:

podman version 4.9.3

El primer contenedor sirve solo para confirmar que todo el stack (kernel, cgroups, conmon) funciona:

podman run --rm quay.io/podman/hello

La salida esperada es el arte ASCII oficial del proyecto:

!... Hello Podman World ...!

Project:   https://github.com/containers/podman
Website:   https://podman.io

Con eso confirmado, el siguiente paso es un servicio real con un límite de memoria explícito. AdGuard Home es un buen primer caso porque su consumo es predecible: ronda los 40 MB con una lista de bloqueo pequeña, según documenta el caso del OpenStick.

podman run -d --name adguardhome \
  --restart=always \
  --memory=60m \
  -p 53:53/tcp -p 53:53/udp \
  -p 3000:3000/tcp \
  docker.io/adguard/adguardhome:latest

El flag --memory=60m deja un margen de 20 MB sobre el consumo esperado, porque el proceso de instalación inicial carga listas de bloqueo en memoria antes de aplicarlas. Para confirmar que el contenedor respeta ese límite:

podman stats --no-stream adguardhome

La columna MEM USAGE / LIMIT debería mostrar algo cercano a 38MiB / 60MiB una vez que el servicio terminó de cargar.

Para que el contenedor sobreviva a un reinicio del dispositivo sin depender de --restart=always, la forma recomendada es generar una unidad systemd con un archivo Quadlet:

mkdir -p ~/.config/containers/systemd
cat > ~/.config/containers/systemd/adguardhome.container <<'EOF'
[Container]
Image=docker.io/adguard/adguardhome:latest
PublishPort=53:53/tcp
PublishPort=53:53/udp
PublishPort=3000:3000/tcp
Memory=60m

[Install]
WantedBy=default.target
EOF
systemctl --user daemon-reload
systemctl --user start adguardhome.service

Para verificar que systemd, y no un reinicio manual, controla el contenedor:

systemctl --user is-active adguardhome.service

La salida esperada es active. Si devuelve failed, journalctl --user -u adguardhome.service muestra el motivo, casi siempre un puerto ya ocupado o memoria insuficiente.

Rootless Podman no puede escuchar en puertos menores a 1024, como el 53 de DNS, sin un ajuste extra, porque esos puertos siguen reservados para procesos con privilegios por diseño del kernel. La solución no es correr el contenedor como root: es bajar el rango de puertos sin privilegios con sysctl, que delega esos puertos específicos a usuarios normales sin abrir el resto.

echo 'net.ipv4.ip_unprivileged_port_start=53' | sudo tee /etc/sysctl.d/99-rootless-dns.conf
sudo sysctl --system
sysctl net.ipv4.ip_unprivileged_port_start

La salida esperada de ese último comando es net.ipv4.ip_unprivileged_port_start = 53.

Casos de uso reales que entran en 450 MB

El caso del OpenStick sirve como presupuesto de referencia. Con 450 MB usables tras el sistema, estos son los servicios que documenta el artículo original y su costo de memoria:

  • AdGuard Home: bloqueo de DNS a nivel de red, alrededor de 40 MB con una lista de bloqueo chica (200k entradas).
  • Beszel: monitoreo de CPU, RAM y disco con gráficos, 20 MB en total.
  • Gatus: chequeo de salud de servicios HTTP con alertas, unos 10 MB.
  • CastSponsorSkip: salta auspicios en dispositivos Chromecast, entre 5 y 10 MB.
  • Cloudflare Tunnel: expone servicios a internet sin abrir puertos, entre 8 y 20 MB según el tráfico.
  • Fusion RSS: lector de feeds propio, 22 MB.

Sumando los seis servicios, el consumo ronda los 125 a 170 MB, muy por debajo de los 450 MB disponibles. El margen restante no es casualidad: queda deliberadamente libre para absorber picos, como una lista de bloqueo que se actualiza o un contenedor que reinicia, sin que systemd-oomd empiece a matar procesos.

Lo que no entra en ese presupuesto es igual de importante. Nextcloud, con su stack de PHP y base de datos MySQL, ya pide entre 300 y 500 MB solo para arrancar sin usuarios activos, lo que deja el dispositivo sin margen para nada más. Immich, pensado para fototecas con reconocimiento facial, necesita GPU o varios gigabytes de RAM para su modelo de machine learning, muy lejos de lo que ofrece un Cortex-A53. La regla práctica es simple: si un servicio necesita una base de datos relacional completa o un modelo propio de machine learning, probablemente no entra en 450 MB.

Seis servicios livianos caben en menos de 170 MB de los 450 MB usables del dispositivo. Foto de Trnava University en Unsplash

Errores comunes y buenas prácticas

El error más frecuente es correr Docker en lugar de Podman porque ya se conoce su sintaxis. El daemon de Docker, solo y sin contenedores, ya consume entre 30 y 60 MB en reposo. En una máquina de 450 MB usables, ese costo fijo equivale a perder uno de los seis servicios de la lista anterior antes de instalar nada.

Otro error común es no poner límites explícitos de memoria por contenedor. Sin --memory, un contenedor con una fuga o un pico de tráfico puede consumir toda la RAM disponible y arrastrar al resto del sistema con él. Fijar un límite por servicio, aunque sea generoso, convierte un fallo aislado en un problema contenido.

El eMMC de 4 GB también es un recurso escaso que se olvida fácil. Journald por defecto guarda logs persistentes en disco sin límite de tamaño. En un dispositivo con flash tan chico conviene limitarlo a memoria volátil:

sudo mkdir -p /etc/systemd/journald.conf.d
sudo tee /etc/systemd/journald.conf.d/volatile.conf <<'EOF'
[Journal]
Storage=volatile
RuntimeMaxUse=20M
EOF
sudo systemctl restart systemd-journald

Por último, acumular imágenes de contenedor sin limpiar también agota el eMMC rápido. El comando podman image prune -a después de cada actualización evita quedarse sin espacio para logs o bases de datos SQLite.

⚠️ Ojo: el eMMC barato de estos dispositivos tiene un número limitado de ciclos de escritura. Mover journald y cualquier base de datos liviana a memoria volátil alarga la vida del almacenamiento, a costa de perder esos datos en cada reinicio.

Comparativa con alternativas de monitoreo liviano

Para observabilidad en un dispositivo con poca memoria, no todas las opciones cuestan lo mismo. Esta tabla compara las tres rutas más comunes:

Opción Cuándo usarla Ventaja Limitación
Beszel Querés gráficos de CPU, RAM y disco con alertas, sin complicarte 20 MB totales, interfaz lista en minutos Menos protocolos de alerta que una stack completa
Prometheus + Grafana Ya tenés varios servidores y necesitás históricos largos Estándar de facto, miles de dashboards prearmados Solo Prometheus ya pide 100 a 200 MB en reposo
Dozzle Solo querés ver logs de contenedores Podman en vivo Interfaz más cómoda que la CLI para logs No da métricas de CPU ni RAM como Beszel

En un dispositivo de 450 MB usables, Prometheus y Grafana juntos ya consumen entre un cuarto y un tercio del presupuesto total antes de monitorear nada. Beszel o Dozzle, en cambio, dejan margen para los servicios que en realidad importan.

Profundizando: cgroups, overcommit y por qué cuentan los contenedores

Linux no reserva memoria física para un proceso hasta que efectivamente la escribe. Eso se llama overcommit y explica por qué un contenedor puede arrancar con --memory=200m en una máquina con solo 450 MB totales, pero también por qué dos contenedores «livianos» pueden tumbar el sistema si ambos tocan su límite al mismo tiempo.

Por defecto, vm.overcommit_memory vale 0: el kernel permite reservas de memoria mayores a la física disponible mientras parezcan razonables, y solo falla si la demanda real excede el límite. Para confirmar el modo activo:

cat /proc/sys/vm/overcommit_memory

La salida esperada en una instalación default de Debian es 0. Cambiarlo a 2 (contabilidad estricta) en un dispositivo de 512 MB suele ser contraproducente: con tan poca memoria libre, hasta los procesos del propio sistema fallan al reservar el margen que piden por precaución.

cgroups v2, el mecanismo que usa Podman para aplicar límites de memoria, expone el estado real de cada contenedor en /sys/fs/cgroup. Para ver cuánta memoria usa un contenedor específico sin pasar por podman stats:

cat /sys/fs/cgroup/user.slice/user-1000.slice/[email protected]/app.slice/*adguardhome*/memory.current

Ese número, en bytes, es la métrica que systemd-oomd vigila para decidir a quién matar primero cuando sube la presión de memoria. La política por defecto prioriza al proceso que más excede su propio límite relativo, no al que más memoria absoluta usa.

La razón real por la que importa más la cantidad de contenedores que el tamaño de cada imagen es el overhead fijo por contenedor. Cada uno suma un proceso conmon de 2 a 4 MB, y en modo rootless con red propia, una instancia de slirp4netns de 5 a 10 MB adicionales. Diez contenedores de 5 MB cada uno terminan pesando casi el doble de esos 50 MB nominales solo por overhead de infraestructura.

sequenceDiagram
    participant U as Usuario
    participant S as systemd
    participant P as Podman
    participant C as Contenedor
    U->>S: systemctl start adguardhome.service
    S->>P: arranca conmon y contenedor
    P->>C: aplica memory.max via cgroups
    Note over S,C: si memory.current supera el limite
    S->>C: systemd-oomd envia SIGKILL
stateDiagram-v2
    [*] --> Normal
    Normal --> Presion: memoria disponible baja
    Presion --> Normal: libera memoria
    Presion --> Matando: supera umbral de oomd
    Matando --> Normal: contenedor terminado

💡 Tip: agregar swap en zram, memoria comprimida dentro de la propia RAM, le da al kernel un colchón antes de que systemd-oomd tenga que matar nada, sin desgastar el almacenamiento flash.

📖 Resumen en Telegram: Ver resumen

Tu próximo paso: instalá Podman en cualquier Raspberry Pi o VM de 512 MB que tengas a mano y corré podman run --rm quay.io/podman/hello para confirmar que cgroups y el modo rootless funcionan antes de sumar tu primer servicio real.

📬 Recibí lo nuevo en tu email

Te avisamos de artículos grandes (1-2 por mes).

Preguntas frecuentes

¿Podman reemplaza completamente a Docker en un home server con poca RAM?

Sí, para la mayoría de los casos de self-hosting casero. Podman usa la misma sintaxis de CLI y los mismos formatos de imagen OCI que Docker, pero sin un daemon persistente, lo que ahorra entre 30 y 60 MB fijos.

¿Cuánta RAM necesita Debian minimal antes de correr cualquier servicio?

Entre 80 y 120 MB en reposo con systemd y los demonios básicos activos, según qué paquetes adicionales incluya la imagen base que uses.

¿Por qué usar Beszel en vez de Prometheus y Grafana en un dispositivo de 512 MB?

Porque Prometheus y Grafana juntos ya consumen entre 100 y 200 MB solo para arrancar, mientras que Beszel cubre gráficos de CPU, RAM y disco con 20 MB totales.

¿Qué pasa si un contenedor de Podman supera el límite de memoria que le asigné?

El kernel, vía cgroups, lo mata con un OOM kill dentro de su propio cgroup antes de que afecte al resto del sistema, siempre que hayas fijado --memory al crearlo.

¿Sirve este enfoque de self-hosting con poca ram en una Raspberry Pi Zero?

Sí, el razonamiento es el mismo. Debian o Raspberry Pi OS Lite como base, systemd para supervisar servicios y Podman rootless para contenedores, ajustando los límites de memoria al total real disponible.

¿Hace falta usar swap para que este stack sea estable?

No es obligatorio, pero zram, swap comprimido dentro de la RAM, da un margen extra sin desgastar el almacenamiento eMMC, algo valioso en dispositivos con flash de baja durabilidad.

Referencias

📱 ¿Te gusta este contenido? Únete a nuestro canal de Telegram @programacion donde publicamos a diario lo más relevante de tecnología, IA y desarrollo. Resúmenes rápidos, contenido fresco todos los días.

Imagen destacada: Foto de BoliviaInteligente en Unsplash

¿Te sirvió? ¿Te dio otro error? Contalo abajo: las preguntas se responden y le sirven al siguiente que llegue.

Dejar un comentario

Andrés Morales

Desarrollador e investigador en inteligencia artificial. Escribe sobre modelos de lenguaje, frameworks, herramientas para devs y lanzamientos open source. Cubre papers de ML, ecosistema de startups tech y tendencias de programación.

0 Comentarios

Deja un comentario

Marcador de posición del avatar

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Podés incluir código entre <code>…</code> o, para varias líneas, <pre><code>…</code></pre>.

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.