Skip to content
Self-hosted radio with Icecast and Navidrome (no AI DJ)
How-To

Self-hosted radio with Icecast and Navidrome (no AI DJ)

What you’ll get

We have been trained to think that a self-hosted radio station in 2026 needs to be a massive, resource-heavy orchestration. If you read the hype around modern setups, you’d think you need a dedicated GPU, an LLM API key, and a fleet of AI voices to read the weather between tracks. We’re told that without an agentic DJ taking requests and talking over your intros, you’re just running a glorified shuffle queue.

That assumption is a great way to run up a massive API bill or turn your homelab server into a space heater.

If you want a shared soundtrack for your house, you don’t need a chatty robot. You just need a bulletproof, low-overhead stream that plays your existing music library 24/7. Anyone who tunes in (whether they are on the kitchen tablet, a desktop browser, or a legacy hardware radio) hears the exact same transition at the exact same second.

This guide will show you how to build a private, continuous radio station using Icecast and MPD (Music Player Daemon), running right alongside your existing Navidrome library.

  • The footprint is tiny. The entire stack runs in Docker and consumes less than 50MB of RAM, making it perfect for a $5 VPS or a dusty Raspberry Pi.
  • The stream is truly shared. This is a real live broadcast, not an isolated web player shuffling files on a per-user basis.
  • No AI overhead. There are no API keys to manage, no GPU dependencies, and zero risk of a local LLM hanging mid-stream because it got confused by a track’s metadata.

Expect to spend about one to two hours setting this up. You will need basic comfort with Docker Compose and editing text configuration files.

Before you start

Before we touch a config file, let’s look at the architecture. Navidrome is excellent at indexing music, managing metadata, and serving individual streams to clients like Plexamp or Symfonium. But Navidrome does not natively broadcast a live, synchronized stream.

To bridge that gap, we are going to run a classic radio stack alongside it:

  • Icecast acts as our distribution hub. It takes a single audio input and broadcasts it to as many listeners as your network bandwidth can handle.
  • MPD (Music Player Daemon) acts as our headless source client. It mounts the exact same music folder that Navidrome indexes, shuffles the tracks, and pipes the audio directly into Icecast.

To get this running, you will need:

  • A running Navidrome instance (or any setup where your music files live in a local directory on your server).
  • Docker and Docker Compose installed on a Linux host.
  • A private connection like a Tailscale mesh network if you want to listen on the go without exposing your stream to the public internet.

If you actually want the chatty AI host, local voice synthesis, and interactive request queues, you should skip this guide and head over to our walkthrough on setting up SUB/WAVE. But if you want a silent, bulletproof stream that just plays the music, keep reading.

Steps

1. Set up the shared music directory

The magic of this setup is that we aren’t duplicating any files. Navidrome and MPD will look at the exact same folder of audio files. Navidrome will continue to handle your high-fidelity personal streams, while MPD will read the files to broadcast the radio stream.

First, identify where your music library lives on the host system. For this guide, we will assume your music is stored at /srv/music.

Next, create a dedicated directory on your server to store MPD’s configuration and state files. This ensures that MPD doesn’t lose its database index or playlist state whenever you update or restart the container:

mkdir -p ~/homelab-radio/mpd/playlists
mkdir -p ~/homelab-radio/icecast

We will mount /srv/music as a read-only volume inside the MPD container. This guarantees that even if MPD misbehaves, it cannot modify, rename, or delete your primary music files. Navidrome remains the sole source of truth for your library.

2. Spin up Icecast and MPD in Docker

Now we will create the core infrastructure. Navigate to your new directory and create a docker-compose.yml file:

cd ~/homelab-radio
nano docker-compose.yml

Paste the following configuration:

version: '3.8'

