Skip to content

Instantly share code, notes, and snippets.

@Vulwsztyn
Last active March 17, 2025 18:26
Show Gist options
  • Save Vulwsztyn/b600cb0a4ced7090cb1408e718b990b0 to your computer and use it in GitHub Desktop.
Save Vulwsztyn/b600cb0a4ced7090cb1408e718b990b0 to your computer and use it in GitHub Desktop.
Arr stack script
import pystache
import os
import secrets
dot_env_template = """ARR_DIR={{arr_dir}}
JELLYFIN_CACHE={{jellyfin_cache}}
TZ={{tz}}
MEDIA_DIR_NAME={{media_dir_name}}
TORRENTS_DIR_NAME={{torrents_dir_name}}
SECRET_ENCRYPTION_KEY={{secret_encryption_key}}
CONFIGS_DIR_NAME={{configs_dir_name}}
"""
docker_compose_template = """services:
jellyfin:
image: jellyfin/jellyfin
container_name: jellyfin
# do not set the use to 1000:1000 here, it will cause permission issues
network_mode: host
# ports: # you do not need it if you are using host network, but I left it to be able to see what port this is running on
# - '8096:8096'
volumes:
- $ARR_DIR:/mnt/arr-stack
- $ARR_DIR/$CONFIGS_DIR_NAME/jellyfin:/config
- $JELLYFIN_CACHE:/cache
restart: unless-stopped
extra_hosts:
- host.docker.internal:host-gateway
environment:
- PUID=1000
- PGID=1000
- TZ=$TZ
{{#use_nvidia_runtime}}
runtime: nvidia
deploy:
resources:
reservations:
devices:
- capabilities:
- gpu
{{/use_nvidia_runtime}}
jellyseerr:
image: fallenbagel/jellyseerr:latest
container_name: jellyseerr
environment:
- LOG_LEVEL=debug
- TZ=$TZ
ports:
- '5055:5055'
volumes:
- $ARR_DIR/$CONFIGS_DIR_NAME/jellyseerr:/app/config
restart: unless-stopped
jackett:
container_name: jackett
image: linuxserver/jackett
environment:
- PUID=1000
- PGID=1000
- TZ=$TZ
volumes:
- '$ARR_DIR/$CONFIGS_DIR_NAME/jackett:/config'
- '$ARR_DIR/$MEDIA_DIR_NAME/torrents:/downloads'
ports:
- '9117:9117'
restart: unless-stopped
{{#use_sonarr}}
sonarr:
container_name: sonarr
image: linuxserver/sonarr
environment:
- PUID=1000
- PGID=1000
- TZ=$TZ
ports:
- '8989:8989'
volumes:
- '$ARR_DIR/$CONFIGS_DIR_NAME/sonarr:/config'
- '$ARR_DIR:/data'
restart: unless-stopped
{{/use_sonarr}}
{{#use_radarr}}
radarr:
container_name: radarr
image: linuxserver/radarr
environment:
- PUID=1000
- PGID=1000
- TZ=$TZ
ports:
- '7878:7878'
volumes:
- '$ARR_DIR/$CONFIGS_DIR_NAME/radarr:/config'
- '$ARR_DIR:/data'
restart: unless-stopped
{{/use_radarr}}
{{#use_lidarr}}
lidarr:
container_name: lidarr
image: ghcr.io/linuxserver/lidarr
environment:
- PUID=1000
- PGID=1000
- TZ=$TZ
volumes:
- '$ARR_DIR/$CONFIGS_DIR_NAME/liadarr:/config'
- '$ARR_DIR:/data'
ports:
- '8686:8686'
restart: unless-stopped
{{/use_lidarr}}
{{#use_readarr}}
readarr:
container_name: readarr
image: 'hotio/readarr:nightly'
ports:
- '8787:8787'
environment:
- PUID=1000
- PGID=1000
- TZ=$TZ
volumes:
- '$ARR_DIR/$CONFIGS_DIR_NAME/readarr:/config'
- '$ARR_DIR:/data'
restart: unless-stopped
{{/use_readarr}}
{{#use_readarr_audiobooks}}
# Notice the different port for the audiobook container
readarr-audio-books:
container_name: readarr-audio-books
image: 'hotio/readarr:nightly'
ports:
- '8786:8787'
environment:
- PUID=1000
- PGID=1000
- TZ=$TZ
volumes:
- '$ARR_DIR/$CONFIGS_DIR_NAME/readarr-audio-books:/config'
- '$ARR_DIR:/data'
restart: unless-stopped
{{/use_readarr_audiobooks}}
{{#use_bazarr}}
bazarr:
container_name: bazarr
image: ghcr.io/linuxserver/bazarr
environment:
- PUID=1000
- PGID=1000
- TZ=$TZ
volumes:
- '$ARR_DIR/$CONFIGS_DIR_NAME/bazarr:/config'
- '$ARR_DIR:/data'
ports:
- '6767:6767'
restart: unless-stopped
{{/use_bazarr}}
qflood:
container_name: qflood
image: hotio/qflood
environment:
- PUID=1000
- PGID=1000
- UMASK=002
- TZ=$TZ
- FLOOD_AUTH=false
volumes:
- '$ARR_DIR/torrents:/data/torrents'
- '$ARR_DIR/$CONFIGS_DIR_NAME/qflood:/config'
restart: unless-stopped
{{#use_vpn}}
network_mode: 'service:vpn'
depends_on:
- vpn
{{/use_vpn}}
{{^use_vpn}}
ports:
- '8080:8080'
- '3005:3000'
{{/use_vpn}}
{{#use_homarr}}
homarr:
container_name: homarr
image: ghcr.io/homarr-labs/homarr:latest
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock # <--- add this line here!
- '$ARR_DIR/$CONFIGS_DIR_NAME/homarr:/appdata'
environment:
- SECRET_ENCRYPTION_KEY=$SECRET_ENCRYPTION_KEY # <--- can be generated with `openssl rand -hex 32`
ports:
- '7575:7575'
{{/use_homarr}}
{{#use_vpn}}
vpn:
container_name: vpn
# image: 'dperson/openvpn-client:latest'
build:
dockerfile: openvpn.Dockerfile
environment:
- 'OTHER_ARGS= --mute-replay-warnings'
cap_add:
- net_admin
restart: unless-stopped
volumes:
- '$ARR_DIR/$CONFIGS_DIR_NAME/openvpn:/vpn'
security_opt:
- 'label:disable'
devices:
- '/dev/net/tun:/dev/net/tun'
ports:
# - '8112:8112' #deluge web UI Port
- '8080:8080'
- '3005:3000'
{{/use_vpn}}
{{#use_heimdall}}
heimdall:
container_name: heimdall
image: ghcr.io/linuxserver/heimdall
environment:
- PUID=1000
- PGID=1000
- TZ=$TZ
volumes:
- '$ARR_DIR/$CONFIGS_DIR_NAME/heimdall:/config'
ports:
- '8090:80'
restart: unless-stopped
{{/use_heimdall}}
"""
dockerfile_template = """FROM dperson/openvpn-client:latest
WORKDIR /etc/openvpn
RUN wget https://raw.githubusercontent.com/ProtonVPN/scripts/refs/heads/master/update-resolv-conf.sh
RUN mv update-resolv-conf.sh update-resolv-conf
RUN chmod +x update-resolv-conf
"""
def input_with_default(prompt, default):
value = input(f"{prompt} [{default = }]: ")
return value if value else default
settings = {
"arr_dir": "/home/$USER/arr",
"jellyfin_cache": "/home/$USER/jellyfin/cache",
"tz": "Europe/Berlin",
"media_dir_name": "media",
"configs_dir_name": "configs",
"shows_dir_name": "shows",
"movies_dir_name": "movies",
"audiobooks_dir_name": "audiobooks",
"music_dir_name": "music",
"books_dir_name": "books",
"torrents_dir_name": "torrents",
"use_vpn": True,
"use_sonarr": True,
"use_radarr": True,
"use_lidarr": True,
"use_readarr": True,
"use_readarr_audiobooks": True,
"use_bazarr": True,
"use_homarr": True,
"use_heimdall": True,
"use_nvidia_runtime": True,
"secret_encryption_key": secrets.token_hex(32),
}
dry_run = input_with_default("Dry run (y/n)?", "no").lower().startswith("y")
create_files_here_if_dry_run = False
if dry_run:
create_files_here_if_dry_run = (
input_with_default("Create files here (y/n)?", "no").lower().startswith("y")
)
media_dirs = [
key
for key in settings
if "dir_name" in key and key != "media_dir_name" and key != "configs_dir_name"
]
change_anything = input_with_default("Do you want to change any directory (y/n)?", "no")
if not change_anything.startswith("n"):
settings["arr_dir"] = input_with_default(
"Enter root arr dir (to put media dir, config dir & dockerfiles)",
settings["arr_dir"],
)
settings["media_dir_name"] = input_with_default(
"Enter media dir name (in arr_dir; shows, movies etc, will be put here)",
settings["media_dir_name"],
)
settings["configs_dir_name"] = input_with_default(
"Enter configs dir name (will be put in arr_dir)",
settings["configs_dir_name"],
)
for key in media_dirs:
settings[key] = input_with_default(f"Enter {key}", settings[key])
settings["jellyfin_cache"] = input_with_default(
"Enter jellyfin cache dir", settings["jellyfin_cache"]
)
settings["tz"] = input_with_default("Enter timezone", settings["tz"])
for key in settings:
if isinstance(settings[key], str):
settings[key] = settings[key].replace("$USER", os.environ["USER"])
settings[key] = settings[key].replace("$HOME", os.environ["HOME"])
settings["use_vpn"] = (
input_with_default("Use VPN (y/n)?", "y" if settings["use_vpn"] else "n")
.lower()
.startswith("y")
)
use_everything = input_with_default("Use every service (y/n)?", "yes")
if use_everything.startswith("n"):
for key in (k for k in settings if "use" in k and "vpn" not in k):
settings[key] = (
input_with_default(
f"Use {key.split('_')[1]} (y/n)?", "y" if settings["key"] else "n"
)
.lower()
.startswith("y")
)
if not dry_run:
os.makedirs(
os.path.join(os.path.expanduser(settings["arr_dir"]), "configs"), exist_ok=True
)
if settings["use_vpn"]:
os.makedirs(
os.path.join(os.path.expanduser(settings["arr_dir"]), "configs", "openvpn"),
exist_ok=True,
)
for dir_name in media_dirs:
os.makedirs(
os.path.join(
os.path.expanduser(settings["arr_dir"]),
settings["media_dir_name"],
settings[dir_name],
),
exist_ok=True,
)
if not dry_run:
dot_env_path = os.path.join(os.path.expanduser(settings["arr_dir"]), ".env")
docker_compose_path = os.path.join(
os.path.expanduser(settings["arr_dir"]), "docker-compose.yml"
)
print(dot_env_path)
print(docker_compose_path)
with open(dot_env_path, "w") as f:
f.write(pystache.render(dot_env_template, settings))
with open(docker_compose_path, "w") as f:
f.write(pystache.render(docker_compose_template, settings))
elif create_files_here_if_dry_run:
with open(".env", "w") as f:
f.write(pystache.render(dot_env_template, settings))
with open("docker-compose.yml", "w") as f:
f.write(pystache.render(docker_compose_template, settings))
with open("openvpn.Dockerfile", "w") as f:
f.write(dockerfile_template)
if settings["use_vpn"]:
openvpn_dockerfile_path = os.path.join(
os.path.expanduser(settings["arr_dir"]), "openvpn.Dockerfile"
)
print(openvpn_dockerfile_path)
with open(openvpn_dockerfile_path, "w") as f:
f.write(dockerfile_template)
show_proton_vpn_instructions = False
if settings["use_vpn"]:
show_proton_vpn_instructions = (
input_with_default("Do you want to see ProtonVPN instructions (y/n)?", "yes")
.lower()
.startswith("y")
)
if show_proton_vpn_instructions:
print("Go to https://account.protonvpn.com/account-password:")
create_vpn_config = (
input_with_default(
"Do you want to create a VPN config with the script or manually (s/m)?",
"script",
)
.lower()
.startswith("s")
)
if create_vpn_config:
print("From section OpenVPN / IKEv2 username")
username = input("Paste OpenVPN / IKEv2 username")
password = input("Paste OpenVPN / IKEv2 password")
if not dry_run:
with open(
os.path.join(
os.path.expanduser(settings["arr_dir"]),
"configs",
"openvpn",
"vpn.auth",
),
"w",
) as f:
f.write(f"{username}\n{password}\n")
elif create_files_here_if_dry_run:
with open("vpn.auth", "w") as f:
f.write(f"{username}\n{password}\n")
else:
print(
f"In directory {os.path.join(os.path.expanduser(settings['arr_dir']), 'configs', 'openvpn')} create a file called vpn.auth with your username and password on separate lines"
)
print("Go to https://account.protonvpn.com/downloads")
print("Download the OpenVPN config file you want")
print(
f"Move the downloaded file to {os.path.join(os.path.expanduser(settings['arr_dir']), 'configs', 'openvpn')}"
)
print(
f"You can go to {os.path.expanduser(settings['arr_dir'])} and run `docker compose up -d` to start the services"
)
show_further_setup_instructions = (
input_with_default("Do you want to see further setup instructions (y/n)?", "yes")
.lower()
.startswith("y")
)
if not show_further_setup_instructions:
exit()
server_ip = input("Enter the server IP: ")
show_heimdall_instructions = (
input_with_default("Do you want to see Heimdall instructions (y/n)?", "yes")
.lower()
.startswith("y")
)
if show_heimdall_instructions:
print(f"Go to http://{server_ip}:8090")
print("For each of apps click 'Application list' -> 'Add' and:")
print(f" -> Add jellyfin with url http://{server_ip}:8096")
print(f" -> Add jellyseerr with url http://{server_ip}:5055")
if settings["use_sonarr"]:
print(f" -> Add sonarr with url http://{server_ip}:8989")
if settings["use_radarr"]:
print(f" -> Add radarr with url http://{server_ip}:7878")
if settings["use_lidarr"]:
print(f" -> Add lidarr with url http://{server_ip}:8686")
if settings["use_readarr"]:
print(f" -> Add readarr with url http://{server_ip}:8787")
if settings["use_readarr_audiobooks"]:
print(f" -> Add readarr-audio-books with url http://{server_ip}:8786")
if settings["use_bazarr"]:
print(f" -> Add bazarr with url http://{server_ip}:6767")
print(f" -> Add qbittorrent with url http://{server_ip}:8080")
input("Press enter to continue")
print(f"Go to http://{server_ip}:9117")
print(
"Click on add indexer and add a few indexers e.g.: 1337x, audiobookbay, TheRARBG, The Pirate Bay"
)
print("These will suffice for now unless you know what you are doing")
input("Press enter to continue")
print(
f"Go to qbittorrent http://{server_ip}:8080. Default username is admin and password is adminadmin."
)
print("Change those under Tools -> Options -> WebUI if you wish.")
print("Under Tools -> Options:")
print(" set 'Default Save Path' to /data/torrents")
print(" set 'Keep incomplete torrents in:' to /data/torrents/incomplete")
input("Press enter to continue")
print("For every arr service (You can go to them from heimdall)")
print("1. set username and password")
print("2. Settings -> Download Client -> Add -> qBittorrent")
print(
f" Host: {server_ip}, Port: 8080, Username: admin, Password: adminadmin (the last 2 will be different if you changed them)"
)
print("3. Settings -> Media management -> Root folder -> Add:")
if settings["use_sonarr"]:
print(
f" /data/{settings['media_dir_name']}/{settings['shows_dir_name']} for sonarr"
)
if settings["use_radarr"]:
print(
f" /data/{settings['media_dir_name']}/{settings['movies_dir_name']} for radarr"
)
if settings["use_lidarr"]:
print(
f" /data/{settings['media_dir_name']}/{settings['music_dir_name']} for lidarr"
)
if settings["use_readarr"]:
print(
f" /data/{settings['media_dir_name']}/{settings['books_dir_name']} for readarr"
)
if settings["use_readarr_audiobooks"]:
print(
f" /data/{settings['media_dir_name']}/{settings['audiobooks_dir_name']} for readarr-audio-books"
)
print(f"4. Add every relevant indexer")
print(f"4.1 Settings -> Indexers -> Add indexer -> Torznab")
print(
f"4.2. In Jackett copy the Torznab link for the indexer you want to add and paste it in URL"
)
print(f"4.3. Set the API key to the one from Jackett (top right)")
input("Press enter to continue")
if settings["use_bazarr"]:
print(f"Go to bazarr at http://{server_ip}:6767")
for service in ["sonarr", "radarr"]:
print("Settings -> {service}")
print(f"Address: {server_ip} or {service}")
print(f"Port: {8989 if service == 'sonarr' else 7878}")
print("API key: get API key from {service} Settings -> General and paste here")
print()
print(f"Go to jellyfin at http://{server_ip}:8096/web/#/dashboard/libraries")
print(f"Add all libraries in /data/{settings['media_dir_name']}")
input("Press enter to continue")
print(f" Go to http://{server_ip}:8096/web/#/dashboard/playback/transcoding")
print("Set proper hardware acceleration")
print("If using nvidia then Nvidia NVENC")
print(
"Check which formats work at https://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new"
)
print("Go to http://{server_ip}:8096/web/#/dashboard/keys")
input("Press enter to continue")
print("Add new key with name Jellyseer and copy it")
print(f"Go to http://{server_ip}:5055/settings/jellyfin")
print(f"Paste the key set hostname to {server_ip} and port to 8096")
print(f"Go to http://{server_ip}:5055/settings/services and add radarr and sonarr")
print("with API keys from their settings -> General")
print("Set the hostname {server_ip} and port to 7878 (radarr) or 8989 (sonarr)")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
OSZAR »