Skip to content
d-bucknerPublic

Repository files navigation

bloud

License: AGPL v3 Status: Alpha

An open-source home server inspired by kubernetes. Self-hosting is kind of unreasonably hard. Not the installing part, but everything after.

In bloud, you install an app and the reverse proxy, unified login, and all inter-app integrations happen for you.

try it

Debian 13, x86_64.

The script is short, and you should read it before piping it to a shell: install.sh. To do it by hand instead, grab the .deb from the releases page and run sudo apt install ./bloud_*.deb.

curl -fsSL https://raw.githubusercontent.com/d-buckner/bloud/main/install.sh | sudo sh

Open the dashboard at http://localhost:8080. Set your host under Settings and that's it.

Please don't expose bloud to the public internet yet. It's alpha, it serves plain HTTP, and there is no mechanism yet for getting security updates to apps or to Bloud itself. Keep it on your LAN for now.

why an engine

Getting a container running isn't the hard part, podman run will do that. The hard part is everything the container needs from the rest of the system: a route in Traefik, an OIDC client or an LDAP binding in Authentik, a database with a password nobody has to copy by hand, and all of it still correct a year later.

Bloud handles that with a reconciliation loop, the same shape as a Kubernetes controller. Each app declares what it provides and what it consumes, the engine resolves those declarations into a graph, works out the wiring, and then keeps checking its work.

Two rules hold that together:

  • Single writer. Only the orchestrator writes lifecycle state or performs side effects. HTTP handlers submit intents and never mutate anything themselves.
  • Idempotent configurators. PreStart and PostStart run on every cycle, not just on install. PreStart brings the config on disk in line with what the app should have, and does nothing when it already matches.

A template can write one config file once. Nothing but a loop keeps every config file in someone's homelab correct through upgrades, crashes, and reboots.

the full graph

Every app, the containers each one declares, and the edges that connect them.

