Files
homelab-optimized/scripts/generate_service_docs.py
Gitea Mirror Bot a69dc2530b
Some checks failed
Documentation / Deploy to GitHub Pages (push) Has been cancelled
Documentation / Build Docusaurus (push) Has been cancelled
Sanitized mirror from private repository - 2026-03-21 11:22:45 UTC
2026-03-21 11:22:45 +00:00

929 lines
41 KiB
Python

#!/usr/bin/env python3
"""
Generate comprehensive documentation for all homelab services.
This script analyzes Docker Compose files and creates detailed documentation for each service.
"""
import os
import yaml
import re
from pathlib import Path
from typing import Dict, List, Any, Optional
class ServiceDocumentationGenerator:
def __init__(self, repo_path: str):
self.repo_path = Path(repo_path)
self.docs_path = self.repo_path / "docs" / "services" / "individual"
self.docs_path.mkdir(parents=True, exist_ok=True)
# Service categories for better organization
self.categories = {
'media': ['plex', 'jellyfin', 'emby', 'tautulli', 'overseerr', 'ombi', 'radarr', 'sonarr', 'lidarr', 'readarr', 'bazarr', 'prowlarr', 'jackett', 'nzbget', 'sabnzbd', 'transmission', 'qbittorrent', 'deluge', 'immich', 'photoprism', 'navidrome', 'airsonic', 'calibre', 'komga', 'kavita'],
'monitoring': ['grafana', 'prometheus', 'uptime-kuma', 'uptimerobot', 'statping', 'healthchecks', 'netdata', 'zabbix', 'nagios', 'icinga', 'librenms', 'observium', 'cacti', 'ntopng', 'bandwidthd', 'darkstat', 'vnstat', 'smokeping', 'blackbox-exporter', 'node-exporter', 'cadvisor', 'exportarr'],
'productivity': ['nextcloud', 'owncloud', 'seafile', 'syncthing', 'filebrowser', 'paperless-ngx', 'paperless', 'docspell', 'teedy', 'bookstack', 'dokuwiki', 'tiddlywiki', 'outline', 'siyuan', 'logseq', 'obsidian', 'joplin', 'standardnotes', 'trilium', 'zettlr', 'typora', 'marktext', 'ghostwriter', 'remarkable', 'xournalpp', 'rnote', 'firefly-iii', 'actual-budget', 'budget-zen', 'maybe-finance', 'kresus', 'homebank', 'gnucash', 'ledger', 'beancount', 'plaintextaccounting'],
'development': ['gitea', 'gitlab', 'github', 'bitbucket', 'sourcehut', 'forgejo', 'cgit', 'gitweb', 'jenkins', 'drone', 'woodpecker', 'buildkite', 'teamcity', 'bamboo', 'travis', 'circleci', 'github-actions', 'gitlab-ci', 'azure-devops', 'aws-codebuild', 'portainer', 'yacht', 'dockge', 'lazydocker', 'ctop', 'dive', 'docker-compose-ui', 'docker-registry', 'harbor', 'quay', 'nexus', 'artifactory', 'verdaccio', 'npm-registry'],
'communication': ['matrix-synapse', 'element', 'riot', 'nheko', 'fluffychat', 'cinny', 'hydrogen', 'schildichat', 'mattermost', 'rocket-chat', 'zulip', 'slack', 'discord', 'telegram', 'signal', 'whatsapp', 'messenger', 'skype', 'zoom', 'jitsi', 'bigbluebutton', 'jami', 'briar', 'session', 'wickr', 'threema', 'wire', 'keybase', 'mastodon', 'pleroma', 'misskey', 'diaspora', 'friendica', 'hubzilla', 'peertube', 'pixelfed', 'lemmy', 'kbin'],
'security': ['vaultwarden', 'bitwarden', 'keepass', 'passbolt', 'psono', 'teampass', 'pleasant-password', 'authelia', 'authentik', 'keycloak', 'gluu', 'freeipa', 'openldap', 'active-directory', 'okta', 'auth0', 'firebase-auth', 'aws-cognito', 'azure-ad', 'google-identity', 'pihole', 'adguard', 'blocky', 'unbound', 'bind9', 'powerdns', 'coredns', 'technitium', 'wireguard', 'openvpn', 'ipsec', 'tinc', 'zerotier', 'tailscale', 'nebula', 'headscale'],
'networking': ['nginx', 'apache', 'caddy', 'traefik', 'haproxy', 'envoy', 'istio', 'linkerd', 'consul', 'vault', 'nomad', 'pfsense', 'opnsense', 'vyos', 'mikrotik', 'ubiquiti', 'tp-link', 'netgear', 'asus', 'linksys', 'dlink', 'zyxel', 'fortinet', 'sonicwall', 'watchguard', 'palo-alto', 'checkpoint', 'juniper', 'cisco', 'arista', 'cumulus', 'sonic', 'frr', 'quagga', 'bird', 'openbgpd'],
'storage': ['minio', 's3', 'ceph', 'glusterfs', 'moosefs', 'lizardfs', 'orangefs', 'lustre', 'beegfs', 'gpfs', 'hdfs', 'cassandra', 'mongodb', 'postgresql', 'mysql', 'mariadb', 'sqlite', 'redis', 'memcached', 'elasticsearch', 'solr', 'sphinx', 'whoosh', 'xapian', 'lucene', 'influxdb', 'prometheus', 'graphite', 'opentsdb', 'kairosdb', 'druid', 'clickhouse', 'timescaledb'],
'gaming': ['minecraft', 'factorio', 'satisfactory', 'valheim', 'terraria', 'starbound', 'dont-starve', 'project-zomboid', 'rust', 'ark', 'conan-exiles', 'space-engineers', 'astroneer', 'raft', 'green-hell', 'the-forest', 'subnautica', 'no-mans-sky', 'elite-dangerous', 'star-citizen', 'eve-online', 'world-of-warcraft', 'final-fantasy-xiv', 'guild-wars-2', 'elder-scrolls-online', 'destiny-2', 'warframe', 'path-of-exile', 'diablo', 'torchlight', 'grim-dawn', 'last-epoch'],
'ai': ['ollama', 'llamagpt', 'chatgpt', 'gpt4all', 'localai', 'text-generation-webui', 'koboldai', 'novelai', 'stable-diffusion', 'automatic1111', 'invokeai', 'comfyui', 'fooocus', 'easydiffusion', 'diffusionbee', 'draw-things', 'whisper', 'faster-whisper', 'vosk', 'deepspeech', 'wav2vec', 'espnet', 'kaldi', 'julius', 'pocketsphinx', 'festival', 'espeak', 'mary-tts', 'mimic', 'tacotron', 'wavenet', 'neural-voices']
}
def find_compose_files(self) -> List[Path]:
"""Find all YAML files that contain Docker Compose configurations."""
compose_files = []
# Find all YAML files
yaml_files = list(self.repo_path.rglob('*.yml')) + list(self.repo_path.rglob('*.yaml'))
# Filter out files in docs, .git, and other non-service directories
filtered_files = []
for file in yaml_files:
path_parts = file.parts
if any(part in path_parts for part in ['.git', 'docs', 'node_modules', '.vscode', '__pycache__', 'ansible']):
continue
# Check if file contains Docker Compose configuration
try:
with open(file, 'r', encoding='utf-8') as f:
content = f.read()
# Look for Docker Compose indicators
if ('services:' in content and
('version:' in content or 'image:' in content or 'build:' in content)):
filtered_files.append(file)
except Exception as e:
print(f"Warning: Could not read {file}: {e}")
continue
return sorted(filtered_files)
def parse_compose_file(self, compose_file: Path) -> Dict[str, Any]:
"""Parse a docker-compose file and extract service information."""
try:
with open(compose_file, 'r', encoding='utf-8') as f:
content = yaml.safe_load(f)
if not content or 'services' not in content:
return {}
# Extract metadata from file path
relative_path = compose_file.relative_to(self.repo_path)
host = relative_path.parts[0] if len(relative_path.parts) > 1 else 'unknown'
services_info = {}
for service_name, service_config in content['services'].items():
services_info[service_name] = {
'config': service_config,
'host': host,
'compose_file': str(relative_path),
'directory': str(compose_file.parent.relative_to(self.repo_path))
}
return services_info
except Exception as e:
print(f"Error parsing {compose_file}: {e}")
return {}
def categorize_service(self, service_name: str, image: str = '') -> str:
"""Categorize a service based on its name and image."""
service_lower = service_name.lower().replace('-', '').replace('_', '')
image_lower = image.lower() if image else ''
for category, keywords in self.categories.items():
for keyword in keywords:
keyword_clean = keyword.replace('-', '').replace('_', '')
if keyword_clean in service_lower or keyword_clean in image_lower:
return category
return 'other'
def extract_ports(self, service_config: Dict) -> List[str]:
"""Extract port mappings from service configuration."""
ports = []
if 'ports' in service_config:
for port in service_config['ports']:
if isinstance(port, str):
ports.append(port)
elif isinstance(port, dict):
target = port.get('target', '')
published = port.get('published', '')
if target and published:
ports.append(f"{published}:{target}")
return ports
def extract_volumes(self, service_config: Dict) -> List[str]:
"""Extract volume mappings from service configuration."""
volumes = []
if 'volumes' in service_config:
for volume in service_config['volumes']:
if isinstance(volume, str):
volumes.append(volume)
elif isinstance(volume, dict):
source = volume.get('source', '')
target = volume.get('target', '')
if source and target:
volumes.append(f"{source}:{target}")
return volumes
def extract_environment(self, service_config: Dict) -> Dict[str, str]:
"""Extract environment variables from service configuration."""
env_vars = {}
if 'environment' in service_config:
env = service_config['environment']
if isinstance(env, list):
for var in env:
if '=' in var:
key, value = var.split('=', 1)
env_vars[key] = value
elif isinstance(env, dict):
env_vars = env
return env_vars
def get_difficulty_level(self, service_name: str, service_config: Dict) -> str:
"""Determine difficulty level based on service complexity."""
# Advanced services (require significant expertise)
advanced_keywords = [
'gitlab', 'jenkins', 'kubernetes', 'consul', 'vault', 'nomad',
'elasticsearch', 'cassandra', 'mongodb-cluster', 'postgresql-cluster',
'matrix-synapse', 'mastodon', 'peertube', 'nextcloud', 'keycloak',
'authentik', 'authelia', 'traefik', 'istio', 'linkerd'
]
# Intermediate services (require basic Docker/Linux knowledge)
intermediate_keywords = [
'grafana', 'prometheus', 'nginx', 'caddy', 'haproxy', 'wireguard',
'openvpn', 'pihole', 'adguard', 'vaultwarden', 'bitwarden',
'paperless', 'bookstack', 'dokuwiki', 'mattermost', 'rocket-chat',
'portainer', 'yacht', 'immich', 'photoprism', 'jellyfin', 'emby'
]
service_lower = service_name.lower()
image = service_config.get('image', '').lower()
# Check for advanced complexity indicators
has_depends_on = 'depends_on' in service_config
has_multiple_volumes = len(service_config.get('volumes', [])) > 3
has_complex_networking = 'networks' in service_config and len(service_config['networks']) > 1
has_build_config = 'build' in service_config
if any(keyword in service_lower or keyword in image for keyword in advanced_keywords):
return '🔴'
elif (any(keyword in service_lower or keyword in image for keyword in intermediate_keywords) or
has_depends_on or has_multiple_volumes or has_complex_networking or has_build_config):
return '🟡'
else:
return '🟢'
def generate_service_documentation(self, service_name: str, service_info: Dict) -> str:
"""Generate comprehensive documentation for a single service."""
config = service_info['config']
host = service_info['host']
compose_file = service_info['compose_file']
directory = service_info['directory']
# Extract key information
image = config.get('image', 'Unknown')
ports = self.extract_ports(config)
volumes = self.extract_volumes(config)
env_vars = self.extract_environment(config)
category = self.categorize_service(service_name, image)
difficulty = self.get_difficulty_level(service_name, config)
# Generate documentation content
doc_content = f"""# {service_name.title().replace('-', ' ').replace('_', ' ')}
**{difficulty} {category.title()} Service**
## 📋 Service Overview
| Property | Value |
|----------|-------|
| **Service Name** | {service_name} |
| **Host** | {host} |
| **Category** | {category.title()} |
| **Difficulty** | {difficulty} |
| **Docker Image** | `{image}` |
| **Compose File** | `{compose_file}` |
| **Directory** | `{directory}` |
## 🎯 Purpose
{self.get_service_description(service_name, image, category)}
## 🚀 Quick Start
### Prerequisites
- Docker and Docker Compose installed
- Basic understanding of REDACTED_APP_PASSWORD
- Access to the host system ({host})
### Deployment
```bash
# Navigate to service directory
cd {directory}
# Start the service
docker-compose up -d
# Check service status
docker-compose ps
# View logs
docker-compose logs -f {service_name}
```
## 🔧 Configuration
### Docker Compose Configuration
```yaml
{self.format_compose_config(config)}
```
### Environment Variables
{self.format_environment_variables(env_vars)}
### Port Mappings
{self.format_ports(ports)}
### Volume Mappings
{self.format_volumes(volumes)}
## 🌐 Access Information
{self.generate_access_info(service_name, ports, host)}
## 🔒 Security Considerations
{self.generate_security_info(service_name, config)}
## 📊 Resource Requirements
{self.generate_resource_info(config)}
## 🔍 Health Monitoring
{self.generate_health_info(config)}
## 🚨 Troubleshooting
### Common Issues
{self.generate_troubleshooting_info(service_name, category)}
### Useful Commands
```bash
# Check service status
docker-compose ps
# View real-time logs
docker-compose logs -f {service_name}
# Restart service
docker-compose restart {service_name}
# Update service
docker-compose pull {service_name}
docker-compose up -d {service_name}
# Access service shell
docker-compose exec {service_name} /bin/bash
# or
docker-compose exec {service_name} /bin/sh
```
## 📚 Additional Resources
{self.generate_additional_resources(service_name, image)}
## 🔗 Related Services
{self.generate_related_services(service_name, category, host)}
---
*This documentation is auto-generated from the Docker Compose configuration. For the most up-to-date information, refer to the official documentation and the actual compose file.*
**Last Updated**: {self.get_current_date()}
**Configuration Source**: `{compose_file}`
"""
return doc_content
def get_service_description(self, service_name: str, image: str, category: str) -> str:
"""Generate a description for the service based on its name and category."""
descriptions = {
'plex': 'Plex Media Server organizes video, music and photos from personal media libraries and streams them to smart TVs, streaming boxes and mobile devices.',
'jellyfin': 'Jellyfin is a Free Software Media System that puts you in control of managing and streaming your media.',
'grafana': 'Grafana is the open source analytics & monitoring solution for every database.',
'prometheus': 'Prometheus is an open-source systems monitoring and alerting toolkit.',
'uptime-kuma': 'Uptime Kuma is a fancy self-hosted monitoring tool.',
'nginx': 'NGINX is a web server that can also be used as a reverse proxy, load balancer, mail proxy and HTTP cache.',
'traefik': 'Traefik is a modern HTTP reverse proxy and load balancer that makes deploying microservices easy.',
'portainer': 'Portainer is a lightweight management UI which allows you to easily manage your different Docker environments.',
'vaultwarden': 'Vaultwarden is an alternative implementation of the Bitwarden server API written in Rust and compatible with upstream Bitwarden clients.',
'pihole': 'Pi-hole is a DNS sinkhole that protects your devices from unwanted content, without installing any client-side software.',
'adguard': 'AdGuard Home is a network-wide software for blocking ads & tracking.',
'wireguard': 'WireGuard is an extremely simple yet fast and modern VPN that utilizes state-of-the-art cryptography.',
'nextcloud': 'Nextcloud is a suite of client-server software for creating and using file hosting services.',
'immich': 'High performance self-hosted photo and video backup solution.',
'paperless-ngx': 'Paperless-ngx is a document management system that transforms your physical documents into a searchable online archive.',
'gitea': 'Gitea is a community managed lightweight code hosting solution written in Go.',
'gitlab': 'GitLab is a web-based DevOps lifecycle tool that provides a Git-repository manager.',
'mattermost': 'Mattermost is an open-source, self-hostable online chat service with file sharing, search, and integrations.',
'matrix-synapse': 'Matrix Synapse is a reference homeserver implementation of the Matrix protocol.',
'mastodon': 'Mastodon is a free and open-source self-hosted social networking service.',
'minecraft': 'Minecraft server for multiplayer gaming.',
'factorio': 'Factorio dedicated server for multiplayer factory building.',
'satisfactory': 'Satisfactory dedicated server for multiplayer factory building in 3D.',
'ollama': 'Ollama is a tool for running large language models locally.',
'whisper': 'OpenAI Whisper is an automatic speech recognition system.',
'stable-diffusion': 'Stable Diffusion is a deep learning, text-to-image model.',
}
service_key = service_name.lower().replace('-', '').replace('_', '')
# Try exact match first
if service_key in descriptions:
return descriptions[service_key]
# Try partial matches
for key, desc in descriptions.items():
if key in service_key or service_key in key:
return desc
# Generate generic description based on category
category_descriptions = {
'media': f'{service_name} is a media management and streaming service that helps organize and serve your digital media content.',
'monitoring': f'{service_name} is a monitoring and observability tool that helps track system performance and health.',
'productivity': f'{service_name} is a productivity application that helps manage tasks, documents, or workflows.',
'development': f'{service_name} is a development tool that assists with code management, CI/CD, or software development workflows.',
'communication': f'{service_name} is a communication platform that enables messaging, collaboration, or social interaction.',
'security': f'{service_name} is a security tool that helps protect systems, manage authentication, or secure communications.',
'networking': f'{service_name} is a networking service that manages network traffic, routing, or connectivity.',
'storage': f'{service_name} is a storage solution that manages data persistence, backup, or file sharing.',
'gaming': f'{service_name} is a gaming server that hosts multiplayer games or gaming-related services.',
'ai': f'{service_name} is an AI/ML service that provides artificial intelligence or machine learning capabilities.',
'other': f'{service_name} is a specialized service that provides specific functionality for the homelab infrastructure.'
}
return category_descriptions.get(category, category_descriptions['other'])
def format_compose_config(self, config: Dict) -> str:
"""Format the Docker Compose configuration for display."""
try:
import yaml
return yaml.dump(config, default_flow_style=False, indent=2)
except:
return str(config)
def format_environment_variables(self, env_vars: Dict[str, str]) -> str:
"""Format environment variables for display."""
if not env_vars:
return "No environment variables configured."
result = "| Variable | Value | Description |\n|----------|-------|-------------|\n"
for key, value in env_vars.items():
# Mask sensitive values
display_value = value
if any(sensitive in key.lower() for sensitive in ['password', 'secret', 'key', 'token']):
display_value = '***MASKED***'
result += f"| `{key}` | `{display_value}` | {self.get_env_var_description(key)} |\n"
return result
def get_env_var_description(self, var_name: str) -> str:
"""Get description for common environment variables."""
descriptions = {
'TZ': 'Timezone setting',
'PUID': 'User ID for file permissions',
'PGID': 'Group ID for file permissions',
'MYSQL_ROOT_PASSWORD': 'MySQL root password',
'POSTGRES_PASSWORD': 'PostgreSQL password',
'REDIS_PASSWORD': 'Redis authentication password',
'ADMIN_PASSWORD': 'Administrator password',
'SECRET_KEY': 'Application secret key',
'JWT_SECRET': 'JWT signing secret',
'DATABASE_URL': 'Database connection string',
'DOMAIN': 'Service domain name',
'BASE_URL': 'Base URL for the service',
'DEBUG': 'Enable debug mode',
'LOG_LEVEL': 'Logging verbosity level'
}
var_lower = var_name.lower()
for key, desc in descriptions.items():
if key.lower() in var_lower:
return desc
return 'Configuration variable'
def format_ports(self, ports: List[str]) -> str:
"""Format port mappings for display."""
if not ports:
return "No ports exposed."
result = "| Host Port | Container Port | Protocol | Purpose |\n|-----------|----------------|----------|----------|\n"
for port in ports:
if ':' in port:
host_port, container_port = port.split(':', 1)
protocol = 'TCP'
if '/' in container_port:
container_port, protocol = container_port.split('/')
purpose = self.get_port_purpose(container_port)
result += f"| {host_port} | {container_port} | {protocol.upper()} | {purpose} |\n"
else:
result += f"| {port} | {port} | TCP | Service port |\n"
return result
def get_port_purpose(self, port: str) -> str:
"""Get the purpose of common ports."""
port_purposes = {
'80': 'HTTP web interface',
'443': 'HTTPS web interface',
'8080': 'Alternative HTTP port',
'8443': 'Alternative HTTPS port',
'3000': 'Web interface',
'9000': 'Management interface',
'5432': 'PostgreSQL database',
'3306': 'MySQL/MariaDB database',
'6379': 'Redis cache',
'27017': 'MongoDB database',
'9090': 'Prometheus metrics',
'3001': 'Monitoring interface',
'8086': 'InfluxDB',
'25565': 'Minecraft server',
'7777': 'Game server',
'22': 'SSH access',
'21': 'FTP',
'53': 'DNS',
'67': 'DHCP',
'123': 'NTP',
'161': 'SNMP',
'514': 'Syslog',
'1883': 'MQTT',
'8883': 'MQTT over SSL'
}
return port_purposes.get(port, 'Service port')
def format_volumes(self, volumes: List[str]) -> str:
"""Format volume mappings for display."""
if not volumes:
return "No volumes mounted."
result = "| Host Path | Container Path | Type | Purpose |\n|-----------|----------------|------|----------|\n"
for volume in volumes:
if ':' in volume:
parts = volume.split(':')
host_path = parts[0]
container_path = parts[1]
volume_type = 'bind' if host_path.startswith('/') or host_path.startswith('./') else 'volume'
purpose = self.get_volume_purpose(container_path)
result += f"| `{host_path}` | `{container_path}` | {volume_type} | {purpose} |\n"
else:
result += f"| `{volume}` | `{volume}` | volume | Data storage |\n"
return result
def get_volume_purpose(self, path: str) -> str:
"""Get the purpose of common volume paths."""
path_purposes = {
'/config': 'Configuration files',
'/data': 'Application data',
'/app/data': 'Application data',
'/var/lib': 'Service data',
'/etc': 'Configuration files',
'/logs': 'Log files',
'/var/log': 'System logs',
'/media': 'Media files',
'/downloads': 'Downloaded files',
'/uploads': 'Uploaded files',
'/backup': 'Backup files',
'/tmp': 'Temporary files',
'/cache': 'Cache data',
'/db': 'Database files',
'/ssl': 'SSL certificates',
'/keys': 'Encryption keys'
}
path_lower = path.lower()
for key, purpose in path_purposes.items():
if key in path_lower:
return purpose
return 'Data storage'
def generate_access_info(self, service_name: str, ports: List[str], host: str) -> str:
"""Generate access information for the service."""
if not ports:
return "This service does not expose any web interfaces."
web_ports = []
for port in ports:
if ':' in port:
host_port = port.split(':')[0]
container_port = port.split(':')[1].split('/')[0]
if container_port in ['80', '443', '8080', '8443', '3000', '9000', '8000', '5000']:
web_ports.append(host_port)
if not web_ports:
return f"Service ports: {', '.join(ports)}"
result = "### Web Interface\n"
for port in web_ports:
protocol = 'https' if port in ['443', '8443'] else 'http'
result += f"- **{protocol.upper()}**: `{protocol}://{host}:{port}`\n"
result += "\n### Default Credentials\n"
result += self.get_default_credentials(service_name)
return result
def get_default_credentials(self, service_name: str) -> str:
"""Get default credentials for common services."""
credentials = {
'grafana': 'Username: `admin`, Password: "REDACTED_PASSWORD" (change on first login)',
'portainer': 'Set admin password on first access',
'jenkins': 'Check logs for initial admin password',
'gitlab': 'Username: `root`, Password: "REDACTED_PASSWORD" `/etc/gitlab/initial_root_password`',
'nextcloud': 'Set admin credentials during initial setup',
'mattermost': 'Create admin account during setup',
'mastodon': 'Create admin account via command line',
'matrix-synapse': 'Create users via command line or admin API',
'uptime-kuma': 'Set admin credentials on first access',
'vaultwarden': 'Create account on first access',
'paperless-ngx': 'Create superuser via management command'
}
service_key = service_name.lower().replace('-', '').replace('_', '')
for key, creds in credentials.items():
if key in service_key or service_key in key:
return creds
return 'Refer to service documentation for default credentials'
def generate_security_info(self, service_name: str, config: Dict) -> str:
"""Generate security information for the service."""
security_info = []
# Check for security options
if 'security_opt' in config:
security_info.append("✅ Security options configured")
else:
security_info.append("⚠️ Consider adding security options (no-new-privileges)")
# Check for user mapping
if 'user' in config:
security_info.append("✅ Non-root user configured")
else:
security_info.append("⚠️ Consider running as non-root user")
# Check for read-only root filesystem
if config.get('read_only', False):
security_info.append("✅ Read-only root filesystem")
# Check for capabilities
if 'cap_drop' in config:
security_info.append("✅ Capabilities dropped")
# Add service-specific security recommendations
service_security = self.get_service_security_recommendations(service_name)
if service_security:
security_info.extend(service_security)
return '\n'.join(f"- {info}" for info in security_info)
def get_service_security_recommendations(self, service_name: str) -> List[str]:
"""Get security recommendations for specific services."""
recommendations = {
'vaultwarden': [
'🔒 Enable HTTPS with reverse proxy',
'🔒 Disable user registration after setup',
'🔒 Enable 2FA for all accounts',
'🔒 Regular database backups'
],
'nextcloud': [
'🔒 Enable HTTPS',
'🔒 Configure trusted domains',
'🔒 Enable 2FA',
'🔒 Regular security updates'
],
'gitlab': [
'🔒 Enable HTTPS',
'🔒 Configure SSH keys',
'🔒 Enable 2FA',
'🔒 Regular backups'
],
'matrix-synapse': [
'🔒 Enable HTTPS',
'🔒 Configure federation carefully',
'🔒 Regular database backups',
'🔒 Monitor resource usage'
]
}
service_key = service_name.lower().replace('-', '').replace('_', '')
for key, recs in recommendations.items():
if key in service_key or service_key in key:
return recs
return []
def generate_resource_info(self, config: Dict) -> str:
"""Generate resource requirement information."""
resource_info = []
# Check for resource limits
deploy_config = config.get('deploy', {})
resources = deploy_config.get('resources', {})
limits = resources.get('limits', {})
if limits:
if 'memory' in limits:
resource_info.append(f"**Memory Limit**: {limits['memory']}")
if 'cpus' in limits:
resource_info.append(f"**CPU Limit**: {limits['cpus']}")
else:
resource_info.append("No resource limits configured")
# Add general recommendations
resource_info.extend([
"",
"### Recommended Resources",
"- **Minimum RAM**: 512MB",
"- **Recommended RAM**: 1GB+",
"- **CPU**: 1 core minimum",
"- **Storage**: Varies by usage",
"",
"### Resource Monitoring",
"Monitor resource usage with:",
"```bash",
"docker stats",
"```"
])
return '\n'.join(resource_info)
def generate_health_info(self, config: Dict) -> str:
"""Generate health monitoring information."""
health_info = []
# Check for health check configuration
if 'healthcheck' in config:
health_config = config['healthcheck']
health_info.append("✅ Health check configured")
if 'test' in health_config:
test_cmd = health_config['test']
if isinstance(test_cmd, list):
test_cmd = ' '.join(test_cmd)
health_info.append(f"**Test Command**: `{test_cmd}`")
if 'interval' in health_config:
health_info.append(f"**Check Interval**: {health_config['interval']}")
if 'timeout' in health_config:
health_info.append(f"**Timeout**: {health_config['timeout']}")
if 'retries' in health_config:
health_info.append(f"**Retries**: {health_config['retries']}")
else:
health_info.append("⚠️ No health check configured")
health_info.append("Consider adding a health check:")
health_info.append("```yaml")
health_info.append("healthcheck:")
health_info.append(" test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:PORT/health\"]")
health_info.append(" interval: 30s")
health_info.append(" timeout: 10s")
health_info.append(" retries: 3")
health_info.append("```")
# Add monitoring commands
health_info.extend([
"",
"### Manual Health Checks",
"```bash",
"# Check container health",
"docker inspect --format='{{.State.Health.Status}}' CONTAINER_NAME",
"",
"# View health check logs",
"docker inspect --format='{{range .State.Health.Log}}{{.Output}}{{end}}' CONTAINER_NAME",
"```"
])
return '\n'.join(health_info)
def generate_troubleshooting_info(self, service_name: str, category: str) -> str:
"""Generate troubleshooting information."""
common_issues = [
"**Service won't start**",
"- Check Docker logs: `docker-compose logs service-name`",
"- Verify port availability: `netstat -tulpn | grep PORT`",
"- Check file permissions on mounted volumes",
"",
"**Can't access web interface**",
"- Verify service is running: `docker-compose ps`",
"- Check firewall settings",
"- Confirm correct port mapping",
"",
"**Performance issues**",
"- Monitor resource usage: `docker stats`",
"- Check available disk space: `df -h`",
"- Review service logs for errors"
]
# Add category-specific troubleshooting
category_issues = {
'media': [
"",
"**Media not showing**",
"- Check media file permissions",
"- Verify volume mounts are correct",
"- Scan media library manually"
],
'monitoring': [
"",
"**Metrics not collecting**",
"- Check target endpoints are accessible",
"- Verify configuration syntax",
"- Check network connectivity"
],
'security': [
"",
"**Authentication issues**",
"- Verify credentials are correct",
"- Check LDAP/SSO configuration",
"- Review authentication logs"
]
}
issues = common_issues.copy()
if category in category_issues:
issues.extend(category_issues[category])
return '\n'.join(issues)
def generate_additional_resources(self, service_name: str, image: str) -> str:
"""Generate additional resources section."""
resources = [
f"- **Official Documentation**: Check the official docs for {service_name}",
f"- **Docker Hub**: [{image}](https://hub.docker.com/r/{image})" if '/' in image else f"- **Docker Hub**: [Official {service_name}](https://hub.docker.com/_/{image})",
"- **Community Forums**: Search for community discussions and solutions",
"- **GitHub Issues**: Check the project's GitHub for known issues"
]
# Add service-specific resources
service_resources = {
'plex': [
"- **Plex Support**: https://support.plex.tv/",
"- **Plex Forums**: https://forums.plex.tv/"
],
'jellyfin': [
"- **Jellyfin Documentation**: https://jellyfin.org/docs/",
"- **Jellyfin Forum**: https://forum.jellyfin.org/"
],
'grafana': [
"- **Grafana Documentation**: https://grafana.com/docs/",
"- **Grafana Community**: https://community.grafana.com/"
],
'nextcloud': [
"- **Nextcloud Documentation**: https://docs.nextcloud.com/",
"- **Nextcloud Community**: https://help.nextcloud.com/"
]
}
service_key = service_name.lower().replace('-', '').replace('_', '')
for key, additional in service_resources.items():
if key in service_key or service_key in key:
resources.extend(additional)
break
return '\n'.join(resources)
def generate_related_services(self, service_name: str, category: str, host: str) -> str:
"""Generate related services information."""
# This would be populated with actual service relationships
# For now, provide category-based suggestions
category_related = {
'media': ['Plex', 'Jellyfin', 'Radarr', 'Sonarr', 'Bazarr', 'Tautulli'],
'monitoring': ['Grafana', 'Prometheus', 'Uptime Kuma', 'Node Exporter'],
'productivity': ['Nextcloud', 'Paperless-NGX', 'BookStack', 'Syncthing'],
'security': ['Vaultwarden', 'Authelia', 'Pi-hole', 'WireGuard'],
'development': ['GitLab', 'Gitea', 'Jenkins', 'Portainer']
}
related = category_related.get(category, [])
if not related:
return f"Other services in the {category} category on {host}"
return f"Services REDACTED_APP_PASSWORD {service_name}:\n" + '\n'.join(f"- {service}" for service in related[:5])
def get_current_date(self) -> str:
"""Get current date for documentation."""
from datetime import datetime
return datetime.now().strftime("%Y-%m-%d")
def generate_all_documentation(self):
"""Generate documentation for all services."""
print("🔍 Finding Docker Compose files...")
compose_files = self.find_compose_files()
print(f"Found {len(compose_files)} compose files")
all_services = {}
print("📋 Parsing service configurations...")
for compose_file in compose_files:
services = self.parse_compose_file(compose_file)
all_services.update(services)
print(f"Found {len(all_services)} total services")
print("📝 Generating individual service documentation...")
for service_name, service_info in all_services.items():
print(f" Documenting {service_name}...")
# Generate documentation content
doc_content = self.generate_service_documentation(service_name, service_info)
# Write to file
doc_filename = f"{service_name.lower().replace('_', '-')}.md"
doc_path = self.docs_path / doc_filename
with open(doc_path, 'w', encoding='utf-8') as f:
f.write(doc_content)
print(f"✅ Generated documentation for {len(all_services)} services")
print(f"📁 Documentation saved to: {self.docs_path}")
# Generate index file
self.generate_service_index(all_services)
return len(all_services)
def generate_service_index(self, all_services: Dict):
"""Generate an index file for all services."""
index_content = f"""# 📚 Individual Service Documentation Index
This directory contains detailed documentation for all {len(all_services)} services in the homelab.
## 📋 Services by Category
"""
# Group services by category
services_by_category = {}
for service_name, service_info in all_services.items():
config = service_info['config']
image = config.get('image', '')
category = self.categorize_service(service_name, image)
if category not in services_by_category:
services_by_category[category] = []
services_by_category[category].append({
'name': service_name,
'host': service_info['host'],
'difficulty': self.get_difficulty_level(service_name, config)
})
# Generate category sections
for category in sorted(services_by_category.keys()):
services = sorted(services_by_category[category], key=lambda x: x['name'])
index_content += f"### {category.title()} ({len(services)} services)\n\n"
for service in services:
filename = f"{service['name'].lower().replace('_', '-')}.md"
index_content += f"- {service['difficulty']} **[{service['name']}]({filename})** - {service['host']}\n"
index_content += "\n"
index_content += f"""
## 📊 Statistics
- **Total Services**: {len(all_services)}
- **Categories**: {len(services_by_category)}
- **Hosts**: {len(set(s['host'] for s in all_services.values()))}
## 🔍 Quick Search
Use your browser's search function (Ctrl+F / Cmd+F) to quickly find specific services.
---
*This index is auto-generated. Last updated: {self.get_current_date()}*
"""
# Write index file
index_path = self.docs_path / "README.md"
with open(index_path, 'w', encoding='utf-8') as f:
f.write(index_content)
print(f"📋 Generated service index: {index_path}")
if __name__ == "__main__":
generator = ServiceDocumentationGenerator("/workspace/homelab")
total_services = generator.generate_all_documentation()
print(f"\n🎉 Documentation generation complete!")
print(f"📊 Total services documented: {total_services}")