Last Tuesday at 3:17am, my Pi 5 choked while trying to download “Lover” in ALAC from Apple Music — not because the song failed, but because the ffmpeg process ate all 8GB RAM and took down my entire homelab’s reverse proxy. I’d been using apple-music-downloader (CLI-only, zero queueing) for six months, but the moment I tried batch-downloading Taylor’s entire discography, it just… keeled over. No retries. No duplicate checks. No logging beyond “error: 403”. And yeah, I know, classic mistake — assuming Apple’s API tokens stay valid for more than 48 hours.
ALACarte isn’t some shiny new SaaS or a rewritten-from-scratch monolith. It’s a TypeScript Express + React app — lean, self-contained, and frankly, built *by someone who also rage-quit their own Python script after the third ffmpeg OOM kill`. It doesn’t pretend to be a music service. It’s a downloader with UI polish: queue management, lyrics fetching from Musixmatch (optional), ALAC → FLAC conversion on-the-fly, and — crucially — library-aware duplicate detection using file hash + track metadata. It’s not trying to replace Plex or Navidrome. It’s trying to replace you manually pasting 47 song links into a terminal.
I ran it on a 2GB RAM VPS (€4.50/mo Hetzner CX11) with Docker Compose. No Kubernetes. No Nginx in front — just Caddy (v2.8) proxying /alacarte to the container. Here’s the real config I used — the one that actually worked after two failed attempts:
# docker-compose.yml
services:
alacarte:
image: sosjalapeno/alacarte:latest
restart: unless-stopped
environment:
- APPLE_MUSIC_TOKEN=ey... # got this from Safari devtools → Network → auth request
- MUSIXMATCH_TOKEN=xxx # optional, but makes lyrics work
- CONVERT_TO_FLAC=true
- DUPLICATE_CHECK=true
- LIBRARY_PATH=/music
volumes:
- ./alacarte-data:/app/data
- /mnt/nas/apple-music-library:/music:rw
ports:
- "3001:3001"
Key detail: LIBRARY_PATH must be inside the container. I wasted 45 minutes wondering why duplicate detection kept failing — turns out I’d mounted /music but hadn’t set LIBRARY_PATH to match. Also, don’t use the --token CLI flag in the container — the env var is the only way it reliably picks it up. The docs say both work. They don’t. Not in Docker.
I let it run for 4 months straight — 322 tracks downloaded, 114 converted to FLAC, 17 duplicates caught (mostly live versions vs studio, or alternate mixes). CPU stayed under 25% avg. RAM peaked at 1.3GB during conversion bursts — fine on my 2GB box. Queueing works: I can paste 20 Apple Music URLs, go make coffee, and come back to 12 finished, 8 still processing, zero manual retries.
But here’s what broke hard:
- Lyrics fail silently if your Musixmatch token is rate-limited. No toast, no log line — just an empty “Lyrics” tab. Took me 3 days and
docker logs -fto spot the 429s buried in Express output. - ALAC-to-FLAC conversion doesn’t preserve embedded album art. You get clean FLAC, but the cover art is stripped. I patched it manually by adding
ffmpeg -i input.m4a -i cover.jpg -map 0 -map 1 -c copy -disposition:v:1 attached_pic output.flac, then rebuilt the container. Not a dealbreaker — but yeah, the docs don’t mention this. - The UI lies about progress during large batches. It says “3/20 done” while the backend is still parsing the 5th URL’s metadata. Turns out it counts started, not completed. I assumed it was stalled — killed the container twice before checking
docker logsand seeing it was quietly working.
Compared to alternatives I’ve actually run:
apple-music-downloader(Python, CLI-only): Simpler, lighter, but zero resilience. One failed token = full batch dies. No duplicates. No lyrics. I used it for 8 months before ALACarte. It works — until it doesn’t. Then you’re grepping logs at 2am.am-dl(Go, also CLI): Faster, better token handling, and it does keep album art. But no web UI. No queueing. No library awareness. I tried wrapping it in a basic Flask UI once — gave up after the thirdsqlite3deadlock during concurrent downloads.
ALACarte isn’t for you if:
- You want lossless streaming, not downloading. This doesn’t serve music — it saves files.
- You expect AirPlay or Chromecast support. Nope. Just
/musicfolder dumping. - You’re on a 1GB RAM Pi 4. It’ll boot, but conversion stutters. I tried — dropped CPU from 78% to 12% just by upgrading to Pi 5 (and using its hardware-accelerated
ffmpeg). - You think “self-hosted Apple Music” means “build your own iCloud Music Library”. It doesn’t. It’s a downloader. A good one — but still just a downloader.
Also — and this is low-key wild — it doesn’t use Apple’s official MusicKit JS. It scrapes the web player. Which means:
✅ Works with regional Apple Music catalogs (I tested ID, SG, and US tokens — all worked).
❌ Breaks immediately if Apple changes their /catalog/ endpoint structure. It did break in early March (v2.1.0 fixed it in 48 hours). I got pinged on GitHub because I’d opened the issue. That kind of responsiveness? Rare.
I run it behind Caddy with basic auth (basicauth { username password }) — not for security (my homelab isn’t public), but because I don’t want my wife accidentally pasting her entire “Chill Vibes” playlist into the UI and triggering a 3-hour FLAC conversion while I’m asleep.
One last thing: the “polished web UI” is actually polished. Not “looks fine in Figma” polished — I mean “I can queue 15 songs, see real-time progress bars, click ‘retry failed’, and delete a track from the queue without refreshing” polished. It uses Socket.IO for live updates — and yes, I had to open port 3001 and set Caddy’s websocket directive, or the progress bar just sat at 0%. Took me two hours to figure that out. My bad. But once it worked? It just worked.
If you’re still paying $10.99/mo for Apple Music and manually saving songs to Dropbox or emailing them to yourself — stop. Just run this. It’s not perfect, but it’s the first tool in 6 years of self-hosting music scrapers that didn’t make me want to throw my keyboard into a lake.
ALACarte isn’t the future of music — it’s the quiet, slightly janky, deeply competent tool that finally stops you from doing the same dumb thing over and over.
Comments