flowchart TD

    subgraph app_authentik["Authentik (system)"]
        c_authentik_postgres["postgres"]
        c_authentik_redis["redis"]
        c_authentik_server["server"]
        c_authentik_worker["worker"]
        c_authentik_ldap["ldap"]
        c_authentik_server --> c_authentik_postgres
        c_authentik_server --> c_authentik_redis
        c_authentik_worker --> c_authentik_postgres
        c_authentik_worker --> c_authentik_redis
        c_authentik_ldap --> c_authentik_server
    end

    subgraph app_traefik["Traefik (system)"]
        c_traefik["traefik"]
    end

    subgraph app_affine["AFFiNE"]
        c_affine_postgres["postgres"]
        c_affine_redis["redis"]
        c_affine["affine"]
        c_affine --> c_affine_postgres
        c_affine --> c_affine_redis
    end

    subgraph app_affine_mcp["AFFiNE MCP"]
        c_affine_mcp["affine-mcp"]
    end

    subgraph app_arr_mcp["Arr MCP"]
        c_arr_mcp["arr-mcp"]
    end

    subgraph app_calino["Calino"]
        c_calino["calino"]
    end

    subgraph app_dav_mcp["DAV MCP"]
        c_dav_mcp["dav-mcp"]
    end

    subgraph app_hermes["Hermes"]
        c_hermes["hermes"]
    end

    subgraph app_hermes_webui["Hermes Web UI"]
        c_hermes_webui["hermes-webui"]
    end

    subgraph app_homeassistant["Home Assistant"]
        c_homeassistant["homeassistant"]
    end

    subgraph app_immich["Immich"]
        c_immich_postgres["postgres"]
        c_immich_redis["redis"]
        c_immich_ml["ml"]
        c_immich_server["server"]
        c_immich_server --> c_immich_postgres
        c_immich_server --> c_immich_redis
    end

    subgraph app_jellyfin["Jellyfin"]
        c_jellyfin["jellyfin"]
    end

    subgraph app_navidrome["Navidrome"]
        c_navidrome["navidrome"]
    end

    subgraph app_paperless_ngx["Paperless-ngx"]
        c_paperless_ngx_postgres["postgres"]
        c_paperless_ngx_redis["redis"]
        c_paperless_ngx_gotenberg["gotenberg"]
        c_paperless_ngx_tika["tika"]
        c_paperless_ngx["paperless-ngx"]
        c_paperless_ngx --> c_paperless_ngx_postgres
        c_paperless_ngx --> c_paperless_ngx_redis
    end

    subgraph app_prowlarr["Prowlarr"]
        c_prowlarr["prowlarr"]
    end

    subgraph app_qbittorrent["qBittorrent"]
        c_qbittorrent["qbittorrent"]
    end

    subgraph app_radarr["Radarr"]
        c_radarr["radarr"]
    end

    subgraph app_radicale["Radicale"]
        c_radicale_pimsync["pimsync"]
        c_radicale["radicale"]
        c_radicale_pimsync --> c_radicale
    end

    subgraph app_seerr["Seerr"]
        c_seerr["seerr"]
    end

    subgraph app_sonarr["Sonarr"]
        c_sonarr["sonarr"]
    end

    subgraph app_vaultwarden["Vaultwarden"]
        c_vaultwarden["vaultwarden"]
    end

    ai_model["AI Model"]

    %% Cross-app integration edges
    app_affine -->|native-oidc| app_authentik
    app_affine -->|caldav| app_radicale
    app_affine -->|inference| ai_model
    app_affine_mcp -->|appApi| app_affine
    app_arr_mcp -->|mediaServer| app_jellyfin
    app_arr_mcp -->|pvr| app_radarr
    app_arr_mcp -->|requestManager| app_seerr
    app_arr_mcp -->|pvr| app_sonarr
    app_calino -->|forward-auth| app_authentik
    app_calino -->|caldav| app_radicale
    app_dav_mcp -->|appApi| app_radicale
    app_dav_mcp -->|caldav| app_radicale
    app_hermes -->|mcp| app_affine_mcp
    app_hermes -->|mcp| app_arr_mcp
    app_hermes -->|native-oidc| app_authentik
    app_hermes -->|mcp| app_dav_mcp
    app_hermes -->|inference| ai_model
    app_hermes_webui -->|agentApi| app_hermes
    app_homeassistant -->|native-oidc| app_authentik
    app_immich -->|native-oidc| app_authentik
    app_jellyfin -->|ldap| app_authentik
    app_navidrome -->|forward-auth| app_authentik
    app_paperless_ngx -->|native-oidc| app_authentik
    app_prowlarr -->|forward-auth| app_authentik
    app_prowlarr -->|pvr| app_radarr
    app_prowlarr -->|pvr| app_sonarr
    app_qbittorrent -->|forward-auth| app_authentik
    app_radarr -->|forward-auth| app_authentik
    app_radarr -->|downloadClient| app_qbittorrent
    app_radicale -->|ldap| app_authentik
    app_radicale -->|icsFeed| app_radarr
    app_radicale -->|icsFeed| app_sonarr
    app_seerr -->|mediaServer| app_jellyfin
    app_seerr -->|pvr| app_radarr
    app_seerr -->|pvr| app_sonarr
    app_sonarr -->|forward-auth| app_authentik
    app_sonarr -->|downloadClient| app_qbittorrent
    app_traefik -->|proxy| app_affine
    app_traefik -->|proxy| app_affine_mcp
    app_traefik -->|proxy| app_arr_mcp
    app_traefik -->|proxy| app_authentik
    app_traefik -->|proxy| app_calino
    app_traefik -->|proxy| app_dav_mcp
    app_traefik -->|proxy| app_hermes
    app_traefik -->|proxy| app_hermes_webui
    app_traefik -->|proxy| app_homeassistant
    app_traefik -->|proxy| app_immich
    app_traefik -->|proxy| app_jellyfin
    app_traefik -->|proxy| app_navidrome
    app_traefik -->|proxy| app_paperless_ngx
    app_traefik -->|proxy| app_prowlarr
    app_traefik -->|proxy| app_qbittorrent
    app_traefik -->|proxy| app_radarr
    app_traefik -->|proxy| app_radicale
    app_traefik -->|proxy| app_seerr
    app_traefik -->|proxy| app_sonarr
    app_traefik -->|proxy| app_vaultwarden
    app_vaultwarden -->|native-oidc| app_authentik
Loading

Each box is one app; the nodes inside it are that app's containers, with an arrow from a container to every container it depends on. Arrows between boxes are integrations: a proxy arrow is drawn from the proxy to the apps it routes, and an SSO arrow is labeled with the app's strategy (ldap, forward-auth, native-oidc). The AI Model node is outside every box because no app provides it: it is the instance's own Settings -> AI endpoint, and any app that declares the inference contract is wired to it.

catalog

