# Homelab Service Setup Guide for AI Agents This document provides patterns, conventions, and best practices for setting up services in this homelab environment. Follow these guidelines when creating new services or modifying existing ones. ## Repository Structure ``` homelab/ ├── .claude/ # Claude Code configuration │ └── skills/ # Custom skills for AI agents │ └── wiki-docs.md # Wiki documentation skill ├── compose/ │ ├── core/ # Infrastructure services (Traefik, Authelia, LLDAP) │ │ ├── traefik/ │ │ ├── authelia/ │ │ └── lldap/ │ ├── services/ # User-facing applications │ │ ├── service-name/ │ │ │ ├── compose.yaml │ │ │ ├── .env │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ └── QUICKSTART.md │ ├── media/ # Media-related services │ │ ├── frontend/ # Media viewers (Jellyfin, Immich) │ │ └── automation/ # Media management (*arr stack) │ └── monitoring/ # Monitoring and logging ├── AGENTS.md # AI agent guidelines (this file) └── README.md # Repository overview ``` **External Directories:** - `/mnt/media/wikijs-content/` - Wiki.js content repository (Git-backed) ## Core Principles ### 1. Domain Convention - **Primary domain:** `fig.systems` - **Secondary domain:** `edfig.dev` - **Pattern:** `service.fig.systems` or `service.edfig.dev` - **Examples:** - `matrix.fig.systems` - Matrix server - `auth.fig.systems` - Authelia - `books.fig.systems` - BookLore - `ai.fig.systems` - Open WebUI #### DNS and DDNS Setup **Automatic DNS Resolution:** - Wildcard DNS records are automatically updated via DDNS Updater - `*.fig.systems` → Points to current public IP (Cloudflare) - `*.edfig.dev` → Points to current public IP (Porkbun) - `fig.systems` (root) → Points to current public IP - `edfig.dev` (root) → Points to current public IP **What this means for new services:** - ✅ DNS is automatic - any `newservice.fig.systems` will resolve to the homelab IP - ✅ No manual DNS record creation needed - ✅ Works for all subdomains automatically - ⚠️ You still need Traefik labels to route traffic to containers (see Traefik Integration section) **DDNS Updater Service:** - Location: `compose/services/ddns-updater/` - Monitors: Public IP changes every 5 minutes - Updates: Both Cloudflare (fig.systems) and Porkbun (edfig.dev) - Web UI: https://ddns.fig.systems (local network only) **Adding a new service:** 1. DNS resolution is already handled by wildcard records 2. Add Traefik labels to your compose.yaml (see Service Setup Pattern below) 3. Start container - Traefik auto-detects and routes traffic 4. Let's Encrypt SSL certificate generated automatically ### 2. Storage Conventions **Media Storage:** `/mnt/media/` - `/mnt/media/books/` - Book library - `/mnt/media/movies/` - Movie library - `/mnt/media/tv/` - TV shows - `/mnt/media/photos/` - Photo library - `/mnt/media/music/` - Music library **Service Data:** `/mnt/media/service-name/` ```bash # Example: Matrix storage structure /mnt/media/matrix/ ├── synapse/ │ ├── data/ # Configuration and database │ └── media/ # Uploaded media files ├── postgres/ # Database files └── bridges/ # Bridge configurations ├── telegram/ ├── whatsapp/ └── googlechat/ ``` **Always create subdirectories for:** - Configuration files - Database data - User uploads/media - Logs (if persistent) ### 3. Network Architecture **External Network:** `homelab` - All services connect to this for Traefik routing - Created externally, referenced as `external: true` **Internal Networks:** `service-internal` - For multi-container service communication - Example: `matrix-internal`, `booklore-internal` - Use `driver: bridge` ```yaml networks: homelab: external: true service-internal: driver: bridge ``` ## Service Setup Pattern ### Directory Structure Every service should have: ``` compose/services/service-name/ ├── compose.yaml # Docker Compose configuration ├── .env # Environment variables and secrets ├── .gitignore # Ignore data directories and secrets ├── README.md # Complete documentation ├── QUICKSTART.md # 5-step quick start guide └── config-files/ # Service-specific configs (optional) ``` ### Required Files #### 1. compose.yaml **Basic template:** ```yaml services: service-name: image: vendor/service:latest container_name: service-name environment: - TZ=${TZ} - PUID=${PUID} - PGID=${PGID} # Service-specific vars volumes: - /mnt/media/service-name:/data restart: unless-stopped networks: - homelab labels: # Traefik routing traefik.enable: true traefik.docker.network: homelab # HTTP Router traefik.http.routers.service-name.rule: Host(`service.fig.systems`) traefik.http.routers.service-name.entrypoints: websecure traefik.http.routers.service-name.tls.certresolver: letsencrypt traefik.http.services.service-name.loadbalancer.server.port: 8080 # Homarr Discovery homarr.name: Service Name homarr.group: Services homarr.icon: mdi:icon-name networks: homelab: external: true ``` **With database:** ```yaml services: app: # ... app config depends_on: database: condition: service_healthy networks: - homelab - service-internal database: image: postgres:16-alpine # or mariadb, redis, etc. container_name: service-database environment: POSTGRES_USER: ${DB_USER} POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_DB: ${DB_NAME} volumes: - /mnt/media/service-name/db:/var/lib/postgresql/data restart: unless-stopped networks: - service-internal healthcheck: test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"] interval: 10s timeout: 5s retries: 5 networks: homelab: external: true service-internal: driver: bridge ``` #### 2. .env File **Standard variables:** ```bash # Domain Configuration DOMAIN=fig.systems SERVICE_DOMAIN=service.fig.systems TRAEFIK_HOST=service.fig.systems # System TZ=America/Los_Angeles PUID=1000 PGID=1000 # Database (if applicable) DB_USER=service DB_PASSWORD= DB_NAME=service # SMTP Configuration (Mailgun) SMTP_HOST=smtp.mailgun.org SMTP_PORT=587 SMTP_USER=noreply@fig.systems SMTP_PASSWORD= SMTP_FROM=Service Name # Optional SMTP settings SMTP_TLS=true SMTP_STARTTLS=true # Service-specific secrets SERVICE_SECRET_KEY= ``` **Generate secrets:** ```bash # Random hex (64 chars) openssl rand -hex 32 # Base64 (32 bytes) openssl rand -base64 32 # Alphanumeric (32 chars) openssl rand -base64 24 | tr -d '/+=' | head -c 32 ``` #### 3. .gitignore **Standard pattern:** ```gitignore # Service data (stored in /mnt/media/) data/ config/ db/ logs/ # Environment secrets .env # Backup files *.bak *.backup ``` #### 4. README.md **Structure:** ```markdown # Service Name - Brief Description One-paragraph overview of what the service does. ## Features - ✅ Feature 1 - ✅ Feature 2 - ✅ Feature 3 ## Access **URL:** https://service.fig.systems **Authentication:** [Authelia SSO | None | Basic Auth] ## Quick Start ### Deploy \`\`\`bash cd /home/eduardo_figueroa/homelab/compose/services/service-name docker compose up -d \`\`\` ### First-Time Setup 1. Step 1 2. Step 2 3. Step 3 ## Configuration ### Environment Variables Explain key .env variables ### Storage Locations - `/mnt/media/service-name/data` - Application data - `/mnt/media/service-name/uploads` - User uploads ## Usage Guide Detailed usage instructions... ## Troubleshooting Common issues and solutions... ## Maintenance ### Backup Important directories to backup... ### Update \`\`\`bash docker compose pull docker compose up -d \`\`\` ## Links - Documentation: https://... - GitHub: https://... ``` #### 5. QUICKSTART.md **Fast 5-step guide:** ```markdown # Service Name - Quick Start ## Step 1: Deploy \`\`\`bash cd /path/to/service docker compose up -d \`\`\` ## Step 2: Access Open https://service.fig.systems ## Step 3: Initial Setup Quick setup steps... ## Step 4: Test Verification steps... ## Common Commands \`\`\`bash # View logs docker compose logs -f # Restart docker compose restart # Stop docker compose down \`\`\` ``` ## Traefik Integration ### Basic HTTP Routing ```yaml labels: traefik.enable: true traefik.docker.network: homelab # Router traefik.http.routers.service.rule: Host(`service.fig.systems`) traefik.http.routers.service.entrypoints: websecure traefik.http.routers.service.tls.certresolver: letsencrypt # Service (port) traefik.http.services.service.loadbalancer.server.port: 8080 ``` ### With Custom Headers ```yaml labels: # ... basic routing ... # Headers middleware traefik.http.middlewares.service-headers.headers.customrequestheaders.X-Forwarded-Proto: https traefik.http.middlewares.service-headers.headers.customresponseheaders.X-Frame-Options: SAMEORIGIN # Apply middleware traefik.http.routers.service.middlewares: service-headers ``` ### With Local-Only Access ```yaml labels: # ... basic routing ... # Apply local-only middleware (defined in Traefik) traefik.http.routers.service.middlewares: local-only ``` ### Large Upload Support ```yaml labels: # ... basic routing ... # Buffering middleware traefik.http.middlewares.service-buffering.buffering.maxRequestBodyBytes: 268435456 traefik.http.middlewares.service-buffering.buffering.memRequestBodyBytes: 268435456 traefik.http.middlewares.service-buffering.buffering.retryExpression: IsNetworkError() && Attempts() < 3 # Apply middleware traefik.http.routers.service.middlewares: service-buffering ``` ## Authelia OIDC Integration ### 1. Generate Client Secret ```bash # Generate plain secret openssl rand -base64 32 # Hash for Authelia docker exec authelia authelia crypto hash generate pbkdf2 --password 'your-secret-here' ``` ### 2. Add Client to Authelia Edit `/home/eduardo_figueroa/homelab/compose/core/authelia/config/configuration.yml`: ```yaml identity_providers: oidc: clients: # Your Service - client_id: service-name client_name: Service Display Name client_secret: '$pbkdf2-sha512$310000$...' # hashed secret authorization_policy: two_factor redirect_uris: - https://service.fig.systems/oauth/callback scopes: - openid - profile - email grant_types: - authorization_code response_types: - code ``` **For public clients (PKCE):** ```yaml - client_id: service-name client_name: Service Name public: true # No client_secret needed authorization_policy: two_factor require_pkce: true pkce_challenge_method: S256 redirect_uris: - https://service.fig.systems/oauth/callback scopes: - openid - profile - email - offline_access # For refresh tokens grant_types: - authorization_code - refresh_token response_types: - code ``` ### 3. Configure Service **Standard OIDC configuration:** ```yaml environment: OIDC_ENABLED: "true" OIDC_CLIENT_ID: "service-name" OIDC_CLIENT_SECRET: "plain-secret-here" OIDC_ISSUER: "https://auth.fig.systems" OIDC_AUTHORIZATION_ENDPOINT: "https://auth.fig.systems/api/oidc/authorization" OIDC_TOKEN_ENDPOINT: "https://auth.fig.systems/api/oidc/token" OIDC_USERINFO_ENDPOINT: "https://auth.fig.systems/api/oidc/userinfo" OIDC_JWKS_URI: "https://auth.fig.systems/jwks.json" ``` ### 4. Restart Services ```bash # Restart Authelia cd compose/core/authelia docker compose restart # Start your service cd compose/services/service-name docker compose up -d ``` ## SMTP/Email Configuration ### Mailgun SMTP **Standard Mailgun configuration for all services:** ```bash # In .env file SMTP_HOST=smtp.mailgun.org SMTP_PORT=587 SMTP_USER=noreply@fig.systems SMTP_PASSWORD= SMTP_FROM=Service Name SMTP_TLS=true SMTP_STARTTLS=true ``` **In compose.yaml:** ```yaml environment: # SMTP Settings SMTP_HOST: ${SMTP_HOST} SMTP_PORT: ${SMTP_PORT} SMTP_USER: ${SMTP_USER} SMTP_PASSWORD: ${SMTP_PASSWORD} SMTP_FROM: ${SMTP_FROM} # Some services may use different variable names: # EMAIL_HOST: ${SMTP_HOST} # EMAIL_PORT: ${SMTP_PORT} # EMAIL_USER: ${SMTP_USER} # EMAIL_PASS: ${SMTP_PASSWORD} # EMAIL_FROM: ${SMTP_FROM} ``` **Common SMTP variable name variations:** Different services use different environment variable names for SMTP configuration. Check the service documentation and use the appropriate format: | Common Name | Alternative Names | |------------|-------------------| | SMTP_HOST | EMAIL_HOST, MAIL_HOST, MAIL_SERVER | | SMTP_PORT | EMAIL_PORT, MAIL_PORT | | SMTP_USER | EMAIL_USER, MAIL_USER, SMTP_USERNAME, EMAIL_USERNAME | | SMTP_PASSWORD | EMAIL_PASSWORD, EMAIL_PASS, MAIL_PASSWORD, SMTP_PASS | | SMTP_FROM | EMAIL_FROM, MAIL_FROM, FROM_EMAIL, DEFAULT_FROM_EMAIL | | SMTP_TLS | EMAIL_USE_TLS, MAIL_USE_TLS, SMTP_SECURE | | SMTP_STARTTLS | EMAIL_USE_STARTTLS, MAIL_STARTTLS | **Getting Mailgun SMTP credentials:** 1. Log into Mailgun dashboard: https://app.mailgun.com 2. Navigate to **Sending → Domain Settings → SMTP credentials** 3. Use the existing `noreply@fig.systems` user or create a new SMTP user 4. Copy the SMTP password and add it to your service's `.env` file **Testing SMTP configuration:** ```bash # Using swaks (SMTP test tool) swaks --to test@example.com \ --from noreply@fig.systems \ --server smtp.mailgun.org:587 \ --auth LOGIN \ --auth-user noreply@fig.systems \ --auth-password 'your-password' \ --tls ``` ## Database Patterns ### PostgreSQL ```yaml postgres: image: postgres:16-alpine container_name: service-postgres environment: POSTGRES_USER: ${DB_USER} POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_DB: ${DB_NAME} POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C" volumes: - /mnt/media/service-name/postgres:/var/lib/postgresql/data restart: unless-stopped networks: - service-internal healthcheck: test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"] interval: 10s timeout: 5s retries: 5 ``` ### MariaDB ```yaml mariadb: image: lscr.io/linuxserver/mariadb:latest container_name: service-mariadb environment: - PUID=${PUID} - PGID=${PGID} - TZ=${TZ} - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} - MYSQL_DATABASE=${MYSQL_DATABASE} - MYSQL_USER=${MYSQL_USER} - MYSQL_PASSWORD=${MYSQL_PASSWORD} volumes: - /mnt/media/service-name/mariadb:/config restart: unless-stopped networks: - service-internal healthcheck: test: ["CMD", "mariadb-admin", "ping", "-h", "localhost"] interval: 5s timeout: 5s retries: 10 ``` ### Redis ```yaml redis: image: redis:alpine container_name: service-redis command: redis-server --save 60 1 --loglevel warning volumes: - /mnt/media/service-name/redis:/data restart: unless-stopped networks: - service-internal healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 5s retries: 5 ``` ## Homarr Integration **Add discovery labels to your service:** ```yaml labels: homarr.name: Display Name homarr.group: Services # or Media, Monitoring, AI, etc. homarr.icon: mdi:icon-name # Material Design Icons ``` **Common groups:** - `Services` - General applications - `Media` - Media-related (Jellyfin, Immich) - `AI` - AI/LLM services - `Monitoring` - Monitoring tools - `Automation` - *arr stack **Find icons:** https://pictogrammers.com/library/mdi/ ## Security Best Practices ### 1. Never Commit Secrets **Always in .gitignore:** - `.env` files - Database directories - Configuration files with credentials - SSL certificates - API keys ### 2. Use Authelia for External Access Services exposed to internet should use Authelia SSO with 2FA. ### 3. Local-Only Services For sensitive services (backups, code editors), use `local-only` middleware: ```yaml traefik.http.routers.service.middlewares: local-only ``` ### 4. Least Privilege - Use non-root users in containers (`PUID`/`PGID`) - Limit network access (internal networks) - Read-only mounts where possible: `./config:/config:ro` ### 5. Secrets Generation **Always generate unique secrets:** ```bash # For each service openssl rand -hex 32 # Different secret each time ``` ## Common Patterns ### Multi-Stage Service Setup **For services requiring initial config generation:** 1. Generate config: ```bash docker run --rm -v /path:/data image:latest --generate-config ``` 2. Edit config files 3. Start service: ```bash docker compose up -d ``` ### Bridge/Plugin Architecture **For services with plugins/bridges:** ```yaml # Main service main-app: # ... config ... volumes: - /mnt/media/service/data:/data - ./registrations:/registrations:ro # Plugin registrations # Plugin 1 plugin-1: # ... config ... volumes: - /mnt/media/service/plugins/plugin-1:/data depends_on: main-app: condition: service_started networks: - service-internal ``` ### Health Checks **Always include health checks for databases:** ```yaml healthcheck: test: ["CMD-SHELL", "command to test health"] interval: 10s timeout: 5s retries: 5 ``` **Then use in depends_on:** ```yaml depends_on: database: condition: service_healthy ``` ## Troubleshooting Checklist ### Service Won't Start 1. Check logs: ```bash docker compose logs -f service-name ``` 2. Verify environment variables: ```bash docker compose config ``` 3. Check disk space: ```bash df -h /mnt/media ``` 4. Verify network exists: ```bash docker network ls | grep homelab ``` ### Can't Access via Domain 1. Check Traefik logs: ```bash docker logs traefik | grep service-name ``` 2. Verify service is on homelab network: ```bash docker inspect service-name | grep -A 10 Networks ``` 3. Test endpoint directly: ```bash curl -k https://service.fig.systems ``` 4. Check DNS resolution: ```bash nslookup service.fig.systems ``` ### OIDC Login Issues 1. Verify client secret matches in both Authelia and service 2. Check redirect URI exactly matches in Authelia config 3. Restart Authelia after config changes 4. Check Authelia logs: ```bash docker logs authelia | grep oidc ``` ### Database Connection Issues 1. Verify database is healthy: ```bash docker compose ps ``` 2. Check database logs: ```bash docker compose logs database ``` 3. Test connection from app container: ```bash docker compose exec app ping database ``` 4. Verify credentials match in .env and config ## Complete Service Template See `compose/services/matrix/` for a complete example of: - ✅ Multi-container setup (app + database + plugins) - ✅ Authelia OIDC integration - ✅ Traefik routing - ✅ Comprehensive documentation - ✅ Bridge/plugin architecture - ✅ Health checks and dependencies - ✅ Proper secret management ## AI Agent Guidelines When setting up new services: 1. **Always create complete config files in /tmp/** for files requiring sudo access 2. **Follow the directory structure** exactly as shown above 3. **Generate unique secrets** for each service 4. **Create both README.md and QUICKSTART.md** 5. **Use the storage conventions** (/mnt/media/service-name/) 6. **Add Traefik labels** for automatic routing 7. **Include Homarr discovery labels** 8. **Set up health checks** for all databases 9. **Use internal networks** for multi-container communication 10. **Document troubleshooting steps** in README.md ### Files to Always Create in /tmp/ When you cannot write directly: - Authelia configuration updates - Traefik configuration changes - System-level configuration files **Format:** ```bash /tmp/service-name-config-file.yml ``` Include clear instructions at the top: ```yaml # Copy this file to: # /path/to/actual/location # # Then run: # sudo chmod 644 /path/to/actual/location # docker compose restart ``` ## Claude Code Skills This repository includes custom skills for Claude Code to enhance productivity and maintain consistency. ### Available Skills #### wiki-docs (Documentation Management) **Purpose:** Create and manage markdown documentation files that automatically sync to Wiki.js **Location:** `.claude/skills/wiki-docs.md` **When to use:** - Documenting new services or infrastructure changes - Creating how-to guides or tutorials - Recording configuration details for future reference - Building a knowledge base for the homelab **Repository:** `/mnt/media/wikijs-content/` **Wiki URL:** https://wiki.fig.systems **Git Remote:** `git.fig.systems/eddie/wiki.git` **How it works:** 1. Markdown files are written to `/mnt/media/wikijs-content/` 2. Files are committed and pushed to the Git repository 3. Wiki.js automatically syncs changes (within 5 minutes) 4. Content appears at https://wiki.fig.systems **Frontmatter format:** ```yaml --- title: Page Title description: Brief description published: true date: 2026-03-15T00:00:00.000Z tags: tag1, tag2, tag3 editor: markdown dateCreated: 2026-03-15T00:00:00.000Z --- ``` **Note:** Tags must be comma-separated, not YAML array format! **Example usage:** ```bash # Create documentation for a service /mnt/media/wikijs-content/homelab/services/jellyfin.md # Commit and push cd /mnt/media/wikijs-content git pull git add homelab/services/jellyfin.md git commit -m "Add: Jellyfin service documentation" git push ``` **Benefits:** - Version-controlled documentation - Accessible via web interface (Wiki.js) - Searchable and organized - Supports markdown with frontmatter - Automatic synchronization ### Using Skills To invoke a skill in Claude Code, use the appropriate skill when the task matches its purpose. The wiki-docs skill is automatically available for documentation tasks. ## Resources - **Traefik:** https://doc.traefik.io/traefik/ - **Authelia:** https://www.authelia.com/ - **Docker Compose:** https://docs.docker.com/compose/ - **Material Design Icons:** https://pictogrammers.com/library/mdi/ - **Wiki.js:** https://docs.requarks.io/ --- **Remember:** Consistency is key. Follow these patterns for all services to maintain a clean, predictable, and maintainable homelab infrastructure.