AI given structure
This commit is contained in:
17
.env.example
Normal file
17
.env.example
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# homelab-infra/.env
|
||||||
|
# Global environment variables shared across all services
|
||||||
|
# Copy to .env and fill in real values. NEVER commit .env.
|
||||||
|
|
||||||
|
# Storage
|
||||||
|
STORAGE_PATH=/mnt/storage/docker-data
|
||||||
|
MEDIA_PATH=/mnt/storage
|
||||||
|
|
||||||
|
# User
|
||||||
|
USER_UID=1000
|
||||||
|
USER_GID=1000
|
||||||
|
|
||||||
|
# Domain
|
||||||
|
DOMAIN=sjhl.nz
|
||||||
|
|
||||||
|
# Timezone
|
||||||
|
TZ=Pacific/Auckland
|
||||||
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Secrets - never commit
|
||||||
|
.env
|
||||||
|
*.env
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Docker runtime
|
||||||
|
**/acme.json
|
||||||
|
**/logs/
|
||||||
|
**/*.log
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# MkDocs
|
||||||
|
site/
|
||||||
|
|
||||||
|
# Editor
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
43
Makefile
Normal file
43
Makefile
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
SERVICES=traefik whoami gitea nextcloud qbittorrent jellyfin devbox obsidian n8n supersync
|
||||||
|
|
||||||
|
.PHONY: up down restart backup docs generate-docs logs status
|
||||||
|
|
||||||
|
up:
|
||||||
|
@for svc in $(SERVICES); do \
|
||||||
|
if [ -f "$$svc/docker-compose.yml" ]; then \
|
||||||
|
echo "Starting $$svc..."; \
|
||||||
|
(cd $$svc && docker compose up -d); \
|
||||||
|
fi; \
|
||||||
|
done
|
||||||
|
|
||||||
|
down:
|
||||||
|
@for svc in $(SERVICES); do \
|
||||||
|
if [ -f "$$svc/docker-compose.yml" ]; then \
|
||||||
|
echo "Stopping $$svc..."; \
|
||||||
|
(cd $$svc && docker compose down); \
|
||||||
|
fi; \
|
||||||
|
done
|
||||||
|
|
||||||
|
restart: down up
|
||||||
|
|
||||||
|
logs:
|
||||||
|
@echo "=== Traefik ===" && (cd traefik && docker compose logs --tail=10)
|
||||||
|
@echo "=== Gitea ===" && (cd gitea && docker compose logs --tail=10)
|
||||||
|
@echo "=== Nextcloud ===" && (cd nextcloud && docker compose logs --tail=10)
|
||||||
|
|
||||||
|
status:
|
||||||
|
@for svc in $(SERVICES); do \
|
||||||
|
if [ -f "$$svc/docker-compose.yml" ]; then \
|
||||||
|
echo "--- $$svc ---"; \
|
||||||
|
(cd $$svc && docker compose ps --format "table {{.Name}}\t{{.Status}}\t{{.Ports}}"); \
|
||||||
|
fi; \
|
||||||
|
done
|
||||||
|
|
||||||
|
backup:
|
||||||
|
./backup.sh
|
||||||
|
|
||||||
|
generate-docs:
|
||||||
|
python3 scripts/generate_docs.py
|
||||||
|
|
||||||
|
docs: generate-docs
|
||||||
|
mkdocs build
|
||||||
79
README.md
Normal file
79
README.md
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# homelab-infra
|
||||||
|
|
||||||
|
GitOps-managed infrastructure for homelab services.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone git@gitea.sjhl.nz:james/homelab-infra.git
|
||||||
|
cd homelab-infra
|
||||||
|
|
||||||
|
# Set up environment
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env with real values
|
||||||
|
|
||||||
|
# Set up service secrets
|
||||||
|
for svc in traefik gitea nextcloud qbittorrent jellyfin devbox obsidian; do
|
||||||
|
[ -f "$svc/.env.example" ] && cp "$svc/.env.example" "$svc/.env"
|
||||||
|
# Edit each .env with real secrets
|
||||||
|
done
|
||||||
|
|
||||||
|
# Create required Docker networks
|
||||||
|
docker network create web
|
||||||
|
|
||||||
|
# Start everything
|
||||||
|
make up
|
||||||
|
```
|
||||||
|
|
||||||
|
## Services
|
||||||
|
|
||||||
|
| 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 |
|
||||||
|
| obsidian | CouchDB for Obsidian LiveSync | Internal |
|
||||||
|
| n8n | Workflow automation (planned) | - |
|
||||||
|
| supersync | File sync (planned) | - |
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make up # Start all services
|
||||||
|
make down # Stop all services
|
||||||
|
make restart # Restart all services
|
||||||
|
make status # Show status of all services
|
||||||
|
make logs # Show recent logs
|
||||||
|
make backup # Run Restic backup
|
||||||
|
make docs # Build MkDocs site
|
||||||
|
```
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Locations
|
||||||
|
|
||||||
|
- Configs (in Git): this repo
|
||||||
|
- Secrets (not in Git): per-service `.env` files
|
||||||
|
- Persistent data: `/mnt/storage/docker-data/<service>`
|
||||||
|
- Media: `/mnt/storage`
|
||||||
|
- Backups: `/mnt/storage/backups`
|
||||||
24
backup.sh
Executable file
24
backup.sh
Executable file
@@ -0,0 +1,24 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Backup all persistent service data using Restic
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Load environment
|
||||||
|
source .env
|
||||||
|
|
||||||
|
export RESTIC_REPOSITORY=/mnt/storage/backups
|
||||||
|
export RESTIC_PASSWORD_FILE=/root/.restic_pass
|
||||||
|
|
||||||
|
SERVICES=(gitea nextcloud qbittorrent jellyfin devbox obsidian traefik)
|
||||||
|
|
||||||
|
for svc in "${SERVICES[@]}"; do
|
||||||
|
target="${STORAGE_PATH}/${svc}"
|
||||||
|
if [ -d "$target" ]; then
|
||||||
|
echo "Backing up $svc..."
|
||||||
|
restic backup "$target" --tag "$svc"
|
||||||
|
else
|
||||||
|
echo "Skipping $svc - directory $target not found"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Backup complete."
|
||||||
|
restic snapshots --latest 5
|
||||||
6
devbox/.env.example
Normal file
6
devbox/.env.example
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# devbox/.env
|
||||||
|
# Copy to .env and fill in real values. NEVER commit .env.
|
||||||
|
|
||||||
|
DEVBOX_SSH_PORT=46573
|
||||||
|
DEVBOX_MEM_LIMIT=10g
|
||||||
|
DEVBOX_SWAP_LIMIT=20g
|
||||||
13
devbox/docker-compose.yml
Normal file
13
devbox/docker-compose.yml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
services:
|
||||||
|
devcontainer:
|
||||||
|
image: devbox-devcontainer
|
||||||
|
container_name: devcontainer
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "${DEVBOX_SSH_PORT:-46573}:2222"
|
||||||
|
volumes:
|
||||||
|
- ${STORAGE_PATH}/devbox:/home/devuser
|
||||||
|
env_file:
|
||||||
|
- ../.env
|
||||||
|
mem_limit: ${DEVBOX_MEM_LIMIT:-10g}
|
||||||
|
memswap_limit: ${DEVBOX_SWAP_LIMIT:-20g}
|
||||||
34
docs/index.md
Normal file
34
docs/index.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Homelab Documentation
|
||||||
|
|
||||||
|
> Auto-generated from `docker-compose.yml` files
|
||||||
|
|
||||||
|
## Data Classification
|
||||||
|
|
||||||
|
| Type | Example | Git Repo? | Backup? | Location |
|
||||||
|
|------|---------|-----------|---------|----------|
|
||||||
|
| **Source Configs** | `docker-compose.yml`, `.env.example`, `Makefile` | Yes | No | `/srv/homelab-infra` |
|
||||||
|
| **Service Secrets** | `.env` (DB passwords, API keys) | No | Yes | `/srv/homelab-infra/<service>/.env` |
|
||||||
|
| **Runtime Configs** | `acme.json`, service configs | No | Yes | `/mnt/storage/docker-data/<service>` |
|
||||||
|
| **Persistent Data** | DB data, uploads, media | No | Yes | `/mnt/storage/docker-data/<service>` |
|
||||||
|
|
||||||
|
## Services
|
||||||
|
|
||||||
|
| Service | Image | Status |
|
||||||
|
|---------|-------|--------|
|
||||||
|
| [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 |
|
||||||
|
| [traefik](services/traefik.md) | `traefik:v3.6` | active |
|
||||||
|
| [whoami](services/whoami.md) | `traefik/whoami` | active |
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
for svc in */; do [ -f "$svc/.env.example" ] && cp "$svc/.env.example" "$svc/.env"; done
|
||||||
|
docker network create web
|
||||||
|
make up
|
||||||
|
```
|
||||||
66
docs/recovery.md
Normal file
66
docs/recovery.md
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# Recovery Reference
|
||||||
|
|
||||||
|
This document was generated from `docker inspect` dumps taken after the accidental deletion of `/home/james`. It preserves the last known-good configuration state for rebuilding services.
|
||||||
|
|
||||||
|
## Container Summary
|
||||||
|
|
||||||
|
### traefik (Reverse Proxy)
|
||||||
|
- **Image:** `traefik:v3.6`
|
||||||
|
- **Config path (old):** `/home/james/apps/traefik/`
|
||||||
|
- **Volumes:** certs, dynamic config, docker.sock (ro)
|
||||||
|
- **Networks:** `web` (external)
|
||||||
|
- **Ports:** 80, 443, 8082->8080
|
||||||
|
- **Let's Encrypt:** HTTP challenge, email `letsencrypt.debug772@passmail.net`
|
||||||
|
|
||||||
|
### gitea
|
||||||
|
- **Image:** `gitea/gitea:1.24.3`
|
||||||
|
- **Data path (old):** `/mnt/storage/apps/gitea`
|
||||||
|
- **DB:** SQLite3
|
||||||
|
- **SSH:** port 222
|
||||||
|
- **URL:** `https://gitea.sjhl.nz/`
|
||||||
|
|
||||||
|
### nextcloud (AIO)
|
||||||
|
- **Image:** `ghcr.io/nextcloud-releases/all-in-one:latest`
|
||||||
|
- **Data path (old):** `/mnt/storage/apps/nextcloud`
|
||||||
|
- **Components:** mastercontainer, apache, nextcloud, database (PostgreSQL 17), redis, collabora, imaginary, whiteboard, notify-push
|
||||||
|
- **URL:** `https://nextcloud.sjhl.nz`
|
||||||
|
- **AIO mgmt:** port 8081
|
||||||
|
- **Apache port:** 11000
|
||||||
|
|
||||||
|
### qbittorrent + VPN
|
||||||
|
- **VPN:** ProtonVPN WireGuard, New Zealand, port forwarding
|
||||||
|
- **Data path (old):** `/home/james/apps/qbittorrent/`
|
||||||
|
- **Downloads:** `/mnt/storage/torrents`
|
||||||
|
- **Components:** gluetun, qbittorrent, jackett
|
||||||
|
- **Ports:** 8080 (qBittorrent), 9117 (Jackett)
|
||||||
|
|
||||||
|
### jellyfin
|
||||||
|
- **Image:** `jellyfin/jellyfin:10.10.7`
|
||||||
|
- **Config path (old):** `/home/james/apps/jellyfin-config`
|
||||||
|
- **Media:** `/mnt/storage`
|
||||||
|
- **Port:** 8096
|
||||||
|
- **Network:** default bridge (NOT on `web` network)
|
||||||
|
- **Note:** Was not behind Traefik in old setup
|
||||||
|
|
||||||
|
### devbox
|
||||||
|
- **Image:** `devbox-devcontainer` (local build)
|
||||||
|
- **Data path (old):** `/mnt/storage/apps/devbox`
|
||||||
|
- **Port:** 46573->2222 (SSH)
|
||||||
|
- **Memory limit:** 10GB, swap 20GB
|
||||||
|
|
||||||
|
### obsidian-livesync
|
||||||
|
- **Image:** `couchdb:latest`
|
||||||
|
- **Status:** Exited (127) at time of backup
|
||||||
|
- **Note:** No inspect data captured, container was stopped
|
||||||
|
|
||||||
|
## Recovery Path Mapping
|
||||||
|
|
||||||
|
| Old Path | New Path |
|
||||||
|
|----------|----------|
|
||||||
|
| `/home/james/apps/traefik/` | `/mnt/storage/docker-data/traefik/` |
|
||||||
|
| `/home/james/apps/qbittorrent/` | `/mnt/storage/docker-data/qbittorrent/` |
|
||||||
|
| `/home/james/apps/jellyfin-config` | `/mnt/storage/docker-data/jellyfin/config` |
|
||||||
|
| `/mnt/storage/apps/gitea` | `/mnt/storage/docker-data/gitea` |
|
||||||
|
| `/mnt/storage/apps/nextcloud` | `/mnt/storage/docker-data/nextcloud` |
|
||||||
|
| `/mnt/storage/apps/devbox` | `/mnt/storage/docker-data/devbox` |
|
||||||
|
| `/mnt/storage/torrents` | `/mnt/storage/torrents` (unchanged) |
|
||||||
42
docs/services/devbox.md
Normal file
42
docs/services/devbox.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# devbox
|
||||||
|
|
||||||
|
> Auto-generated from `docker-compose.yml`
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| **Image** | `devbox-devcontainer` |
|
||||||
|
| **Container** | `devcontainer` |
|
||||||
|
| **Restart** | `unless-stopped` |
|
||||||
|
|
||||||
|
### Environment
|
||||||
|
|
||||||
|
**Env files:** `../.env`
|
||||||
|
|
||||||
|
### 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}`
|
||||||
|
|
||||||
|
---
|
||||||
58
docs/services/gitea.md
Normal file
58
docs/services/gitea.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# gitea
|
||||||
|
|
||||||
|
> Auto-generated from `docker-compose.yml`
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| **Image** | `gitea/gitea:1.24.3` |
|
||||||
|
| **Container** | `gitea` |
|
||||||
|
| **Restart** | `always` |
|
||||||
|
|
||||||
|
### Environment
|
||||||
|
|
||||||
|
**Env files:** `../.env`, `.env`
|
||||||
|
|
||||||
|
| 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)
|
||||||
|
|
||||||
|
---
|
||||||
55
docs/services/jellyfin.md
Normal file
55
docs/services/jellyfin.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# 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)
|
||||||
|
|
||||||
|
---
|
||||||
9
docs/services/n8n.md
Normal file
9
docs/services/n8n.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# N8N
|
||||||
|
|
||||||
|
Workflow automation platform. **Planned - not yet deployed.**
|
||||||
|
|
||||||
|
## Planned Configuration
|
||||||
|
|
||||||
|
- **URL:** `https://n8n.sjhl.nz`
|
||||||
|
- **Port:** 5678
|
||||||
|
- **Data:** `/mnt/storage/docker-data/n8n`
|
||||||
56
docs/services/nextcloud.md
Normal file
56
docs/services/nextcloud.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# nextcloud
|
||||||
|
|
||||||
|
> Auto-generated from `docker-compose.yml`
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| **Image** | `ghcr.io/nextcloud-releases/all-in-one:latest` |
|
||||||
|
| **Container** | `nextcloud-aio-mastercontainer` |
|
||||||
|
| **Restart** | `always` |
|
||||||
|
|
||||||
|
### Environment
|
||||||
|
|
||||||
|
**Env files:** `../.env`
|
||||||
|
|
||||||
|
| 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)
|
||||||
|
|
||||||
|
---
|
||||||
42
docs/services/obsidian.md
Normal file
42
docs/services/obsidian.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# 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 |
|
||||||
|
|
||||||
|
---
|
||||||
153
docs/services/qbittorrent.md
Normal file
153
docs/services/qbittorrent.md
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
# 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)
|
||||||
|
|
||||||
|
---
|
||||||
9
docs/services/supersync.md
Normal file
9
docs/services/supersync.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Supersync
|
||||||
|
|
||||||
|
File synchronization service. **Planned - not yet deployed.**
|
||||||
|
|
||||||
|
## Planned Configuration
|
||||||
|
|
||||||
|
- **URL:** `https://supersync.sjhl.nz`
|
||||||
|
- **Port:** 8443
|
||||||
|
- **Data:** `/mnt/storage/docker-data/supersync`
|
||||||
67
docs/services/traefik.md
Normal file
67
docs/services/traefik.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# traefik
|
||||||
|
|
||||||
|
> Auto-generated from `docker-compose.yml`
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| **Image** | `traefik:v3.6` |
|
||||||
|
| **Container** | `traefik` |
|
||||||
|
| **Restart** | `unless-stopped` |
|
||||||
|
|
||||||
|
### 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
|
||||||
|
```
|
||||||
|
|
||||||
|
### Secrets (from `.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)
|
||||||
|
|
||||||
|
---
|
||||||
28
docs/services/whoami.md
Normal file
28
docs/services/whoami.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# whoami
|
||||||
|
|
||||||
|
> Auto-generated from `docker-compose.yml`
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| **Image** | `traefik/whoami` |
|
||||||
|
| **Container** | `whoami` |
|
||||||
|
| **Restart** | `unless-stopped` |
|
||||||
|
|
||||||
|
### 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)
|
||||||
|
|
||||||
|
---
|
||||||
3
gitea/.env.example
Normal file
3
gitea/.env.example
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# gitea/.env
|
||||||
|
# Copy to .env and fill in real values. NEVER commit .env.
|
||||||
|
# Add any Gitea-specific secrets here.
|
||||||
35
gitea/docker-compose.yml
Normal file
35
gitea/docker-compose.yml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
services:
|
||||||
|
gitea:
|
||||||
|
image: gitea/gitea:1.24.3
|
||||||
|
container_name: gitea
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- ${STORAGE_PATH}/gitea:/data
|
||||||
|
- /etc/timezone:/etc/timezone:ro
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
ports:
|
||||||
|
- "222:22"
|
||||||
|
networks:
|
||||||
|
- gitea
|
||||||
|
- web
|
||||||
|
env_file:
|
||||||
|
- ../.env
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- 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
|
||||||
|
labels:
|
||||||
|
- "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:
|
||||||
|
gitea:
|
||||||
|
web:
|
||||||
|
external: true
|
||||||
4
jellyfin/.env.example
Normal file
4
jellyfin/.env.example
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# jellyfin/.env
|
||||||
|
# Copy to .env and fill in real values. NEVER commit .env.
|
||||||
|
|
||||||
|
JELLYFIN_PORT=8096
|
||||||
25
jellyfin/docker-compose.yml
Normal file
25
jellyfin/docker-compose.yml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
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
|
||||||
18
mkdocs.yml
Normal file
18
mkdocs.yml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
site_name: Homelab Documentation
|
||||||
|
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
|
||||||
|
theme:
|
||||||
|
name: material
|
||||||
|
palette:
|
||||||
|
scheme: slate
|
||||||
4
nextcloud/.env.example
Normal file
4
nextcloud/.env.example
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# nextcloud/.env
|
||||||
|
# Copy to .env and fill in real values. NEVER commit .env.
|
||||||
|
|
||||||
|
AIO_PORT=8081
|
||||||
27
nextcloud/docker-compose.yml
Normal file
27
nextcloud/docker-compose.yml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
services:
|
||||||
|
nextcloud-aio-mastercontainer:
|
||||||
|
image: ghcr.io/nextcloud-releases/all-in-one:latest
|
||||||
|
container_name: nextcloud-aio-mastercontainer
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "${AIO_PORT:-8081}:8080"
|
||||||
|
volumes:
|
||||||
|
- nextcloud_aio_mastercontainer:/mnt/docker-aio-config
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
environment:
|
||||||
|
- APACHE_PORT=11000
|
||||||
|
- NEXTCLOUD_DATADIR=${STORAGE_PATH}/nextcloud
|
||||||
|
- APACHE_ADDITIONAL_NETWORK=web
|
||||||
|
networks:
|
||||||
|
- nextcloud-aio
|
||||||
|
- web
|
||||||
|
env_file:
|
||||||
|
- ../.env
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
nextcloud_aio_mastercontainer:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
nextcloud-aio:
|
||||||
|
web:
|
||||||
|
external: true
|
||||||
6
obsidian-livesync/.env.example
Normal file
6
obsidian-livesync/.env.example
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# obsidian/.env
|
||||||
|
# Copy to .env and fill in real values. NEVER commit .env.
|
||||||
|
|
||||||
|
COUCHDB_USER=admin
|
||||||
|
COUCHDB_PASSWORD=REPLACE_WITH_STRONG_PASSWORD
|
||||||
|
COUCHDB_PORT=5984
|
||||||
15
obsidian-livesync/docker-compose.yml
Normal file
15
obsidian-livesync/docker-compose.yml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
services:
|
||||||
|
couchdb:
|
||||||
|
image: couchdb:latest
|
||||||
|
container_name: obsidian-livesync
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "${COUCHDB_PORT:-5984}:5984"
|
||||||
|
volumes:
|
||||||
|
- ${STORAGE_PATH}/obsidian:/opt/couchdb/data
|
||||||
|
env_file:
|
||||||
|
- ../.env
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- COUCHDB_USER=${COUCHDB_USER:-admin}
|
||||||
|
- COUCHDB_PASSWORD=${COUCHDB_PASSWORD}
|
||||||
8
qbittorrent/.env.example
Normal file
8
qbittorrent/.env.example
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# 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
|
||||||
61
qbittorrent/docker-compose.yml
Normal file
61
qbittorrent/docker-compose.yml
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
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:
|
||||||
BIN
scripts/__pycache__/generate_docs.cpython-313.pyc
Normal file
BIN
scripts/__pycache__/generate_docs.cpython-313.pyc
Normal file
Binary file not shown.
473
scripts/generate_docs.py
Executable file
473
scripts/generate_docs.py
Executable file
@@ -0,0 +1,473 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Auto-generate markdown docs from docker-compose.yml files.
|
||||||
|
Usage: python3 scripts/generate_docs.py
|
||||||
|
"""
|
||||||
|
import yaml
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).parent.parent
|
||||||
|
SERVICES_DIR = REPO_ROOT
|
||||||
|
DOCS_DIR = REPO_ROOT / "docs" / "services"
|
||||||
|
|
||||||
|
# Load root .env.example for shared variables
|
||||||
|
def load_root_env_example():
|
||||||
|
env_vars = {}
|
||||||
|
env_file = REPO_ROOT / ".env.example"
|
||||||
|
if env_file.exists():
|
||||||
|
for line in env_file.read_text().splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if line and not line.startswith("#") and "=" in line:
|
||||||
|
key, _, val = line.partition("=")
|
||||||
|
env_vars[key.strip()] = val.strip()
|
||||||
|
return env_vars
|
||||||
|
|
||||||
|
# Load service .env.example
|
||||||
|
def load_service_env_example(service_dir):
|
||||||
|
env_vars = {}
|
||||||
|
env_file = service_dir / ".env.example"
|
||||||
|
if env_file.exists():
|
||||||
|
for line in env_file.read_text().splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if line and not line.startswith("#") and "=" in line:
|
||||||
|
key, _, val = line.partition("=")
|
||||||
|
env_vars[key.strip()] = val.strip()
|
||||||
|
return env_vars
|
||||||
|
|
||||||
|
|
||||||
|
def format_volumes(volumes):
|
||||||
|
if not volumes:
|
||||||
|
return []
|
||||||
|
rows = []
|
||||||
|
for v in volumes:
|
||||||
|
if isinstance(v, str):
|
||||||
|
parts = v.split(":")
|
||||||
|
if len(parts) >= 2:
|
||||||
|
host = parts[0]
|
||||||
|
container = parts[1]
|
||||||
|
mode = parts[2] if len(parts) > 2 else "rw"
|
||||||
|
rows.append((host, container, mode))
|
||||||
|
else:
|
||||||
|
rows.append((v, "-", "-"))
|
||||||
|
elif isinstance(v, dict):
|
||||||
|
source = v.get("source", v.get("type", ""))
|
||||||
|
target = v.get("target", "")
|
||||||
|
mode = v.get("mode", "rw")
|
||||||
|
rows.append((source, target, mode))
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def format_ports(ports):
|
||||||
|
if not ports:
|
||||||
|
return []
|
||||||
|
rows = []
|
||||||
|
for p in ports:
|
||||||
|
if isinstance(p, str):
|
||||||
|
# Handle protocol suffix like "8080:8080/tcp"
|
||||||
|
proto = "tcp"
|
||||||
|
port_str = p
|
||||||
|
if "/" in p:
|
||||||
|
port_str, proto = p.rsplit("/", 1)
|
||||||
|
# Use rsplit to handle ${VAR:-default}:port correctly
|
||||||
|
parts = port_str.rsplit(":", 1)
|
||||||
|
if len(parts) == 2:
|
||||||
|
rows.append((parts[0], parts[1], proto))
|
||||||
|
else:
|
||||||
|
rows.append((parts[0], parts[0], proto))
|
||||||
|
elif isinstance(p, dict):
|
||||||
|
host = str(p.get("published", ""))
|
||||||
|
container = str(p.get("target", ""))
|
||||||
|
proto = p.get("protocol", "tcp")
|
||||||
|
rows.append((host, container, proto))
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def format_env(env_list):
|
||||||
|
if not env_list:
|
||||||
|
return []
|
||||||
|
rows = []
|
||||||
|
for e in env_list:
|
||||||
|
if isinstance(e, str) and "=" in e:
|
||||||
|
key, _, val = e.partition("=")
|
||||||
|
rows.append((key.strip(), val.strip()))
|
||||||
|
elif isinstance(e, str):
|
||||||
|
rows.append((e.strip(), ""))
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def format_labels(labels):
|
||||||
|
if not labels:
|
||||||
|
return []
|
||||||
|
rows = []
|
||||||
|
if isinstance(labels, list):
|
||||||
|
for l in labels:
|
||||||
|
if isinstance(l, str) and "=" in l:
|
||||||
|
key, _, val = l.partition("=")
|
||||||
|
rows.append((key.strip(), val.strip()))
|
||||||
|
elif isinstance(l, dict):
|
||||||
|
for k, v in l.items():
|
||||||
|
rows.append((k, str(v)))
|
||||||
|
elif isinstance(labels, dict):
|
||||||
|
for k, v in labels.items():
|
||||||
|
rows.append((k, str(v)))
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def format_command(cmd):
|
||||||
|
if not cmd:
|
||||||
|
return []
|
||||||
|
if isinstance(cmd, str):
|
||||||
|
return [cmd]
|
||||||
|
if isinstance(cmd, list):
|
||||||
|
return [str(c) for c in cmd]
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def format_networks(networks):
|
||||||
|
if not networks:
|
||||||
|
return []
|
||||||
|
rows = []
|
||||||
|
if isinstance(networks, dict):
|
||||||
|
for name, cfg in networks.items():
|
||||||
|
external = False
|
||||||
|
if isinstance(cfg, dict):
|
||||||
|
external = cfg.get("external", False)
|
||||||
|
rows.append((name, "external" if external else "internal"))
|
||||||
|
elif isinstance(networks, list):
|
||||||
|
for n in networks:
|
||||||
|
if isinstance(n, str):
|
||||||
|
rows.append((n, "internal"))
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def format_depends(depends):
|
||||||
|
if not depends:
|
||||||
|
return []
|
||||||
|
if isinstance(depends, list):
|
||||||
|
return [str(d) for d in depends]
|
||||||
|
if isinstance(depends, dict):
|
||||||
|
return list(depends.keys())
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def format_cap_add(caps):
|
||||||
|
if not caps:
|
||||||
|
return []
|
||||||
|
if isinstance(caps, list):
|
||||||
|
return [str(c) for c in caps]
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def format_security(sec):
|
||||||
|
if not sec:
|
||||||
|
return []
|
||||||
|
if isinstance(sec, list):
|
||||||
|
return [str(s) for s in sec]
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def generate_service_md(service_name, compose_data, root_env, service_env):
|
||||||
|
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)
|
||||||
|
|
||||||
|
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("")
|
||||||
|
|
||||||
|
# Command
|
||||||
|
cmd = format_command(svc.get("command"))
|
||||||
|
if cmd:
|
||||||
|
lines.append("### Command")
|
||||||
|
lines.append("")
|
||||||
|
lines.append("```")
|
||||||
|
lines.append(" ".join(cmd))
|
||||||
|
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("---")
|
||||||
|
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("```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 main():
|
||||||
|
root_env = load_root_env_example()
|
||||||
|
all_services = {}
|
||||||
|
|
||||||
|
# Find all service directories with docker-compose.yml
|
||||||
|
for item in sorted(SERVICES_DIR.iterdir()):
|
||||||
|
if not item.is_dir():
|
||||||
|
continue
|
||||||
|
compose_file = item / "docker-compose.yml"
|
||||||
|
if not compose_file.exists():
|
||||||
|
continue
|
||||||
|
# Skip scripts/docs directories
|
||||||
|
if item.name in ("scripts", "docs", "__pycache__"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
service_name = item.name
|
||||||
|
try:
|
||||||
|
with open(compose_file) as f:
|
||||||
|
compose_data = yaml.safe_load(f)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error parsing {compose_file}: {e}", file=sys.stderr)
|
||||||
|
continue
|
||||||
|
|
||||||
|
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)
|
||||||
|
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)}")
|
||||||
|
|
||||||
|
print(f"\nDone. Generated docs for {len(all_services)} services.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
6
traefik/.env.example
Normal file
6
traefik/.env.example
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# traefik/.env
|
||||||
|
# Copy to .env and fill in real values. NEVER commit .env.
|
||||||
|
|
||||||
|
TRAEFIK_DASHBOARD_PORT=8082
|
||||||
|
ACME_EMAIL=letsencrypt@example.com
|
||||||
|
DASHBOARD_BASIC_AUTH=admin:$$apr1$$changeme$$REPLACE_WITH_HTPASSWD_HASH
|
||||||
45
traefik/docker-compose.yml
Normal file
45
traefik/docker-compose.yml
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
services:
|
||||||
|
traefik:
|
||||||
|
image: traefik:v3.6
|
||||||
|
container_name: traefik
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
- "${TRAEFIK_DASHBOARD_PORT:-8082}:8080"
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- ${STORAGE_PATH}/traefik/certs:/certs:rw
|
||||||
|
- ${STORAGE_PATH}/traefik/dynamic:/dynamic:ro
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
labels:
|
||||||
|
- "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"
|
||||||
|
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"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
web:
|
||||||
|
external: true
|
||||||
16
whoami/docker-compose.yml
Normal file
16
whoami/docker-compose.yml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
services:
|
||||||
|
whoami:
|
||||||
|
image: traefik/whoami
|
||||||
|
container_name: whoami
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.whoami.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.whoami.rule=Host(`whoami.${DOMAIN}`)"
|
||||||
|
- "traefik.http.routers.whoami.tls=true"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
web:
|
||||||
|
external: true
|
||||||
Reference in New Issue
Block a user