services:
  icecast:
    image: moul/icecast:latest
    container_name: icecast
    restart: unless-stopped
    ports:
      - "8000:8000"
    volumes:
      - ./icecast/icecast.xml:/etc/icecast.xml
    environment:
      - ICECAST_SOURCE_PASSWORD=hackme
      - ICECAST_ADMIN_PASSWORD=adminhackme
      - ICECAST_PASSWORD=hackme

  mpd:
    image: vimagick/mpd:latest
    container_name: mpd
    restart: unless-stopped
    ports:
      - "127.0.0.1:6600:6600"
    volumes:
      - /srv/music:/var/lib/mpd/music:ro
      - ./mpd/mpd.conf:/etc/mpd.conf:ro
      - ./mpd/playlists:/var/lib/mpd/playlists
      - ./mpd/state:/var/lib/mpd/state
    depends_on:
      - icecast

Let’s look at the security choices in this file:

  • We bind MPD’s control port (6600) to 127.0.0.1. This prevents anyone on your local network (or the public internet) from connecting to your MPD instance and hijacking the queue. If you want to control the radio from your phone, you should bind this to your Tailscale IP instead.
  • We mount the music directory as :ro. As mentioned, MPD only needs to read your files. It has no business writing to them.
  • Change the passwords. The ICECAST_SOURCE_PASSWORD is what MPD will use to authenticate and push the audio stream to Icecast. The ICECAST_PASSWORD is what listeners will use if you make the stream private (leave it public for easy household listening).

Before we start the containers, we need to create the configuration files for both services. Let’s start with Icecast. Create the default config folder and file:

nano ./icecast/icecast.xml

For a basic setup, you can use Icecast’s default configuration, but make sure to update the <source-password>, <admin-password>, and <listen-socket> blocks to match the passwords in your compose file and listen on port 8000. If you use the moul/icecast image, it can generate a default config automatically if you don’t mount one, but mounting a custom icecast.xml gives you control over mountpoints and stream formats. For now, we will let the image handle the default XML using the environment variables we passed in the compose file, so you can actually skip creating icecast.xml and remove the volume mount from the compose file if you want to keep it dead simple. Let’s update our compose file to do exactly that, saving us from managing a 200-line XML file.

Here is the simplified docker-compose.yml without the Icecast XML mount:

version: '3.8'

services:
  icecast:
    image: moul/icecast:latest
    container_name: icecast
    restart: unless-stopped
    ports:
      - "8000:8000"
    environment:
      - ICECAST_SOURCE_PASSWORD=hackme
      - ICECAST_ADMIN_PASSWORD=adminhackme
      - ICECAST_PASSWORD=hackme

  mpd:
    image: vimagick/mpd:latest
    container_name: mpd
    restart: unless-stopped
    ports:
      - "127.0.0.1:6600:6600"
    volumes:
      - /srv/music:/var/lib/mpd/music:ro
      - ./mpd/mpd.conf:/etc/mpd.conf:ro
      - ./mpd/playlists:/var/lib/mpd/playlists
      - ./mpd/state:/var/lib/mpd/state
    depends_on:
      - icecast

3. Configure MPD to feed the Icecast stream

Next, we need to configure MPD. This is where we tell the player daemon where to find the music, how to index it, and how to encode and stream the audio directly to our Icecast container.

Create the configuration directory and file:

mkdir -p ~/homelab-radio/mpd
nano ./mpd/mpd.conf

Paste the following configuration:

music_directory    "/var/lib/mpd/music"
playlist_directory "/var/lib/mpd/playlists"
db_file            "/var/lib/mpd/database"
state_file         "/var/lib/mpd/state"
bind_to_address    "0.0.0.0"
port               "6600"

# Audio output for the Icecast stream
audio_output {
    type            "shout"
    name            "Everyday Terminal Radio"
    host            "icecast"
    port            "8000"
    mount           "/radio.mp3"
    password        "hackme"
    quality         "5.0"
    # Or use bitrate:
    # bitrate         "192"
    format          "44100:16:2"
    protocol        "icecast2"
    user            "source"
    description     "Our self-hosted continuous radio stream"
    genre           "Various"
    public          "no"
}

