Self-Hosting Beets with Docker Compose

What Is Beets?

Beets is a music library manager and auto-tagger. Drop poorly-tagged music files into a folder, and beets identifies them against MusicBrainz (the world’s largest open music database), corrects metadata, fetches album art, normalizes volume levels, and organizes everything into a clean directory structure. It also includes a web UI for browsing your library and 60+ plugins for tasks like lyrics fetching, acoustic fingerprinting, and format conversion.

Beets on GitHub

Prerequisites

  • A Linux server (Ubuntu 22.04+ recommended)
  • Docker and Docker Compose installed (guide)
  • 512 MB of free RAM (beets idles at ~50-80 MB; imports with fingerprinting can spike to 500 MB)
  • A music library or collection of untagged music files
  • A domain name (optional, for remote web UI access)

Docker Compose Configuration

Create a docker-compose.yml file:

services:
  beets:
    image: lscr.io/linuxserver/beets:2.7.1
    container_name: beets
    restart: unless-stopped
    environment:
      # Set these to match your host user's UID/GID (run 'id' to find them)
      - PUID=1000
      - PGID=1000
      - TZ=America/New_York
    ports:
      # Web UI (requires web plugin enabled in config.yaml)
      - "8337:8337"
    volumes:
      # Beets config, database, and logs
      - ./beets-config:/config
      # Your organized music library (beets writes imported music here)
      - /path/to/music/library:/music
      # Drop untagged music here for import
      - /path/to/downloads:/downloads

volumes: {}

Replace /path/to/music/library with where you want beets to store organized music, and /path/to/downloads with your inbox for unprocessed files.

Create the config directory and beets configuration:

mkdir -p beets-config

Create beets-config/config.yaml:

# Core paths (must match Docker volume mounts)
directory: /music
library: /config/library.db

# Plugins — enable what you need
plugins: web fetchart embedart chroma lastgenre replaygain scrub missing duplicates

# Web UI — accessible at http://your-server:8337
web:
  host: 0.0.0.0
  port: 8337

# Import behavior
import:
  move: yes           # Move files from /downloads to /music (saves disk space)
  write: yes          # Write corrected tags to audio files
  quiet: no           # Ask for confirmation on uncertain matches

# Match confidence threshold (lower = stricter)
match:
  strong_rec_thresh: 0.04

# Auto-fetch and embed album art
fetchart:
  auto: yes

embedart:
  auto: yes

# Auto-tag genres using Last.fm data
lastgenre:
  auto: yes

# Volume normalization
replaygain:
  backend: ffmpeg
  auto: yes

Start the stack:

docker compose up -d

Initial Setup

  1. Import your existing music library:

    docker exec -it beets beet import /downloads

    Beets will scan each album, match it against MusicBrainz, and show you the result with a confidence score. Press a to accept a match, s to skip, or t to enter a manual search.

  2. Access the web UI at http://your-server-ip:8337 — browse your library, search, and play music directly in the browser via HTML5 audio.

  3. Verify the import — check that files were moved from /downloads to /music with a clean directory structure:

    docker exec -it beets beet stats

For large libraries (10,000+ tracks), the initial import takes hours due to MusicBrainz API rate limits. Use quiet: yes in the config to auto-accept high-confidence matches, but pair it with quiet_fallback: skip to avoid accepting bad matches.

Configuration

All beets configuration lives in /config/config.yaml inside the container. Edit it on the host at ./beets-config/config.yaml.

Path Templates

Control how beets organizes files in your library:

paths:
  default: $albumartist/$album%aunique{}/$track - $title
  singleton: Non-Album/$artist/$title
  comp: Compilations/$album%aunique{}/$track - $title

Non-Interactive Import

For automated workflows (e.g., importing from Lidarr or a download script), configure unattended imports:

import:
  quiet: yes               # Accept best match automatically
  quiet_fallback: skip     # Skip low-confidence matches instead of accepting them
  timid: no
  move: yes

Then run imports non-interactively:

docker exec beets beet import /downloads

Acoustic Fingerprinting (Chromaprint)

The chroma plugin identifies music by its audio fingerprint — useful for files with missing or incorrect tags. The LinuxServer image includes fpcalc (the Chromaprint tool). No additional setup needed beyond enabling the plugin.

Note: Fingerprinting significantly increases import time and CPU usage. Disable it if your library is already well-tagged.

Advanced Configuration (Optional)

Format Conversion

The convert plugin transcodes music between formats during or after import:

plugins: convert  # Add to your existing plugins list

convert:
  auto: no          # Set to 'yes' to auto-convert during import
  format: opus
  formats:
    opus:
      command: ffmpeg -i $source -y -vn -acodec libopus -ab 128k $dest
      extension: opus
    mp3:
      command: ffmpeg -i $source -y -vn -acodec libmp3lame -ab 320k $dest
      extension: mp3

Convert your entire library to Opus:

docker exec beets beet convert

Smart Playlists

Generate M3U playlists based on queries:

plugins: smartplaylist

smartplaylist:
  playlist_dir: /music/Playlists
  playlists:
    - name: recently-added.m3u
      query: 'added:30d..'
    - name: high-rated.m3u
      query: 'rating:4..5'
    - name: flac-only.m3u
      query: 'format:FLAC'

