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