Let’s break down the critical settings in this configuration:

  • bind_to_address "0.0.0.0": This tells MPD to listen on all interfaces inside the container. Because we mapped port 6600 to 127.0.0.1 in our docker-compose.yml, it remains completely shielded from the outside world while allowing us to control it from the host.
  • host "icecast": Since we are using Docker Compose, the containers share a virtual network. MPD can resolve the Icecast container simply by using its service name icecast.
  • mount "/radio.mp3": This is the URL path where listeners will tune in. Your stream will be accessible at http://<your-server-ip>:8000/radio.mp3.
  • password "hackme": This must match the ICECAST_SOURCE_PASSWORD environment variable you defined in the docker-compose.yml file.
  • type "shout": This tells MPD to use the libshout library to stream the audio over the network instead of trying to play it through a physical sound card (which your headless server probably doesn’t have anyway).
  • quality "5.0": This sets the variable bitrate (VBR) for the MP3 encoder. A quality of 5.0 is roughly equivalent to a solid 160–192 kbps, which strikes a great balance between audio fidelity and bandwidth usage. If you want a fixed bitrate, comment out the quality line and uncomment bitrate "192".

4. Start the stream and set up the shuffle

With our compose file and MPD configuration in place, we are ready to fire up the containers. Run the following command from your ~/homelab-radio directory:

docker compose up -d

Verify that both containers are running and healthy:

docker compose ps

Now we need to tell MPD to scan your music library, build its database, and start playing. Instead of installing client tools on your host OS, we can execute commands directly inside the running MPD container using docker compose exec.

First, trigger a database update so MPD indexes all the audio files we mounted:

docker compose exec mpd mpc update

Depending on the size of your library, this can take anywhere from a few seconds to a couple of minutes. You can monitor the progress by running docker compose exec mpd mpc status.

Once the update finishes, load your entire library into the active play queue:

docker compose exec mpd mpc add /

Next, configure the playback settings. We want the radio station to run forever, shuffling tracks continuously without stopping:

# Enable random (shuffle) mode
docker compose exec mpd mpc random on

# Enable repeat mode so the queue loops indefinitely
docker compose exec mpd mpc repeat on

# Start playing
docker compose exec mpd mpc play

If you run docker compose exec mpd mpc status now, you should see a track playing, along with a volume indicator and the elapsed time.

Because we mounted ./mpd/state as a persistent volume in our compose file, MPD will write its active queue, playback position, and random/repeat settings to disk. If your server reboots or you restart the container, MPD will automatically reload this state and resume broadcasting the exact same queue. You don’t need to write a custom startup script to kickstart the stream. You’re the on-call team now, and this is one less thing to worry about.

5. Connect your players and smart speakers

Your radio station is now broadcasting live. To tune in, all you need is a client that can open an HTTP audio stream. Your stream URL is:

http://<your-server-ip>:8000/radio.mp3

