Meelo
Self-hosted music server for collectors, focused on metadata and browsing
Alternative to: spotify, apple music
Meelo is a self-hosted music server built for music collectors, similar in spirit to Plex or Jellyfin but focused on rich browsing and listening experiences. It handles music videos, B-sides, and multiple releases of the same album, and enriches your library with metadata pulled from sources like MusicBrainz, Genius, and Wikipedia. It ships as Docker images with companion mobile apps for Android and iOS.
Meelo Docker Compose example
Self-host Meelo on your own server, homelab, or VPS starting from this Docker Compose example.
It runs Meelo in Docker containers using the official arthichaud/meelo-server:latest, arthichaud/meelo-scanner:latest, arthichaud/meelo-matcher:latest, arthichaud/meelo-front:latest, ghcr.io/zoriya/kyoo_transcoder:master, postgres:alpine3.14, getmeili/meilisearch:v1.5, rabbitmq:4.3-alpine, nginx:alpine images, with persistent volumes and automatic restarts preconfigured.
Review the environment variables and adjust them to your setup, save the file as compose.yml (or docker-compose.yml), and start the stack with docker compose up -d.
services:
server:
init: true
image: arthichaud/meelo-server:latest
restart: unless-stopped
depends_on:
db:
condition: service_healthy
meilisearch:
condition: service_healthy
mq:
condition: service_healthy
volumes:
- data:/data
- config:/config
- ./settings.json:/config/settings.json:ro
environment:
TRANSCODER_URL: "http://transcoder:7666"
MEILI_HOST: "http://meilisearch:7700"
# Master key used to authenticate against the internal Meilisearch instance (must match the meilisearch service).
MEILI_MASTER_KEY: "meelo_meili_master_key_change_me"
RABBITMQ_URL: "amqp://meelo:meelo@mq:5672"
DATABASE_URL: "postgresql://meelo:meelo@db:5432/meelo?schema=public"
# Random string used to sign JWT session tokens.
JWT_SIGNATURE: "changeme"
# Comma-separated random strings used for internal service-to-service authentication.
API_KEYS: "changeme"
# Public URL to access Meelo, without trailing slash (e.g. https://meelo.example.org).
PUBLIC_URL: "http://localhost:5000"
# Set to 1 to allow anonymous (unauthenticated) API requests.
ALLOW_ANONYMOUS: "0"
# Set to 0 to disable account creation (do not disable before the first admin account exists).
ENABLE_USER_REGISTRATION: "1"
# Optional Last.fm scrobbler API key (https://www.last.fm/api/account/create).
LASTFM_API_KEY: ""
# Optional Last.fm scrobbler API secret.
LASTFM_API_SECRET: ""
INTERNAL_DATA_DIR: "/data"
INTERNAL_CONFIG_DIR: "/config"
healthcheck:
test: ["CMD-SHELL", "wget -qO- localhost:4000"]
interval: 3s
timeout: 5s
retries: 20
scanner:
image: arthichaud/meelo-scanner:latest
restart: unless-stopped
depends_on:
server:
condition: service_healthy
environment:
API_URL: "http://server:4000"
INTERNAL_CONFIG_DIR: "/config"
INTERNAL_DATA_DIR: "/data"
# Comma-separated random strings used for internal service-to-service authentication.
API_KEYS: "changeme"
volumes:
- data:/data:ro
- config:/config:ro
- ./settings.json:/config/settings.json:ro
healthcheck:
test: ["CMD-SHELL", "wget -qO- localhost:8133"]
interval: 3s
timeout: 5s
retries: 20
matcher:
image: arthichaud/meelo-matcher:latest
restart: unless-stopped
depends_on:
server:
condition: service_healthy
mq:
condition: service_healthy
environment:
API_URL: "http://server:4000"
INTERNAL_CONFIG_DIR: "/config"
# Comma-separated random strings used for internal service-to-service authentication.
API_KEYS: "changeme"
RABBITMQ_URL: "amqp://meelo:meelo@mq:5672"
volumes:
- config:/config:ro
- ./settings.json:/config/settings.json:ro
front:
image: arthichaud/meelo-front:latest
restart: unless-stopped
depends_on:
server:
condition: service_healthy
scanner:
condition: service_healthy
environment:
# Browser-facing API URL. Path-routed through the bundled nginx to the server service.
PUBLIC_SERVER_URL: "http://localhost:5000/api"
SSR_SERVER_URL: "http://server:4000"
# Browser-facing scanner URL. Path-routed through the bundled nginx to the scanner service.
PUBLIC_SCANNER_URL: "http://localhost:5000/scanner"
SSR_SCANNER_URL: "http://scanner:8133"
# Browser-facing matcher URL. Path-routed through the bundled nginx to the matcher service.
PUBLIC_MATCHER_URL: "http://localhost:5000/matcher"
SSR_MATCHER_URL: "http://matcher:6789"
transcoder:
image: ghcr.io/zoriya/kyoo_transcoder:master
restart: unless-stopped
cpus: 1
depends_on:
db:
condition: service_healthy
environment:
GOCODER_SAFE_PATH: "/data"
PGUSER: "meelo"
PGPASSWORD: "meelo"
PGDATABASE: "meelo"
PGHOST: "db"
PGPORT: "5432"
volumes:
- data:/data:ro
- transcoder_cache:/cache
db:
image: postgres:alpine3.14
restart: unless-stopped
environment:
POSTGRES_USER: "meelo"
# Internal-only database password; not reachable outside the compose network.
POSTGRES_PASSWORD: "meelo"
POSTGRES_DB: "meelo"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U meelo -d meelo"]
interval: 3s
timeout: 3s
retries: 20
volumes:
- db:/var/lib/postgresql/data
meilisearch:
image: getmeili/meilisearch:v1.5
restart: unless-stopped
environment:
MEILI_ENV: "production"
# Master key securing the internal Meilisearch instance (must match the server service).
MEILI_MASTER_KEY: "meelo_meili_master_key_change_me"
MEILI_LOG_LEVEL: "WARN"
volumes:
- search:/meili_data
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--spider", "http://0.0.0.0:7700/health"]
interval: 3s
timeout: 3s
retries: 20
mq:
image: rabbitmq:4.3-alpine
hostname: meelo-mq
restart: unless-stopped
environment:
RABBITMQ_DEFAULT_USER: "meelo"
# Internal-only message-queue password; not reachable outside the compose network.
RABBITMQ_DEFAULT_PASS: "meelo"
healthcheck:
test: rabbitmq-diagnostics -q ping
interval: 5s
timeout: 5s
retries: 20
volumes:
- rabbitmq_data:/var/lib/rabbitmq
# Bundled nginx: the single entry point. Path-routes / to the front UI, /api to
# the server, /scanner to the scanner and /matcher to the matcher. Its server
# block is rendered from nginx.conf.template by the nginx image's envsubst
# entrypoint using the FRONT_URL/SERVER_URL/SCANNER_URL/MATCHER_URL/PORT vars.
nginx:
image: nginx:alpine
restart: unless-stopped
depends_on:
server:
condition: service_started
front:
condition: service_started
environment:
PORT: "5000"
FRONT_URL: "http://front:3000"
SERVER_URL: "http://server:4000"
SCANNER_URL: "http://scanner:8133"
MATCHER_URL: "http://matcher:6789"
volumes:
- ./nginx.conf.template:/etc/nginx/templates/meelo.conf.template:ro
volumes:
db:
config:
data:
search:
transcoder_cache:
rabbitmq_data:
Values set to changeme are required — replace them with your own
values before starting Meelo.
Extra files
The Docker Compose configuration above bind-mounts the following files. Save them next to your compose.yml,
keeping the same relative paths.
nginx.conf.template
server {
listen ${PORT} default_server;
listen [::]:${PORT} default_server;
access_log off;
server_name _;
location = /api {
return 302 /api/;
}
location /api/ {
proxy_pass ${SERVER_URL}/;
}
location = /scanner {
return 302 /scanner/;
}
location /scanner/ {
proxy_pass ${SCANNER_URL}/;
}
location = /matcher {
return 302 /matcher/;
}
location /matcher/ {
proxy_pass ${MATCHER_URL}/;
}
location / {
proxy_pass ${FRONT_URL};
}
} settings.json
{
"trackRegex": [
"^([\\/\\\\]+.*)*[\\/\\\\]+(?P<AlbumArtist>.+)[\\/\\\\]+Unknown Album[\\/\\\\]+(?P<Track>.*)\\..*$",
"^([\\/\\\\]+.*)*[\\/\\\\]+(?P<AlbumArtist>.+)[\\/\\\\]+(?P<Album>.+)(\\s+\\((?P<Year>\\d{4})\\))?[\\/\\\\]+((?P<Disc>[0-9]+)-)?(?P<Index>[0-9]+)\\s+(?P<Track>.*)\\..*$"
],
"metadata": {
"source": "embedded",
"order": "only",
"useExternalProviderGenres": true
},
"providers": {
"musicbrainz": {}
},
"compilations": {
"useID3CompTag": true,
"artists": [
"Various Artists"
]
},
"useEmbeddedThumbnails": false
} Prefer a managed setup? WinterFlow installs, configures, and updates Meelo for you using this same Docker Compose configuration.