From 06addf396f9acf24694e937dacf2cd6c05d38b23 Mon Sep 17 00:00:00 2001 From: Sean Robinson Date: Mon, 15 Jun 2026 21:46:33 -0400 Subject: [PATCH] skill --- SKILL.md | 298 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 298 insertions(+) create mode 100644 SKILL.md diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..1bbae55 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,298 @@ +--- +name: homelab-docker-compose +description: Creates compose files and .env.example for Robbie's homelab (Unraid, FireEye NX4500). Follows curated_compose conventions with stack-specific networks, shared external pipeline network + aliases, bundled infra, Venice.ai for OpenAI, and optional/commented SWAG external network. +--- + +# Homelab Docker Compose Skill + +Use this skill when Robbie asks you to create a docker-compose stack for his self-hosted homelab. This skill encodes all of his preferences, system constraints, and conventions. + +## System Context + +- **Server**: FireEye NX4500 repurposed for homelab +- **CPU**: 2x E5-2620 v4 (2x E5-2699 v4 coming soon) +- **RAM**: 126GB +- **GPU**: No GPU currently installed (2x T4 Tesla planned for future) +- **OS**: Unraid +- **Orchestration**: Docker Compose + Dockhand (`.env` files and compose files are deployed via Dockhand) +- **Shared cross-stack network**: External Docker network `pipeline` used for inter-stack service connectivity (Headroom, Chroma, Langfuse, Dify, OTEL-LGTM, etc.) +- **Compose storage**: Stacks live in `/mnt/user/robbie/git/curated_compose//` with a companion `.env.example` in the same directory + +## Conventions for Every Compose + +### 1. Stack-specific Network + +Every stack MUST have its own bridge network so services can communicate by hostname. Name the network after the stack (e.g., `mystack`). Every service in the stack joins this network. + +```yaml +networks: + mystack: + name: mystack + driver: bridge +``` + +### 2. External Networks + +#### 2a. Shared Pipeline Network (cross-stack service-to-service) + +Use the external `pipeline` network for services that must be reachable by other stacks (e.g., shared APIs, vector DBs, observability endpoints, proxy endpoints). + +Declare it at the bottom and attach only the services that need cross-stack access. + +```yaml +networks: + mystack: + name: mystack + driver: bridge + pipeline: + name: pipeline + external: true +``` + +For services on `pipeline`, use explicit aliases so other stacks can resolve stable hostnames. + +```yaml +services: + api: + networks: + mystack: {} + pipeline: + aliases: + - mystack-api +``` + +Do **not** attach internal-only services (databases, caches, workers) to `pipeline` unless cross-stack access is explicitly required. + +##### Network Attach Matrix (canonical) + +Use this decision table when wiring `networks:` for each service: + +| Service type | Attach to stack network | Attach to `pipeline` | Attach to `swag` | +| -------------------------------------------------------------------------------- | ----------------------- | -------------------- | ----------------------------- | +| Internal-only infra (Postgres, Redis, RabbitMQ, workers) | Yes | No (default) | No | +| Cross-stack private service (vector DB, proxy, telemetry endpoint, internal API) | Yes | Yes (with alias) | No (default) | +| Public web/API behind reverse proxy | Yes | Usually yes | Optional/commented by default | + +#### 2b. OTEL-LGTM Reserved Pipeline Aliases + +For the OTEL-LGTM stack, reserve these **pipeline aliases** on the telemetry ingress service: + +- `otel` +- `lgtm` + +This ensures every other stack can use stable hostnames for telemetry configuration. + +```yaml +services: + otel-lgtm-otelcol: + networks: + otel-lgtm: {} + pipeline: + aliases: + - otel + - lgtm +``` + +Recommended cross-stack telemetry endpoints over `pipeline`: + +- OTLP gRPC: `otel:4317` +- OTLP HTTP: `http://otel:4318` + +#### 2c. SWAG Network (reverse proxy) + +SWAG remains optional. If a stack needs SWAG reverse-proxy access, include SWAG as a second external network declaration at the bottom, but keep it **COMMENTED OUT by default**. + +```yaml +networks: + mystack: + name: mystack + driver: bridge + pipeline: + name: pipeline + external: true + # swag: + # name: swag + # external: true +``` + +If a service needs SWAG exposure (web/API), add SWAG in that service's `networks:` section, also commented out by default. + +```yaml +services: + nginx: + networks: + - mystack + - pipeline + # - swag +``` + +Do NOT assume public exposure by default. Only SWAG-enable services that realistically need reverse-proxying (web frontends, APIs), not internal-only services. + +### 3. Bundled Infrastructure + +If a stack requires PostgreSQL, Redis, RabbitMQ, or any other infrastructure dependency, include it **within the same compose file**. Do not share databases, caches, or message brokers between stacks unless Robbie explicitly instructs otherwise. + +Each infrastructure service should: + +- Use a well-known, official image (e.g., `postgres:15-alpine`, `redis:7-alpine`) +- Set `restart: unless-stopped` +- Include a `healthcheck` +- Use a **relative bind mount** with `./-data` for persistent data +- Join the stack's internal network +- Accept configuration via `${VAR}` environment variables from the `.env` file + +Example pattern: + +```yaml +services: + prowler-db: + image: postgres:15-alpine + restart: unless-stopped + environment: + POSTGRES_USER: ${DB_USERNAME} + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_DB: ${DB_DATABASE} + volumes: + - ./prowler-db-data:/var/lib/postgresql/data + healthcheck: + test: + [ + "CMD", + "pg_isready", + "-h", + "prowler-db", + "-U", + "${DB_USERNAME:-postgres}", + ] + interval: 5s + timeout: 3s + retries: 30 + networks: + - prowler +``` + +### 3a. Persistent Storage (Relative Bind Mounts) + +All persistent data uses **relative bind mounts** with the convention `./-data`. This creates a directory alongside the compose file in `curated_compose//`, keeping data co-located and easy to find. + +Examples: + +| Service | Mount path | Creates directory | +| -------------------------- | -------------------------------------------- | --------------------------------------------- | +| PostgreSQL for `prowler` | `./prowler-db-data:/var/lib/postgresql/data` | `curated_compose/prowler/prowler-db-data/` | +| Redis for `prowler` | `./prowler-redis-data:/data` | `curated_compose/prowler/prowler-redis-data/` | +| File storage for `prowler` | `./prowler-storage:/app/storage` | `curated_compose/prowler/prowler-storage/` | +| Chroma vector DB | `./data:/chroma/chroma/` | `curated_compose/chroma/data/` | + +For supporting config files (e.g., nginx configs, custom scripts), also use relative bind mounts like `./nginx/nginx.conf:/etc/nginx/nginx.conf`. + +These directories should be added to `.gitignore` at the `curated_compose` level so persistent data is not committed to version control. + +### 4. OpenAI / LLM Configuration + +If a stack calls for OpenAI or an LLM provider, **always configure it to use an OpenAI-compatible endpoint** pointing to Venice.ai: + +- **Base URL**: `https://api.venice.ai/api/v1` +- **API Key**: `${VENICE_API_KEY}` (Robbie will supply this in his `.env`) + +If the application supports an `OPENAI_API_BASE` / `OPENAI_API_KEY` environment variable pattern, use that: + +```yaml +OPENAI_API_KEY: ${VENICE_API_KEY} +OPENAI_API_BASE: https://api.venice.ai/api/v1 +``` + +If the application does **NOT** support OpenAI-compatible endpoints (i.e., it requires a direct OpenAI API key or uses a non-standard API format), **alert Robbie immediately** before proceeding. Do not attempt to patch, proxy, or work around this without his explicit approval. + +### 5. Well-maintained Projects Only + +Prefer actively maintained, official, or widely-adopted images and projects. Do not suggest unmaintained forks, obscure repositories, or projects with low community trust. + +### 6. No Patching or Custom Builds Without Approval + +Do NOT modify a service's code, create a Dockerfile patch, or build a custom container unless **both** of these are true: + +1. The patch has been tested and confirmed working by Robbie in the past +2. The patch is a simple injection or small configuration overlay + +If neither condition is met, ask Robbie for approval first. + +### 7. Naming Conventions + +Service names (and thus hostnames) MUST be descriptive and unmistakable about what they are. The pattern is: + +- **Standalone service** (the service IS the stack): Use the service name directly. + - Example: A Chroma server → `chroma` + - Example: A Qdrant server → `qdrant` + +- **Multi-service stack**: Prefix every service name with the stack name, then append the role. + - Example: Prowler frontend → `prowler-web` + - Example: Prowler API backend → `prowler-api` + - Example: Prowler PostgreSQL → `prowler-db` + - Example: Prowler Redis → `prowler-redis` + - Example: Prowler worker → `prowler-worker` + +This makes it trivially obvious which stack a service belongs to and what its role is just from the hostname. + +- **Network**: `mystack` (named after the stack directory) +- **Volumes**: Relative bind mounts with `./-data` pattern — see section 3a +- **Container names**: Do NOT set explicit `container_name:` — let Docker generate them. Dockhand handles identification. + +### 8. Volume Declarations + +Since persistent data uses relative bind mounts (`./-data`), the `volumes:` section at the bottom of the compose file is typically **empty** or omitted entirely. Only add named volumes here if a service specifically requires one (e.g., for ephemeral caches or Docker-specific features). + +```yaml +volumes: {} +``` + +### 9. Port Exposure + +Only expose ports on services that need external access (a web UI, an API that other machines on the network hit, etc.). + +If a service is **internal-only** (a database, cache, worker, or any service that is only accessed by other services within the same stack network), do **NOT** expose its ports. Exposing ports on internal services creates port conflicts when multiple stacks use the same database or cache image on the same host. + +Examples of when to expose ports: + +- A web frontend that needs to be accessed via the host IP: expose port 80/443 +- An API that other stacks or external tools hit directly: expose its port + +Examples of when NOT to expose ports: + +- PostgreSQL, Redis, RabbitMQ — these are accessed by other services in the stack via the internal network hostname, not via `localhost:PORT` +- Internal workers, background job processors, or sidecars + +When a port **is** exposed, always pull the host port from an environment variable with a sensible default: + +```yaml +ports: + - ${EXPOSE_HTTP_PORT:-80}:80 +``` + +### 10. Environment Variables via `.env` + +Every configurable value should be pulled from the environment via `${VAR}` or `${VAR:-default}`. Create a companion `.env.example` file that: + +- Contains **all non-sensitive** configuration variables with sensible defaults +- Uses placeholder values for secrets (e.g., `your-secure-password-here`, `change-me`) +- Does NOT contain real passwords, API keys, or tokens +- Is well-commented so Robbie knows what each value does +- The actual `.env` file is deployed by Dockhand and should NOT be committed + +## General Structure + +``` +curated_compose// +├── compose.yaml +├── .env.example +└── +``` + +## When Robbie Gives You a Stack Request + +1. Research the project to find its recommended Docker Compose configuration +2. Adapt it to Robbie's conventions (stack network, `pipeline` aliases, bundled infra, Venice.ai, optional/commented SWAG, etc.) +3. Write `compose.yaml` in the appropriate `curated_compose//` directory +4. Create `.env.example` with all configurable variables documented +5. If the stack requests OpenAI and does NOT support openai-compatible endpoints, alert Robbie +6. If you're unsure about any preference, ask Robbie — do not guess