CLean up site generation
This commit is contained in:
1
.env.example
Normal file → Executable file
1
.env.example
Normal file → Executable file
@@ -12,6 +12,7 @@ USER_GID=1000
|
|||||||
|
|
||||||
# Domain
|
# Domain
|
||||||
DOMAIN=sjhl.nz
|
DOMAIN=sjhl.nz
|
||||||
|
DOMAIN_WWW=__CHANGEME__
|
||||||
|
|
||||||
# Timezone
|
# Timezone
|
||||||
TZ=Pacific/Auckland
|
TZ=Pacific/Auckland
|
||||||
|
|||||||
14
.gitignore
vendored
Normal file → Executable file
14
.gitignore
vendored
Normal file → Executable file
@@ -3,21 +3,21 @@
|
|||||||
*.env
|
*.env
|
||||||
!.env.example
|
!.env.example
|
||||||
|
|
||||||
# Docker runtime
|
# Docker runtime files
|
||||||
**/acme.json
|
**/*.json
|
||||||
**/logs/
|
**/logs/
|
||||||
**/*.log
|
**/*.log
|
||||||
|
|
||||||
# OS
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
|
|
||||||
# MkDocs
|
# MkDocs
|
||||||
site/
|
site/
|
||||||
|
|
||||||
# Editor
|
# Editor
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
*~
|
*~
|
||||||
|
|
||||||
|
# python
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
|||||||
9
.pre-commit-config.yaml
Normal file
9
.pre-commit-config.yaml
Normal file
@@ -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
|
||||||
36
Makefile
Normal file → Executable file
36
Makefile
Normal file → Executable file
@@ -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:
|
up:
|
||||||
@for svc in $(SERVICES); do \
|
@for svc in $(SERVICES); do \
|
||||||
@@ -36,8 +36,38 @@ status:
|
|||||||
backup:
|
backup:
|
||||||
./backup.sh
|
./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:
|
generate-docs:
|
||||||
python3 scripts/generate_docs.py
|
python3 scripts/generate_docs.py
|
||||||
|
|
||||||
docs: generate-docs
|
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
|
||||||
|
|||||||
76
README.md
Normal file → Executable file
76
README.md
Normal file → Executable file
@@ -29,15 +29,10 @@ make up
|
|||||||
|
|
||||||
| Service | Description | URL |
|
| Service | Description | URL |
|
||||||
|---------|-------------|-----|
|
|---------|-------------|-----|
|
||||||
| traefik | Reverse proxy, TLS termination | `dashboard.sjhl.nz` |
|
| gitea | Self-hosted Git | [gitea.sjhl.nz](https://gitea.sjhl.nz) |
|
||||||
| gitea | Self-hosted Git | `gitea.sjhl.nz` |
|
| nextcloud | Cloud storage (AIO) | [nextcloud.sjhl.nz](https://nextcloud.sjhl.nz) |
|
||||||
| nextcloud | Cloud storage (AIO) | `nextcloud.sjhl.nz` |
|
| devbox | Dev container | Internal only |
|
||||||
| qbittorrent | Torrent client (VPN) | Internal only |
|
|
||||||
| jellyfin | Media server | `jellyfin.sjhl.nz` |
|
|
||||||
| devbox | Dev container | SSH:46573 |
|
|
||||||
| obsidian | CouchDB for Obsidian LiveSync | Internal |
|
| obsidian | CouchDB for Obsidian LiveSync | Internal |
|
||||||
| n8n | Workflow automation (planned) | - |
|
|
||||||
| supersync | File sync (planned) | - |
|
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
@@ -48,9 +43,56 @@ make restart # Restart all services
|
|||||||
make status # Show status of all services
|
make status # Show status of all services
|
||||||
make logs # Show recent logs
|
make logs # Show recent logs
|
||||||
make backup # Run Restic backup
|
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
|
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
|
## Directory Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -58,16 +100,13 @@ homelab-infra/
|
|||||||
├── .env.example # Global env template
|
├── .env.example # Global env template
|
||||||
├── Makefile # Service management
|
├── Makefile # Service management
|
||||||
├── backup.sh # Restic backup script
|
├── backup.sh # Restic backup script
|
||||||
├── traefik/ # Reverse proxy
|
├── .pre-commit-config.yaml # Pre-commit hooks
|
||||||
├── gitea/ # Git hosting
|
├── docs/ # MkDocs documentation
|
||||||
├── nextcloud/ # Cloud storage (AIO)
|
├── mkdocs.yml # MkDocs config
|
||||||
├── qbittorrent/ # Torrent + VPN stack
|
├── scripts/ # Utility scripts
|
||||||
├── jellyfin/ # Media server
|
└── [service subdirs]/ # All services have their own subdir
|
||||||
├── devbox/ # Development container
|
|
||||||
├── obsidian/ # CouchDB for LiveSync
|
|
||||||
├── n8n/ # Workflow automation
|
|
||||||
├── supersync/ # File sync
|
|
||||||
└── docs/ # MkDocs documentation
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Data Locations
|
## Data Locations
|
||||||
@@ -75,5 +114,4 @@ homelab-infra/
|
|||||||
- Configs (in Git): this repo
|
- Configs (in Git): this repo
|
||||||
- Secrets (not in Git): per-service `.env` files
|
- Secrets (not in Git): per-service `.env` files
|
||||||
- Persistent data: `/mnt/storage/docker-data/<service>`
|
- Persistent data: `/mnt/storage/docker-data/<service>`
|
||||||
- Media: `/mnt/storage`
|
|
||||||
- Backups: `/mnt/storage/backups`
|
- Backups: `/mnt/storage/backups`
|
||||||
|
|||||||
0
devbox/.env.example
Normal file → Executable file
0
devbox/.env.example
Normal file → Executable file
0
devbox/docker-compose.yml
Normal file → Executable file
0
devbox/docker-compose.yml
Normal file → Executable file
5
docs/dev/check_env_sync.md
Normal file
5
docs/dev/check_env_sync.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# check_env_sync
|
||||||
|
|
||||||
|
> Python script documentation
|
||||||
|
|
||||||
|
::: scripts.check_env_sync
|
||||||
5
docs/dev/generate_docs.md
Normal file
5
docs/dev/generate_docs.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# generate_docs
|
||||||
|
|
||||||
|
> Python script documentation
|
||||||
|
|
||||||
|
::: scripts.generate_docs
|
||||||
8
docs/index.md
Normal file → Executable file
8
docs/index.md
Normal file → Executable file
@@ -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
|
## Data Classification
|
||||||
|
|
||||||
@@ -17,10 +17,8 @@
|
|||||||
|---------|-------|--------|
|
|---------|-------|--------|
|
||||||
| [devbox](services/devbox.md) | `devbox-devcontainer` | active |
|
| [devbox](services/devbox.md) | `devbox-devcontainer` | active |
|
||||||
| [gitea](services/gitea.md) | `gitea/gitea:1.24.3` | 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 |
|
| [nextcloud](services/nextcloud.md) | `ghcr.io/nextcloud-releases/all-in-one:latest` | active |
|
||||||
| [obsidian](services/obsidian.md) | `couchdb:latest` | active |
|
| [obsidian-livesync](services/obsidian-livesync.md) | `couchdb:latest` | active |
|
||||||
| [qbittorrent](services/qbittorrent.md) | `qmcgaw/gluetun`, `lscr.io/linuxserver/qbittorrent:latest`, `linuxserver/jackett:latest` | active |
|
|
||||||
| [traefik](services/traefik.md) | `traefik:v3.6` | active |
|
| [traefik](services/traefik.md) | `traefik:v3.6` | active |
|
||||||
| [whoami](services/whoami.md) | `traefik/whoami` | active |
|
| [whoami](services/whoami.md) | `traefik/whoami` | active |
|
||||||
|
|
||||||
|
|||||||
0
docs/recovery.md
Normal file → Executable file
0
docs/recovery.md
Normal file → Executable file
45
docs/services/devbox.md
Normal file → Executable file
45
docs/services/devbox.md
Normal file → Executable file
@@ -1,42 +1,15 @@
|
|||||||
# devbox
|
# devbox
|
||||||
|
|
||||||
> Auto-generated from `docker-compose.yml`
|
> This page auto-includes the service configuration files.
|
||||||
|
|
||||||
| Field | Value |
|
## Docker Compose Configuration
|
||||||
|-------|-------|
|
|
||||||
| **Image** | `devbox-devcontainer` |
|
|
||||||
| **Container** | `devcontainer` |
|
|
||||||
| **Restart** | `unless-stopped` |
|
|
||||||
|
|
||||||
### Environment
|
```yaml
|
||||||
|
--8<-- "devbox/docker-compose.yml"
|
||||||
|
```
|
||||||
|
|
||||||
**Env files:** `../.env`
|
## Environment Variables (`.env.example`)
|
||||||
|
|
||||||
### Secrets (from `.env.example`)
|
```bash
|
||||||
|
--8<-- "devbox/.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}`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|||||||
61
docs/services/gitea.md
Normal file → Executable file
61
docs/services/gitea.md
Normal file → Executable file
@@ -1,58 +1,15 @@
|
|||||||
# gitea
|
# gitea
|
||||||
|
|
||||||
> Auto-generated from `docker-compose.yml`
|
> This page auto-includes the service configuration files.
|
||||||
|
|
||||||
| Field | Value |
|
## Docker Compose Configuration
|
||||||
|-------|-------|
|
|
||||||
| **Image** | `gitea/gitea:1.24.3` |
|
|
||||||
| **Container** | `gitea` |
|
|
||||||
| **Restart** | `always` |
|
|
||||||
|
|
||||||
### Environment
|
```yaml
|
||||||
|
--8<-- "gitea/docker-compose.yml"
|
||||||
|
```
|
||||||
|
|
||||||
**Env files:** `../.env`, `.env`
|
## Environment Variables (`.env.example`)
|
||||||
|
|
||||||
| Variable | Value |
|
```bash
|
||||||
|----------|-------|
|
--8<-- "gitea/.env.example"
|
||||||
| `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)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|
||||||
---
|
|
||||||
@@ -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`
|
|
||||||
59
docs/services/nextcloud.md
Normal file → Executable file
59
docs/services/nextcloud.md
Normal file → Executable file
@@ -1,56 +1,15 @@
|
|||||||
# nextcloud
|
# nextcloud
|
||||||
|
|
||||||
> Auto-generated from `docker-compose.yml`
|
> This page auto-includes the service configuration files.
|
||||||
|
|
||||||
| Field | Value |
|
## Docker Compose Configuration
|
||||||
|-------|-------|
|
|
||||||
| **Image** | `ghcr.io/nextcloud-releases/all-in-one:latest` |
|
|
||||||
| **Container** | `nextcloud-aio-mastercontainer` |
|
|
||||||
| **Restart** | `always` |
|
|
||||||
|
|
||||||
### Environment
|
```yaml
|
||||||
|
--8<-- "nextcloud/docker-compose.yml"
|
||||||
|
```
|
||||||
|
|
||||||
**Env files:** `../.env`
|
## Environment Variables (`.env.example`)
|
||||||
|
|
||||||
| Variable | Value |
|
```bash
|
||||||
|----------|-------|
|
--8<-- "nextcloud/.env.example"
|
||||||
| `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)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|||||||
15
docs/services/obsidian-livesync.md
Executable file
15
docs/services/obsidian-livesync.md
Executable file
@@ -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"
|
||||||
|
```
|
||||||
@@ -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 |
|
|
||||||
|
|
||||||
---
|
|
||||||
@@ -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)
|
|
||||||
|
|
||||||
---
|
|
||||||
@@ -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`
|
|
||||||
69
docs/services/traefik.md
Normal file → Executable file
69
docs/services/traefik.md
Normal file → Executable file
@@ -1,67 +1,16 @@
|
|||||||
# traefik
|
# traefik
|
||||||
|
|
||||||
> Auto-generated from `docker-compose.yml`
|
> This page auto-includes the service configuration files.
|
||||||
|
|
||||||
| Field | Value |
|
--8<-- "traefik/README.md"
|
||||||
|-------|-------|
|
## Docker Compose Configuration
|
||||||
| **Image** | `traefik:v3.6` |
|
|
||||||
| **Container** | `traefik` |
|
|
||||||
| **Restart** | `unless-stopped` |
|
|
||||||
|
|
||||||
### Command
|
```yaml
|
||||||
|
--8<-- "traefik/docker-compose.yml"
|
||||||
```
|
|
||||||
--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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Secrets (from `.env.example`)
|
## Environment Variables (`.env.example`)
|
||||||
|
|
||||||
These variables must be set in the service's `.env` file:
|
```bash
|
||||||
|
--8<-- "traefik/.env.example"
|
||||||
| 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)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|||||||
29
docs/services/whoami.md
Normal file → Executable file
29
docs/services/whoami.md
Normal file → Executable file
@@ -1,28 +1,9 @@
|
|||||||
# whoami
|
# whoami
|
||||||
|
|
||||||
> Auto-generated from `docker-compose.yml`
|
> This page auto-includes the service configuration files.
|
||||||
|
|
||||||
| Field | Value |
|
## Docker Compose Configuration
|
||||||
|-------|-------|
|
|
||||||
| **Image** | `traefik/whoami` |
|
|
||||||
| **Container** | `whoami` |
|
|
||||||
| **Restart** | `unless-stopped` |
|
|
||||||
|
|
||||||
### Networks
|
```yaml
|
||||||
|
--8<-- "whoami/docker-compose.yml"
|
||||||
- `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)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|||||||
0
gitea/.env.example
Normal file → Executable file
0
gitea/.env.example
Normal file → Executable file
0
gitea/docker-compose.yml
Normal file → Executable file
0
gitea/docker-compose.yml
Normal file → Executable file
@@ -1,4 +0,0 @@
|
|||||||
# jellyfin/.env
|
|
||||||
# Copy to .env and fill in real values. NEVER commit .env.
|
|
||||||
|
|
||||||
JELLYFIN_PORT=8096
|
|
||||||
@@ -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
|
|
||||||
27
mkdocs.yml
Normal file → Executable file
27
mkdocs.yml
Normal file → Executable file
@@ -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:
|
nav:
|
||||||
- Home: index.md
|
- Home: index.md
|
||||||
- Recovery Reference: recovery.md
|
- Recovery Reference: recovery.md
|
||||||
- Services:
|
- Services:
|
||||||
- Traefik: services/traefik.md
|
- Devbox: services/devbox.md
|
||||||
- Gitea: services/gitea.md
|
- Gitea: services/gitea.md
|
||||||
- Nextcloud: services/nextcloud.md
|
- Nextcloud: services/nextcloud.md
|
||||||
- qBittorrent: services/qbittorrent.md
|
- Obsidian Livesync: services/obsidian-livesync.md
|
||||||
- Jellyfin: services/jellyfin.md
|
- Traefik: services/traefik.md
|
||||||
- Devbox: services/devbox.md
|
- Whoami: services/whoami.md
|
||||||
- Obsidian: services/obsidian.md
|
|
||||||
- N8N: services/n8n.md
|
|
||||||
- Supersync: services/supersync.md
|
|
||||||
theme:
|
theme:
|
||||||
name: material
|
name: material
|
||||||
palette:
|
palette:
|
||||||
|
|||||||
0
nextcloud/.env.example
Normal file → Executable file
0
nextcloud/.env.example
Normal file → Executable file
0
nextcloud/docker-compose.yml
Normal file → Executable file
0
nextcloud/docker-compose.yml
Normal file → Executable file
0
obsidian-livesync/.env.example
Normal file → Executable file
0
obsidian-livesync/.env.example
Normal file → Executable file
0
obsidian-livesync/docker-compose.yml
Normal file → Executable file
0
obsidian-livesync/docker-compose.yml
Normal file → Executable file
@@ -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
|
|
||||||
@@ -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:
|
|
||||||
Binary file not shown.
197
scripts/check_env_sync.py
Normal file
197
scripts/check_env_sync.py
Normal file
@@ -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())
|
||||||
@@ -12,6 +12,11 @@ REPO_ROOT = Path(__file__).parent.parent
|
|||||||
SERVICES_DIR = REPO_ROOT
|
SERVICES_DIR = REPO_ROOT
|
||||||
DOCS_DIR = REPO_ROOT / "docs" / "services"
|
DOCS_DIR = REPO_ROOT / "docs" / "services"
|
||||||
|
|
||||||
|
DISPLAY_NAME_OVERRIDES = {
|
||||||
|
"qbittorrent": "qBittorrent",
|
||||||
|
"n8n": "N8N",
|
||||||
|
}
|
||||||
|
|
||||||
# Load root .env.example for shared variables
|
# Load root .env.example for shared variables
|
||||||
def load_root_env_example():
|
def load_root_env_example():
|
||||||
env_vars = {}
|
env_vars = {}
|
||||||
@@ -168,264 +173,108 @@ def format_security(sec):
|
|||||||
return []
|
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 = []
|
||||||
lines.append(f"# {service_name}")
|
lines.append(f"# {service_name}")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append(f"> Auto-generated from `docker-compose.yml`")
|
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
services = compose_data.get("services", {})
|
# Include README.md first if exists
|
||||||
if not services:
|
readme_path = service_dir / "README.md"
|
||||||
lines.append("No services defined.")
|
if readme_path.exists():
|
||||||
return "\n".join(lines)
|
lines.append(f'--8<-- "{service_dir.name}/README.md"')
|
||||||
|
|
||||||
for svc_name, svc in services.items():
|
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.")
|
||||||
if len(services) > 1:
|
# Include docker-compose.yml
|
||||||
lines.append(f"## `{svc_name}`")
|
compose_file = service_dir / "docker-compose.yml"
|
||||||
|
if compose_file.exists():
|
||||||
|
lines.append("## Docker Compose Configuration")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
lines.append("```yaml")
|
||||||
# Image
|
lines.append(f'--8<-- "{service_dir.name}/docker-compose.yml"')
|
||||||
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("")
|
|
||||||
|
|
||||||
# Command
|
|
||||||
cmd = format_command(svc.get("command"))
|
|
||||||
if cmd:
|
|
||||||
lines.append("### Command")
|
|
||||||
lines.append("")
|
|
||||||
lines.append("```")
|
|
||||||
lines.append(" ".join(cmd))
|
|
||||||
lines.append("```")
|
lines.append("```")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
# Environment
|
# Include Dockerfile if exists
|
||||||
env = format_env(svc.get("environment"))
|
dockerfile_path = service_dir / "Dockerfile"
|
||||||
env_file = svc.get("env_file")
|
if dockerfile_path.exists():
|
||||||
env_from_file = []
|
lines.append("## Dockerfile")
|
||||||
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("")
|
lines.append("")
|
||||||
if env_from_file:
|
lines.append("```dockerfile")
|
||||||
lines.append("**Env files:** " + ", ".join(f"`{e}`" for e in env_from_file))
|
lines.append(f'--8<-- "{service_dir.name}/Dockerfile"')
|
||||||
lines.append("")
|
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("")
|
lines.append("")
|
||||||
|
|
||||||
# Service secrets from .env.example
|
# Include .env.example if exists
|
||||||
if service_env:
|
env_example_path = service_dir / ".env.example"
|
||||||
lines.append("### Secrets (from `.env.example`)")
|
if env_example_path.exists():
|
||||||
lines.append("")
|
lines.append("## Environment Variables (`.env.example`)")
|
||||||
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("---")
|
|
||||||
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/<service>/.env` |")
|
|
||||||
lines.append("| **Runtime Configs** | `acme.json`, service configs | No | Yes | `/mnt/storage/docker-data/<service>` |")
|
|
||||||
lines.append("| **Persistent Data** | DB data, uploads, media | No | Yes | `/mnt/storage/docker-data/<service>` |")
|
|
||||||
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("")
|
||||||
lines.append("```bash")
|
lines.append("```bash")
|
||||||
lines.append("cp .env.example .env")
|
lines.append(f'--8<-- "{service_dir.name}/.env.example"')
|
||||||
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("```")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
return "\n".join(lines)
|
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():
|
def main():
|
||||||
root_env = load_root_env_example()
|
|
||||||
all_services = {}
|
all_services = {}
|
||||||
|
|
||||||
# Find all service directories with docker-compose.yml
|
# Find all service directories with docker-compose.yml
|
||||||
@@ -450,21 +299,20 @@ def main():
|
|||||||
if not compose_data:
|
if not compose_data:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
service_env = load_service_env_example(item)
|
|
||||||
all_services[service_name] = compose_data
|
all_services[service_name] = compose_data
|
||||||
|
|
||||||
# Generate service doc
|
# Generate service doc (stub that includes files)
|
||||||
md = generate_service_md(service_name, compose_data, root_env, service_env)
|
md = generate_service_md(service_name, item)
|
||||||
out_file = DOCS_DIR / f"{service_name}.md"
|
out_file = DOCS_DIR / f"{service_name}.md"
|
||||||
out_file.parent.mkdir(parents=True, exist_ok=True)
|
out_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
out_file.write_text(md)
|
out_file.write_text(md)
|
||||||
print(f"Generated {out_file.relative_to(REPO_ROOT)}")
|
print(f"Generated {out_file.relative_to(REPO_ROOT)}")
|
||||||
|
|
||||||
# Generate index
|
# Remove docs for services that no longer exist
|
||||||
index_md = generate_index(all_services)
|
cleanup_stale_service_docs(all_services.keys())
|
||||||
index_file = REPO_ROOT / "docs" / "index.md"
|
|
||||||
index_file.write_text(index_md)
|
# Keep mkdocs.yml service nav in sync with discovered service folders
|
||||||
print(f"Generated {index_file.relative_to(REPO_ROOT)}")
|
update_mkdocs_nav(all_services.keys())
|
||||||
|
|
||||||
print(f"\nDone. Generated docs for {len(all_services)} services.")
|
print(f"\nDone. Generated docs for {len(all_services)} services.")
|
||||||
|
|
||||||
|
|||||||
0
traefik/.env.example
Normal file → Executable file
0
traefik/.env.example
Normal file → Executable file
3
traefik/README.md
Normal file
3
traefik/README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
THis is the project
|
||||||
|
|
||||||
|
More information hear.
|
||||||
0
traefik/docker-compose.yml
Normal file → Executable file
0
traefik/docker-compose.yml
Normal file → Executable file
0
whoami/docker-compose.yml
Normal file → Executable file
0
whoami/docker-compose.yml
Normal file → Executable file
Reference in New Issue
Block a user