From 90073c1d7a2ce962be2f8fc974f9d747032b771f Mon Sep 17 00:00:00 2001 From: James Thompson <1jamesthompson1@gmail.com> Date: Mon, 23 Mar 2026 04:29:25 +0000 Subject: [PATCH] CLean up site generation --- .env.example | 1 + .gitignore | 14 +- .pre-commit-config.yaml | 9 + Makefile | 36 +- README.md | 76 +++- devbox/.env.example | 0 devbox/docker-compose.yml | 0 docs/dev/check_env_sync.md | 5 + docs/dev/generate_docs.md | 5 + docs/index.md | 8 +- docs/recovery.md | 0 docs/services/devbox.md | 45 +-- docs/services/gitea.md | 61 +--- docs/services/jellyfin.md | 55 --- docs/services/n8n.md | 9 - docs/services/nextcloud.md | 59 +-- docs/services/obsidian-livesync.md | 15 + docs/services/obsidian.md | 42 --- docs/services/qbittorrent.md | 153 -------- docs/services/supersync.md | 9 - docs/services/traefik.md | 69 +--- docs/services/whoami.md | 29 +- gitea/.env.example | 0 gitea/docker-compose.yml | 0 jellyfin/.env.example | 4 - jellyfin/docker-compose.yml | 25 -- mkdocs.yml | 31 +- nextcloud/.env.example | 0 nextcloud/docker-compose.yml | 0 obsidian-livesync/.env.example | 0 obsidian-livesync/docker-compose.yml | 0 qbittorrent/.env.example | 8 - qbittorrent/docker-compose.yml | 61 ---- .../__pycache__/generate_docs.cpython-313.pyc | Bin 23068 -> 0 bytes scripts/check_env_sync.py | 197 ++++++++++ scripts/generate_docs.py | 338 +++++------------- traefik/.env.example | 0 traefik/README.md | 3 + traefik/docker-compose.yml | 0 whoami/docker-compose.yml | 0 40 files changed, 487 insertions(+), 880 deletions(-) mode change 100644 => 100755 .env.example mode change 100644 => 100755 .gitignore create mode 100644 .pre-commit-config.yaml mode change 100644 => 100755 Makefile mode change 100644 => 100755 README.md mode change 100644 => 100755 devbox/.env.example mode change 100644 => 100755 devbox/docker-compose.yml create mode 100644 docs/dev/check_env_sync.md create mode 100644 docs/dev/generate_docs.md mode change 100644 => 100755 docs/index.md mode change 100644 => 100755 docs/recovery.md mode change 100644 => 100755 docs/services/devbox.md mode change 100644 => 100755 docs/services/gitea.md delete mode 100644 docs/services/jellyfin.md delete mode 100644 docs/services/n8n.md mode change 100644 => 100755 docs/services/nextcloud.md create mode 100755 docs/services/obsidian-livesync.md delete mode 100644 docs/services/obsidian.md delete mode 100644 docs/services/qbittorrent.md delete mode 100644 docs/services/supersync.md mode change 100644 => 100755 docs/services/traefik.md mode change 100644 => 100755 docs/services/whoami.md mode change 100644 => 100755 gitea/.env.example mode change 100644 => 100755 gitea/docker-compose.yml delete mode 100644 jellyfin/.env.example delete mode 100644 jellyfin/docker-compose.yml mode change 100644 => 100755 mkdocs.yml mode change 100644 => 100755 nextcloud/.env.example mode change 100644 => 100755 nextcloud/docker-compose.yml mode change 100644 => 100755 obsidian-livesync/.env.example mode change 100644 => 100755 obsidian-livesync/docker-compose.yml delete mode 100644 qbittorrent/.env.example delete mode 100644 qbittorrent/docker-compose.yml delete mode 100644 scripts/__pycache__/generate_docs.cpython-313.pyc create mode 100644 scripts/check_env_sync.py mode change 100644 => 100755 traefik/.env.example create mode 100644 traefik/README.md mode change 100644 => 100755 traefik/docker-compose.yml mode change 100644 => 100755 whoami/docker-compose.yml diff --git a/.env.example b/.env.example old mode 100644 new mode 100755 index a5e5d61..6684dd2 --- a/.env.example +++ b/.env.example @@ -12,6 +12,7 @@ USER_GID=1000 # Domain DOMAIN=sjhl.nz +DOMAIN_WWW=__CHANGEME__ # Timezone TZ=Pacific/Auckland diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index 869f935..b8defd9 --- a/.gitignore +++ b/.gitignore @@ -3,21 +3,21 @@ *.env !.env.example -# Docker runtime -**/acme.json +# Docker runtime files +**/*.json **/logs/ **/*.log -# OS -.DS_Store -Thumbs.db - # MkDocs site/ # Editor .vscode/ -.idea/ *.swp *.swo *~ + +# python +__pycache__/ +*.pyc +*.pyo diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..06f8078 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,9 @@ +repos: + - repo: local + hooks: + - id: env-sync + name: Sync .env files from .env.example + entry: python3 scripts/check_env_sync.py --fix + language: system + pass_filenames: false + always_run: true diff --git a/Makefile b/Makefile old mode 100644 new mode 100755 index 07a887e..8767db8 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ -SERVICES=traefik whoami gitea nextcloud qbittorrent jellyfin devbox obsidian n8n supersync +SERVICES=traefik whoami gitea nextcloud devbox -.PHONY: up down restart backup docs generate-docs logs status +.PHONY: up down restart backup init-env env-sync docs generate-docs serve-docs logs status up: @for svc in $(SERVICES); do \ @@ -36,8 +36,38 @@ status: backup: ./backup.sh +init-env: + @echo "Initializing .env files from .env.example..." + @find . -path './.git' -prune -o -type f -name '.env.example' -print | while read -r example; do \ + env_file="$${example%.example}"; \ + if [ -f "$$env_file" ]; then \ + echo "Skip (exists): $$env_file"; \ + else \ + cp "$$example" "$$env_file"; \ + echo "Created: $$env_file"; \ + fi; \ + done + +env-sync: + python3 scripts/check_env_sync.py --fix + generate-docs: python3 scripts/generate_docs.py docs: generate-docs - mkdocs build + @command -v uvx >/dev/null 2>&1 || { \ + echo "Error: uvx is not installed or not in PATH."; \ + echo "Install with: curl -LsSf https://astral.sh/uv/install.sh | sh"; \ + echo "Then run: source $$HOME/.local/bin/env"; \ + exit 1; \ + } + uvx --from mkdocs-material --with mkdocs-include-markdown-plugin mkdocs build + +serve-docs: generate-docs + @command -v uvx >/dev/null 2>&1 || { \ + echo "Error: uvx is not installed or not in PATH."; \ + echo "Install with: curl -LsSf https://astral.sh/uv/install.sh | sh"; \ + echo "Then run: source $$HOME/.local/bin/env"; \ + exit 1; \ + } + uvx --from mkdocs-material --with mkdocs-include-markdown-plugin mkdocs serve --dev-addr 0.0.0.0:8000 diff --git a/README.md b/README.md old mode 100644 new mode 100755 index c7f2325..46f1000 --- a/README.md +++ b/README.md @@ -29,15 +29,10 @@ make up | Service | Description | URL | |---------|-------------|-----| -| traefik | Reverse proxy, TLS termination | `dashboard.sjhl.nz` | -| gitea | Self-hosted Git | `gitea.sjhl.nz` | -| nextcloud | Cloud storage (AIO) | `nextcloud.sjhl.nz` | -| qbittorrent | Torrent client (VPN) | Internal only | -| jellyfin | Media server | `jellyfin.sjhl.nz` | -| devbox | Dev container | SSH:46573 | +| gitea | Self-hosted Git | [gitea.sjhl.nz](https://gitea.sjhl.nz) | +| nextcloud | Cloud storage (AIO) | [nextcloud.sjhl.nz](https://nextcloud.sjhl.nz) | +| devbox | Dev container | Internal only | | obsidian | CouchDB for Obsidian LiveSync | Internal | -| n8n | Workflow automation (planned) | - | -| supersync | File sync (planned) | - | ## Commands @@ -48,9 +43,56 @@ make restart # Restart all services make status # Show status of all services make logs # Show recent logs make backup # Run Restic backup +make init-env # Create missing .env files from templates +make env-sync # Rewrite .env files to match templates make docs # Build MkDocs site ``` +## Documentation + +This repo uses **MkDocs** with the **include-markdown** plugin to generate live documentation. + +### How it works: +1. Each service page is auto-generated to include: + - `docker-compose.yml` configuration + - `Dockerfile` (if exists) + - `README.md` (if exists) + - `.env.example` for environment variables + +2. Files are automatically pulled at build time—changes to service files appear in docs immediately + +3. Build docs with: + ```bash + make docs # Generate docs and build site + make serve-docs # Serve docs locally at http://localhost:8000 + ``` + +### Pre-commit Checks + +This repo includes a pre-commit hook that syncs .env.example from .env schema. + +Behavior: +- `.env` defines the schema: keys, structure, comments +- `.env.example` provides defaults and is regenerated from `.env` +- Keys in `.env.example` are kept only if they exist in `.env` +- Default values are preserved from `.env.example` +- New keys from `.env` get `__CHANGEME__` placeholder if not in template + +One-time install: + +```bash +uvx pre-commit install +``` + +Run manually at any time: + +```bash +uvx pre-commit run --all-files +make env-sync +``` + +If files were updated, re-stage and commit again. + ## Directory Structure ``` @@ -58,16 +100,13 @@ homelab-infra/ ├── .env.example # Global env template ├── Makefile # Service management ├── backup.sh # Restic backup script -├── traefik/ # Reverse proxy -├── gitea/ # Git hosting -├── nextcloud/ # Cloud storage (AIO) -├── qbittorrent/ # Torrent + VPN stack -├── jellyfin/ # Media server -├── devbox/ # Development container -├── obsidian/ # CouchDB for LiveSync -├── n8n/ # Workflow automation -├── supersync/ # File sync -└── docs/ # MkDocs documentation +├── .pre-commit-config.yaml # Pre-commit hooks +├── docs/ # MkDocs documentation +├── mkdocs.yml # MkDocs config +├── scripts/ # Utility scripts +└── [service subdirs]/ # All services have their own subdir + + ``` ## Data Locations @@ -75,5 +114,4 @@ homelab-infra/ - Configs (in Git): this repo - Secrets (not in Git): per-service `.env` files - Persistent data: `/mnt/storage/docker-data/` -- Media: `/mnt/storage` - Backups: `/mnt/storage/backups` diff --git a/devbox/.env.example b/devbox/.env.example old mode 100644 new mode 100755 diff --git a/devbox/docker-compose.yml b/devbox/docker-compose.yml old mode 100644 new mode 100755 diff --git a/docs/dev/check_env_sync.md b/docs/dev/check_env_sync.md new file mode 100644 index 0000000..5f9e1c1 --- /dev/null +++ b/docs/dev/check_env_sync.md @@ -0,0 +1,5 @@ +# check_env_sync + +> Python script documentation + +::: scripts.check_env_sync diff --git a/docs/dev/generate_docs.md b/docs/dev/generate_docs.md new file mode 100644 index 0000000..959b338 --- /dev/null +++ b/docs/dev/generate_docs.md @@ -0,0 +1,5 @@ +# generate_docs + +> Python script documentation + +::: scripts.generate_docs diff --git a/docs/index.md b/docs/index.md old mode 100644 new mode 100755 index fc0b0eb..2712f28 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,6 @@ -# Homelab Documentation +# SJHL Documentation -> Auto-generated from `docker-compose.yml` files +Welcome to Seirian & James' homelab documentation! This is a docs site that is built to easily show all the configs we use. MOst of the content is auto-generated from the actual config files, so it should always be up to date. This should be completely publically viewable as all private information is kept in `.env` files that are not committed to Git. It should provide good information on how to recover and rebuild the homelab if needed, and also just be a nice reference for how everything is configured. ## Data Classification @@ -17,10 +17,8 @@ |---------|-------|--------| | [devbox](services/devbox.md) | `devbox-devcontainer` | active | | [gitea](services/gitea.md) | `gitea/gitea:1.24.3` | active | -| [jellyfin](services/jellyfin.md) | `jellyfin/jellyfin:latest` | active | | [nextcloud](services/nextcloud.md) | `ghcr.io/nextcloud-releases/all-in-one:latest` | active | -| [obsidian](services/obsidian.md) | `couchdb:latest` | active | -| [qbittorrent](services/qbittorrent.md) | `qmcgaw/gluetun`, `lscr.io/linuxserver/qbittorrent:latest`, `linuxserver/jackett:latest` | active | +| [obsidian-livesync](services/obsidian-livesync.md) | `couchdb:latest` | active | | [traefik](services/traefik.md) | `traefik:v3.6` | active | | [whoami](services/whoami.md) | `traefik/whoami` | active | diff --git a/docs/recovery.md b/docs/recovery.md old mode 100644 new mode 100755 diff --git a/docs/services/devbox.md b/docs/services/devbox.md old mode 100644 new mode 100755 index a84a4d6..7c29b44 --- a/docs/services/devbox.md +++ b/docs/services/devbox.md @@ -1,42 +1,15 @@ # devbox -> Auto-generated from `docker-compose.yml` +> This page auto-includes the service configuration files. -| Field | Value | -|-------|-------| -| **Image** | `devbox-devcontainer` | -| **Container** | `devcontainer` | -| **Restart** | `unless-stopped` | +## Docker Compose Configuration -### Environment +```yaml +--8<-- "devbox/docker-compose.yml" +``` -**Env files:** `../.env` +## Environment Variables (`.env.example`) -### Secrets (from `.env.example`) - -These variables must be set in the service's `.env` file: - -| Variable | Default | -|----------|---------| -| `DEVBOX_SSH_PORT` | `46573` | -| `DEVBOX_MEM_LIMIT` | `10g` | -| `DEVBOX_SWAP_LIMIT` | `20g` | - -### Ports - -| Host | Container | Protocol | -|------|-----------|----------| -| `${DEVBOX_SSH_PORT:-46573}` | `2222` | tcp | - -### Volumes - -| Host Path | Container Path | Mode | -|-----------|----------------|------| -| `${STORAGE_PATH}/devbox` | `/home/devuser` | rw | - -### Resources - -- Memory limit: `${DEVBOX_MEM_LIMIT:-10g}` -- Memory+Swap limit: `${DEVBOX_SWAP_LIMIT:-20g}` - ---- +```bash +--8<-- "devbox/.env.example" +``` diff --git a/docs/services/gitea.md b/docs/services/gitea.md old mode 100644 new mode 100755 index f66a31f..23d472b --- a/docs/services/gitea.md +++ b/docs/services/gitea.md @@ -1,58 +1,15 @@ # gitea -> Auto-generated from `docker-compose.yml` +> This page auto-includes the service configuration files. -| Field | Value | -|-------|-------| -| **Image** | `gitea/gitea:1.24.3` | -| **Container** | `gitea` | -| **Restart** | `always` | +## Docker Compose Configuration -### Environment +```yaml +--8<-- "gitea/docker-compose.yml" +``` -**Env files:** `../.env`, `.env` +## Environment Variables (`.env.example`) -| Variable | Value | -|----------|-------| -| `USER_UID` | `${USER_UID}` | -| `USER_GID` | `${USER_GID}` | -| `GITEA__database__DB_TYPE` | `sqlite3` | -| `GITEA__server__ROOT_URL` | `https://gitea.${DOMAIN}/` | -| `USER` | `git` | -| `GITEA_CUSTOM` | `/data/gitea` | - -### Ports - -| Host | Container | Protocol | -|------|-----------|----------| -| `222` | `22` | tcp | - -### Volumes - -| Host Path | Container Path | Mode | -|-----------|----------------|------| -| `${STORAGE_PATH}/gitea` | `/data` | rw | -| `/etc/timezone` | `/etc/timezone` | ro | -| `/etc/localtime` | `/etc/localtime` | ro | - -### Networks - -- `gitea` (internal) -- `web` (internal) - -### Labels - -| Key | Value | -|-----|-------| -| `traefik.enable` | `true` | -| `traefik.http.routers.gitea.entrypoints` | `websecure` | -| `traefik.http.routers.gitea.rule` | `Host(`gitea.${DOMAIN}`)` | -| `traefik.http.routers.gitea.tls.certresolver` | `letsencrypt` | -| `traefik.http.services.gitea.loadbalancer.server.port` | `3000` | - -### Networks (compose-level) - -- `gitea` (internal) -- `web` (external) - ---- +```bash +--8<-- "gitea/.env.example" +``` diff --git a/docs/services/jellyfin.md b/docs/services/jellyfin.md deleted file mode 100644 index ad3b506..0000000 --- a/docs/services/jellyfin.md +++ /dev/null @@ -1,55 +0,0 @@ -# jellyfin - -> Auto-generated from `docker-compose.yml` - -| Field | Value | -|-------|-------| -| **Image** | `jellyfin/jellyfin:latest` | -| **Container** | `jellyfin` | -| **Restart** | `unless-stopped` | - -### Environment - -**Env files:** `../.env` - -### Secrets (from `.env.example`) - -These variables must be set in the service's `.env` file: - -| Variable | Default | -|----------|---------| -| `JELLYFIN_PORT` | `8096` | - -### Ports - -| Host | Container | Protocol | -|------|-----------|----------| -| `${JELLYFIN_PORT:-8096}` | `8096` | tcp | - -### Volumes - -| Host Path | Container Path | Mode | -|-----------|----------------|------| -| `${STORAGE_PATH}/jellyfin/config` | `/config` | rw | -| `${STORAGE_PATH}/jellyfin/cache` | `/cache` | rw | -| `${MEDIA_PATH}` | `/media` | rw | - -### Networks - -- `web` (internal) - -### Labels - -| Key | Value | -|-----|-------| -| `traefik.enable` | `true` | -| `traefik.http.routers.jellyfin.entrypoints` | `websecure` | -| `traefik.http.routers.jellyfin.rule` | `Host(`jellyfin.${DOMAIN}`)` | -| `traefik.http.routers.jellyfin.tls.certresolver` | `letsencrypt` | -| `traefik.http.services.jellyfin.loadbalancer.server.port` | `8096` | - -### Networks (compose-level) - -- `web` (external) - ---- diff --git a/docs/services/n8n.md b/docs/services/n8n.md deleted file mode 100644 index 162fe62..0000000 --- a/docs/services/n8n.md +++ /dev/null @@ -1,9 +0,0 @@ -# N8N - -Workflow automation platform. **Planned - not yet deployed.** - -## Planned Configuration - -- **URL:** `https://n8n.sjhl.nz` -- **Port:** 5678 -- **Data:** `/mnt/storage/docker-data/n8n` diff --git a/docs/services/nextcloud.md b/docs/services/nextcloud.md old mode 100644 new mode 100755 index 51d8fb9..d581b26 --- a/docs/services/nextcloud.md +++ b/docs/services/nextcloud.md @@ -1,56 +1,15 @@ # nextcloud -> Auto-generated from `docker-compose.yml` +> This page auto-includes the service configuration files. -| Field | Value | -|-------|-------| -| **Image** | `ghcr.io/nextcloud-releases/all-in-one:latest` | -| **Container** | `nextcloud-aio-mastercontainer` | -| **Restart** | `always` | +## Docker Compose Configuration -### Environment +```yaml +--8<-- "nextcloud/docker-compose.yml" +``` -**Env files:** `../.env` +## Environment Variables (`.env.example`) -| Variable | Value | -|----------|-------| -| `APACHE_PORT` | `11000` | -| `NEXTCLOUD_DATADIR` | `${STORAGE_PATH}/nextcloud` | -| `APACHE_ADDITIONAL_NETWORK` | `web` | - -### Secrets (from `.env.example`) - -These variables must be set in the service's `.env` file: - -| Variable | Default | -|----------|---------| -| `AIO_PORT` | `8081` | - -### Ports - -| Host | Container | Protocol | -|------|-----------|----------| -| `${AIO_PORT:-8081}` | `8080` | tcp | - -### Volumes - -| Host Path | Container Path | Mode | -|-----------|----------------|------| -| `nextcloud_aio_mastercontainer` | `/mnt/docker-aio-config` | rw | -| `/var/run/docker.sock` | `/var/run/docker.sock` | ro | - -### Networks - -- `nextcloud-aio` (internal) -- `web` (internal) - -### Named Volumes (compose-level) - -- `nextcloud_aio_mastercontainer` (managed by compose) - -### Networks (compose-level) - -- `nextcloud-aio` (internal) -- `web` (external) - ---- +```bash +--8<-- "nextcloud/.env.example" +``` diff --git a/docs/services/obsidian-livesync.md b/docs/services/obsidian-livesync.md new file mode 100755 index 0000000..02203e7 --- /dev/null +++ b/docs/services/obsidian-livesync.md @@ -0,0 +1,15 @@ +# obsidian-livesync + +> This page auto-includes the service configuration files. + +## Docker Compose Configuration + +```yaml +--8<-- "obsidian-livesync/docker-compose.yml" +``` + +## Environment Variables (`.env.example`) + +```bash +--8<-- "obsidian-livesync/.env.example" +``` diff --git a/docs/services/obsidian.md b/docs/services/obsidian.md deleted file mode 100644 index 2f58329..0000000 --- a/docs/services/obsidian.md +++ /dev/null @@ -1,42 +0,0 @@ -# obsidian - -> Auto-generated from `docker-compose.yml` - -| Field | Value | -|-------|-------| -| **Image** | `couchdb:latest` | -| **Container** | `obsidian-livesync` | -| **Restart** | `unless-stopped` | - -### Environment - -**Env files:** `../.env`, `.env` - -| Variable | Value | -|----------|-------| -| `COUCHDB_USER` | `${COUCHDB_USER:-admin}` | -| `COUCHDB_PASSWORD` | `${COUCHDB_PASSWORD}` | - -### Secrets (from `.env.example`) - -These variables must be set in the service's `.env` file: - -| Variable | Default | -|----------|---------| -| `COUCHDB_USER` | `admin` | -| `COUCHDB_PASSWORD` | `REPLACE_WITH_STRONG_PASSWORD` | -| `COUCHDB_PORT` | `5984` | - -### Ports - -| Host | Container | Protocol | -|------|-----------|----------| -| `${COUCHDB_PORT:-5984}` | `5984` | tcp | - -### Volumes - -| Host Path | Container Path | Mode | -|-----------|----------------|------| -| `${STORAGE_PATH}/obsidian` | `/opt/couchdb/data` | rw | - ---- diff --git a/docs/services/qbittorrent.md b/docs/services/qbittorrent.md deleted file mode 100644 index 9534bd0..0000000 --- a/docs/services/qbittorrent.md +++ /dev/null @@ -1,153 +0,0 @@ -# qbittorrent - -> Auto-generated from `docker-compose.yml` - -## `gluetun` - -| Field | Value | -|-------|-------| -| **Image** | `qmcgaw/gluetun` | -| **Container** | `qbittorrent_gluetun` | -| **Restart** | `unless-stopped` | - -### Environment - -**Env files:** `../.env`, `.env` - -| Variable | Value | -|----------|-------| -| `VPN_SERVICE_PROVIDER` | `protonvpn` | -| `VPN_TYPE` | `wireguard` | -| `VPN_PORT_FORWARDING` | `on` | -| `TZ` | `${TZ}` | -| `SERVER_COUNTRIES` | `New Zealand` | -| `PORT_FORWARD_ONLY` | `on` | -| `VPN_PORT_FORWARDING_UP_COMMAND` | `/bin/sh -c 'wget -O- --post-data "json={\"listen_port\":{{PORTS}}}" http://localhost:8080/api/v2/app/setPreferences 2>/dev/null || true'` | - -### Secrets (from `.env.example`) - -These variables must be set in the service's `.env` file: - -| Variable | Default | -|----------|---------| -| `WIREGUARD_PRIVATE_KEY` | `REPLACE_WITH_YOUR_WIREGUARD_PRIVATE_KEY` | -| `QBITTORRENT_PORT` | `8080` | -| `JACKETT_PORT` | `9117` | - -### Ports - -| Host | Container | Protocol | -|------|-----------|----------| -| `${QBITTORRENT_PORT:-8080}` | `8080` | tcp | -| `${JACKETT_PORT:-9117}` | `9117` | tcp | - -### Volumes - -| Host Path | Container Path | Mode | -|-----------|----------------|------| -| `${STORAGE_PATH}/qbittorrent/gluetun` | `/gluetun` | rw | -| `${STORAGE_PATH}/qbittorrent/wireguard` | `/gluetun/wireguard` | rw | - -### Networks - -- `qbittorrent` (internal) - -### Capabilities - -`NET_ADMIN` - -### Devices - -- `/dev/net/tun:/dev/net/tun` - -### Networks (compose-level) - -- `qbittorrent` (internal) - ---- - -## `qbittorrent` - -| Field | Value | -|-------|-------| -| **Image** | `lscr.io/linuxserver/qbittorrent:latest` | -| **Container** | `qbittorrent` | -| **Restart** | `unless-stopped` | - -### Environment - -| Variable | Value | -|----------|-------| -| `TZ` | `${TZ}` | -| `WEBUI_PORT` | `8080` | -| `PUID` | `${USER_UID}` | -| `PGID` | `${USER_GID}` | - -### Secrets (from `.env.example`) - -These variables must be set in the service's `.env` file: - -| Variable | Default | -|----------|---------| -| `WIREGUARD_PRIVATE_KEY` | `REPLACE_WITH_YOUR_WIREGUARD_PRIVATE_KEY` | -| `QBITTORRENT_PORT` | `8080` | -| `JACKETT_PORT` | `9117` | - -### Volumes - -| Host Path | Container Path | Mode | -|-----------|----------------|------| -| `${STORAGE_PATH}/qbittorrent/config` | `/config` | rw | -| `${MEDIA_PATH}/torrents` | `/downloads` | rw | - -### Dependencies - -- `gluetun` - -### Networks (compose-level) - -- `qbittorrent` (internal) - ---- - -## `jackett` - -| Field | Value | -|-------|-------| -| **Image** | `linuxserver/jackett:latest` | -| **Container** | `jackett` | -| **Restart** | `unless-stopped` | - -### Environment - -| Variable | Value | -|----------|-------| -| `TZ` | `${TZ}` | -| `PUID` | `${USER_UID}` | -| `PGID` | `${USER_GID}` | - -### Secrets (from `.env.example`) - -These variables must be set in the service's `.env` file: - -| Variable | Default | -|----------|---------| -| `WIREGUARD_PRIVATE_KEY` | `REPLACE_WITH_YOUR_WIREGUARD_PRIVATE_KEY` | -| `QBITTORRENT_PORT` | `8080` | -| `JACKETT_PORT` | `9117` | - -### Volumes - -| Host Path | Container Path | Mode | -|-----------|----------------|------| -| `${STORAGE_PATH}/qbittorrent/config/jackett` | `/config` | rw | - -### Dependencies - -- `gluetun` - -### Networks (compose-level) - -- `qbittorrent` (internal) - ---- diff --git a/docs/services/supersync.md b/docs/services/supersync.md deleted file mode 100644 index 24ae5bc..0000000 --- a/docs/services/supersync.md +++ /dev/null @@ -1,9 +0,0 @@ -# Supersync - -File synchronization service. **Planned - not yet deployed.** - -## Planned Configuration - -- **URL:** `https://supersync.sjhl.nz` -- **Port:** 8443 -- **Data:** `/mnt/storage/docker-data/supersync` diff --git a/docs/services/traefik.md b/docs/services/traefik.md old mode 100644 new mode 100755 index 4ca12ca..0eef96f --- a/docs/services/traefik.md +++ b/docs/services/traefik.md @@ -1,67 +1,16 @@ # traefik -> Auto-generated from `docker-compose.yml` +> This page auto-includes the service configuration files. -| Field | Value | -|-------|-------| -| **Image** | `traefik:v3.6` | -| **Container** | `traefik` | -| **Restart** | `unless-stopped` | +--8<-- "traefik/README.md" +## Docker Compose Configuration -### Command - -``` ---entrypoints.web.address=:80 --entrypoints.web.http.redirections.entrypoint.to=websecure --entrypoints.web.http.redirections.entrypoint.scheme=https --entrypoints.websecure.address=:443 --providers.docker=true --providers.docker.exposedbydefault=false --providers.docker.network=web --certificatesresolvers.letsencrypt.acme.httpchallenge=true --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web --certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL} --certificatesresolvers.letsencrypt.acme.storage=/certs/acme.json --metrics.prometheus=true --accesslog=true --providers.file.directory=/dynamic --providers.file.watch=true +```yaml +--8<-- "traefik/docker-compose.yml" ``` -### Secrets (from `.env.example`) +## Environment Variables (`.env.example`) -These variables must be set in the service's `.env` file: - -| Variable | Default | -|----------|---------| -| `TRAEFIK_DASHBOARD_PORT` | `8082` | -| `ACME_EMAIL` | `letsencrypt@example.com` | -| `DASHBOARD_BASIC_AUTH` | `admin:$$apr1$$changeme$$REPLACE_WITH_HTPASSWD_HASH` | - -### Ports - -| Host | Container | Protocol | -|------|-----------|----------| -| `80` | `80` | tcp | -| `443` | `443` | tcp | -| `${TRAEFIK_DASHBOARD_PORT:-8082}` | `8080` | tcp | - -### Volumes - -| Host Path | Container Path | Mode | -|-----------|----------------|------| -| `/var/run/docker.sock` | `/var/run/docker.sock` | ro | -| `${STORAGE_PATH}/traefik/certs` | `/certs` | rw | -| `${STORAGE_PATH}/traefik/dynamic` | `/dynamic` | ro | - -### Networks - -- `web` (internal) - -### Security Options - -- `no-new-privileges:true` - -### Labels - -| Key | Value | -|-----|-------| -| `traefik.enable` | `true` | -| `traefik.http.middlewares.dashboard-auth.basicauth.users` | `${DASHBOARD_BASIC_AUTH}` | -| `traefik.http.routers.dashboard.entrypoints` | `websecure` | -| `traefik.http.routers.dashboard.middlewares` | `dashboard-auth@docker` | -| `traefik.http.routers.dashboard.rule` | `Host(`dashboard.${DOMAIN}`)` | -| `traefik.http.routers.dashboard.service` | `api@internal` | -| `traefik.http.routers.dashboard.tls` | `true` | - -### Networks (compose-level) - -- `web` (external) - ---- +```bash +--8<-- "traefik/.env.example" +``` diff --git a/docs/services/whoami.md b/docs/services/whoami.md old mode 100644 new mode 100755 index adeeccc..7af73f9 --- a/docs/services/whoami.md +++ b/docs/services/whoami.md @@ -1,28 +1,9 @@ # whoami -> Auto-generated from `docker-compose.yml` +> This page auto-includes the service configuration files. -| Field | Value | -|-------|-------| -| **Image** | `traefik/whoami` | -| **Container** | `whoami` | -| **Restart** | `unless-stopped` | +## Docker Compose Configuration -### Networks - -- `web` (internal) - -### Labels - -| Key | Value | -|-----|-------| -| `traefik.enable` | `true` | -| `traefik.http.routers.whoami.entrypoints` | `websecure` | -| `traefik.http.routers.whoami.rule` | `Host(`whoami.${DOMAIN}`)` | -| `traefik.http.routers.whoami.tls` | `true` | - -### Networks (compose-level) - -- `web` (external) - ---- +```yaml +--8<-- "whoami/docker-compose.yml" +``` diff --git a/gitea/.env.example b/gitea/.env.example old mode 100644 new mode 100755 diff --git a/gitea/docker-compose.yml b/gitea/docker-compose.yml old mode 100644 new mode 100755 diff --git a/jellyfin/.env.example b/jellyfin/.env.example deleted file mode 100644 index 7691a3e..0000000 --- a/jellyfin/.env.example +++ /dev/null @@ -1,4 +0,0 @@ -# jellyfin/.env -# Copy to .env and fill in real values. NEVER commit .env. - -JELLYFIN_PORT=8096 diff --git a/jellyfin/docker-compose.yml b/jellyfin/docker-compose.yml deleted file mode 100644 index 46221a6..0000000 --- a/jellyfin/docker-compose.yml +++ /dev/null @@ -1,25 +0,0 @@ -services: - jellyfin: - image: jellyfin/jellyfin:latest - container_name: jellyfin - restart: unless-stopped - volumes: - - ${STORAGE_PATH}/jellyfin/config:/config - - ${STORAGE_PATH}/jellyfin/cache:/cache - - ${MEDIA_PATH}:/media - ports: - - "${JELLYFIN_PORT:-8096}:8096" - networks: - - web - env_file: - - ../.env - labels: - - "traefik.enable=true" - - "traefik.http.routers.jellyfin.entrypoints=websecure" - - "traefik.http.routers.jellyfin.rule=Host(`jellyfin.${DOMAIN}`)" - - "traefik.http.routers.jellyfin.tls.certresolver=letsencrypt" - - "traefik.http.services.jellyfin.loadbalancer.server.port=8096" - -networks: - web: - external: true diff --git a/mkdocs.yml b/mkdocs.yml old mode 100644 new mode 100755 index 2021448..2c4437d --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,17 +1,22 @@ -site_name: Homelab Documentation +site_name: Seirian & James' Homelab +plugins: +- search +markdown_extensions: +- toc: + permalink: true +- pymdownx.snippets: + base_path: + - . nav: - - Home: index.md - - Recovery Reference: recovery.md - - Services: - - Traefik: services/traefik.md - - Gitea: services/gitea.md - - Nextcloud: services/nextcloud.md - - qBittorrent: services/qbittorrent.md - - Jellyfin: services/jellyfin.md - - Devbox: services/devbox.md - - Obsidian: services/obsidian.md - - N8N: services/n8n.md - - Supersync: services/supersync.md +- Home: index.md +- Recovery Reference: recovery.md +- Services: + - Devbox: services/devbox.md + - Gitea: services/gitea.md + - Nextcloud: services/nextcloud.md + - Obsidian Livesync: services/obsidian-livesync.md + - Traefik: services/traefik.md + - Whoami: services/whoami.md theme: name: material palette: diff --git a/nextcloud/.env.example b/nextcloud/.env.example old mode 100644 new mode 100755 diff --git a/nextcloud/docker-compose.yml b/nextcloud/docker-compose.yml old mode 100644 new mode 100755 diff --git a/obsidian-livesync/.env.example b/obsidian-livesync/.env.example old mode 100644 new mode 100755 diff --git a/obsidian-livesync/docker-compose.yml b/obsidian-livesync/docker-compose.yml old mode 100644 new mode 100755 diff --git a/qbittorrent/.env.example b/qbittorrent/.env.example deleted file mode 100644 index 423f8c4..0000000 --- a/qbittorrent/.env.example +++ /dev/null @@ -1,8 +0,0 @@ -# qbittorrent/.env -# Copy to .env and fill in real values. NEVER commit .env. -# WireGuard private key from ProtonVPN - -WIREGUARD_PRIVATE_KEY=REPLACE_WITH_YOUR_WIREGUARD_PRIVATE_KEY - -QBITTORRENT_PORT=8080 -JACKETT_PORT=9117 diff --git a/qbittorrent/docker-compose.yml b/qbittorrent/docker-compose.yml deleted file mode 100644 index 04e858b..0000000 --- a/qbittorrent/docker-compose.yml +++ /dev/null @@ -1,61 +0,0 @@ -services: - gluetun: - image: qmcgaw/gluetun - container_name: qbittorrent_gluetun - restart: unless-stopped - cap_add: - - NET_ADMIN - devices: - - /dev/net/tun:/dev/net/tun - volumes: - - ${STORAGE_PATH}/qbittorrent/gluetun:/gluetun - - ${STORAGE_PATH}/qbittorrent/wireguard:/gluetun/wireguard - ports: - - "${QBITTORRENT_PORT:-8080}:8080" - - "${JACKETT_PORT:-9117}:9117" - networks: - - qbittorrent - env_file: - - ../.env - - .env - environment: - - VPN_SERVICE_PROVIDER=protonvpn - - VPN_TYPE=wireguard - - VPN_PORT_FORWARDING=on - - TZ=${TZ} - - SERVER_COUNTRIES=New Zealand - - PORT_FORWARD_ONLY=on - - VPN_PORT_FORWARDING_UP_COMMAND=/bin/sh -c 'wget -O- --post-data "json={\"listen_port\":{{PORTS}}}" http://localhost:8080/api/v2/app/setPreferences 2>/dev/null || true' - - qbittorrent: - image: lscr.io/linuxserver/qbittorrent:latest - container_name: qbittorrent - restart: unless-stopped - network_mode: "service:gluetun" - depends_on: - - gluetun - environment: - - TZ=${TZ} - - WEBUI_PORT=8080 - - PUID=${USER_UID} - - PGID=${USER_GID} - volumes: - - ${STORAGE_PATH}/qbittorrent/config:/config - - ${MEDIA_PATH}/torrents:/downloads - - jackett: - image: linuxserver/jackett:latest - container_name: jackett - restart: unless-stopped - network_mode: "service:gluetun" - depends_on: - - gluetun - environment: - - TZ=${TZ} - - PUID=${USER_UID} - - PGID=${USER_GID} - volumes: - - ${STORAGE_PATH}/qbittorrent/config/jackett:/config - -networks: - qbittorrent: diff --git a/scripts/__pycache__/generate_docs.cpython-313.pyc b/scripts/__pycache__/generate_docs.cpython-313.pyc deleted file mode 100644 index f938b14d0c2128c78d88d44fada4de8efe443a08..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23068 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 b(W@#9H!nsr8rP?|{`W>7X)wH&!{Gk|Sx;YI diff --git a/scripts/check_env_sync.py b/scripts/check_env_sync.py new file mode 100644 index 0000000..0697a33 --- /dev/null +++ b/scripts/check_env_sync.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +"""Sync .env.example: keep structure/comments, merge in keys from .env.""" + +from __future__ import annotations + +import argparse +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +SKIP_DIRS = {".git", "site", "__pycache__"} + + +class EnvParseError(Exception): + pass + + +def discover_pairs(repo_root: Path) -> list[tuple[Path, Path]]: + pairs: list[tuple[Path, Path]] = [] + for example_path in sorted(repo_root.rglob(".env.example")): + rel_parts = set(example_path.relative_to(repo_root).parts) + if rel_parts & SKIP_DIRS: + continue + env_path = example_path.with_name(".env") + pairs.append((example_path, env_path)) + return pairs + + +def parse_env_key_values(path: Path) -> tuple[list[str], dict[str, str]]: + keys: list[str] = [] + values: dict[str, str] = {} + seen: set[str] = set() + + for idx, raw_line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1): + line = raw_line.strip() + if not line or line.startswith("#"): + continue + + if "=" not in line: + raise EnvParseError(f"{path}: line {idx} is not KEY=VALUE format") + + key, value = line.split("=", 1) + key = key.strip() + if not key: + raise EnvParseError(f"{path}: line {idx} has empty key") + + if key in seen: + raise EnvParseError(f"{path}: duplicate key '{key}'") + seen.add(key) + keys.append(key) + values[key] = value + + return keys, values + + +def parse_env_keys(path: Path) -> list[str]: + keys, _ = parse_env_key_values(path) + return keys + + +def merge_env_into_example(env_path: Path, example_values: dict[str, str]) -> str: + """ + Build .env.example from .env structure. + .env defines: keys, order, comments. + .env.example provides: default values (or placeholders for new keys). + """ + out_lines: list[str] = [] + + # Read .env to get structure/order/comments, fill in values from .env.example + for raw_line in env_path.read_text(encoding="utf-8").splitlines(): + line = raw_line.strip() + if not line or line.startswith("#") or "=" not in raw_line: + out_lines.append(raw_line) + continue + + key, value_from_env = raw_line.split("=", 1) + key = key.strip() + + # Use default from .env.example if it exists, otherwise use placeholder + if key in example_values: + out_lines.append(f"{key}={example_values[key]}") + else: + # New key not in template: use placeholder + out_lines.append(f"{key}=__CHANGEME__") + + return "\n".join(out_lines) + "\n" + + +def compare_env(example_path: Path, env_path: Path) -> list[str]: + """Check if .env.example matches .env schema.""" + errors: list[str] = [] + + if not env_path.exists(): + rel_env = env_path.relative_to(REPO_ROOT) + errors.append(f"Missing .env (schema): {rel_env}") + return errors + + try: + env_keys, _ = parse_env_key_values(env_path) + example_keys = parse_env_keys(example_path) if example_path.exists() else [] + except EnvParseError as exc: + errors.append(str(exc)) + return errors + + example_set = set(example_keys) + env_set = set(env_keys) + + missing_in_example = [k for k in env_keys if k not in example_set] + + if missing_in_example: + rel_example = example_path.relative_to(REPO_ROOT) + errors.append(f"{rel_example}: missing keys from .env schema: {', '.join(missing_in_example)}") + + return errors + + +def fix_env_sync(env_path: Path, example_path: Path) -> tuple[bool, str | None]: + """Update .env.example: .env defines schema, .env.example provides defaults.""" + rel_env = env_path.relative_to(REPO_ROOT) + rel_example = example_path.relative_to(REPO_ROOT) + + if not env_path.exists(): + return False, f"Missing .env file (schema source): {rel_env}" + + try: + _, example_values = parse_env_key_values(example_path) if example_path.exists() else ([], {}) + desired = merge_env_into_example(env_path, example_values) + except EnvParseError as exc: + return False, str(exc) + + current = example_path.read_text(encoding="utf-8") if example_path.exists() else None + if current == desired: + return False, None + + example_path.write_text(desired, encoding="utf-8") + if current is None: + return True, f"Created {rel_example} from {rel_env} schema" + return True, f"Synced {rel_example} to match {rel_env}" + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument( + "--fix", + action="store_true", + help="Update .env.example from .env schema, keeping default values", + ) + args = parser.parse_args() + + pairs = discover_pairs(REPO_ROOT) + if not pairs: + print("No .env.example files found.") + return 0 + + if args.fix: + changed: list[str] = [] + errors: list[str] = [] + + for example_path, env_path in pairs: + did_change, message = fix_env_sync(env_path, example_path) + if message: + if did_change: + changed.append(message) + else: + errors.append(message) + + if changed: + print("Synced .env.example from .env schema:\n") + for item in changed: + print(f"- {item}") + print("\nRe-stage changed .env.example files and commit again.") + return 1 + + if errors: + print("Environment file sync failed:\n") + for err in errors: + print(f"- {err}") + return 1 + + print(f"Environment files in sync ({len(pairs)} checked).") + return 0 + + all_errors: list[str] = [] + for example_path, env_path in pairs: + all_errors.extend(compare_env(example_path, env_path)) + + if all_errors: + print("Environment file validation failed:\n") + for err in all_errors: + print(f"- {err}") + return 1 + + print(f"Environment files look good ({len(pairs)} checked).") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/generate_docs.py b/scripts/generate_docs.py index b00297a..bb7f523 100755 --- a/scripts/generate_docs.py +++ b/scripts/generate_docs.py @@ -12,6 +12,11 @@ REPO_ROOT = Path(__file__).parent.parent SERVICES_DIR = REPO_ROOT DOCS_DIR = REPO_ROOT / "docs" / "services" +DISPLAY_NAME_OVERRIDES = { + "qbittorrent": "qBittorrent", + "n8n": "N8N", +} + # Load root .env.example for shared variables def load_root_env_example(): env_vars = {} @@ -168,264 +173,108 @@ def format_security(sec): return [] -def generate_service_md(service_name, compose_data, root_env, service_env): +def generate_service_md(service_name, service_dir): + """Generate service doc that includes docker-compose.yml, Dockerfile, README, and .env.example.""" lines = [] lines.append(f"# {service_name}") lines.append("") - lines.append(f"> Auto-generated from `docker-compose.yml`") lines.append("") - services = compose_data.get("services", {}) - if not services: - lines.append("No services defined.") - return "\n".join(lines) + # Include README.md first if exists + readme_path = service_dir / "README.md" + if readme_path.exists(): + lines.append(f'--8<-- "{service_dir.name}/README.md"') - for svc_name, svc in services.items(): - if len(services) > 1: - lines.append(f"## `{svc_name}`") - lines.append("") - - # Image - image = svc.get("image", "N/A") - container = svc.get("container_name", svc_name) - lines.append(f"| Field | Value |") - lines.append(f"|-------|-------|") - lines.append(f"| **Image** | `{image}` |") - lines.append(f"| **Container** | `{container}` |") - lines.append(f"| **Restart** | `{svc.get('restart', 'no')}` |") + lines.append("> Below are the configuration files for this service. For details on how to deploy or customize, refer to the README above or the official documentation for the service.") + # Include docker-compose.yml + compose_file = service_dir / "docker-compose.yml" + if compose_file.exists(): + lines.append("## Docker Compose Configuration") + lines.append("") + lines.append("```yaml") + lines.append(f'--8<-- "{service_dir.name}/docker-compose.yml"') + lines.append("```") lines.append("") - # Command - cmd = format_command(svc.get("command")) - if cmd: - lines.append("### Command") - lines.append("") - lines.append("```") - lines.append(" ".join(cmd)) - lines.append("```") - lines.append("") + # Include Dockerfile if exists + dockerfile_path = service_dir / "Dockerfile" + if dockerfile_path.exists(): + lines.append("## Dockerfile") + lines.append("") + lines.append("```dockerfile") + lines.append(f'--8<-- "{service_dir.name}/Dockerfile"') + lines.append("```") + lines.append("") - # Environment - env = format_env(svc.get("environment")) - env_file = svc.get("env_file") - env_from_file = [] - if env_file: - if isinstance(env_file, str): - env_file = [env_file] - env_from_file = env_file - - if env or env_from_file: - lines.append("### Environment") - lines.append("") - if env_from_file: - lines.append("**Env files:** " + ", ".join(f"`{e}`" for e in env_from_file)) - lines.append("") - if env: - lines.append("| Variable | Value |") - lines.append("|----------|-------|") - for key, val in env: - if "${" in val or val == "": - lines.append(f"| `{key}` | `{val}` |") - else: - lines.append(f"| `{key}` | `{val}` |") - lines.append("") - - # Service secrets from .env.example - if service_env: - lines.append("### Secrets (from `.env.example`)") - lines.append("") - lines.append("These variables must be set in the service's `.env` file:") - lines.append("") - lines.append("| Variable | Default |") - lines.append("|----------|---------|") - for key, val in service_env.items(): - lines.append(f"| `{key}` | `{val}` |") - lines.append("") - - # Ports - ports = format_ports(svc.get("ports")) - if ports: - lines.append("### Ports") - lines.append("") - lines.append("| Host | Container | Protocol |") - lines.append("|------|-----------|----------|") - for host, container, proto in ports: - lines.append(f"| `{host}` | `{container}` | {proto} |") - lines.append("") - - # Volumes - volumes = format_volumes(svc.get("volumes")) - if volumes: - lines.append("### Volumes") - lines.append("") - lines.append("| Host Path | Container Path | Mode |") - lines.append("|-----------|----------------|------|") - for host, container, mode in volumes: - lines.append(f"| `{host}` | `{container}` | {mode} |") - lines.append("") - - # Networks - nets = format_networks(svc.get("networks")) - if nets: - lines.append("### Networks") - lines.append("") - for name, ntype in nets: - lines.append(f"- `{name}` ({ntype})") - lines.append("") - - # Depends on - depends = format_depends(svc.get("depends_on")) - if depends: - lines.append("### Dependencies") - lines.append("") - for d in depends: - lines.append(f"- `{d}`") - lines.append("") - - # Capabilities - caps = format_cap_add(svc.get("cap_add")) - if caps: - lines.append("### Capabilities") - lines.append("") - lines.append(", ".join(f"`{c}`" for c in caps)) - lines.append("") - - # Security - sec = format_security(svc.get("security_opt")) - if sec: - lines.append("### Security Options") - lines.append("") - for s in sec: - lines.append(f"- `{s}`") - lines.append("") - - # Labels - labels = format_labels(svc.get("labels")) - if labels: - lines.append("### Labels") - lines.append("") - lines.append("| Key | Value |") - lines.append("|-----|-------|") - for key, val in labels: - lines.append(f"| `{key}` | `{val}` |") - lines.append("") - - # Devices - devices = svc.get("devices") - if devices: - lines.append("### Devices") - lines.append("") - for d in devices: - if isinstance(d, str): - lines.append(f"- `{d}`") - lines.append("") - - # tmpfs - tmpfs = svc.get("tmpfs") - if tmpfs: - lines.append("### Tmpfs") - lines.append("") - if isinstance(tmpfs, list): - for t in tmpfs: - lines.append(f"- `{t}`") - lines.append("") - - # shm_size - shm = svc.get("shm_size") - if shm: - lines.append(f"### SHM Size: `{shm}`") - lines.append("") - - # resource limits - mem = svc.get("mem_limit") - swap = svc.get("memswap_limit") - if mem or swap: - lines.append("### Resources") - lines.append("") - if mem: - lines.append(f"- Memory limit: `{mem}`") - if swap: - lines.append(f"- Memory+Swap limit: `{swap}`") - lines.append("") - - # Compose-level volumes (named) - comp_volumes = compose_data.get("volumes") - if comp_volumes and isinstance(comp_volumes, dict): - lines.append("### Named Volumes (compose-level)") - lines.append("") - for vname, vcfg in comp_volumes.items(): - external = False - if isinstance(vcfg, dict): - external = vcfg.get("external", False) - ntype = "external" if external else "managed by compose" - lines.append(f"- `{vname}` ({ntype})") - lines.append("") - - # Compose-level networks - comp_nets = compose_data.get("networks") - if comp_nets and isinstance(comp_nets, dict): - lines.append("### Networks (compose-level)") - lines.append("") - for nname, ncfg in comp_nets.items(): - external = False - if isinstance(ncfg, dict): - external = ncfg.get("external", False) - ntype = "external" if external else "internal" - lines.append(f"- `{nname}` ({ntype})") - lines.append("") - - lines.append("---") + # Include .env.example if exists + env_example_path = service_dir / ".env.example" + if env_example_path.exists(): + lines.append("## Environment Variables (`.env.example`)") + lines.append("") + lines.append("```bash") + lines.append(f'--8<-- "{service_dir.name}/.env.example"') + lines.append("```") lines.append("") return "\n".join(lines) -def generate_index(all_services): - lines = [] - lines.append("# Homelab Documentation") - lines.append("") - lines.append("> Auto-generated from `docker-compose.yml` files") - lines.append("") - lines.append("## Data Classification") - lines.append("") - lines.append("| Type | Example | Git Repo? | Backup? | Location |") - lines.append("|------|---------|-----------|---------|----------|") - lines.append("| **Source Configs** | `docker-compose.yml`, `.env.example`, `Makefile` | Yes | No | `/srv/homelab-infra` |") - lines.append("| **Service Secrets** | `.env` (DB passwords, API keys) | No | Yes | `/srv/homelab-infra//.env` |") - lines.append("| **Runtime Configs** | `acme.json`, service configs | No | Yes | `/mnt/storage/docker-data/` |") - lines.append("| **Persistent Data** | DB data, uploads, media | No | Yes | `/mnt/storage/docker-data/` |") - lines.append("") - lines.append("## Services") - lines.append("") - lines.append("| Service | Image | Status |") - lines.append("|---------|-------|--------|") - for name, data in sorted(all_services.items()): - services = data.get("services", {}) - images = [] - for svc in services.values(): - img = svc.get("image", "N/A") - images.append(f"`{img}`") - status = "active" if "planned" not in name.lower() else "planned" - lines.append(f"| [{name}](services/{name}.md) | {', '.join(images)} | {status} |") - lines.append("") - lines.append("## Quick Start") - lines.append("") - lines.append("```bash") - lines.append("cp .env.example .env") - lines.append("for svc in */; do [ -f \"$svc/.env.example\" ] && cp \"$svc/.env.example\" \"$svc/.env\"; done") - lines.append("docker network create web") - lines.append("make up") - lines.append("```") - lines.append("") - return "\n".join(lines) +def service_display_name(service_name): + if service_name in DISPLAY_NAME_OVERRIDES: + return DISPLAY_NAME_OVERRIDES[service_name] + return " ".join(part.capitalize() for part in service_name.split("-")) + + +def update_mkdocs_nav(service_names): + mkdocs_file = REPO_ROOT / "mkdocs.yml" + cfg = {} + if mkdocs_file.exists(): + existing = yaml.safe_load(mkdocs_file.read_text()) + if isinstance(existing, dict): + cfg = existing + + existing_nav = cfg.get("nav", []) + nav_without_services = [] + for item in existing_nav: + if isinstance(item, dict) and "Services" in item: + continue + nav_without_services.append(item) + + if not nav_without_services: + nav_without_services = [ + {"Home": "index.md"}, + {"Recovery Reference": "recovery.md"}, + ] + + services_nav = [] + for name in sorted(service_names): + services_nav.append({service_display_name(name): f"services/{name}.md"}) + + cfg["nav"] = nav_without_services + [{"Services": services_nav}] + + if "site_name" not in cfg: + cfg["site_name"] = "Homelab Documentation" + if "theme" not in cfg: + cfg["theme"] = {"name": "material", "palette": {"scheme": "slate"}} + + mkdocs_file.write_text(yaml.safe_dump(cfg, sort_keys=False)) + print(f"Updated {mkdocs_file.relative_to(REPO_ROOT)}") + + +def cleanup_stale_service_docs(service_names): + DOCS_DIR.mkdir(parents=True, exist_ok=True) + valid = set(service_names) + for md_file in DOCS_DIR.glob("*.md"): + if md_file.stem not in valid: + md_file.unlink() + print(f"Removed stale {md_file.relative_to(REPO_ROOT)}") def main(): - root_env = load_root_env_example() all_services = {} # Find all service directories with docker-compose.yml @@ -450,21 +299,20 @@ def main(): if not compose_data: continue - service_env = load_service_env_example(item) all_services[service_name] = compose_data - # Generate service doc - md = generate_service_md(service_name, compose_data, root_env, service_env) + # Generate service doc (stub that includes files) + md = generate_service_md(service_name, item) out_file = DOCS_DIR / f"{service_name}.md" out_file.parent.mkdir(parents=True, exist_ok=True) out_file.write_text(md) print(f"Generated {out_file.relative_to(REPO_ROOT)}") - # Generate index - index_md = generate_index(all_services) - index_file = REPO_ROOT / "docs" / "index.md" - index_file.write_text(index_md) - print(f"Generated {index_file.relative_to(REPO_ROOT)}") + # Remove docs for services that no longer exist + cleanup_stale_service_docs(all_services.keys()) + + # Keep mkdocs.yml service nav in sync with discovered service folders + update_mkdocs_nav(all_services.keys()) print(f"\nDone. Generated docs for {len(all_services)} services.") diff --git a/traefik/.env.example b/traefik/.env.example old mode 100644 new mode 100755 diff --git a/traefik/README.md b/traefik/README.md new file mode 100644 index 0000000..0a591ec --- /dev/null +++ b/traefik/README.md @@ -0,0 +1,3 @@ +THis is the project + +More information hear. \ No newline at end of file diff --git a/traefik/docker-compose.yml b/traefik/docker-compose.yml old mode 100644 new mode 100755 diff --git a/whoami/docker-compose.yml b/whoami/docker-compose.yml old mode 100644 new mode 100755