Add all swarm service definitions, CLAUDE.md, and gen-env.sh

Track all active Proxmox swarm stack YMLs, NATS config, Postgres
init SQL, and env generation script. Update .gitignore for homelab
project. Add CLAUDE.md for project context.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Samantha Atkins 2026-03-29 03:51:11 -04:00
parent ca16c1d815
commit f3017b9ab5
15 changed files with 561 additions and 34 deletions

42
.gitignore vendored
View file

@ -1,22 +1,8 @@
# Byte-compiled / optimized / DLL files # Secrets
__pycache__/ .env
*.py[cod] .env.*
*$py.class *.pem
*.key
# C extensions
*.so
# Distribution / packaging
build/
dist/
wheels/
*.egg-info/
*.egg
# Virtual environments
.venv/
venv/
ENV/
# IDE # IDE
.idea/ .idea/
@ -25,21 +11,9 @@ ENV/
*.swo *.swo
*~ *~
# Jupyter
.ipynb_checkpoints/
# Testing
.pytest_cache/
.coverage
htmlcov/
# Type checking
.mypy_cache/
# Environment variables
.env
.env.*
# OS # OS
.DS_Store .DS_Store
Thumbs.db Thumbs.db
# Claude Code
.claude/

57
CLAUDE.md Normal file
View file

@ -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 <service.yml>
# Copy yml to manager and deploy
scp proxmox/services/<service>.yml pve-postgres:~/
ssh pve-postgres "<ENV_VARS> docker stack deploy -c <service>.yml <stack_name>"
# Remove a stack
ssh pve-postgres "docker stack rm <stack_name>"
```
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

View file

@ -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;

View file

@ -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://<host>: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}

View file

@ -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}

View file

@ -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}

17
proxmox/services/gen-env.sh Executable file
View file

@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -uo pipefail
if [ $# -eq 0 ]; then
echo "Usage: gen-env.sh <service.yml>"
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"

View file

@ -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}

View file

@ -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}

View file

View file

@ -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}

40
proxmox/services/n8n.yml Normal file
View file

@ -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}

View file

@ -0,0 +1,10 @@
jetstream {
store_dir: /data
}
http_port: 8222
websocket {
port: 8080
no_tls: true
}

46
proxmox/services/nats.yml Normal file
View file

@ -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}

View file

@ -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}