homelab/AGENTS.md
Eduardo Figueroa 9e23a54852 feat: Add wiki-docs skill for documentation management
- Create .claude/skills/wiki-docs.md skill for managing Wiki.js documentation
- Skill enables writing markdown files to /mnt/media/wikijs-content/
- Files automatically sync to Wiki.js via Git storage backend
- Update AGENTS.md with Claude Code Skills section
- Document wiki-docs skill usage and benefits
- Add /mnt/media/wikijs-content/ to repository structure

The wiki-docs skill allows AI agents to create version-controlled
documentation that syncs to https://wiki.fig.systems automatically.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-15 23:45:12 +00:00

22 KiB

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/

# 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
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:

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:

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:

# 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=<generated-password>
DB_NAME=service

# SMTP Configuration (Mailgun)
SMTP_HOST=smtp.mailgun.org
SMTP_PORT=587
SMTP_USER=noreply@fig.systems
SMTP_PASSWORD=<mailgun-smtp-password>
SMTP_FROM=Service Name <noreply@fig.systems>
# Optional SMTP settings
SMTP_TLS=true
SMTP_STARTTLS=true

# Service-specific secrets
SERVICE_SECRET_KEY=<generated-secret>

Generate secrets:

# 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:

# Service data (stored in /mnt/media/)
data/
config/
db/
logs/

# Environment secrets
.env

# Backup files
*.bak
*.backup

4. README.md

Structure:

# 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:

# 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

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

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

labels:
  # ... basic routing ...

  # Apply local-only middleware (defined in Traefik)
  traefik.http.routers.service.middlewares: local-only

Large Upload Support

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

# 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:

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):

      - 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:

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

# 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:

# In .env file
SMTP_HOST=smtp.mailgun.org
SMTP_PORT=587
SMTP_USER=noreply@fig.systems
SMTP_PASSWORD=<your-mailgun-smtp-password>
SMTP_FROM=Service Name <noreply@fig.systems>
SMTP_TLS=true
SMTP_STARTTLS=true

In compose.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:

# 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

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

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

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:

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:

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:

# 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:

    docker run --rm -v /path:/data image:latest --generate-config
    
  2. Edit config files

  3. Start service:

    docker compose up -d
    

Bridge/Plugin Architecture

For services with plugins/bridges:

# 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:

healthcheck:
  test: ["CMD-SHELL", "command to test health"]
  interval: 10s
  timeout: 5s
  retries: 5

Then use in depends_on:

depends_on:
  database:
    condition: service_healthy

Troubleshooting Checklist

Service Won't Start

  1. Check logs:

    docker compose logs -f service-name
    
  2. Verify environment variables:

    docker compose config
    
  3. Check disk space:

    df -h /mnt/media
    
  4. Verify network exists:

    docker network ls | grep homelab
    

Can't Access via Domain

  1. Check Traefik logs:

    docker logs traefik | grep service-name
    
  2. Verify service is on homelab network:

    docker inspect service-name | grep -A 10 Networks
    
  3. Test endpoint directly:

    curl -k https://service.fig.systems
    
  4. Check DNS resolution:

    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:
    docker logs authelia | grep oidc
    

Database Connection Issues

  1. Verify database is healthy:

    docker compose ps
    
  2. Check database logs:

    docker compose logs database
    
  3. Test connection from app container:

    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:

/tmp/service-name-config-file.yml

Include clear instructions at the top:

# 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:

---
title: Page Title
description: Brief description
published: true
date: 2026-03-15T00:00:00.000Z
tags:
  - tag1
  - tag2
---

Example usage:

# 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


Remember: Consistency is key. Follow these patterns for all services to maintain a clean, predictable, and maintainable homelab infrastructure.