Skip to content

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" not 3000: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):

devices:
  - /dev/dri:/dev/dri # Intel VAAPI render nodes
  • Mount /dev/dri device — 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:
extra_hosts:
  - host.docker.internal:host-gateway

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:

  1. Create database on pigsty:
docker exec pigsty-postgres psql -U postgres -c "CREATE DATABASE myservice OWNER postgres;"
  1. 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;"
  1. 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}
  1. In TOML (unraid services):
environment = """
DB_PASSWORD=[[DB_PASSWORD]]
"""
  1. In TOML (cross-server / development):
    environment = """
    DB_HOST=192.168.100.59
    DB_PORT=5439
    DB_NAME=myservice
    DB_USER=postgres
    DB_PASSWORD=[[DB_PASSWORD]]
    """
    

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_paths in 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:100 to pre_deploy.command for 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_repo must be set (empty string does not work)
  • Periphery resolves relative to stack_dir (/etc/komodo/stacks/), not the repo clone

Formatting

  • Enforced by taplo-format via npx @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

pyproject.toml          # Root workspace
scripts/mikrotik/       # Workspace member with own pyproject.toml

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)
# Run all hooks
pre-commit run --all-files

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(scope): description
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