YotoShelf
Self-Host

Quick Start

Get YotoShelf's container running from the registry image in under five minutes.

Pull and run

The recommended way to run YotoShelf is with Docker Compose. Create a compose.yml file, replace the placeholder secrets with random strings of at least 32 characters each, then bring it up.

The published image is multi-arch, supporting linux/amd64 and linux/arm64 — Raspberry Pi 4/5 and other arm64 hosts pull natively.

services:
  yotoshelf:
    image: registry.gitlab.com/yotoshelf/yotoshelf:latest
    container_name: yotoshelf
    ports:
      - "8080:8080"
    volumes:
      - yotoshelf-data:/data
    environment:
      YOTOSHELF_DB_PATH: /data/yotoshelf.db
      YOTOSHELF_LIBRARY_PATH: /data/library
      YOTOSHELF_SESSION_SECRET: <random-32-bytes>
      YOTOSHELF_ENCRYPTION_KEY: <random-32-bytes>
      YOTOSHELF_SECURE_COOKIES: false # plain HTTP on a LAN address only; remove this line behind HTTPS
      # YOTOSHELF_PUBLIC_URL: https://yotoshelf.example.com
    restart: unless-stopped

volumes:
  yotoshelf-data:
docker compose up -d

The container is now running. Next, create your admin account; YotoShelf has no browser sign-up.

Or, a one-liner with docker run

If you prefer a quick one-off start without a Compose file:

docker run -d \
  --name yotoshelf \
  -p 8080:8080 \
  -v yotoshelf-data:/data \
  -e YOTOSHELF_DB_PATH=/data/yotoshelf.db \
  -e YOTOSHELF_LIBRARY_PATH=/data/library \
  -e YOTOSHELF_SESSION_SECRET=<random-32-bytes> \
  -e YOTOSHELF_ENCRYPTION_KEY=<random-32-bytes> \
  -e YOTOSHELF_SECURE_COOKIES=false \
  registry.gitlab.com/yotoshelf/yotoshelf:latest
  # YOTOSHELF_SECURE_COOKIES=false is for plain HTTP on a LAN address only; remove it behind HTTPS
  # -e YOTOSHELF_PUBLIC_URL=https://yotoshelf.example.com \

Create your admin account

If local auth is enabled (the default), the server exposes a create-admin subcommand; there is no browser sign-up. Run it once before your first login:

docker exec yotoshelf /yotoshelf create-admin \
  --email admin@example.com \
  --password <password> \
  --db-path /data/yotoshelf.db

Alternatively, configure OIDC and set YOTOSHELF_LOCAL_AUTH_ENABLED=false; the first OIDC user to log in is automatically promoted to admin. See Configuration.

Then open http://localhost:8080 and log in with the account you just created.

Link your Yoto account

Open the Accounts page and click Link Yoto Account to start the OAuth flow. This needs a Yoto OAuth application configured first; see the Yoto API section of Configuration for the client ID/secret and the exact redirect URL to register on dashboard.yoto.dev.

Required environment variables

The following variables have no defaults and must be set before the server will start in production:

VariablePurpose
YOTOSHELF_SESSION_SECRET Signs session cookies. At least 32 bytes. Rotate to invalidate all sessions.
YOTOSHELF_ENCRYPTION_KEY Encrypts Yoto OAuth tokens at rest. At least 32 bytes. Changing this invalidates stored tokens.

See Configuration for the full variable reference, including database path, library path, SMTP, OIDC, and observability settings.

Volumes and data

YotoShelf writes two kinds of data:

  • Database: a single SQLite file at YOTOSHELF_DB_PATH (default: yotoshelf.db in the working directory). Back this up regularly.
  • Library: audio files, cover images, and generated assets at YOTOSHELF_LIBRARY_PATH (default: library/). Can be large; put it on the storage you intend to keep.

Both paths should be on a persistent volume. A single volume mount works fine; set both variables to paths inside it. The volume must be local disk (SD card or SSD), not a network share (CIFS/NFS): SQLite's file locking is unreliable over those protocols and can corrupt the database.

The image runs as uid 1000 and ships /data owned 1000:1000, so a named volume works out of the box, but a bind mount must be chowned to 1000:1000 on the host (or the container run with --user matching the host user).

Reverse proxy

YotoShelf listens on :8080 (configurable via YOTOSHELF_LISTEN_ADDR). Place it behind a reverse proxy (Caddy, nginx, Traefik) and terminate TLS there. Set YOTOSHELF_TRUST_PROXY=true so audit logs record real client IPs from X-Forwarded-For.

Set YOTOSHELF_PUBLIC_URL to the externally visible URL (e.g. https://yotoshelf.example.com). Unless overridden, it drives both OAuth redirect URLs: the Yoto callback at <PUBLIC_URL>/api/v1/yoto-accounts/callback and, if OIDC is configured, <PUBLIC_URL>/api/v1/auth/oidc/callback. Enter the exact Yoto callback URL as the Redirect URI on dashboard.yoto.dev. Leaving YOTOSHELF_PUBLIC_URL unset also disables CSRF origin checking.

Troubleshooting

exec format error / no matching manifest for linux/arm64

The image tag you pulled predates the first multi-arch release and is amd64-only. Pull the current image to get a manifest list covering both linux/amd64 and linux/arm64.

403 … User does not have required scope

An account linked before the scope fix holds a token without resource scopes. Re-link it from the Accounts page. Ticking more permission boxes on dashboard.yoto.dev does not widen a token that was already issued; only re-linking does.

Verify the server is up

curl http://localhost:8080/healthz   # → 200 OK
curl http://localhost:8080/readyz    # → 200 OK (checks DB + OIDC + SMTP)