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 -f to 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 logs and 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 third sqlite3 deadlock 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 /music folder 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.