The catalog is small on purpose. A half-supported app is worse than no app at all, because it looks like an answer right up until the first time you depend on it. Every entry here carries the same contract and we verify each one of them: install, shared login, persistence, reboot, removal.

  • AFFiNE: AI-native knowledge base that unifies docs, databases, and whiteboards
  • AFFiNE MCP: Full read-write MCP tool server for AFFiNE, for agents that author documents, databases, and canvases
  • Arr MCP: MCP tool server over the request and arr stack, so an agent can answer what was requested and what is stuck
  • Calino: Browser calendar for the CalDAV calendars Bloud already serves
  • DAV MCP: MCP tool server for the calendars and contacts Bloud already serves, so agents read and write events, to-dos, and address books
  • Hermes: Self-improving AI agent with persistent memory, scheduled automations, and a web dashboard
  • Hermes Web UI: Browser and mobile front end for the Hermes agent, with SSO sign-in and a dedicated client password
  • Home Assistant: Open-source home automation platform
  • Immich: Self-hosted photo and video management
  • Jellyfin: Free software media system for streaming movies, TV, and music
  • Navidrome: Modern music server and streamer compatible with Subsonic/Airsonic clients
  • Paperless-ngx: Document management system that turns scans and PDFs into a searchable archive
  • Prowlarr: Indexer manager that syncs indexers to Sonarr, Radarr, and other PVRs
  • qBittorrent: BitTorrent client with a web interface
  • Radarr: Movie collection manager for Usenet and BitTorrent users
  • Radicale: CalDAV and CardDAV server for calendars, contacts, and to-do lists
  • Seerr: Request and discovery manager for your media server
  • Sonarr: PVR for TV series that monitors, grabs, and organises episodes
  • Vaultwarden: Lightweight, Bitwarden-compatible password manager

Plus the system apps: Authentik (security) and Traefik (network).

The media stack is where this shows up most. Sonarr, Radarr, Prowlarr, qBittorrent, and Seerr are five separate projects that only become a pipeline once they're wired to each other, and that wiring is the tedious part by hand: add qBittorrent as a download client in each PVR with its own category and folder, get the indexers from Prowlarr into both, then connect Seerr to Jellyfin and to the PVRs so a request actually lands somewhere. Bloud does all of it from the declarations, so five apps is five clicks rather than an afternoon of copy-paste.

We ship no media, no indexers, and no trackers, and Bloud has no view on what you point it at. It's built for things you have the right to use.

one login

Strategy Apps
LDAP Jellyfin, Radicale, Seerr
Forward auth Calino, Navidrome, Prowlarr, qBittorrent, Radarr, Sonarr
Native OIDC AFFiNE, Hermes, Hermes Web UI, Home Assistant, Immich, Paperless-ngx, Vaultwarden
App-local accounts AFFiNE MCP, Arr MCP, DAV MCP

Clients that speak a native protocol, a Subsonic player or a TV app talking to Jellyfin, keep the login path their protocol defines. Bloud fronts the web UIs and stays out of the way of those.

what is not done yet

Alpha in specific ways, so you know which gaps you're signing up for.

  • No TLS. Plain HTTP only, and this is the biggest gap. Let's Encrypt on Traefik, or Tailscale Serve, is the planned follow-up. Fine on your LAN if you accept it; not acceptable off-LAN.
  • No bloud init. First-run host config happens in the dashboard.
  • Debian 13 only. A support contract has to be true somewhere before it spreads.
  • The loop is not yet hardened against every failure mode. The auth bypass is remotely forgeable and is the current shipping blocker. Full ledger: docs/operations/tech-debt.md.

developing it

npm run setup            # pick backend, check prereqs, build ./bloud
./bloud dev              # build + deploy + run (Ctrl-C to stop)
./bloud install jellyfin # through the real API
./bloud validate --tier fast

Backends: Lima on macOS (automatic), QEMU on Linux (default), native on Linux CI (BLOUD_BACKEND=native). On the native backend ./bloud dev hot-reloads: save a Go file and the host-agent rebuilds and restarts in about 3s with the app containers left running, and the dashboard hot-reloads through vite. Use --no-watch for the one-shot build-deploy-run. Add --reset to wipe the runtime first (the same wipe as ./bloud reset -y, no prompt) and come up from empty data. Apps land at http://<app>.localhost:8080.

The integration tier runs the real graph path rather than a shortcut: host-agent deployed as a systemd user service, Jellyfin installed through POST /api/apps/jellyfin/install, the orchestrator converged, behavioral tests run inside the VM. Tests assert what the app's own API reports, not what our config values happen to be.

./bloud validate --tier integration   # real install/reconcile flow
./bloud e2e lifecycle                 # install -> restart -> uninstall -> cleanup

ai disclosure

While the high level technical design and architecture are done by me personally, much of the low level implementation is done by LLM. For me, this is done with local models hosted on my own hardware (qwen3.8-flash-next at the time of writing). If this does not align with the values you want your software to have, I understand and this project may not be for you.

further reading

license

AGPL v3. See LICENSE.

Releases

Contributors

Languages