Arquitectura del homeserver

Filosofía

Infraestructura privada, modular y de bajo mantenimiento. Sin telemetría, sin dependencia de servicios cloud comerciales, con cero puertos expuestos al público salvo la landing principal.

Cada stack corre como un pod de Podman independiente, gestionado como servicio de systemd a nivel usuario. Esto mantiene las responsabilidades aisladas: reiniciar o actualizar un pod no afecta a los demás.

Arquitectura

Diagrama de las dos rutas de entrada al homeserver: Tailscale Funnel hacia la landing pública, y Tailscale WireGuard hacia el resto del homeserver y sus pods

graph TD

    subgraph Internet_Publico ["Internet Público"]
        Visitor["Visitante / Reclutador"]
    end

    subgraph Tailnet_Privada ["Tailnet Privada - Tailscale"]
        Owner["Dispositivos Autenticados
(Único acceso a todo lo demás)"] end subgraph Host_Server ["Host: Debian 13 Trixie
(único puerto abierto en LAN: 22/SSH)"] TSFunnel["Tailscale Funnel
(expone SOLO la landing a Internet)"] TSServe["Tailscale + MagicDNS
Proxy HTTPS único
(misma URL, cambia puertos internamente
hacia cada servicio)"] Landing["Landing Page Portfolio
HTML / CSS / JS estático
servido directo por Tailscale Serve
(sin servidor web propio)"] Cockpit["Cockpit
Corre en el Host, fuera de Podman
Solo accesible vía Tailnet HTTPS"] Cron["Cron Jobs
Backups programados"] subgraph Podman_Engine ["Podman (rootless) - Pods"] PodmanRoot["Motor Podman"] subgraph Pod_Core ["Pod: Core"] Vaultwarden["Vaultwarden"] Radicale["Radicale"] end subgraph Pod_Network ["Pod: Network"] Pihole["Pi-hole
(filtrado DNS)"] Unbound["Unbound
DoT → Mullvad"] end subgraph Pod_Storage ["Pod: Storage"] Syncthing["Syncthing"] Filebrowser["Filebrowser"] end subgraph Pod_Utils ["Pod: Utils"] Homepage["Homepage"] Ntfy["Ntfy"] Kuma["Uptime Kuma"] end subgraph Pod_Immich ["Pod: Immich"] ImmichSrv["Immich Server"] ImmichML["Immich Machine Learning"] end subgraph Pod_Entertainment ["Pod: Entertainment"] Miniflux["Miniflux"] Suwayomi["Suwayomi"] Kavita["Kavita"] Flaresolverr["Flaresolverr"] end end end subgraph Almacenamiento ["Hardware: Almacenamiento"] SSD_OS[("SSD 120GB
Sistema Operativo")] SSD_Data[("SSD 480GB
Datos: volúmenes Podman
(todos los contenedores)")] HDD_Daily[("HDD 2.5 pulg 480GB
Backup Diario")] HDD_Weekly[("HDD 2.5 pulg 480GB
Backup Semanal")] end subgraph Salida_DNS ["Salida DNS Externa"] Mullvad[("Mullvad DNS
vía DoT")] end %% Conexiones de acceso Visitor --> TSFunnel TSFunnel --> Landing Owner --> TSServe TSServe --> Cockpit TSServe --> PodmanRoot PodmanRoot --> Pod_Core PodmanRoot --> Pod_Network PodmanRoot --> Pod_Storage PodmanRoot --> Pod_Utils PodmanRoot --> Pod_Immich PodmanRoot --> Pod_Entertainment %% DNS Pihole --> Unbound Unbound -.->|DoT| Mullvad %% Notificaciones Kuma -.->|notifica| Ntfy %% Storage Landing -.-> SSD_OS Cockpit -.-> SSD_OS PodmanRoot ==> SSD_Data Cron --> HDD_Daily Cron --> HDD_Weekly SSD_Data -.->|backup| HDD_Daily SSD_Data -.->|backup| HDD_Weekly classDef public fill:#f9d5e5,stroke:#333,stroke-width:2px; classDef private fill:#d5e8d4,stroke:#333,stroke-width:2px; classDef host fill:#fff2cc,stroke:#333,stroke-width:2px; classDef storage fill:#e1d5e7,stroke:#333,stroke-width:2px; classDef dns fill:#cfe2f3,stroke:#333,stroke-width:2px; classDef standalone fill:#ffe6cc,stroke:#333,stroke-width:2px; class Visitor public; class Owner private; class Host_Server host; class Podman_Engine host; class Landing,Cockpit,TSFunnel,TSServe standalone; class PodmanRoot host; class SSD_OS,SSD_Data,HDD_Daily,HDD_Weekly storage; class Mullvad dns;

Notas de arquitectura

Stack técnico

Servicios

Core — Vaultwarden (contraseñas), Radicale (calendarios y contactos)

Gateway — Pi-hole (bloqueo de publicidad), Unbound (DNS recursivo con DoT)

Immich — Backup y galería de fotos y videos con búsqueda por ML

Entertainment — Miniflux (RSS), Suwayomi (lector de manga), Kavita (biblioteca digital: libros, manga, cómics)

Storage — Syncthing (sincronización), Filebrowser (gestión de archivos)

Utils — Homepage (dashboard), Uptime Kuma (monitoreo), Ntfy (notificaciones push)

Decisiones técnicas

Trade-offs de arquitectura

Vaultwarden e Immich — self-hosted vs. cloud (Bitwarden, Google Photos)

En ambos casos el criterio fue el mismo: privacidad y control sobre datos sensibles (contraseñas y fotos personales). El costo es mantenimiento propio — backups, actualizaciones, disponibilidad — que en un servicio cloud vendría resuelto de fábrica. Para este tipo de datos, priorizo ese control incluso a costa del trabajo extra.

Un problema resuelto

Cockpit detrás de Tailscale Serve: doble TLS y CSRF

Cockpit ya sirve su propia interfaz por HTTPS con certificado autofirmado. Al exponerlo detrás de tailscale serve, que también termina TLS, el resultado era una segunda capa de cifrado envolviendo a la primera — el navegador recibía una respuesta cifrada dos veces y fallaba al renderizar la interfaz.

Sumado a eso, Cockpit valida el header Origin de cada request como protección CSRF. Al llegar las requests desde el dominio de Tailscale (*.ts.net) en vez del hostname local que Cockpit esperaba, las rechazaba silenciosamente.

La solución fue configurar Cockpit para escuchar en HTTP plano localmente (dejando que Tailscale sea la única capa de TLS real) y agregar el dominio de Tailscale a los orígenes permitidos en la configuración de Cockpit, para que dejara de tratar esas requests como intentos de CSRF.

Referencias

Documentación y fuentes técnicas usadas para diseñar esta infraestructura:

Código abierto

La configuración completa (compose files, scripts de deploy) está disponible en el repositorio:

github.com/ncorrea-13/homeserver


Última actualización: julio 2026 · MIT License