Files
ouranos/docs/ouranos.html

809 lines
48 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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/ouranos/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>