Media Server Integration

Notify Plex, Jellyfin (via MPD), or Kodi when beets updates the library:

plugins: plexupdate

plex:
  host: 192.168.1.100
  port: 32400
  token: YOUR_PLEX_TOKEN
  library_name: Music

Reverse Proxy

The beets web UI runs on port 8337. Example Nginx configuration:

server {
    listen 443 ssl;
    server_name beets.yourdomain.com;

    location / {
        proxy_pass http://127.0.0.1:8337;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

For full reverse proxy setup instructions, see Reverse Proxy Setup.

Backup

Back up the ./beets-config/ directory — it contains config.yaml (your settings), library.db (the SQLite database with all metadata, ratings, and import history), and beets logs.

Your /music directory is the organized library itself and should be backed up as part of your broader storage strategy.

See Backup Strategy for a comprehensive approach.

Troubleshooting

Web UI Not Accessible

Symptom: Can’t reach http://your-server:8337 — connection refused. Fix: The web plugin defaults to binding on 127.0.0.1 (localhost only). In Docker, you must set host: 0.0.0.0 in the beets config. Also verify the web plugin is listed in your plugins: line.

Import Says “No Matching Release Found”

Symptom: Beets can’t match your albums against MusicBrainz. Fix: Try a manual search by pressing t during import and entering the album name. For obscure releases, check if the album exists on MusicBrainz — if not, you can add it. Enable the chroma plugin for acoustic fingerprinting to improve identification of poorly-tagged files.

File Permission Errors

Symptom: “Permission denied” when importing or writing tags. Fix: Ensure PUID and PGID in Docker Compose match the owner of your music files on the host. Run id on the host to find your UID/GID, and ls -la /path/to/music to check file ownership.

Import Is Extremely Slow

Symptom: Importing a large library takes many hours. Fix: MusicBrainz rate-limits API requests. This is normal for initial imports of large libraries. The chroma plugin (acoustic fingerprinting) adds additional time per track. If your files are already well-tagged, disable chroma to speed up imports. Using quiet: yes also speeds things up by skipping user prompts.

Database Corruption

Symptom: Beets crashes with SQLite errors. Fix: Never run multiple beet import processes simultaneously — SQLite doesn’t support concurrent writes. If the database is corrupted, restore from backup or delete library.db and re-import.

Resource Requirements

  • RAM: ~50-80 MB idle with web UI running, 200-500 MB during active imports (fingerprinting can spike higher)
  • CPU: Minimal at idle. Import phase is moderately CPU-intensive (fingerprinting, metadata lookups)
  • Disk: Application + database under 100 MB. Your music library is the primary storage consumer

Beets runs well on a Raspberry Pi 4 or any small server, though imports will be slower on low-powered hardware.

Verdict

Beets is the best tool for organizing and tagging a messy music collection. If you have thousands of files with inconsistent metadata, missing album art, or no tags at all, beets fixes that. It pairs perfectly with Navidrome or gonic — use beets to clean your library, then serve it with a streaming server.

Beets is not a music streaming server itself. The web UI is functional for basic listening but lacks the features of dedicated players. Think of it as the librarian, not the jukebox. For streaming, pair it with Navidrome (polished UI) or gonic (minimal resource usage).

Frequently Asked Questions

Does beets modify my original music files?

By default, yes — beets writes corrected metadata tags directly to your files. It can also rename and move files based on your configured path format. To preview changes before applying them, use beet import -t (timid mode) which asks for confirmation on each album. You can also configure copy: yes or move: yes in config.yaml to leave originals untouched and work with copies.

Can I use beets with Navidrome or Jellyfin?

Absolutely — this is the recommended workflow. Use beets to organize and tag your music library, then point Navidrome, Jellyfin, or gonic at the same directory. Beets handles the librarian work (tagging, organizing, fetching album art), while the streaming server handles playback. They complement each other perfectly.

How does beets identify albums?

Beets queries the MusicBrainz database — the largest open music metadata database — to match your files against known releases. It uses a combination of existing tags, acoustic fingerprinting (via the chroma plugin), and file naming patterns. For common albums, matching is near-instant and highly accurate. For obscure releases, you may need to manually confirm matches.

Does beets work with FLAC, MP3, and other formats?

Yes. Beets supports MP3, FLAC, AAC, OGG Vorbis, Opus, WMA, ALAC, and APE. It reads and writes metadata tags for all supported formats. The convert plugin can transcode between formats during import (e.g., keep FLAC originals but create MP3 copies for mobile devices).

Can I run beets on a schedule to auto-import new music?

Not natively — beets is designed as an interactive import tool that asks you to confirm matches. For automated imports, use beet import -q (quiet mode) which auto-accepts high-confidence matches and skips uncertain ones. Combine this with a cron job or a file watcher that triggers import when new files appear in a staging directory.

Is the beets web UI required?

No. The web UI is an optional plugin. Beets works entirely from the command line, and most users manage their library that way. The web UI adds a basic browser for your library and simple playback — useful for quick browsing but not a full music streaming experience. For streaming, pair beets with Navidrome or gonic.

Comments