> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hill90.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Overview

> A personal cloud platform on one VPS — open-source counterparts to the Azure primitives, and what is actually running.

# The Hill90 Homelab

Hill90 is homelab infrastructure on a single [Hostinger](https://www.hostinger.com/) VPS
running AlmaLinux 10. It is the automation that takes a bare VPS to a running,
TLS-terminated, observable, Tailscale-secured Docker host — and keeps it there.

**It is not a single-app host.** An AI agent application previously ran here. It was
shelved in June 2026 and removed from the repository in July 2026. See
[Decisions](/homelab/decisions) for the record, and the
[AI Application](/ai-app/overview) section for what it was.

The infrastructure lives in the
[jonhill90/Hill90](https://github.com/jonhill90/Hill90) repository. This site is the
narrative layer; the operational depth stays in that repo, beside the code it describes.

## A personal cloud platform

The organising idea is capability parity with Azure. Jon works with Azure
professionally, and each platform service here is the open-source counterpart of a
primitive he uses at work — chosen so the concepts, failure modes and operational habits
transfer in both directions.

| Azure primitive     | Hill90                                  | Present today                                                                                                           |
| ------------------- | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| Entra ID            | Keycloak                                | Yes — running, but [SSO is not wired up](#single-sign-on-is-not-available-yet)                                          |
| Key Vault           | OpenBao                                 | Yes — see [Secrets](/homelab/secrets)                                                                                   |
| Storage Account     | MinIO                                   | **No** — not deployed                                                                                                   |
| Azure Monitor       | OTEL + Prometheus, Loki, Tempo, Grafana | Partly — metrics and logs cover the stack; traces come from Keycloak alone. See [Observability](/homelab/observability) |
| Application Gateway | Traefik                                 | Yes — see [Architecture](/homelab/architecture)                                                                         |
| VNet / Private Link | Tailscale                               | Yes                                                                                                                     |
| Azure SQL           | Postgres                                | Yes — the platform database, currently serving Keycloak only                                                            |

That framing is also the honest answer to why a homelab runs an identity provider at
all. It is not single sign-on for four dashboards. It is capability parity plus real
multi-user support: family accounts, demo accounts and test accounts, a realm per
consumer, with the AI application as one tenant among several rather than the reason the
platform exists.

The reasoning behind the mapping, and the consequences that follow from it, are recorded
in the Hill90 repository as
[Hill90 as an open-source mirror of the Azure platform primitives](https://github.com/jonhill90/Hill90/blob/main/docs/decisions/platform-primitives.md)
rather than restated here.

### Single sign-on is not available yet

Keycloak 26.4.0 is deployed and running. It serves a `platform` realm at
`https://auth.hill90.com/realms/platform`, which holds an OIDC client for OpenBao
(`hill90-vault`).

**Nothing signs in through it yet.** Portainer, OpenBao and Grafana each still use their
own local authentication behind Tailscale. Wiring them — and MinIO, once it exists — to
Keycloak is [issue 530](https://github.com/jonhill90/Hill90/issues/530), which has not
started. The identity provider exists; the single sign-on experience does not.

When that work happens, each service is intended to keep a working local admin fallback,
so that an unhealthy Keycloak cannot lock administration out of the platform.

### What is missing

The parity is partial, and the gaps are real rather than pending:

* **No container registry.** Nothing corresponds to Azure Container Registry; images come
  from public registries and GHCR.
* **No messaging tier.** Nothing corresponds to Service Bus or Event Hubs.
* **No object storage.** MinIO went with the application teardown and has not returned.
  There is no MinIO container on the host, running or stopped.

## What runs

Fourteen running containers, deployed as five independent units. A sixth, `openbao-init`,
is a one-shot that runs and exits.

| Deploy unit   | Containers                                                                        | Command                        |
| ------------- | --------------------------------------------------------------------------------- | ------------------------------ |
| Edge          | `traefik`, `dns-manager`, `portainer`                                             | `deploy.sh infra prod`         |
| Observability | `prometheus`, `grafana`, `loki`, `tempo`, `promtail`, `node-exporter`, `cadvisor` | `deploy.sh observability prod` |
| Secrets       | `openbao`                                                                         | `deploy.sh vault prod`         |
| Database      | `postgres`, `postgres-exporter`                                                   | `deploy.sh db prod`            |
| Identity      | `keycloak`                                                                        | `deploy.sh auth prod`          |

Order matters: `db` before `auth`, because Keycloak will not start without its database —
the deploy script refuses the `auth` deploy if it cannot query Postgres first.

The edge unit owns all three Docker networks; everything else attaches to them as
external. On a fresh host, edge deploys first.

## Surfaces

| Service           | URL                            | Access                                         |
| ----------------- | ------------------------------ | ---------------------------------------------- |
| Keycloak          | `https://auth.hill90.com`      | **Public**                                     |
| Traefik dashboard | `https://traefik.hill90.com`   | Tailscale only                                 |
| Portainer         | `https://portainer.hill90.com` | Tailscale only                                 |
| Grafana           | `https://grafana.hill90.com`   | Tailscale only                                 |
| OpenBao           | `https://vault.hill90.com`     | Tailscale only                                 |
| dns-manager       | internal                       | Traefik only                                   |
| Postgres          | internal                       | No route; reachable only on the Docker network |

Every administrative dashboard except Keycloak is reachable only over Tailscale
(`100.64.0.0/10`), enforced by a Traefik IP-allowlist middleware on top of the host
firewall. Only ports 80 and 443 are open publicly.

Keycloak is the exception, and deliberately so: an identity provider that users sign in
to has to be reachable by those users. It is routed on the public entrypoint with an
HTTP-01 certificate and no IP allowlist, which means its admin console at
`https://auth.hill90.com/admin/` is publicly reachable too, protected by credentials
alone.

## The stack

* **Host** — AlmaLinux 10 on a Hostinger VPS
* **Runtime** — Docker Engine and Docker Compose
* **Edge** — Traefik v2.11 with Let's Encrypt, HTTP-01 for public hostnames and DNS-01
  for Tailscale-only ones
* **Observability** — Prometheus, Grafana, Loki, Tempo, plus Promtail, node-exporter and
  cAdvisor
* **Identity** — Keycloak 26.4.0, serving the `platform` realm
* **Database** — PostgreSQL 16, the platform store behind Keycloak
* **Secrets** — OpenBao as the intended runtime store, SOPS + age for bootstrap and
  disaster recovery
* **Admin access** — Tailscale VPN, SSH by key only
* **Provisioning** — Ansible playbooks driven by the Hostinger and Tailscale APIs
* **CI/CD** — GitHub Actions
* **DNS** — Hostinger DNS API via `scripts/hostinger.sh`

## The one piece of application code

`services/dns-manager` is the only application code in the infrastructure repository — a
small Flask webhook implementing Traefik's `httpreq` DNS-01 provider against the
Hostinger DNS API. Without it, certificates for Tailscale-only hosts cannot issue, because
those hosts have no public A record for HTTP-01 to validate against. See
[Certificates](/homelab/certificates).

## Where to go next

<CardGroup cols={2}>
  <Card title="Architecture" icon="sitemap" href="/homelab/architecture">
    Network topology, isolation boundaries, and what each service is responsible for.
  </Card>

  <Card title="Local development" icon="laptop" href="/homelab/local-development">
    Run the whole stack on a Mac with one command. No secrets, no VPS.
  </Card>

  <Card title="Operations" icon="terminal" href="/homelab/operations">
    Deploy, rebuild, back up, recover.
  </Card>

  <Card title="Secrets" icon="key" href="/homelab/secrets">
    The vault model, and an honest account of what the vault holds today.
  </Card>
</CardGroup>
