Allow splitting config, downloads and media paths via .env
Some checks failed
Check running / run (push) Has been cancelled

Adds optional CONFIG_DIR, DOWNLOADS_DIR and MEDIA_DIR env variables so
users can spread configs, downloads and media across different drives
without restructuring ROOT_DIR. Each falls back to its ROOT_DIR-based
default when left empty, so existing setups keep working unchanged.

The -arr services (sonarr, radarr, lidarr, mylar3) now use a single
${DOWNLOADS_DIR}:/data parent bind with a ${MEDIA_DIR}:/data/media
overlay. This preserves TRaSH's single-/data-mount semantics inside
the container (hardlinks still work on one filesystem) while letting
the host-side media tree live wherever the user wants.

README documents the new variables, the generated folder structure,
recommended storage layouts, and the same-filesystem requirement for
hardlinks between downloads and media.
This commit is contained in:
2026-04-17 10:40:44 +02:00
parent d9a03a5d25
commit 92aa9b4c22
7 changed files with 292 additions and 112 deletions

View File

@@ -7,21 +7,42 @@ class ContainerConfig:
root_dir,
timezone,
plex_claim='',
config_dir=None,
downloads_dir=None,
media_dir=None,
):
self.root_dir = root_dir
self.timezone = timezone
self.config_dir = f'{root_dir}/config'
self.plex_claim = plex_claim
self.movie_dir = f'{root_dir}/media/movies'
self.tv_dir = f'{root_dir}/media/tv'
self.music_dir = f'{root_dir}/media/music'
self.book_dir = f'{root_dir}/media/books'
self.comic_dir = f'{root_dir}/media/comics'
self.torrent_dir = f'{root_dir}/data/torrents'
self.usenet_dir = f'{root_dir}/data/usenet'
# Allow each of config / downloads / media to be overridden independently.
# When not provided, fall back to the ROOT_DIR-based defaults used historically.
self.config_dir = config_dir or f'{root_dir}/config'
self.downloads_dir = downloads_dir or f'{root_dir}/data'
self.media_dir = media_dir or f'{root_dir}/data/media'
self.movie_dir = f'{self.media_dir}/movies'
self.tv_dir = f'{self.media_dir}/tv'
self.music_dir = f'{self.media_dir}/music'
self.book_dir = f'{self.media_dir}/books'
self.comic_dir = f'{self.media_dir}/comics'
self.torrent_dir = f'{self.downloads_dir}/torrents'
self.usenet_dir = f'{self.downloads_dir}/usenet'
self.homarr_dir = f'{root_dir}/data/homarr/appdata'
self.UID = pwd.getpwnam(getpass.getuser()).pw_uid
def _arr_data_volumes(self):
"""Volume lines for services that need the full /data tree (sonarr, radarr, lidarr, mylar3).
Emits a single ``${downloads_dir}:/data`` parent mount plus a ``${media_dir}:/data/media``
overlay. The overlay is a no-op when ``media_dir`` is the default ``${downloads_dir}/media``
(Docker just binds the same subpath) and transparently redirects ``/data/media`` when the
user has pointed ``media_dir`` at a different location. This preserves TRaSH's single-
``/data``-mount semantics while still allowing an independent ``MEDIA_DIR``.
"""
return (
f' - {self.downloads_dir}:/data\n'
f' - {self.media_dir}:/data/media\n'
)
def plex(self):
return (
' plex:\n'
@@ -35,7 +56,7 @@ class ContainerConfig:
f' - PLEX_CLAIM={self.plex_claim}\n'
' volumes:\n'
f' - {self.config_dir}/plex-config:/config\n'
f' - {self.root_dir}/data/media:/media\n'
f' - {self.media_dir}:/media\n'
' restart: unless-stopped\n\n'
)
@@ -69,7 +90,7 @@ class ContainerConfig:
f' - TZ={self.timezone}\n'
' volumes:\n'
f' - {self.config_dir}/jellyfin-config:/config\n'
f' - {self.root_dir}/data/media:/data\n'
f' - {self.media_dir}:/data\n'
' ports:\n'
' - "8096:8096"\n'
' restart: unless-stopped\n\n'
@@ -87,7 +108,7 @@ class ContainerConfig:
f' - TZ={self.timezone}\n'
' volumes:\n'
f' - {self.config_dir}/sonarr-config:/config\n'
f' - {self.root_dir}/data:/data\n'
f'{self._arr_data_volumes()}'
' ports:\n'
' - "8989:8989"\n'
' restart: unless-stopped\n\n'
@@ -105,7 +126,7 @@ class ContainerConfig:
f' - TZ={self.timezone}\n'
' volumes:\n'
f' - {self.config_dir}/radarr-config:/config\n'
f' - {self.root_dir}/data:/data\n'
f'{self._arr_data_volumes()}'
' ports:\n'
' - "7878:7878"\n'
' restart: unless-stopped\n\n'
@@ -122,7 +143,7 @@ class ContainerConfig:
f' - TZ={self.timezone}\n'
' volumes:\n'
f' - {self.config_dir}/bazarr-config:/config\n'
f' - {self.root_dir}/data/media:/media\n'
f' - {self.media_dir}:/media\n'
' ports:\n'
' - "6767:6767"\n'
' restart: unless-stopped\n\n'
@@ -140,7 +161,7 @@ class ContainerConfig:
f' - TZ={self.timezone}\n'
' volumes:\n'
f' - {self.config_dir}/lidarr-config:/config\n'
f' - {self.root_dir}/data:/data\n'
f'{self._arr_data_volumes()}'
' ports:\n'
' - "8686:8686"\n'
' restart: unless-stopped\n\n'
@@ -157,7 +178,7 @@ class ContainerConfig:
' - UMASK=002\n'
' volumes:\n'
f' - {self.config_dir}/mylar-config:/config\n'
f' - {self.root_dir}/data:/data\n'
f'{self._arr_data_volumes()}'
' ports:\n'
' - "8090:8090"\n'
' restart: unless-stopped\n\n'
@@ -173,9 +194,9 @@ class ContainerConfig:
f' - TZ={self.timezone}\n'
' volumes:\n'
f' - {self.config_dir}/audiobookshelf:/config\n'
f' - {self.root_dir}/data/media/audiobooks:/audiobooks\n'
f' - {self.root_dir}/data/media/podcasts:/podcasts\n'
f' - {self.root_dir}/data/media/audiobookshelf-metadata:/metadata\n'
f' - {self.media_dir}/audiobooks:/audiobooks\n'
f' - {self.media_dir}/podcasts:/podcasts\n'
f' - {self.media_dir}/audiobookshelf-metadata:/metadata\n'
' ports:\n'
' - "13378:80"\n'
' restart: unless-stopped\n\n'