- Change tags from YAML array to comma-separated format - Add required editor and dateCreated fields - Update all examples with correct frontmatter format - Add note about tag format requirement
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.systemsorservice.edfig.dev - Examples:
matrix.fig.systems- Matrix serverauth.fig.systems- Autheliabooks.fig.systems- BookLoreai.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 IPedfig.dev(root) → Points to current public IP
What this means for new services:
- ✅ DNS is automatic - any
newservice.fig.systemswill 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:
- DNS resolution is already handled by wildcard records
- Add Traefik labels to your compose.yaml (see Service Setup Pattern below)
- Start container - Traefik auto-detects and routes traffic
- 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:
- Log into Mailgun dashboard: https://app.mailgun.com
- Navigate to Sending → Domain Settings → SMTP credentials
- Use the existing
noreply@fig.systemsuser or create a new SMTP user - Copy the SMTP password and add it to your service's
.envfile
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 applicationsMedia- Media-related (Jellyfin, Immich)AI- AI/LLM servicesMonitoring- Monitoring toolsAutomation- *arr stack
Find icons: https://pictogrammers.com/library/mdi/
Security Best Practices
1. Never Commit Secrets
Always in .gitignore:
.envfiles- 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:
-
Generate config:
docker run --rm -v /path:/data image:latest --generate-config -
Edit config files
-
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
-
Check logs:
docker compose logs -f service-name -
Verify environment variables:
docker compose config -
Check disk space:
df -h /mnt/media -
Verify network exists:
docker network ls | grep homelab
Can't Access via Domain
-
Check Traefik logs:
docker logs traefik | grep service-name -
Verify service is on homelab network:
docker inspect service-name | grep -A 10 Networks -
Test endpoint directly:
curl -k https://service.fig.systems -
Check DNS resolution:
nslookup service.fig.systems
OIDC Login Issues
- Verify client secret matches in both Authelia and service
- Check redirect URI exactly matches in Authelia config
- Restart Authelia after config changes
- Check Authelia logs:
docker logs authelia | grep oidc
Database Connection Issues
-
Verify database is healthy:
docker compose ps -
Check database logs:
docker compose logs database -
Test connection from app container:
docker compose exec app ping database -
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:
- Always create complete config files in /tmp/ for files requiring sudo access
- Follow the directory structure exactly as shown above
- Generate unique secrets for each service
- Create both README.md and QUICKSTART.md
- Use the storage conventions (/mnt/media/service-name/)
- Add Traefik labels for automatic routing
- Include Homarr discovery labels
- Set up health checks for all databases
- Use internal networks for multi-container communication
- 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:
- Markdown files are written to
/mnt/media/wikijs-content/ - Files are committed and pushed to the Git repository
- Wiki.js automatically syncs changes (within 5 minutes)
- 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, tag3
editor: markdown
dateCreated: 2026-03-15T00:00:00.000Z
---
Note: Tags must be comma-separated, not YAML array format!
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
- 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.