Featured image of post Two Years of LXC Hell - Why I Crawled Back to Docker (And You Should Too)

Two Years of LXC Hell - Why I Crawled Back to Docker (And You Should Too)

How I Escaped LXC Hell and Built a Media Stack That Doesn’t Suck

So, you’re feeling clever. You’ve read the blogs, watched the YouTube tutorials, and decided that unprivileged LXC containers are the “right” way to run your Arr stack. Lightweight! Efficient! So much better than Docker!

Fast forward two years. You’re debugging NFS stale handle errors at 2 AM, your downloads corrupt mid-transfer, and you’re questioning every life choice that led you to this moment.

That was me. Now I’m back on Docker, tail between my legs, with a rock-solid docker-compose.yml that works.

💭 TL;DR
Thought LXC was smarter than Docker? So did I. Until stale NFS handles, ghost downloads, and permission nightmares broke my will to live. This Docker Compose setup just works. Copy, run, exhale.
ASROCK Mini-Desktop Computer

Need a Mini Server?

The DeskMini B760 is a compact and powerful barebone system perfect for homelab use. It supports 14th Gen Intel CPUs, dual DDR4 RAM up to 64GB, and fast storage via M.2 slots plus dual 2.5" drive bays. It’s ideal for running lightweight VMs and/or containers.

Contains affiliate links. I may earn a commission at no cost to you.

The Hard Truth About LXC vs Docker Compose

Let me save you the pain I went through. LXC sounds perfect for media servers until you try to run something complex like the full Arr suite and NFS shares.

Here’s what Docker Compose gives you that LXC never could:

One File to Rule Them All

With LXC, adding a new service means spinning up another container, configuring networking, setting up mounts, and praying everything talks to each other. With Docker Compose: Edit a few lines, run docker compose up -d, and you’re done.

Want to restart everything?

docker compose restart

Want to migrate to a new server? Copy two files: your .env and docker-compose.yml and you are back online in minutes.

No More Permission Purgatory

Ever watch a file download successfully but never move to your media folder? Or see Sonarr throw “access denied” errors with zero explanation? That’s LXC’s unprivileged user permissions playing games with your sanity.

This is the one I could never resolve: the file is downloaded and moved to the NFS share but the LXC host never sees it in the mount point until the system is rebooted.

This Docker setup uses PUID and PGID across every container, so they all behave like the same user on your host. No more mystery permission errors. No more chmod 777 voodoo dances.

Shared Downloads That Actually Work

One /downloads directory for everything. SABnzbd drops files there. Sonarr, Radarr, and their friends watch that same folder and move files cleanly. No bind mount spaghetti. No symbolic link nightmares. Just clean, predictable file handling.

What Finally Broke Me

After two years of LXC “optimizations,” these were the straws that broke the camel’s back:

NFS + LXC = Stale File Handle Roulette
Try debugging why your mounts randomly go read-only mid-download or why Jellyfin can’t see a file that’s clearly in the share until you restart NFS. I spent months chasing these ghosts.

Every Container Needs Its Own IP (Maybe)
Unless you NAT everything (gross), LXC means manually assigning static IPs and keeping track of them. Docker Compose skips this entirely. All your apps talk over an internal bridge network.

Updates Are a Gamble
Something breaks in LXC? Hope you documented every tweak you made over the past six months. Docker? Blow it away and recreate it in seconds. Your config survives because it’s volume-mounted.

The Stack That Actually Works

Here’s the battle-tested docker-compose.yml that ended my suffering:

services:

  #################################
  ##  PROWLARR                   ##
  #################################
  prowlarr:
    image: lscr.io/linuxserver/prowlarr:latest
    container_name: prowlarr
    user: "${PUID}:${PGID}"
    env_file: .env
    restart: unless-stopped
    ports:
      - ${PROWLARR_PORT}:9696
    volumes:
      - ${CONFIG_PATH}/prowlarr:/config
    environment:
      - PUID=${PUID}
      - PGID=${PGID}
      - TZ=${TZ}
    networks:
      - media_network

  #################################
  ##  SONARR                     ##
  #################################
  sonarr:
    image: lscr.io/linuxserver/sonarr:latest
    container_name: sonarr
    user: "${PUID}:${PGID}"
    env_file: .env
    restart: unless-stopped
    ports:
      - ${SONARR_PORT}:8989
    volumes:
      - ${CONFIG_PATH}/sonarr:/config
      - ${MEDIA_PATH}/Shows:/tv
      - ${DOWNLOADS_PATH}:/downloads
    environment:
      - PUID=${PUID}
      - PGID=${PGID}
      - UMASK=0007
      - TZ=${TZ}
    networks:
      - media_network

  #################################
  ##  RADARR                     ##
  #################################
  radarr:
    image: lscr.io/linuxserver/radarr:latest
    container_name: radarr
    user: "${PUID}:${PGID}"
    env_file: .env
    restart: unless-stopped
    ports:
      - ${RADARR_PORT}:7878
    volumes:
      - ${CONFIG_PATH}/radarr:/config
      - ${MEDIA_PATH}/Movies:/movies
      - ${DOWNLOADS_PATH}:/downloads
    environment:
      - PUID=${PUID}
      - PGID=${PGID}
      - UMASK=0007
      - TZ=${TZ}
    networks:
      - media_network

  #################################
  ##  Lidarr                     ##
  #################################
  lidarr:
    image: lscr.io/linuxserver/lidarr:latest
    container_name: lidarr
    user: "${PUID}:${PGID}"
    env_file: .env
    restart: unless-stopped
    ports:
      - ${LIDARR_PORT}:8686
    volumes:
      - ${CONFIG_PATH}/lidarr:/config
      - ${DOWNLOADS_PATH}:/downloads
      - ${MEDIA_PATH}/Music:/music
    environment:
      - PUID=${PUID}
      - PGID=${PGID}
      - UMASK=0007
      - TZ=${TZ}
    networks:
      - media_network

  #################################
  ##  Readarr1 eBooks            ##
  #################################
  readarr1:
    image: lscr.io/linuxserver/readarr:develop
    container_name: readarr1
    user: "${PUID}:${PGID}"
    env_file: .env
    restart: unless-stopped
    ports:
      - ${READARR1_PORT}:8787
    volumes:
      - ${CONFIG_PATH}/readarr1:/config
      - ${DOWNLOADS_PATH}:/downloads
      - ${MEDIA_PATH}/eBooks:/books
    environment:
      - PUID=${PUID}
      - PGID=${PGID}
      - UMASK=0007
      - TZ=${TZ}
    networks:
      - media_network

  #################################
  ##  Readarr2                   ##
  #################################
  readarr2:
    image: lscr.io/linuxserver/readarr:develop
    container_name: readarr2
    user: "${PUID}:${PGID}"
    env_file: .env
    restart: unless-stopped
    ports:
      - ${READARR2_PORT}:8787
    volumes:
      - ${CONFIG_PATH}/readarr2:/config
      - ${DOWNLOADS_PATH}:/downloads
      - ${MEDIA_PATH}/AudioBooks:/books
    environment:
      - PUID=${PUID}
      - PGID=${PGID}
      - UMASK=0007
      - TZ=${TZ}
    networks:
      - media_network

  #################################
  ##  Bazarr                     ##
  #################################
  bazarr:
    image: lscr.io/linuxserver/bazarr:latest
    container_name: bazarr
    user: "${PUID}:${PGID}"
    env_file: .env
    restart: unless-stopped
    ports:
      - ${BAZARR_PORT}:6767
    volumes:
      - ${CONFIG_PATH}/bazarr:/config
      - ${MEDIA_PATH}/Movies:/movies
    environment:
      - PUID=${PUID}
      - PGID=${PGID}
      - UMASK=0007
      - TZ=${TZ}
    networks:
      - media_network

  #################################
  ##  SABnzbd                    ##
  #################################
  sabnzbd:
    image: lscr.io/linuxserver/sabnzbd:latest
    container_name: sabnzbd
    user: "${PUID}:${PGID}"
    env_file: .env
     restart: unless-stopped
    ports:
      - ${SABNZBD_PORT}:8080
    volumes:
      - ${CONFIG_PATH}/sabnzbd:/config
      - ${DOWNLOADS_PATH}:/downloads
    environment:
      - PUID=${PUID}
      - PGID=${PGID}
      - UMASK=0007
      - TZ=${TZ}
    networks:
      - media_network

#################################
##  NETWORK                    ##
#################################
networks:
  media_network:
    name: media_network
    external: true

