Code Standards¶
Conventions, formatting rules, and quality gates for this project.
Docker Compose Conventions¶
Service Ordering¶
Services in compose.yaml must be listed alphabetically (enforced by dclint).
Ports¶
- Ports must be double-quoted:
"3000:3000"not3000:3000(enforced by dclint) - Avoid common ports (80, 443, 3000, 8080) on Unraid — nginx occupies 80/443
- Use unique ports per service — check existing inventory before assigning
Security¶
- Always use
security_opt: ["no-new-privileges:true"](no double quotes around value) - Drop all capabilities where possible:
cap_drop: ["ALL"]
Hardware Transcoding (GPU)¶
When a service needs GPU access (e.g., Jellyfin hardware transcoding):
- Mount
/dev/dridevice — provides renderD128 and card0 nodes - Only for services that need hardware transcoding (Jellyfin)
- Verify host has Intel iGPU with VAAPI support before enabling
Networking¶
Internal Service Communication Rules (MANDATORY):
| Scenario | Use | Example | Why |
|---|---|---|---|
| Same server, same Docker network | Container name | redis:6379 |
Fastest, Docker DNS |
| Same server, different network | LAN IP | 192.168.100.59:6379 |
No Tailscale overhead |
| Cross-server | LAN IP | 192.168.100.31:9120 |
Direct, lower latency |
| Tailscale-only reach | Tailscale IP | 100.126.172.96:9120 |
When LAN not reachable |
| Never | Public domain | ❌ service.minhluc.info |
Unnecessary CF + auth hop |
- Prefer LAN IP (
192.168.100.x) over Tailscale IP for service-to-service — lower latency, no WireGuard overhead - Only use Tailscale IP when LAN is not reachable (e.g., from outside network)
- Never use public domains (
*.minhluc.info) for internal communication - Cross-compose DNS unreliable — use
host.docker.internal+extra_hosts:
Database Connections¶
Default PostgreSQL: pigsty-postgres (paradedb/paradedb:latest)
| Scenario | Connection String | Example |
|---|---|---|
| Same server, same Docker network | pigsty-postgres:5432 |
DATABASE_URL=postgresql://postgres:pwd@pigsty-postgres:5432/mydb |
| Same server, different network | 192.168.100.59:5439 |
DB_HOST=192.168.100.59 DB_PORT=5439 |
| Cross-server | 192.168.100.59:5439 |
DB_HOST=192.168.100.59 DB_PORT=5439 |
| From host (non-Docker) | 192.168.100.59:5439 |
psql -h 192.168.100.59 -p 5439 -U postgres |
Available extensions: pg_search (BM25), pgvector, pg_trgm, pg_cron, pg_stat_statements
Adding a new service that needs PostgreSQL:
- Create database on pigsty:
- For dedicated user (optional):
docker exec pigsty-postgres psql -U postgres -c "CREATE USER myservice WITH PASSWORD 'xxx';"
docker exec pigsty-postgres psql -U postgres -c "CREATE DATABASE myservice OWNER myservice;"
- In compose.yaml (same server, same network):
environment:
- DB_HOST=pigsty-postgres # Container name on reverse-proxy network
- DB_PORT=5432 # Internal container port
- DB_NAME=myservice
- DB_USER=postgres
- DB_PASSWORD=${DB_PASSWORD}
- In TOML (unraid services):
- In TOML (cross-server / development):
Password: All services use [[DB_PASSWORD]] Komodo Variable (maps to PIGSTY_PASSWORD on pigsty stack).
Exceptions: Immich uses its own PG 14 image (postgres-immich, port 5437) — do not migrate.
File Paths¶
file_pathsin Komodo TOML must only contain YAML files (Core parses them as YAML)- Non-YAML config files (Caddyfile, provisioning) go through
pre_deploy.command
Volume Ownership¶
- All services on unraid MUST write files as
nobody(UID 99, GID 100) - Set
user: "99:100"in compose for non-root services - Add
chown -R 99:100topre_deploy.commandfor volume mounts - Exception: services running as specific UIDs (e.g., Airflow 50000:0)
TOML Conventions¶
Tag Patterns¶
| Pattern | Example | When to Use |
|---|---|---|
server:{name} |
server:unraid |
Always — target server |
category:{name} |
category:applications |
Always — service category |
saas:{name} |
saas:tailscale |
Exposed via SaaS integration |
selfhost:{name} |
selfhost:photo |
Replaces a cloud SaaS |
app:{name} |
app:immich |
Application identifier |
Environment Variables¶
# Komodo resolves [[VAR]] from Variables UI or config file secrets
environment = """
TZ=Asia/Ho_Chi_Minh
DATABASE_URL=[[DATABASE_URL]]
"""
- Default timezone:
Asia/Ho_Chi_Minh - Secrets:
[[SECRET_NAME]]syntax (never hardcoded for external secrets) - Internal keys (service-to-service, homelab-only) can be hardcoded
- Multiple env vars: one per line inside triple-quoted block
run_directory (CRITICAL)¶
Must be an absolute path on the periphery server:
# CORRECT
run_directory = "/etc/komodo/repos/docker-compose/stacks/{category}/{service}/"
# WRONG — resolves to /etc/komodo/stacks/stacks/... (double path)
run_directory = "./stacks/{category}/{service}/"
linked_repomust be set (empty string does not work)- Periphery resolves relative to
stack_dir(/etc/komodo/stacks/), not the repo clone
Formatting¶
- Enforced by
taplo-formatvianpx @taplo/cli align_entries = true— aligns values in tables
Python Standards¶
Configuration¶
| Setting | Value |
|---|---|
| Version | 3.12 (.python-version) |
| Package manager | uv |
| ruff target | py311 (pyproject.toml) |
| Line length | 100 |
| String quotes | Double quotes |
| mypy mode | Strict (disallow_untyped_defs, strict_equality) |
Workspace Structure¶
Commands¶
# Install
uv sync
uv sync --group dev # Adds ruff, mypy, yamlfix
# Lint
uv run ruff check .
uv run ruff format --check .
uv run mypy .
# Auto-fix
uv run ruff check --fix .
uv run ruff format .
Pre-Commit Hook Chain¶
Hooks run in this order on every commit:
| Order | Hook | Tool | What It Checks |
|---|---|---|---|
| 1 | taplo-format | npx @taplo/cli |
TOML formatting |
| 2 | ruff | ruff | Python lint + auto-fix |
| 3 | ruff-format | ruff | Python formatting |
| 4 | mypy | mypy | Strict type checking |
| 5 | dclint | dclint | Docker Compose lint |
| 6 | hadolint | hadolint | Dockerfile lint (ignores DL3008) |
| 7 | yamllint | yamllint | YAML lint (max 120 chars warning) |
YAML Standards¶
Linting (yamllint)¶
- Max line length: 120 chars (warning)
- Indentation: disabled (flexible)
- Config:
.yamllint.yml
Fixing (yamlfix)¶
- Line length: 120
- Block style sequences
- Configured in dev dependencies
Naming Conventions¶
| Item | Convention | Example |
|---|---|---|
| Service directory | kebab-case | otel-collector, adguard-home |
| Compose file | compose.yaml |
Always this exact name |
| TOML file | {service-name}.toml |
grafana.toml, redis.toml |
| Environment file | compose.env |
Local overrides |
| Tags | prefix:value |
server:unraid, app:immich |
| Git commits | type(scope): message |
fix(grafana): update dashboard path |
Git Commit Format¶
| Type | Usage |
|---|---|
feat |
New service or feature |
fix |
Bug fix in compose/config |
chore |
Maintenance, updates |
docs |
Documentation changes |
refactor |
Restructure without behavior change |
test |
Test additions |
perf |
Performance improvements |
Related Documentation¶
- Design Guidelines — Why these standards exist
- Deployment Guide — Operational procedures