12 KiB
| name | description |
|---|---|
| homelab-docker-compose | 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 (
.envfiles and compose files are deployed via Dockhand) - Shared cross-stack network: External Docker network
pipelineused for inter-stack service connectivity (Headroom, Chroma, Langfuse, Dify, OTEL-LGTM, etc.) - Compose storage: Stacks live in
/mnt/user/robbie/git/curated_compose/<stack-name>/with a companion.env.examplein 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.
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.
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.
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:
otellgtm
This ensures every other stack can use stable hostnames for telemetry configuration.
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.
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.
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
./<service>-datafor persistent data - Join the stack's internal network
- Accept configuration via
${VAR}environment variables from the.envfile
Example pattern:
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 ./<service>-data. This creates a directory alongside the compose file in curated_compose/<stack>/, 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:
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:
- The patch has been tested and confirmed working by Robbie in the past
- 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
- Example: A Chroma server →
-
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
- Example: Prowler frontend →
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
./<service>-datapattern — 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 (./<service>-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).
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:
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
.envfile is deployed by Dockhand and should NOT be committed
General Structure
curated_compose/<stack-name>/
├── compose.yaml
├── .env.example
└── <any supporting config dirs or files>
When Robbie Gives You a Stack Request
- Research the project to find its recommended Docker Compose configuration
- Adapt it to Robbie's conventions (stack network,
pipelinealiases, bundled infra, Venice.ai, optional/commented SWAG, etc.) - Write
compose.yamlin the appropriatecurated_compose/<name>/directory - Create
.env.examplewith all configurable variables documented - If the stack requests OpenAI and does NOT support openai-compatible endpoints, alert Robbie
- If you're unsure about any preference, ask Robbie — do not guess