If you are using Tailscale, replace <your-server-ip> with your server’s MagicDNS name (e.g., http://servername.your-tailnet.ts.net:8000/radio.mp3).

Here is how to connect the most common playback devices in your household:

  • Desktop and Mobile Browsers: Simply paste the stream URL into Chrome, Safari, or Firefox. The browser will render a native HTML5 audio player and start playing immediately.
  • VLC Media Player: Open VLC, go to Media → Open Network Stream (or File → Open Network on macOS), paste the URL, and hit play.
  • Home Assistant: You can broadcast the stream to any smart speaker in your house. In your Home Assistant dashboard, use the media_player.play_media service, set the media_content_id to your Icecast URL, and set the media_content_type to music.
  • Sonos Speakers: Open the Sonos app on your phone or desktop, go to Settings → Services & Voice → Add a Service, select TuneIn, and then add a custom radio station using your Icecast URL.
  • Dedicated MPD Clients: If you want to see what track is currently playing or skip a song from your phone, you can install an MPD client like Malp (Android) or Rigel (iOS). To make this work, update the port mapping in your docker-compose.yml to bind to your Tailscale IP (e.g., 100.x.y.z:6600:6600), run docker compose up -d to apply it, and point the app at that IP and port 6600. You now have a remote control for your household station.

What breaks on mobile networks

This setup is perfect for household co-listening, but if you try to take your custom stream on your morning commute, you will quickly discover where the retro broadcast philosophy collides with modern cellular reality.

That proxy turned out to be the weak spot, but in this case, the weak spot is the fundamental nature of live streaming protocols.

Unlike Spotify, Plexamp, or standard Navidrome clients, an Icecast stream is a continuous, real-time HTTP socket. It does not use HTTP Live Streaming (HLS) or Dynamic Adaptive Streaming over HTTP (DASH), which break audio into small chunks and dynamically adjust quality based on your signal strength.

Here is what happens when you take your stream on the road:

  • TCP socket drops are fatal. When your phone switches between 5G towers or drops into a brief dead zone, the continuous TCP connection to your Icecast server breaks. Standard music players aggressively buffer three or four entire songs ahead. An Icecast stream is live, meaning it can only buffer a few seconds of audio. When the connection blinks, the audio cuts out instantly.
  • VPN overhead stutters. If you are routing your stream through a Tailscale tunnel or a home WireGuard VPN, the encryption overhead and routing latency can cause the stream to stutter on weak cellular connections. Every dropped packet requires a retransmission, which quickly starves the client’s tiny playback buffer.
  • No automatic resume. Many basic audio players and browsers do not know how to gracefully reconnect to a live stream after a socket drop. They simply stop playing, forcing you to manually tap play to re-establish the connection.

If you plan to listen on mobile networks frequently, you can mitigate these issues with two adjustments:

  1. Switch to Opus. Opus is vastly superior to MP3 at low bitrates. You can add a second audio_output block to your mpd.conf that encodes to a 96kbps Opus stream (mount "/radio.opus"). It will sound just as good as a 192kbps MP3 but use half the bandwidth, making it much more resilient to cellular drops.
  2. Increase client caching. In players like VLC, go to settings and increase the network caching buffer from the default 1000ms to 5000ms or even 10000ms. This gives your phone a ten-second safety net to survive brief signal drops.

Common mistakes

  • Omitting persistent state volumes. If you forget to mount /var/lib/mpd/state to your host disk, MPD will lose its entire playback state, shuffle queue, and database index every time you restart the container.
  • Leaving default passwords active. If you expose your Icecast port (8000) to the public internet without changing the default hackme credentials, anyone can hijack your mountpoint and broadcast their own audio to your household.
  • Neglecting the MPD database update. If you add new music to your library, Navidrome will index it automatically, but MPD will remain completely unaware of the new tracks until you manually trigger a database sync via mpc update.
  • Saturating your home egress bandwidth. If you configure MPD to stream lossless FLAC or high-bitrate MP3 over a limited home upload connection, the stream will buffer constantly for anyone listening outside your local network.

Tooling that helps

  • Malp (Android) and Rigel (iOS): These are excellent, lightweight MPD controllers that let you view active track metadata, skip songs, or pause the stream directly from your phone.
  • Caddy: This is the easiest reverse proxy to put in front of Icecast if you want a public HTTPS URL, which is required because iOS Safari and many modern browsers will block unencrypted HTTP audio streams on HTTPS websites.
  • butt (Broadcast Using This Tool): This is a fantastic desktop utility that lets you temporarily hijack your own Icecast stream to broadcast a live DJ set or talk radio show from your laptop’s microphone.
  • Plexamp and Symfonium: These remain the best dedicated Subsonic clients for individual, high-fidelity listening when you want to skip tracks, build personal queues, or cache albums offline for your commute.

Wrap-up

By pairing Icecast with MPD and pointing them at your existing Navidrome library, you get the best of both worlds. You keep your high-fidelity, individual streaming via Subsonic clients, and you gain a lightweight, 24/7 household radio station that everyone can tune into simultaneously.

If a $5 server replaces a subscription, that’s a good afternoon. In this case, a few lines of Docker Compose replace both the complexity of an AI-driven broadcast and the isolation of individual shuffle queues. You don’t need a massive GPU or a continuous API bill to make your homelab feel alive. You just need the right tools wired together properly.

Who should skip this: If your household members demand instant gratification, individual skip buttons, and personalized queues on every speaker, stick to standard Navidrome clients. If you want a chatty host to break up the silence, go with SUB/WAVE. But if you want a reliable, zero-maintenance background soundtrack that just works, spin up this stack and let the music play.

Related