docs: rewrite README with structured overview and quick start guide

Replaces the minimal project description with a comprehensive README
including a component overview table, quick start instructions, common
Ansible operations, and links to detailed documentation. Aligns with
Red Panda Approval™ standards.
This commit is contained in:
2026-03-03 12:49:06 +00:00
parent c7be03a743
commit b4d60f2f38
219 changed files with 34586 additions and 2 deletions

808
docs/ouranos.html Normal file
View File

@@ -0,0 +1,808 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ouranos Lab - Red Panda Approved Infrastructure</title>
<!-- Bootswatch Flatly -->
<link href="https://cdn.jsdelivr.net/npm/bootswatch@5.3.2/dist/flatly/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<style>
html { scroll-behavior: smooth; }
#scrollTopBtn {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1000;
display: none;
border-radius: 50%;
width: 50px;
height: 50px;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
}
</style>
</head>
<body>
<div class="container-fluid px-4">
<!-- Navbar -->
<nav class="navbar navbar-expand-lg navbar-dark bg-primary rounded mb-4 mt-3">
<div class="container-fluid">
<a class="navbar-brand fw-bold" href="#">
<i class="bi bi-diagram-3-fill"></i> Ouranos Lab
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item"><a class="nav-link" href="#overview"><i class="bi bi-info-circle"></i> Overview</a></li>
<li class="nav-item"><a class="nav-link" href="#hosts"><i class="bi bi-hdd-network"></i> Hosts</a></li>
<li class="nav-item"><a class="nav-link" href="#routing"><i class="bi bi-signpost-split"></i> Routing</a></li>
<li class="nav-item"><a class="nav-link" href="#infrastructure"><i class="bi bi-gear"></i> Infrastructure</a></li>
<li class="nav-item"><a class="nav-link" href="#automation"><i class="bi bi-play-circle"></i> Automation</a></li>
<li class="nav-item"><a class="nav-link" href="#dataflow"><i class="bi bi-diagram-2"></i> Data Flow</a></li>
</ul>
<button id="darkModeToggle" class="btn btn-outline-light btn-sm" title="Toggle dark mode">
<i class="bi bi-moon-fill"></i>
</button>
</div>
</div>
</nav>
<!-- Hero -->
<header class="bg-primary text-white py-5 rounded mb-4">
<div class="container">
<div class="row align-items-center">
<div class="col-lg-8">
<h1 class="display-4 fw-bold"><i class="bi bi-diagram-3-fill"></i> Ouranos Lab</h1>
<p class="lead">Red Panda Approved™ Infrastructure as Code</p>
<p class="mb-0">10 Incus containers named after moons of Uranus, provisioned with Terraform and configured with Ansible. Accessible at <a href="https://ouranos.helu.ca" class="text-white fw-bold">ouranos.helu.ca</a></p>
</div>
<div class="col-lg-4 text-center mt-3 mt-lg-0">
<div class="badge bg-success fs-6 p-3">
<i class="bi bi-check-circle-fill"></i> Red Panda Approved™
</div>
</div>
</div>
</div>
</header>
<!-- Overview -->
<section id="overview" class="mb-5">
<h2 class="h2 mb-4"><i class="bi bi-info-circle text-primary me-2"></i>Project Overview</h2>
<div class="alert alert-info border-start border-4 border-info">
<p class="mb-1">Ouranos is a comprehensive infrastructure-as-code project that provisions and manages a complete development sandbox environment. All infrastructure and configuration is tracked in Git for reproducible deployments.</p>
<p class="mb-0"><i class="bi bi-exclamation-triangle-fill text-warning me-1"></i><strong>DNS Domain:</strong> Incus resolves containers via the <code>.incus</code> suffix (e.g., <code>oberon.incus</code>). IPv4 addresses are dynamically assigned — always use DNS names, never hardcode IPs.</p>
</div>
<div class="row g-4">
<div class="col-md-6">
<div class="card h-100 border-primary">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="bi bi-diagram-3 me-2"></i>Terraform</h5>
</div>
<div class="card-body">
<p class="card-text">Provisions the Uranian host containers with:</p>
<ul class="mb-0">
<li>10 specialised Incus containers (LXC)</li>
<li>DNS-resolved networking (<code>.incus</code> domain)</li>
<li>Security policies and nested Docker support</li>
<li>Port proxy devices and resource dependencies</li>
<li>Incus S3 buckets for object storage (Casdoor, LobeChat)</li>
</ul>
</div>
<div class="card-footer text-muted small"><i class="bi bi-check-circle me-1"></i>Idempotent, elegant, observable</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100 border-success">
<div class="card-header bg-success text-white">
<h5 class="mb-0"><i class="bi bi-gear-fill me-2"></i>Ansible</h5>
</div>
<div class="card-body">
<p class="card-text">Deploys and configures all services:</p>
<ul class="mb-0">
<li>Docker engine on nested-capable hosts</li>
<li>Databases: PostgreSQL (Portia), Neo4j (Ariel)</li>
<li>Observability: Prometheus, Loki, Grafana (Prospero)</li>
<li>Application runtimes and LLM proxies</li>
<li>HAProxy TLS termination and Casdoor SSO (Titania)</li>
</ul>
</div>
<div class="card-footer text-muted small"><i class="bi bi-check-circle me-1"></i>Idempotent, auditable, integrated</div>
</div>
</div>
</div>
</section>
<!-- Hosts -->
<section id="hosts" class="mb-5">
<h2 class="h2 mb-4"><i class="bi bi-hdd-network text-primary me-2"></i>Uranian Host Architecture</h2>
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="bi bi-table me-2"></i>Hosts Summary</h5>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover table-bordered mb-0 align-middle">
<thead class="table-light">
<tr>
<th><i class="bi bi-tag me-1"></i>Name</th>
<th><i class="bi bi-briefcase me-1"></i>Role</th>
<th><i class="bi bi-list-ul me-1"></i>Key Services</th>
<th class="text-center"><i class="bi bi-shield me-1"></i>Nesting</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>ariel</strong></td>
<td><span class="badge bg-warning text-dark">graph_database</span></td>
<td>Neo4j 5.26.0</td>
<td class="text-center"><i class="bi bi-check-circle-fill text-success"></i></td>
</tr>
<tr>
<td><strong>caliban</strong></td>
<td><span class="badge bg-secondary">agent_automation</span></td>
<td>Agent S MCP Server, Kernos, MATE Desktop, GPU</td>
<td class="text-center"><i class="bi bi-check-circle-fill text-success"></i></td>
</tr>
<tr>
<td><strong>miranda</strong></td>
<td><span class="badge bg-info">mcp_docker_host</span></td>
<td>MCPO, Grafana MCP, Gitea MCP, Neo4j MCP, Argos MCP</td>
<td class="text-center"><i class="bi bi-check-circle-fill text-success"></i></td>
</tr>
<tr>
<td><strong>oberon</strong></td>
<td><span class="badge bg-primary">container_orchestration</span></td>
<td>MCP Switchboard, RabbitMQ, Open WebUI, SearXNG, Home Assistant, smtp4dev</td>
<td class="text-center"><i class="bi bi-check-circle-fill text-success"></i></td>
</tr>
<tr>
<td><strong>portia</strong></td>
<td><span class="badge bg-success">database</span></td>
<td>PostgreSQL 16</td>
<td class="text-center"><i class="bi bi-x-circle-fill text-danger"></i></td>
</tr>
<tr>
<td><strong>prospero</strong></td>
<td><span class="badge bg-dark">observability</span></td>
<td>Prometheus, Loki, Grafana, PgAdmin, AlertManager</td>
<td class="text-center"><i class="bi bi-x-circle-fill text-danger"></i></td>
</tr>
<tr>
<td><strong>puck</strong></td>
<td><span class="badge bg-danger">application_runtime</span></td>
<td>JupyterLab, Gitea Runner, Django apps (6×)</td>
<td class="text-center"><i class="bi bi-check-circle-fill text-success"></i></td>
</tr>
<tr>
<td><strong>rosalind</strong></td>
<td><span class="badge bg-success">collaboration</span></td>
<td>Gitea, LobeChat, Nextcloud, AnythingLLM</td>
<td class="text-center"><i class="bi bi-check-circle-fill text-success"></i></td>
</tr>
<tr>
<td><strong>sycorax</strong></td>
<td><span class="badge bg-secondary">language_models</span></td>
<td>Arke LLM Proxy</td>
<td class="text-center"><i class="bi bi-check-circle-fill text-success"></i></td>
</tr>
<tr>
<td><strong>titania</strong></td>
<td><span class="badge bg-primary">proxy_sso</span></td>
<td>HAProxy, Casdoor SSO, certbot</td>
<td class="text-center"><i class="bi bi-check-circle-fill text-success"></i></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Host Detail Cards -->
<div class="row g-4">
<div class="col-lg-6">
<div class="card h-100 border-primary">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="bi bi-box me-2"></i>oberon — Container Orchestration</h5>
</div>
<div class="card-body">
<p class="text-muted fst-italic small">King of the Fairies orchestrating containers and managing MCP infrastructure.</p>
<ul class="mb-0">
<li>Docker engine</li>
<li><strong>MCP Switchboard</strong> (port 22785) — Django app routing MCP tool calls</li>
<li><strong>RabbitMQ</strong> message queue</li>
<li><strong>Open WebUI</strong> LLM interface (port 22088, PostgreSQL backend on Portia)</li>
<li><strong>SearXNG</strong> privacy search (port 22073, behind OAuth2-Proxy)</li>
<li><strong>Home Assistant</strong> (port 8123)</li>
<li><strong>smtp4dev</strong> SMTP test server (port 22025)</li>
</ul>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card h-100 border-success">
<div class="card-header bg-success text-white">
<h5 class="mb-0"><i class="bi bi-database me-2"></i>portia — Relational Database</h5>
</div>
<div class="card-body">
<p class="text-muted fst-italic small">Intelligent and resourceful — the reliability of relational databases.</p>
<ul class="mb-0">
<li>PostgreSQL 16 (port 5432)</li>
<li>Databases: <code>arke</code>, <code>anythingllm</code>, <code>gitea</code>, <code>hass</code>, <code>lobechat</code>, <code>mcp_switchboard</code>, <code>nextcloud</code>, <code>openwebui</code>, <code>spelunker</code></li>
</ul>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card h-100 border-warning">
<div class="card-header bg-warning text-dark">
<h5 class="mb-0"><i class="bi bi-diagram-2 me-2"></i>ariel — Graph Database</h5>
</div>
<div class="card-body">
<p class="text-muted fst-italic small">Air spirit — ethereal, interconnected nature mirroring graph relationships.</p>
<ul class="mb-0">
<li>Neo4j 5.26.0 (Docker)</li>
<li>HTTP API: port 25554</li>
<li>Bolt: port 7687</li>
</ul>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card h-100 border-danger">
<div class="card-header bg-danger text-white">
<h5 class="mb-0"><i class="bi bi-code-slash me-2"></i>puck — Application Runtime</h5>
</div>
<div class="card-body">
<p class="text-muted fst-italic small">Shape-shifting trickster embodying Python's versatility.</p>
<ul class="mb-0">
<li>Docker engine</li>
<li><strong>JupyterLab</strong> (port 22071 via OAuth2-Proxy)</li>
<li><strong>Gitea Runner</strong> CI/CD agent</li>
<li>Django apps: <strong>Angelia</strong> (22281), <strong>Athena</strong> (22481), <strong>Kairos</strong> (22581), <strong>Icarlos</strong> (22681), <strong>Spelunker</strong> (22881), <strong>Peitho</strong> (22981)</li>
</ul>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card h-100 border-dark">
<div class="card-header bg-dark text-white">
<h5 class="mb-0"><i class="bi bi-graph-up me-2"></i>prospero — Observability Stack</h5>
</div>
<div class="card-body">
<p class="text-muted fst-italic small">Master magician observing all events.</p>
<ul class="mb-0">
<li>PPLG stack via Docker Compose: Prometheus, Loki, Grafana, PgAdmin</li>
<li>Internal HAProxy with OAuth2-Proxy for all dashboards</li>
<li>AlertManager with Pushover notifications</li>
<li>Prometheus node-exporter metrics from all hosts</li>
<li>Loki log aggregation via Alloy (all hosts)</li>
<li>Grafana with Casdoor SSO integration</li>
</ul>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card h-100 border-info">
<div class="card-header bg-info text-white">
<h5 class="mb-0"><i class="bi bi-chat-dots me-2"></i>miranda — MCP Docker Host</h5>
</div>
<div class="card-body">
<p class="text-muted fst-italic small">Curious bridge between worlds — hosting MCP server containers.</p>
<ul class="mb-0">
<li>Docker engine (API on port 2375 for MCP Switchboard)</li>
<li><strong>MCPO</strong> OpenAI-compatible MCP proxy</li>
<li><strong>Grafana MCP Server</strong> — Grafana API integration (port 25533)</li>
<li><strong>Gitea MCP Server</strong> (port 25535)</li>
<li><strong>Neo4j MCP Server</strong></li>
<li><strong>Argos MCP Server</strong> — web search via SearXNG (port 25534)</li>
</ul>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card h-100">
<div class="card-header bg-secondary text-white">
<h5 class="mb-0"><i class="bi bi-magic me-2"></i>sycorax — Language Models</h5>
</div>
<div class="card-body">
<p class="text-muted fst-italic small">Original magical power wielding language magic.</p>
<ul class="mb-0">
<li><strong>Arke</strong> LLM API Proxy (port 25540)</li>
<li>Multi-provider support (OpenAI, Anthropic, etc.)</li>
<li>Session management with Memcached</li>
<li>Database backend on Portia</li>
</ul>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card h-100">
<div class="card-header bg-secondary text-white">
<h5 class="mb-0"><i class="bi bi-robot me-2"></i>caliban — Agent Automation</h5>
</div>
<div class="card-body">
<p class="text-muted fst-italic small">Autonomous computer agent learning through environmental interaction.</p>
<ul class="mb-0">
<li>Docker engine</li>
<li><strong>Agent S MCP Server</strong> (MATE desktop, AT-SPI automation)</li>
<li><strong>Kernos</strong> MCP Shell Server (port 22021)</li>
<li>GPU passthrough for vision tasks</li>
<li>RDP access (port 25521)</li>
</ul>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card h-100 border-success">
<div class="card-header bg-success text-white">
<h5 class="mb-0"><i class="bi bi-people me-2"></i>rosalind — Collaboration Services</h5>
</div>
<div class="card-body">
<p class="text-muted fst-italic small">Witty and resourceful moon for PHP, Go, and Node.js runtimes.</p>
<ul class="mb-0">
<li><strong>Gitea</strong> self-hosted Git (port 22082, SSH on 22022)</li>
<li><strong>LobeChat</strong> AI chat interface (port 22081)</li>
<li><strong>Nextcloud</strong> file sharing and collaboration (port 22083)</li>
<li><strong>AnythingLLM</strong> document AI workspace (port 22084)</li>
<li>Nextcloud data on dedicated Incus storage volume</li>
</ul>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card h-100 border-primary">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="bi bi-shield-check me-2"></i>titania — Proxy &amp; SSO Services</h5>
</div>
<div class="card-body">
<p class="text-muted fst-italic small">Queen of the Fairies managing access control and authentication.</p>
<ul class="mb-0">
<li><strong>HAProxy 3.x</strong> with TLS termination (port 443)</li>
<li>Let's Encrypt wildcard certificate via certbot DNS-01 (Namecheap)</li>
<li>HTTP to HTTPS redirect (port 80)</li>
<li>Gitea SSH proxy (port 22022)</li>
<li><strong>Casdoor SSO</strong> (port 22081, local PostgreSQL)</li>
<li>Prometheus metrics at <code>:8404/metrics</code></li>
</ul>
</div>
</div>
</div>
</div>
</section>
<!-- Routing -->
<section id="routing" class="mb-5">
<h2 class="h2 mb-4"><i class="bi bi-signpost-split text-primary me-2"></i>External Access via HAProxy</h2>
<div class="alert alert-primary border-start border-4 border-primary">
<p class="mb-0">Titania provides TLS termination and reverse proxy for all services. <strong>Base domain:</strong> <a href="https://ouranos.helu.ca" class="alert-link">ouranos.helu.ca</a> — HTTPS port 443, HTTP port 80 (redirects to HTTPS). Certificate: Let's Encrypt wildcard via certbot DNS-01 (Namecheap).</p>
</div>
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="bi bi-table me-2"></i>Route Table</h5>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover table-bordered mb-0 align-middle">
<thead class="table-light">
<tr>
<th><i class="bi bi-link-45deg me-1"></i>Subdomain</th>
<th><i class="bi bi-hdd-network me-1"></i>Backend</th>
<th><i class="bi bi-app me-1"></i>Service</th>
</tr>
</thead>
<tbody>
<tr><td><code>ouranos.helu.ca</code> <span class="badge bg-secondary">root</span></td><td><code>puck.incus:22281</code></td><td>Angelia (Django)</td></tr>
<tr><td><code>alertmanager.ouranos.helu.ca</code></td><td><code>prospero.incus:443</code> <span class="badge bg-info text-dark">SSL</span></td><td>AlertManager</td></tr>
<tr><td><code>angelia.ouranos.helu.ca</code></td><td><code>puck.incus:22281</code></td><td>Angelia (Django)</td></tr>
<tr><td><code>anythingllm.ouranos.helu.ca</code></td><td><code>rosalind.incus:22084</code></td><td>AnythingLLM</td></tr>
<tr><td><code>arke.ouranos.helu.ca</code></td><td><code>sycorax.incus:25540</code></td><td>Arke LLM Proxy</td></tr>
<tr><td><code>athena.ouranos.helu.ca</code></td><td><code>puck.incus:22481</code></td><td>Athena (Django)</td></tr>
<tr><td><code>gitea.ouranos.helu.ca</code></td><td><code>rosalind.incus:22082</code></td><td>Gitea</td></tr>
<tr><td><code>grafana.ouranos.helu.ca</code></td><td><code>prospero.incus:443</code> <span class="badge bg-info text-dark">SSL</span></td><td>Grafana</td></tr>
<tr><td><code>hass.ouranos.helu.ca</code></td><td><code>oberon.incus:8123</code></td><td>Home Assistant</td></tr>
<tr><td><code>id.ouranos.helu.ca</code></td><td><code>titania.incus:22081</code></td><td>Casdoor SSO</td></tr>
<tr><td><code>icarlos.ouranos.helu.ca</code></td><td><code>puck.incus:22681</code></td><td>Icarlos (Django)</td></tr>
<tr><td><code>jupyterlab.ouranos.helu.ca</code></td><td><code>puck.incus:22071</code></td><td>JupyterLab <span class="badge bg-secondary">OAuth2-Proxy</span></td></tr>
<tr><td><code>kairos.ouranos.helu.ca</code></td><td><code>puck.incus:22581</code></td><td>Kairos (Django)</td></tr>
<tr><td><code>lobechat.ouranos.helu.ca</code></td><td><code>rosalind.incus:22081</code></td><td>LobeChat</td></tr>
<tr><td><code>loki.ouranos.helu.ca</code></td><td><code>prospero.incus:443</code> <span class="badge bg-info text-dark">SSL</span></td><td>Loki</td></tr>
<tr><td><code>mcp-switchboard.ouranos.helu.ca</code></td><td><code>oberon.incus:22785</code></td><td>MCP Switchboard</td></tr>
<tr><td><code>nextcloud.ouranos.helu.ca</code></td><td><code>rosalind.incus:22083</code></td><td>Nextcloud</td></tr>
<tr><td><code>openwebui.ouranos.helu.ca</code></td><td><code>oberon.incus:22088</code></td><td>Open WebUI</td></tr>
<tr><td><code>peitho.ouranos.helu.ca</code></td><td><code>puck.incus:22981</code></td><td>Peitho (Django)</td></tr>
<tr><td><code>pgadmin.ouranos.helu.ca</code></td><td><code>prospero.incus:443</code> <span class="badge bg-info text-dark">SSL</span></td><td>PgAdmin 4</td></tr>
<tr><td><code>prometheus.ouranos.helu.ca</code></td><td><code>prospero.incus:443</code> <span class="badge bg-info text-dark">SSL</span></td><td>Prometheus</td></tr>
<tr><td><code>searxng.ouranos.helu.ca</code></td><td><code>oberon.incus:22073</code></td><td>SearXNG <span class="badge bg-secondary">OAuth2-Proxy</span></td></tr>
<tr><td><code>smtp4dev.ouranos.helu.ca</code></td><td><code>oberon.incus:22085</code></td><td>smtp4dev</td></tr>
<tr><td><code>spelunker.ouranos.helu.ca</code></td><td><code>puck.incus:22881</code></td><td>Spelunker (Django)</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</section>
<!-- Infrastructure Management -->
<section id="infrastructure" class="mb-5">
<h2 class="h2 mb-4"><i class="bi bi-gear text-primary me-2"></i>Infrastructure Management</h2>
<div class="row g-4 mb-4">
<div class="col-md-6">
<div class="card h-100 border-primary">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="bi bi-play-circle me-2"></i>Quick Start</h5>
</div>
<div class="card-body">
<pre class="mb-0"><code># Provision containers
cd terraform
terraform init
terraform plan
terraform apply
# Start all containers
cd ../ansible
source ~/env/agathos/bin/activate
ansible-playbook sandbox_up.yml
# Deploy all services
ansible-playbook site.yml
# Stop all containers
ansible-playbook sandbox_down.yml</code></pre>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100 border-warning">
<div class="card-header bg-warning text-dark">
<h5 class="mb-0"><i class="bi bi-shield-lock me-2"></i>Vault Management</h5>
</div>
<div class="card-body">
<pre class="mb-0"><code># Edit secrets
ansible-vault edit \
inventory/group_vars/all/vault.yml
# View secrets
ansible-vault view \
inventory/group_vars/all/vault.yml
# Encrypt a new file
ansible-vault encrypt new_secrets.yml</code></pre>
</div>
</div>
</div>
</div>
<div class="row g-4">
<div class="col-md-6">
<div class="alert alert-primary border-start border-4 border-primary h-100 mb-0">
<h5><i class="bi bi-lightning-fill me-2"></i>Terraform Workflow</h5>
<ol class="mb-0">
<li><strong>Define</strong> — Containers, networks, and resources in <code>*.tf</code> files</li>
<li><strong>Plan</strong> — Review changes with <code>terraform plan</code></li>
<li><strong>Apply</strong> — Provision with <code>terraform apply</code></li>
<li><strong>Verify</strong> — Check outputs and container status</li>
</ol>
</div>
</div>
<div class="col-md-6">
<div class="alert alert-success border-start border-4 border-success h-100 mb-0">
<h5><i class="bi bi-check-circle-fill me-2"></i>Ansible Workflow</h5>
<ol class="mb-0">
<li><strong>Bootstrap</strong> — Update packages, install essentials (<code>apt_update.yml</code>)</li>
<li><strong>Agents</strong> — Deploy Alloy and Node Exporter on all hosts</li>
<li><strong>Services</strong> — Configure databases, Docker, applications, observability</li>
<li><strong>Verify</strong> — Check service health and connectivity</li>
</ol>
</div>
</div>
</div>
<div class="alert alert-info border-start border-4 border-info mt-4">
<h5><i class="bi bi-bucket me-2"></i>S3 Storage Provisioning</h5>
<p>Terraform provisions Incus S3 buckets for services requiring object storage:</p>
<div class="table-responsive">
<table class="table table-sm table-bordered mb-1">
<thead class="table-light">
<tr><th>Service</th><th>Host</th><th>Purpose</th></tr>
</thead>
<tbody>
<tr><td><strong>Casdoor</strong></td><td>Titania</td><td>User avatars and SSO resource storage</td></tr>
<tr><td><strong>LobeChat</strong></td><td>Rosalind</td><td>File uploads and attachments</td></tr>
</tbody>
</table>
</div>
<p class="mb-0 small"><i class="bi bi-shield-lock me-1"></i>S3 credentials are stored as sensitive Terraform outputs and in Ansible Vault with the <code>vault_*_s3_*</code> prefix.</p>
</div>
</section>
<!-- Automation -->
<section id="automation" class="mb-5">
<h2 class="h2 mb-4"><i class="bi bi-play-circle text-primary me-2"></i>Ansible Automation</h2>
<div class="accordion" id="playbookAccordion">
<!-- site.yml -->
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#colSiteYml">
<i class="bi bi-list-check me-2"></i>Full Deployment — <code>site.yml</code> (in order)
</button>
</h2>
<div id="colSiteYml" class="accordion-collapse collapse show" data-bs-parent="#playbookAccordion">
<div class="accordion-body">
<div class="table-responsive">
<table class="table table-hover table-bordered mb-0 align-middle">
<thead class="table-light">
<tr><th>Playbook</th><th>Host(s)</th><th>Purpose</th></tr>
</thead>
<tbody>
<tr><td><code>apt_update.yml</code></td><td>All</td><td>Update packages and install essentials</td></tr>
<tr><td><code>alloy/deploy.yml</code></td><td>All</td><td>Grafana Alloy log/metrics collection</td></tr>
<tr><td><code>prometheus/node_deploy.yml</code></td><td>All</td><td>Node Exporter metrics</td></tr>
<tr><td><code>docker/deploy.yml</code></td><td>Oberon, Ariel, Miranda, Puck, Rosalind, Sycorax, Caliban, Titania</td><td>Docker engine</td></tr>
<tr><td><code>smtp4dev/deploy.yml</code></td><td>Oberon</td><td>SMTP test server</td></tr>
<tr><td><code>pplg/deploy.yml</code></td><td>Prospero</td><td>Full observability stack + internal HAProxy + OAuth2-Proxy</td></tr>
<tr><td><code>postgresql/deploy.yml</code></td><td>Portia</td><td>PostgreSQL with all databases</td></tr>
<tr><td><code>postgresql_ssl/deploy.yml</code></td><td>Titania</td><td>Dedicated PostgreSQL for Casdoor</td></tr>
<tr><td><code>neo4j/deploy.yml</code></td><td>Ariel</td><td>Neo4j graph database</td></tr>
<tr><td><code>searxng/deploy.yml</code></td><td>Oberon</td><td>SearXNG privacy search</td></tr>
<tr><td><code>haproxy/deploy.yml</code></td><td>Titania</td><td>HAProxy TLS termination and routing</td></tr>
<tr><td><code>casdoor/deploy.yml</code></td><td>Titania</td><td>Casdoor SSO</td></tr>
<tr><td><code>mcpo/deploy.yml</code></td><td>Miranda</td><td>MCPO MCP proxy</td></tr>
<tr><td><code>openwebui/deploy.yml</code></td><td>Oberon</td><td>Open WebUI LLM interface</td></tr>
<tr><td><code>hass/deploy.yml</code></td><td>Oberon</td><td>Home Assistant</td></tr>
<tr><td><code>gitea/deploy.yml</code></td><td>Rosalind</td><td>Gitea self-hosted Git</td></tr>
<tr><td><code>nextcloud/deploy.yml</code></td><td>Rosalind</td><td>Nextcloud collaboration</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Individual services -->
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#colIndividual">
<i class="bi bi-puzzle me-2"></i>Individual Service Deployments
</button>
</h2>
<div id="colIndividual" class="accordion-collapse collapse" data-bs-parent="#playbookAccordion">
<div class="accordion-body">
<div class="table-responsive">
<table class="table table-hover table-bordered mb-0 align-middle">
<thead class="table-light">
<tr><th>Playbook</th><th>Host</th><th>Service</th></tr>
</thead>
<tbody>
<tr><td><code>anythingllm/deploy.yml</code></td><td>Rosalind</td><td>AnythingLLM document AI</td></tr>
<tr><td><code>arke/deploy.yml</code></td><td>Sycorax</td><td>Arke LLM proxy</td></tr>
<tr><td><code>argos/deploy.yml</code></td><td>Miranda</td><td>Argos MCP web search server</td></tr>
<tr><td><code>caliban/deploy.yml</code></td><td>Caliban</td><td>Agent S MCP Server</td></tr>
<tr><td><code>certbot/deploy.yml</code></td><td>Titania</td><td>Let's Encrypt certificate renewal</td></tr>
<tr><td><code>gitea_mcp/deploy.yml</code></td><td>Miranda</td><td>Gitea MCP Server</td></tr>
<tr><td><code>gitea_runner/deploy.yml</code></td><td>Puck</td><td>Gitea CI/CD runner</td></tr>
<tr><td><code>grafana_mcp/deploy.yml</code></td><td>Miranda</td><td>Grafana MCP Server</td></tr>
<tr><td><code>jupyterlab/deploy.yml</code></td><td>Puck</td><td>JupyterLab + OAuth2-Proxy</td></tr>
<tr><td><code>kernos/deploy.yml</code></td><td>Caliban</td><td>Kernos MCP shell server</td></tr>
<tr><td><code>lobechat/deploy.yml</code></td><td>Rosalind</td><td>LobeChat AI chat</td></tr>
<tr><td><code>neo4j_mcp/deploy.yml</code></td><td>Miranda</td><td>Neo4j MCP Server</td></tr>
<tr><td><code>rabbitmq/deploy.yml</code></td><td>Oberon</td><td>RabbitMQ message queue</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Lifecycle -->
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#colLifecycle">
<i class="bi bi-arrow-repeat me-2"></i>Lifecycle Playbooks
</button>
</h2>
<div id="colLifecycle" class="accordion-collapse collapse" data-bs-parent="#playbookAccordion">
<div class="accordion-body">
<div class="row g-3">
<div class="col-md-3">
<div class="card border-success text-center h-100">
<div class="card-body">
<i class="bi bi-play-fill text-success" style="font-size:2rem;"></i>
<h6 class="mt-2"><code>sandbox_up.yml</code></h6>
<p class="small mb-0">Start all Uranian host containers</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-primary text-center h-100">
<div class="card-body">
<i class="bi bi-list-check text-primary" style="font-size:2rem;"></i>
<h6 class="mt-2"><code>site.yml</code></h6>
<p class="small mb-0">Full deployment orchestration</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-warning text-center h-100">
<div class="card-body">
<i class="bi bi-arrow-up-circle text-warning" style="font-size:2rem;"></i>
<h6 class="mt-2"><code>apt_update.yml</code></h6>
<p class="small mb-0">Update packages on all hosts</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-danger text-center h-100">
<div class="card-body">
<i class="bi bi-stop-fill text-danger" style="font-size:2rem;"></i>
<h6 class="mt-2"><code>sandbox_down.yml</code></h6>
<p class="small mb-0">Gracefully stop all containers</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Data Flow -->
<section id="dataflow" class="mb-5">
<h2 class="h2 mb-4"><i class="bi bi-diagram-2 text-primary me-2"></i>Data Flow Architecture</h2>
<div class="card mb-4">
<div class="card-header bg-dark text-white">
<h5 class="mb-0"><i class="bi bi-diagram-3 me-2"></i>Observability Pipeline</h5>
</div>
<div class="card-body">
<div class="mermaid">
flowchart LR
subgraph hosts["All Hosts"]
alloy["Alloy\n(syslog + journal)"]
node_exp["Node Exporter\n(metrics)"]
end
subgraph prospero["Prospero"]
loki["Loki\n(logs)"]
prom["Prometheus\n(metrics)"]
grafana["Grafana\n(dashboards)"]
alert["AlertManager"]
end
pushover["Pushover\n(notifications)"]
alloy -->|"HTTP push"| loki
node_exp -->|"scrape 15s"| prom
loki --> grafana
prom --> grafana
grafana --> alert
alert -->|"webhook"| pushover
</div>
</div>
</div>
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="bi bi-link-45deg me-2"></i>Service Integration Points</h5>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover table-bordered mb-0 align-middle">
<thead class="table-light">
<tr><th>Consumer</th><th>Provider</th><th>Connection</th></tr>
</thead>
<tbody>
<tr><td>All LLM apps</td><td>Arke (Sycorax)</td><td><code>http://sycorax.incus:25540</code></td></tr>
<tr><td>Open WebUI, Arke, Gitea, Nextcloud, LobeChat</td><td>PostgreSQL (Portia)</td><td><code>portia.incus:5432</code></td></tr>
<tr><td>Neo4j MCP</td><td>Neo4j (Ariel)</td><td><code>ariel.incus:7687</code> (Bolt)</td></tr>
<tr><td>MCP Switchboard</td><td>Docker API (Miranda)</td><td><code>tcp://miranda.incus:2375</code></td></tr>
<tr><td>MCP Switchboard, Kairos, Spelunker</td><td>RabbitMQ (Oberon)</td><td><code>oberon.incus:5672</code></td></tr>
<tr><td>All apps (SMTP)</td><td>smtp4dev (Oberon)</td><td><code>oberon.incus:22025</code></td></tr>
<tr><td>All hosts (logs)</td><td>Loki (Prospero)</td><td><code>http://prospero.incus:3100</code></td></tr>
<tr><td>All hosts (metrics)</td><td>Prometheus (Prospero)</td><td><code>http://prospero.incus:9090</code></td></tr>
</tbody>
</table>
</div>
</div>
</div>
</section>
<!-- Important Notes -->
<section id="notes" class="mb-5">
<h2 class="h2 mb-4"><i class="bi bi-exclamation-triangle text-warning me-2"></i>Important Notes</h2>
<div class="alert alert-warning border-start border-4 border-warning">
<h5><i class="bi bi-exclamation-triangle-fill me-2"></i>Alloy Host Variables Required</h5>
<p class="mb-0">Every host with <code>alloy</code> in its <code>services</code> list must define <code>alloy_log_level</code> in <code>inventory/host_vars/&lt;host&gt;.incus.yml</code>. The playbook will fail with an undefined variable error if this is missing.</p>
</div>
<div class="alert alert-warning border-start border-4 border-warning">
<h5><i class="bi bi-exclamation-triangle-fill me-2"></i>Alloy Syslog Listeners Required for Docker Services</h5>
<p class="mb-0">Any Docker Compose service using the <code>syslog</code> logging driver must have a corresponding <code>loki.source.syslog</code> listener in the host's Alloy config template (<code>ansible/alloy/&lt;hostname&gt;/config.alloy.j2</code>). Missing listeners cause Docker containers to fail on start because the syslog driver cannot connect to its configured port.</p>
</div>
<div class="alert alert-warning border-start border-4 border-warning">
<h5><i class="bi bi-exclamation-triangle-fill me-2"></i>Local Terraform State</h5>
<p class="mb-0">This project uses local Terraform state (no remote backend). Do not run <code>terraform apply</code> from multiple machines simultaneously.</p>
</div>
<div class="alert alert-warning border-start border-4 border-warning">
<h5><i class="bi bi-exclamation-triangle-fill me-2"></i>Nested Docker</h5>
<p class="mb-0">Docker runs inside Incus containers (nested), requiring <code>security.nesting = true</code> and <code>lxc.apparmor.profile=unconfined</code> AppArmor override on all Docker-enabled hosts.</p>
</div>
<div class="alert alert-warning border-start border-4 border-warning">
<h5><i class="bi bi-exclamation-triangle-fill me-2"></i>Deployment Order</h5>
<p class="mb-0">Prospero (observability) must be fully deployed before other hosts, as Alloy on every host pushes logs and metrics to <code>prospero.incus</code>. Run <code>pplg/deploy.yml</code> before <code>site.yml</code> on a fresh environment.</p>
</div>
</section>
<!-- Footer -->
<footer class="bg-dark text-white py-4 rounded mt-2 mb-4">
<div class="container text-center">
<p class="mb-1"><i class="bi bi-heart-fill text-danger"></i> Built with love and approved by red pandas</p>
<small class="text-muted">Ouranos Lab — <a href="https://ouranos.helu.ca" class="text-muted">ouranos.helu.ca</a> — Infrastructure as Code for Development Excellence</small>
</div>
</footer>
<!-- Scroll to top button -->
<button id="scrollTopBtn" class="btn btn-primary" title="Scroll to top">
<i class="bi bi-arrow-up-circle"></i>
</button>
</div><!-- /container-fluid -->
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<!-- Mermaid JS -->
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
const isDark = () => document.documentElement.getAttribute('data-bs-theme') === 'dark';
mermaid.initialize({ startOnLoad: true, theme: isDark() ? 'dark' : 'default' });
document.getElementById('darkModeToggle').addEventListener('click', () => {
setTimeout(() => mermaid.initialize({ startOnLoad: false, theme: isDark() ? 'dark' : 'default' }), 50);
});
</script>
<script>
// Dark mode toggle
const toggleBtn = document.getElementById('darkModeToggle');
function applyTheme(dark) {
document.documentElement.setAttribute('data-bs-theme', dark ? 'dark' : 'light');
toggleBtn.innerHTML = dark ? '<i class="bi bi-sun-fill"></i>' : '<i class="bi bi-moon-fill"></i>';
toggleBtn.title = dark ? 'Switch to light mode' : 'Switch to dark mode';
}
toggleBtn.addEventListener('click', () => {
applyTheme(document.documentElement.getAttribute('data-bs-theme') !== 'dark');
});
// Scroll to top
window.addEventListener('scroll', () => {
document.getElementById('scrollTopBtn').style.display =
(document.body.scrollTop > 300 || document.documentElement.scrollTop > 300) ? 'block' : 'none';
});
document.getElementById('scrollTopBtn').addEventListener('click', () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
});
</script>
</body>
</html>