Each app lives in apps/<app-name>/compose.yml — a single file with no info.json.
All metadata is in the top-level x-yantr key. Docker Compose ignores x-* fields, so the file remains fully deployable.
Required fields:
name— display name (lowercase, alphanumeric only, no spaces)tags— 3–5 lowercase stringsshort_description— 50–100 charsdescription— 200–300 chars (use YAML>block scalar for multi-line)usecases— ≥2 stringswebsite— https:// URL
Optional fields:
logo— omitted; placelogo.svgin the app folder instead (auto-detected)notes— list of strings explaining manual setup stepscustomapp— boolean;truefor Yantr-built apps with Dockerfileenv_generators— map ofVAR → {length, charset}for auto-generated secrets.charsetvalues:alnum,hex,numeric,alpha,base64url,alnum_symbols
YAML style — always use flow sequences for flat arrays (tags, usecases, notes):
# ✅ correct
tags: [tools, utility, self-hosted, homelab, docker]
usecases: ["Use case one.", "Use case two."]
notes: ["Note one.", "Note two."]
# ❌ wrong — check.js WILL fail
tags:
- tools
- utilitylabels MUST always be a map (key-value dict), never a sequence. yantr.app is strictly required on every service.
labels:
yantr.app: "my-app"
yantr.service.8080: "Web UI"
yantr.port.8080: "HTTP"Supported protocols: HTTP, HTTPS, TCP, UDP
x-auth:
port: 3002 # Caddy listens here (auth-protected public port)
username: admin
password: secret # bcrypted by Yantr at deploy time, then discardedAll persistent data MUST use named Docker volumes — never bind mounts. Declare every volume at the top-level volumes: key.
# ✅ correct
volumes:
- my_app_data:/data
volumes:
my_app_data:
# ❌ wrong
volumes:
- ./data:/dataAlways use the :latest tag (or the upstream's equivalent rolling tag). Never pin to a specific version number.
# ✅ correct
image: ghcr.io/example/my-app:latest
# ❌ wrong
image: ghcr.io/example/my-app:1.2.3Place a logo.svg in the app folder alongside compose.yml (auto-detected, no field needed). SVGs must be square, minimum 256×256. Omit if no logo is available — do not use URLs.
# ✅ correct — local SVG (auto-detected, no logo field needed)
# apps/my-app/logo.svg (256x256 or larger, square)
# ❌ wrong
logo: "https://example.com/logo.png"Use Docker's automatic port assignment "8080" instead of explicit host mappings "8080:8080". Only use mapped ports when the app absolutely cannot function without a fixed host port (e.g. a VPN or peer protocol that must bind to a specific port).
# ✅ correct — let Docker assign the host port
ports:
- "8080"
# ❌ wrong — avoid unless the app cannot work without it
ports:
- "8080:8080"# apps/my-app/compose.yml
x-yantr:
name: "myapp"
tags: [productivity, self-hosted, webapp, tools, docker]
short_description: "Self-hosted note-taking app."
description: >
A self-hosted note-taking service that lets you capture, organize,
and share notes with your team. Runs entirely on your own hardware.
usecases: ["Capture notes.", "Organize docs.", "Share with team."]
website: "https://example.com/docs"
env_generators:
ADMIN_PASSWORD: {length: 20, charset: alnum_symbols}
notes: ["Set admin email in the deploy form if the app requires one."]
services:
my-app:
image: ghcr.io/example/my-app:latest
container_name: my-app
labels:
yantr.app: "my-app"
yantr.service.8080: "Web UI"
yantr.port.8080: "HTTP"
environment:
TZ: ${TZ:-UTC}
ADMIN_USER: ${ADMIN_USER:-admin}
ADMIN_PASSWORD: ${ADMIN_PASSWORD}
ports:
- "8080"
volumes:
- my_app_data:/data
restart: unless-stopped
volumes:
my_app_data:Run node check.js after any app changes. It will fail if:
compose.ymluses${VAR}without a default andenv_generatorsis missing the matching entry- Flat arrays use block sequences instead of flow sequences