diff --git a/.gitignore b/.gitignore index addfb5d..6686406 100644 --- a/.gitignore +++ b/.gitignore @@ -1,22 +1,8 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -build/ -dist/ -wheels/ -*.egg-info/ -*.egg - -# Virtual environments -.venv/ -venv/ -ENV/ +# Secrets +.env +.env.* +*.pem +*.key # IDE .idea/ @@ -25,21 +11,9 @@ ENV/ *.swo *~ -# Jupyter -.ipynb_checkpoints/ - -# Testing -.pytest_cache/ -.coverage -htmlcov/ - -# Type checking -.mypy_cache/ - -# Environment variables -.env -.env.* - # OS .DS_Store Thumbs.db + +# Claude Code +.claude/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..0c6d863 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,57 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Overview + +Proxmox VE homelab cluster running Docker Swarm services. Three Proxmox hosts (pve, adder, game) connected via VXLAN overlay on vmbr1 (10.10.10.0/24). VMs run Docker CE and form a swarm with pve-postgres as manager. + +## Deployment + +All services deploy as Docker Swarm stacks from the manager node (pve-postgres). The active service definitions are in `proxmox/services/`. The `services/` directory contains older pre-migration copies. + +```bash +# Generate env var prefix from pass (run locally) +./proxmox/services/gen-env.sh + +# Copy yml to manager and deploy +scp proxmox/services/.yml pve-postgres:~/ +ssh pve-postgres " docker stack deploy -c .yml " + +# Remove a stack +ssh pve-postgres "docker stack rm " +``` + +Secrets come from `pass` under the `homelab/` prefix. Use `gen-env.sh` to extract required `${VAR}` references from a yml and resolve them. Never hardcode secrets in files. + +## Service YML Conventions + +- All use `version: '3.8'` and Docker Swarm deploy mode +- Each service is pinned to a specific node via `node.hostname` constraint +- All connect to the external overlay network `homelab-net` via `${OVERLAY_NETWORK:-homelab-net}` +- Named volumes for persistence, `on-failure` restart policy with 3 max attempts +- Every service that has a web UI or accepts external connections must publish its ports +- Only include environment variables required to start the service — no optional/nice-to-have vars + +## Cluster Topology + +| Node | Host IP | Swarm IP | Role | +|------|---------|----------|------| +| pve | 192.168.40.198 | 10.10.10.1 | Proxmox host | +| adder | 192.168.40.150 | 10.10.10.86 | Proxmox host | +| game | 192.168.40.109 | 10.10.10.172 | Proxmox host | +| pve-postgres | — | 10.10.10.2 | VM on pve, swarm manager | +| pve-tools | — | 10.10.10.3 | VM on pve, swarm worker | +| adder-ghost | — | 10.10.10.20 | VM on adder, swarm worker | + +## SSH Access + +Use SSH config aliases (`pve-postgres`, `pve-tools`, `adder-ghost`, `pve`, `adder`, `game`). ProxyJump is configured in `~/.ssh/config`. Never manually hop through intermediate nodes. + +## Key Files + +- `proxmox/services/*.yml` — Active swarm stack definitions +- `proxmox/services/gen-env.sh` — Extracts env vars from yml, resolves from `pass homelab/` +- `proxmox/services/nats.conf` — NATS server config (JetStream, websocket, monitoring) +- `proxmox/services/01-init.sql` — Postgres init script (creates users/databases) +- `proxmox/post_init_node.org` — Fresh Proxmox node setup steps diff --git a/proxmox/services/01-init.sql b/proxmox/services/01-init.sql new file mode 100644 index 0000000..bc752f2 --- /dev/null +++ b/proxmox/services/01-init.sql @@ -0,0 +1,18 @@ +-- Postgres init script +-- Runs once on first boot when data volume is empty. +-- Creates a database and scoped user for each service. + +-- FusionAuth +CREATE USER fusionauth_user WITH PASSWORD 'lXG9oNwbj0DEVaCjwjI9Fomhjs7mSgRd'; +CREATE DATABASE fusionauth_db OWNER fusionauth_user; +GRANT ALL PRIVILEGES ON DATABASE fusionauth_db TO fusionauth_user; + +-- n8n +CREATE USER n8n_user WITH PASSWORD 'peiKxDe3X7QWZhhrP1M8jKlnV2nLgqSp'; +CREATE DATABASE n8n_db OWNER n8n_user; +GRANT ALL PRIVILEGES ON DATABASE n8n_db TO n8n_user; + +-- c4trou +CREATE USER c4trou_user WITH PASSWORD 'mlhWiZcp5I2kXwRmyFEMPjZz6BX6ZjLi'; +CREATE DATABASE c4trou_db OWNER c4trou_user; +GRANT ALL PRIVILEGES ON DATABASE c4trou_db TO c4trou_user; diff --git a/proxmox/services/authentik.yml b/proxmox/services/authentik.yml new file mode 100644 index 0000000..e01bb44 --- /dev/null +++ b/proxmox/services/authentik.yml @@ -0,0 +1,74 @@ +version: '3.8' + +# Deploy with: +# docker stack deploy -c authentik.yml authentik +# +# Runs on: docker-swarm-1 +# Authentik server + worker, using the shared postgres stack. +# No Redis required as of 2026.2.x. +# Initial setup wizard at http://:9000/if/flow/initial-setup/ + +services: + server: + image: ghcr.io/goauthentik/server:${AUTHENTIK_TAG:-2026.2.1} + command: server + environment: + AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY} + AUTHENTIK_POSTGRESQL__HOST: postgres_postgres + AUTHENTIK_POSTGRESQL__PORT: 5432 + AUTHENTIK_POSTGRESQL__NAME: authentik_db + AUTHENTIK_POSTGRESQL__USER: authentik_user + AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_DB_PASSWORD} + ports: + - "9000:9000" + - "9443:9443" + volumes: + - authentik_media:/media + - authentik_templates:/templates + networks: + - overlay-net + deploy: + replicas: 1 + placement: + constraints: + - node.hostname == pve-tools + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + + worker: + image: ghcr.io/goauthentik/server:${AUTHENTIK_TAG:-2026.2.1} + command: worker + environment: + AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY} + AUTHENTIK_POSTGRESQL__HOST: postgres_postgres + AUTHENTIK_POSTGRESQL__PORT: 5432 + AUTHENTIK_POSTGRESQL__NAME: authentik_db + AUTHENTIK_POSTGRESQL__USER: authentik_user + AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_DB_PASSWORD} + volumes: + - authentik_media:/media + - authentik_templates:/templates + - authentik_certs:/certs + networks: + - overlay-net + deploy: + replicas: 1 + placement: + constraints: + - node.hostname == pve-tools + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + +volumes: + authentik_media: + authentik_templates: + authentik_certs: + +networks: + overlay-net: + external: true + name: ${OVERLAY_NETWORK:-homelab-net} diff --git a/proxmox/services/caddy.yml b/proxmox/services/caddy.yml new file mode 100644 index 0000000..1c0b3d7 --- /dev/null +++ b/proxmox/services/caddy.yml @@ -0,0 +1,40 @@ +version: '3.8' + +# Deploy with: +# docker stack deploy -c caddy.yml caddy +# +# Runs on: CADDY_INSTANCE (ip-10-0-1-168) +# Caddy handles TLS automatically via Let's Encrypt. +# Reverse proxies all public subdomains to the correct services on erda-net. +# Caddyfile is bind mounted from the host — edit /etc/caddy/Caddyfile on the caddy instance. + +services: + caddy: + image: caddy:latest + ports: + - "80:80" + - "443:443" + volumes: + - /etc/caddy/Caddyfile:/etc/caddy/Caddyfile + - caddy_data:/data + - caddy_config:/config + networks: + - overlay-net + deploy: + replicas: 1 + placement: + constraints: + - node.hostname == ip-10-0-1-168 + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + +volumes: + caddy_data: + caddy_config: + +networks: + overlay-net: + external: true + name: ${OVERLAY_NETWORK:-homelab-net} diff --git a/proxmox/services/fusionauth.yml b/proxmox/services/fusionauth.yml new file mode 100644 index 0000000..7bd8c9f --- /dev/null +++ b/proxmox/services/fusionauth.yml @@ -0,0 +1,37 @@ +version: '3.8' + +# Deploy with: +# docker stack deploy -c fusionauth.yml fusionauth +# +# Runs on: CADDY_INSTANCE (ip-10-0-1-168) +# FusionAuth is Java-based and memory hungry — deployed on caddy node (t3.large, 8GB) +# Accessible publicly via Caddy reverse proxy at auth.yourdomain.com + +services: + fusionauth: + image: fusionauth/fusionauth-app:latest + environment: + DATABASE_URL: jdbc:postgresql://postgres:5432/fusionauth_db + DATABASE_ROOT_USERNAME: postgres + DATABASE_ROOT_PASSWORD: ${POSTGRES_PASSWORD} + DATABASE_USERNAME: fusionauth_user + DATABASE_PASSWORD: ${FUSIONAUTH_DB_PASSWORD} + FUSIONAUTH_APP_MEMORY: 512M + FUSIONAUTH_APP_RUNTIME_MODE: production + SEARCH_TYPE: database + networks: + - overlay-net + deploy: + replicas: 1 + placement: + constraints: + - node.hostname == ip-10-0-1-168 + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + +networks: + overlay-net: + external: true + name: ${OVERLAY_NETWORK:-homelab-net} diff --git a/proxmox/services/gen-env.sh b/proxmox/services/gen-env.sh new file mode 100755 index 0000000..1a6e6b7 --- /dev/null +++ b/proxmox/services/gen-env.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -uo pipefail + +if [ $# -eq 0 ]; then + echo "Usage: gen-env.sh " + exit 1 +fi + +PREFIX="homelab" +out="" + +for var in $(sed -n 's/.*\${{\([A-Z_]*\)}}.*/\1/p' "$1" | grep -v OVERLAY_NETWORK | sort -u); do + val=$(pass "$PREFIX/$var" 2>/dev/null) || continue + out+="$var=$val " +done + +echo "$out" diff --git a/proxmox/services/ghost.yml b/proxmox/services/ghost.yml new file mode 100644 index 0000000..4388f94 --- /dev/null +++ b/proxmox/services/ghost.yml @@ -0,0 +1,99 @@ +version: '3.8' + +# Deploy with: +# docker stack deploy -c ghost.yml ghost +# +# Runs on: adder-ghost +# Three Ghost blog instances, each with its own port and MariaDB database. +# Ghost 1: port 2368, Ghost 2: port 2369, Ghost 3: port 2370 + +services: + ghost1: + image: ghost:5-alpine + environment: + database__client: mysql + database__connection__host: mariadb_mariadb + database__connection__port: 3306 + database__connection__user: ghost1_user + database__connection__password: ${GHOST1_DB_PASSWORD} + database__connection__database: ghost1_db + url: ${GHOST1_URL:-http://localhost:2368} + ports: + - "2368:2368" + volumes: + - ghost1_data:/var/lib/ghost/content + networks: + - overlay-net + deploy: + replicas: 1 + placement: + constraints: + - node.hostname == adder-ghost + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + + ghost2: + image: ghost:5-alpine + environment: + database__client: mysql + database__connection__host: mariadb_mariadb + database__connection__port: 3306 + database__connection__user: ghost2_user + database__connection__password: ${GHOST2_DB_PASSWORD} + database__connection__database: ghost2_db + url: ${GHOST2_URL:-http://localhost:2369} + server__port: 2369 + ports: + - "2369:2369" + volumes: + - ghost2_data:/var/lib/ghost/content + networks: + - overlay-net + deploy: + replicas: 1 + placement: + constraints: + - node.hostname == adder-ghost + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + + ghost3: + image: ghost:5-alpine + environment: + database__client: mysql + database__connection__host: mariadb_mariadb + database__connection__port: 3306 + database__connection__user: ghost3_user + database__connection__password: ${GHOST3_DB_PASSWORD} + database__connection__database: ghost3_db + url: ${GHOST3_URL:-http://localhost:2370} + server__port: 2370 + ports: + - "2370:2370" + volumes: + - ghost3_data:/var/lib/ghost/content + networks: + - overlay-net + deploy: + replicas: 1 + placement: + constraints: + - node.hostname == adder-ghost + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + +volumes: + ghost1_data: + ghost2_data: + ghost3_data: + +networks: + overlay-net: + external: true + name: ${OVERLAY_NETWORK:-homelab-net} diff --git a/proxmox/services/mariadb.yml b/proxmox/services/mariadb.yml new file mode 100644 index 0000000..0142852 --- /dev/null +++ b/proxmox/services/mariadb.yml @@ -0,0 +1,35 @@ +version: '3.8' + +# Deploy with: +# docker stack deploy -c mariadb.yml mariadb +# +# Runs on: adder-ghost + +services: + mariadb: + image: mariadb:11 + environment: + MYSQL_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD} + ports: + - "3306:3306" + volumes: + - mariadb_data:/var/lib/mysql + networks: + - overlay-net + deploy: + replicas: 1 + placement: + constraints: + - node.hostname == adder-ghost + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + +volumes: + mariadb_data: + +networks: + overlay-net: + external: true + name: ${OVERLAY_NETWORK:-homelab-net} diff --git a/proxmox/services/monerod-ban-list.txt b/proxmox/services/monerod-ban-list.txt new file mode 100644 index 0000000..e69de29 diff --git a/proxmox/services/monerod.yml b/proxmox/services/monerod.yml new file mode 100644 index 0000000..c80b097 --- /dev/null +++ b/proxmox/services/monerod.yml @@ -0,0 +1,42 @@ +version: '3.8' + +# Deploy with: +# docker stack deploy -c monerod.yml monerod +# +# Runs on: game node +# Restricted RPC node with pruning enabled to reduce disk usage. +# Blockchain data is bind-mounted from NAS at /mnt/nas/.bitmonero. +# Restricted RPC exposed on port 18089 for external wallet access. + +services: + monerod: + image: ghcr.io/sethforprivacy/simple-monerod:latest + command: + - --rpc-restricted-bind-ip=0.0.0.0 + - --rpc-restricted-bind-port=18089 + - --no-igd + - --enable-dns-blocklist + - --ban-list=/home/monero/ban_list.txt + - --prune-blockchain + ports: + - "18080:18080" + - "18089:18089" + volumes: + - /mnt/nas/.bitmonero:/home/monero/.bitmonero + - ./monerod-ban-list.txt:/home/monero/ban_list.txt:ro + networks: + - overlay-net + deploy: + replicas: 1 + placement: + constraints: + - node.hostname == game + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + +networks: + overlay-net: + external: true + name: ${OVERLAY_NETWORK:-homelab-net} diff --git a/proxmox/services/n8n.yml b/proxmox/services/n8n.yml new file mode 100644 index 0000000..ced0604 --- /dev/null +++ b/proxmox/services/n8n.yml @@ -0,0 +1,40 @@ +version: '3.8' + +# Deploy with: +# docker stack deploy -c n8n.yml n8n +# +# Runs on: pve-tools + +services: + n8n: + image: n8nio/n8n:latest + environment: + DB_TYPE: postgresdb + DB_POSTGRESDB_HOST: postgres_postgres + DB_POSTGRESDB_PORT: 5432 + DB_POSTGRESDB_DATABASE: n8n_db + DB_POSTGRESDB_USER: n8n_user + DB_POSTGRESDB_PASSWORD: ${N8N_DB_PASSWORD} + ports: + - "5678:5678" + volumes: + - n8n_data:/home/node/.n8n + networks: + - overlay-net + deploy: + replicas: 1 + placement: + constraints: + - node.hostname == pve-tools + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + +volumes: + n8n_data: + +networks: + overlay-net: + external: true + name: ${OVERLAY_NETWORK:-homelab-net} diff --git a/proxmox/services/nats.conf b/proxmox/services/nats.conf new file mode 100644 index 0000000..f6700fe --- /dev/null +++ b/proxmox/services/nats.conf @@ -0,0 +1,10 @@ +jetstream { + store_dir: /data +} + +http_port: 8222 + +websocket { + port: 8080 + no_tls: true +} diff --git a/proxmox/services/nats.yml b/proxmox/services/nats.yml new file mode 100644 index 0000000..9a856a8 --- /dev/null +++ b/proxmox/services/nats.yml @@ -0,0 +1,46 @@ +version: '3.8' + +# Deploy with: +# docker stack deploy -c nats.yml nats +# +# Runs on: pve-tools +# JetStream enabled for persistent messaging. +# Services connect to nats_nats:4222 via overlay, or host:4222 externally. + +services: + nats: + image: nats:latest + command: + - -c=/etc/nats/nats.conf + ports: + - "4222:4222" + - "8080:8080" + - "8223:8222" + volumes: + - nats_data:/data + configs: + - source: nats_conf + target: /etc/nats/nats.conf + networks: + - overlay-net + deploy: + replicas: 1 + placement: + constraints: + - node.hostname == pve-tools + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + +configs: + nats_conf: + file: ./nats.conf + +volumes: + nats_data: + +networks: + overlay-net: + external: true + name: ${OVERLAY_NETWORK:-homelab-net} diff --git a/proxmox/services/postgres.yml b/proxmox/services/postgres.yml new file mode 100644 index 0000000..3f0b8fa --- /dev/null +++ b/proxmox/services/postgres.yml @@ -0,0 +1,38 @@ +version: '3.8' + +# Deploy with: +# docker stack deploy -c postgres.yml postgres +# +# Runs on: POSTGRES_INSTANCE (ip-10-0-1-173) +# Creates databases and users for all services on first boot via init scripts. +# Data is persisted in a named Docker volume on the postgres node. + +services: + postgres: + image: postgres:16 + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - overlay-net + deploy: + replicas: 1 + placement: + constraints: + - node.hostname == pve-postgres + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + +volumes: + postgres_data: + +networks: + overlay-net: + external: true + name: ${OVERLAY_NETWORK:-homelab-net}