What Each Piece Actually Does

services:
Each app gets its own container. Easier to debug, update, or nuke from orbit when things go sideways.

image:
I use linuxserver.io images exclusively. It is clean, well-documented, and no weird surprises hiding in latest.

user: "${PUID}:${PGID}"
This is the magic that prevents permission hell. Every container runs as your host user.

env_file: .env
Keeps sensitive info and paths out of the compose file. Makes the whole thing portable between servers.

volumes:

  • /config: App settings and databases that persist forever
  • /downloads: Shared workspace for all apps and SAB
  • /media: Your precious media collection

networks:
Everything sits on media_network. Internal traffic stays inside Docker. No extra IPs to manage.

restart: unless-stopped
Containers come back after reboots, crashes, or when you’re mid-binge and don’t want to babysit anything.

The Essential .env File

# User and Group ID (Prevents permission issues)
# Main user ID
PUID=1000
# Main group ID:
PGID=1001

# Timezone (Ensures correct scheduling and logs)
TZ=America/Denver

# Define Ports (Ports for each container are defined here)
RADARR_PORT=7878
SONARR_PORT=8989
SABNZBD_PORT=8080
PROWLARR_PORT=9696
LIDARR_PORT=8686
READARR1_PORT=8787
READARR2_PORT=8788
BAZARR_PORT=6767

# Data Directories (Keeps storage paths centralized)
CONFIG_PATH=/docker
DOWNLOADS_PATH=/downloads
MEDIA_PATH=/media/Storage
💡
Tip:
  • Run id to get your PUID and PGID
  • Set TZ correctly or your downloads will happen at weird hours
  • Use absolute paths everywhere—don’t get cute with relative paths

Getting Started (The Easy Way)

  1. Drop both files in the same folder: Edit the .env to match your real paths and UID/GID.

  2. Fire it up:

    docker compose up -d
    
  3. Access your apps:

    • Sonarr: http://your-ip:8989
    • Radarr: http://your-ip:7878
    • Prowlarr: http://your-ip:9696
    • (and so on…)

Real-World Scenarios Where This Shines

Running Jellyfin?

This stack becomes your automated feeder system. Jellyfin handles playback, and the Arr apps handle acquisition. New episode downloads → automatically appear in Jellyfin. No more manual file moves. No more metadata headaches.

Tired of Snap’s BS on Ubuntu?

Snap has a mind of its own. Sometimes Snap won’t update, force-updates when you don’t want it to, or your Docker CLI vanishes.

This stack uses real Docker, on your terms, with predictable behavior. (Also why I moved everything back to Debian, but that’s another rant.)

Want Boring Updates? Yes please.

docker compose pull
docker compose up -d

That’s it. Your config persists, your ports don’t change, and you’re back online in seconds with fresh code.

The One Gotcha That’ll Bite You

Mounts matter. If your paths don’t match between host and container, nothing works.

Double-check that:

  • Host /downloads maps to container /downloads
  • Same for /media and /config
  • Your PUID/PGID match your actual user

Get this wrong, and you’ll be back to debugging permission errors like it’s 2019.

The One Gotcha That’ll Bite You

⚠️
Warning:

Mounts matter. If your paths don’t match between host and container, nothing is going to work.

Double-check that:

  • Host /downloads maps to container /downloads
  • Same for /media and /config
  • Your PUID/PGID matches your actual user

If these are wrong, you will be back to debugging permission errors.

The Bottom Line

You now have the complete stack. One YAML file. One .env. All your media apps work together without fighting over ports, permissions, or your sanity.

I fought for two years trying to outsmart this problem with clever LXC setups. Turns out the solution was to stop being clever and just let Docker do the work.

I hope my pain and suffering saves you some time and effort.

UGREEN NASync DXP4800 Plus 4-Bay Desktop NAS

Need A NAS?

UGREEN NASync DXP4800, 4-Bay NAS with Intel N100 Quad-Core CPU (Up to 3.4GHz) 8GB DDR5, 2x M.2 PCIe Slots and a 2.5GbE Port (Diskless). This is perfect if you don’t want to DIY your NAS.

Contains affiliate links. I may earn a commission at no cost to you.

© 2024 - 2025 DiyMediaServer

Buy Me a Coffee

Built with Hugo
Using a modified Theme Stack designed by Jimmy