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.
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
-
Import your existing music library:
docker exec -it beets beet import /downloadsBeets will scan each album, match it against MusicBrainz, and show you the result with a confidence score. Press
ato accept a match,sto skip, ortto enter a manual search. -
Access the web UI at
http://your-server-ip:8337— browse your library, search, and play music directly in the browser via HTML5 audio. -
Verify the import — check that files were moved from
/downloadsto/musicwith 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.
Related
- Best Self-Hosted Music Streaming Servers
- Self-Hosting Navidrome with Docker Compose
- Self-Hosting gonic with Docker Compose
- Self-Hosting Funkwhale with Docker Compose
- Navidrome vs gonic: Which Music Server to Self-Host?
- Self-Hosted Spotify Alternatives
- Docker Compose Basics
- Reverse Proxy Setup
- Backup Strategy
Get self-hosting tips in your inbox
Get the Docker Compose configs, hardware picks, and setup shortcuts we don't put in articles. Weekly. No spam.
Comments