Media Servers (Jellyfin & Navidrome)¶
Linux Tuning¶
Download/etc/sysctl.d/99-network-performance.conf
# /etc/sysctl.d/99-network-performance.conf
# Kernel-level network performance tuning for maximum throughput
# === Core Network Buffer Sizes ===
net.core.rmem_max = 134217728 # 128MB receive buffer
net.core.wmem_max = 134217728 # 128MB send buffer
net.core.rmem_default = 16777216 # 16MB default receive
net.core.wmem_default = 16777216 # 16MB default send
net.core.optmem_max = 40960 # Ancillary buffer size
# === TCP Buffer Tuning ===
net.ipv4.tcp_rmem = 4096 87380 134217728 # TCP receive buffer (min/default/max)
net.ipv4.tcp_wmem = 4096 65536 134217728 # TCP send buffer (min/default/max)
net.ipv4.tcp_mem = 16777216 16777216 16777216 # TCP memory pages
# === Queue and Backlog Sizes ===
net.core.netdev_max_backlog = 250000 # Packets queued on INPUT side
net.core.netdev_budget = 600 # Packets per NAPI poll
net.core.netdev_budget_usecs = 8000 # Time limit per NAPI poll (microseconds)
net.core.somaxconn = 65535 # Max listen() backlog
net.ipv4.tcp_max_syn_backlog = 8192 # SYN backlog
# === TCP Performance Optimizations ===
net.ipv4.tcp_congestion_control = bbr # Use BBR congestion control
net.core.default_qdisc = fq # Fair queue scheduler (optimal for BBR)
net.ipv4.tcp_window_scaling = 1 # Enable window scaling
net.ipv4.tcp_timestamps = 1 # Enable timestamps
net.ipv4.tcp_sack = 1 # Selective acknowledgment
net.ipv4.tcp_fack = 1 # Forward acknowledgment
net.ipv4.tcp_low_latency = 1 # Optimize for latency
net.ipv4.tcp_slow_start_after_idle = 0 # Don't reduce cwnd after idle
net.ipv4.tcp_tw_reuse = 1 # Reuse TIME_WAIT sockets
net.ipv4.tcp_fin_timeout = 15 # Faster FIN timeout
net.ipv4.tcp_keepalive_time = 300 # Keepalive probe frequency (seconds)
net.ipv4.tcp_keepalive_probes = 5 # Keepalive probe count
net.ipv4.tcp_keepalive_intvl = 15 # Keepalive probe interval (seconds)
net.ipv4.tcp_max_tw_buckets = 2000000 # Max TIME_WAIT sockets
net.ipv4.tcp_fastopen = 3 # Enable TCP Fast Open (client+server)
net.ipv4.tcp_mtu_probing = 1 # Enable MTU probing
net.ipv4.tcp_no_metrics_save = 1 # Don't cache connection metrics
net.ipv4.tcp_moderate_rcvbuf = 1 # Auto-tune receive buffer
# === UDP Buffer Tuning ===
net.ipv4.udp_rmem_min = 8192
net.ipv4.udp_wmem_min = 8192
# === Connection Tracking ===
net.netfilter.nf_conntrack_max = 2000000
net.nf_conntrack_max = 2000000
net.netfilter.nf_conntrack_tcp_timeout_established = 1200
# === IPv4 General ===
net.ipv4.ip_forward = 0 # Disable if not routing
net.ipv4.ip_local_port_range = 1024 65535 # Expanded port range
# === Security ===
net.ipv4.tcp_syncookies = 1 # SYN flood protection
net.ipv4.conf.all.rp_filter = 1 # Reverse path filtering
net.ipv4.conf.default.rp_filter = 1
# === Core System Limits ===
fs.file-max = 2097152 # Max file descriptors
fs.inotify.max_user_watches = 524288
fs.inotify.max_user_instances = 524288
# === Virtual Memory ===
vm.swappiness = 10 # Reduce swap usage
vm.dirty_ratio = 15 # Start background writeback at 15%
vm.dirty_background_ratio = 5 # Background writeback at 5%
vm.vfs_cache_pressure = 50 # Retain more inode/dentry cache
vm.max_map_count=262144
# === RPS/RFS ===
net.core.rps_sock_flow_entries = 32768
command to reload sysctl.d without restarting
command brick to generate swapfile
sudo fallocate -l 40G /swapfile && \
sudo chmod 600 /swapfile && \
sudo mkswap /swapfile && \
sudo swapon /swapfile && \
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
Nvidia NVENC Patching¶
Nvidia is an asshole company and artifically limit the amount of running NVENC streams on consumer GPUs. Here's a super cool project that unfucks this: https://github.com/keylase/nvidia-patch
Live Network Filesystem on Linux¶
For some reason this doesn't exist? I wrote a program to fix it: https://github.com/alchemyyy/smb_changenotify_to_inotify_translator
You NEED this.
Jellyfin¶
Installation (Linux)¶
I use Jellarr to make sure my configurations are saved. I have a bat file for easy deployment here (requires git and Node 20+):Downloadrun_jellarr.bat
@echo off
cd /d "%~dp0"
if not exist "jellarr" (
git clone https://github.com/venkyr77/jellarr.git jellarr
cd jellarr
npm install
) else (
cd jellarr
)
npm run dev -- --configFile "%~dp0jellarr_config.yml"
Plugins¶
- Webhook
- Playback Reporting
- TVDB
- Meilisearch
- https://github.com/arnesacnussem/jellyfin-plugin-meilisearch
- installer script for debian (does everything besides install the jellyfin plugin, you dont even have to configure the plugin)
Downloadsetup-meilisearch-jellyfin.sh
#!/usr/bin/env bash
set -euo pipefail
# ============================================================================
# Meilisearch setup for Jellyfin (bare Debian)
# ----------------------------------------------------------------------------
# Installs Meilisearch, creates a dedicated service user, generates a master
# key, writes a config file + systemd unit, and drops a Jellyfin environment
# override so the jellyfin-plugin-meilisearch plugin auto-discovers the
# instance via MEILI_URL / MEILI_MASTER_KEY.
# ============================================================================
# ── Tunables (override via environment before running) ──────────────────────
MEILI_BIND="${MEILI_BIND:-127.0.0.1:7700}" # listen address
MEILI_DB_PATH="${MEILI_DB_PATH:-/var/lib/meilisearch/data}"
MEILI_DUMP_DIR="${MEILI_DUMP_DIR:-/var/lib/meilisearch/dumps}"
MEILI_SNAP_DIR="${MEILI_SNAP_DIR:-/var/lib/meilisearch/snapshots}"
MEILI_CONFIG="/etc/meilisearch.toml"
JELLYFIN_ENV_DIR="/etc/jellyfin"
JELLYFIN_ENV_FILE="${JELLYFIN_ENV_DIR}/meilisearch.env"
# If you already have a master key you want to reuse, export MEILI_MASTER_KEY
# before running this script. Otherwise one will be generated.
# ── Preflight ───────────────────────────────────────────────────────────────
if [[ $EUID -ne 0 ]]; then
echo "ERROR: Please run this script as root (or with sudo)." >&2
exit 1
fi
echo "==> Updating package lists…"
apt-get update -qq
echo "==> Installing prerequisites…"
apt-get install -y -qq curl openssl >/dev/null
# ── Install Meilisearch via official APT repo ───────────────────────────────
if command -v meilisearch &>/dev/null; then
echo "==> Meilisearch binary already present: $(meilisearch --version 2>&1 | head -1)"
else
echo "==> Adding Meilisearch APT repository…"
echo "deb [trusted=yes] https://apt.fury.io/meilisearch/ /" \
| tee /etc/apt/sources.list.d/fury.list >/dev/null
apt-get update -qq
echo "==> Installing Meilisearch…"
apt-get install -y -qq meilisearch >/dev/null
echo " Installed: $(meilisearch --version 2>&1 | head -1)"
fi
# ── Create service user ────────────────────────────────────────────────────
if id meilisearch &>/dev/null; then
echo "==> System user 'meilisearch' already exists."
else
echo "==> Creating system user 'meilisearch'…"
useradd -r -s /usr/sbin/nologin -d /var/lib/meilisearch -m meilisearch
fi
# ── Data directories ───────────────────────────────────────────────────────
echo "==> Setting up data directories…"
mkdir -p "$MEILI_DB_PATH" "$MEILI_DUMP_DIR" "$MEILI_SNAP_DIR"
chown -R meilisearch:meilisearch /var/lib/meilisearch
chmod 750 /var/lib/meilisearch
# ── Master key ─────────────────────────────────────────────────────────────
if [[ -z "${MEILI_MASTER_KEY:-}" ]]; then
MEILI_MASTER_KEY="$(openssl rand -hex 24)"
echo "==> Generated master key (save this somewhere safe):"
echo " $MEILI_MASTER_KEY"
else
echo "==> Using provided MEILI_MASTER_KEY."
fi
# ── Config file ────────────────────────────────────────────────────────────
echo "==> Writing config to ${MEILI_CONFIG}…"
cat > "$MEILI_CONFIG" <<TOML
# /etc/meilisearch.toml — managed by setup script
env = "production"
master_key = "${MEILI_MASTER_KEY}"
http_addr = "${MEILI_BIND}"
db_path = "${MEILI_DB_PATH}"
dump_dir = "${MEILI_DUMP_DIR}"
snapshot_dir = "${MEILI_SNAP_DIR}"
# Uncomment to enable scheduled snapshots (every 24 h by default):
# schedule_snapshot = true
TOML
chmod 640 "$MEILI_CONFIG"
chown root:meilisearch "$MEILI_CONFIG"
# ── Systemd service ────────────────────────────────────────────────────────
echo "==> Installing systemd service…"
cat > /etc/systemd/system/meilisearch.service <<UNIT
[Unit]
Description=Meilisearch search engine
After=network.target
[Service]
Type=simple
User=meilisearch
Group=meilisearch
WorkingDirectory=/var/lib/meilisearch
ExecStart=/usr/bin/meilisearch --config-file-path ${MEILI_CONFIG}
Restart=on-failure
RestartSec=5
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
UNIT
systemctl daemon-reload
systemctl enable --now meilisearch
echo "==> Meilisearch service started."
# ── Wait for readiness ─────────────────────────────────────────────────────
echo -n "==> Waiting for Meilisearch to become ready"
for i in $(seq 1 30); do
if curl -sf "http://${MEILI_BIND}/health" >/dev/null 2>&1; then
echo " OK"
break
fi
echo -n "."
sleep 1
done
# Quick health check
HEALTH="$(curl -sf "http://${MEILI_BIND}/health" || true)"
if echo "$HEALTH" | grep -q "available"; then
echo "==> Meilisearch is healthy."
else
echo "WARNING: Could not confirm Meilisearch health. Check 'journalctl -u meilisearch'." >&2
fi
# ── Jellyfin environment override ──────────────────────────────────────────
# The jellyfin-plugin-meilisearch plugin reads MEILI_URL and MEILI_MASTER_KEY
# from the Jellyfin process environment. The cleanest way to inject them on a
# bare-metal install is a systemd drop-in.
echo "==> Configuring Jellyfin environment for the Meilisearch plugin…"
JELLYFIN_SERVICE="jellyfin.service"
DROPIN_DIR="/etc/systemd/system/${JELLYFIN_SERVICE}.d"
mkdir -p "$DROPIN_DIR"
cat > "${DROPIN_DIR}/meilisearch.conf" <<DROPIN
[Service]
Environment="MEILI_URL=http://${MEILI_BIND}"
Environment="MEILI_MASTER_KEY=${MEILI_MASTER_KEY}"
DROPIN
systemctl daemon-reload
# Restart Jellyfin only if it is already running
if systemctl is-active --quiet "$JELLYFIN_SERVICE" 2>/dev/null; then
echo "==> Restarting Jellyfin to pick up new environment…"
systemctl restart "$JELLYFIN_SERVICE"
else
echo "==> Jellyfin is not currently running; it will pick up the env vars on next start."
fi
# ── Summary ────────────────────────────────────────────────────────────────
cat <<EOF
============================================================
Setup complete!
============================================================
Meilisearch
URL : http://${MEILI_BIND}
Key : ${MEILI_MASTER_KEY}
Config : ${MEILI_CONFIG}
Data : ${MEILI_DB_PATH}
Service: systemctl status meilisearch
Jellyfin integration
Drop-in: ${DROPIN_DIR}/meilisearch.conf
The plugin will auto-detect the instance via
MEILI_URL and MEILI_MASTER_KEY env vars.
Next steps
1. Install the plugin in Jellyfin using this repo URL:
https://raw.githubusercontent.com/arnesacnussem/jellyfin-plugin-meilisearch/refs/heads/master/manifest.json
2. Restart Jellyfin if you haven't already.
3. Open the plugin settings in Jellyfin and click Save —
verify the status shows "ok".
EOF
My Customizations¶
ATOW I don't know when these changes, if any, will actually get merged into Jellyfin. So, here's what I've done and how to set it up.
https://github.com/alchemyyy/jellyfin
place these script outside of the respective jellyfin repo clone folders and run it.
Downloadbuild-jellyfin-debian.bat
@echo off
setlocal
set REPO_DIR=%~dp0jellyfin
set OUTPUT_DIR=%~dp0jellyfin-build-output
echo Building Jellyfin Server for Debian (linux-x64)...
echo.
cd /d "%REPO_DIR%"
dotnet publish Jellyfin.Server/Jellyfin.Server.csproj ^
-c Release ^
-f net10.0 ^
-r linux-x64 ^
--self-contained true ^
-o "%OUTPUT_DIR%"
if %ERRORLEVEL% NEQ 0 (
echo.
echo BUILD FAILED
pause
exit /b 1
)
echo.
echo Build complete. Output: %OUTPUT_DIR%
echo Transfer this folder to your Debian server and run with: ./jellyfin --webdir /path/to/jellyfin-web/dist
pause
Downloadbuild-jellyfin-web.bat
@echo off
cd /d "%~dp0jellyfin-web"
echo Installing dependencies...
call npm ci --engine-strict=false
echo Building local hls.js...
cd /d "%~dp0jellyfin-web\hls.js"
call npm run build
echo Copying local hls.js build over node_modules...
copy /Y "dist\hls.js" "%~dp0jellyfin-web\node_modules\hls.js\dist\hls.js"
copy /Y "dist\hls.js.map" "%~dp0jellyfin-web\node_modules\hls.js\dist\hls.js.map"
cd /d "%~dp0jellyfin-web"
echo Building jellyfin-web (production)...
call npm run build:production
echo Done. Output is in the dist folder.
pause
(you must clone hls.js and put dump it into the clone repo root)
to install web: zip up the "dist" folder and copy onto the jellyfin machine. then dump it into /usr/share/jellyfin-web/ the server gets built to "dist" too, dump it into usr/lib/jellyfin/ on the jellyfin machine.
This script re-does the permissions from that custom server build, and fixes possibly broken symlinks. Run on the jellyfin machine AFTER swapping the server out.
Downloadjellyfin_server_build_swapper_helper.sh
chmod -R 777 /usr/lib/jellyfin/
ln -sf /usr/lib/jellyfin/jellyfin /usr/bin/jellyfin
ln -s /usr/lib/x86_64-linux-gnu/libjemalloc.so.2 /usr/lib/jellyfin/libjemalloc.so
atow I don't have these settings definable in the UI. So, AFTER creating your libraries, run this script on the jellyfin machine.
Downloadjellyfin_crosslibraryseriesmergeenableonall.sh
#!/bin/bash
# Enables EnableCrossLibrarySeriesMerging on all Jellyfin libraries.
# Usage: sudo bash enable-cross-library-merging.sh
JELLYFIN_ROOT="/var/lib/jellyfin/root/default"
if [ ! -d "$JELLYFIN_ROOT" ]; then
echo "Jellyfin root not found at $JELLYFIN_ROOT"
exit 1
fi
count=0
for opts in "$JELLYFIN_ROOT"/*/options.xml; do
[ -f "$opts" ] || continue
lib="$(basename "$(dirname "$opts")")"
if grep -q '<EnableCrossLibrarySeriesMerging>true</EnableCrossLibrarySeriesMerging>' "$opts"; then
echo "[$lib] already enabled"
elif grep -q '<EnableCrossLibrarySeriesMerging>' "$opts"; then
sed -i 's|<EnableCrossLibrarySeriesMerging>false</EnableCrossLibrarySeriesMerging>|<EnableCrossLibrarySeriesMerging>true</EnableCrossLibrarySeriesMerging>|' "$opts"
echo "[$lib] updated false -> true"
((count++))
else
sed -i 's|</LibraryOptions>| <EnableCrossLibrarySeriesMerging>true</EnableCrossLibrarySeriesMerging>\n</LibraryOptions>|' "$opts"
echo "[$lib] inserted"
((count++))
fi
done
echo ""
echo "Updated $count libraries. Restart Jellyfin and rescan libraries to regenerate keys."
echo " sudo systemctl restart jellyfin"
Jellyfin-Server¶
Features
-
Aspect Ratio Computation from Trickplay Data** (
26901ca9) Server-side counterpart to the web UI feature — computes the actual aspect ratio of content by analyzing trickplay images for black bars. -
Cross-Library Series Joining (
a340180d) Joins series across multiple libraries (not just across folders within a single library) and deduplicates cross-library series in search results.
Jellyfin-Web¶
Features
-
Detected Aspect Ratio via Trickplay Analysis (
563dbcea) Adds a "Detected aspect ratio" option that uses trickplay image black bar analysis to automatically determine and apply the correct aspect ratio during playback. (Went through a couple revert/re-apply cycles before landing.) -
Mixed Libraries Show Tall Cards (
e54fddf9) Makes mixed-content libraries display tall (portrait) cards instead of the default layout.
Fixes
-
HLS.js Tuning for FFmpeg Segment Gaps (
bf1c253f) Adjusts hls.js config (stretchShortVideoTrack,nudgeMaxRetry: 10,maxBufferHole: 0.5) to prevent micro-stalls and fatal errors caused by audio/video frame boundary mismatches in FFmpeg-muxed HLS streams — especially during long playback sessions. -
CSP-safe HLS.js Worker Loading (
07805ffc) Loads the hls.js transmuxer worker from a file URL instead of an inline blob URL, avoidinginternalExceptionerrors on origins with strict Content Security Policy. -
Image Loading During Fast Scrolling (
6957b66d) Stops aggressively unloading images that scroll out of view, and adds retry logic for failed image preloads so thumbnails don't stay grey permanently. -
Notification Permission Check (
70604623) ChecksNotification.permissionbefore callingshowNotification()to prevent uncaughtTypeErrorwhen permission hasn't been granted.
CSS Customziations¶
Dump this into branding->css textbox in the admin dashboard
Downloadjellyfin.css
.homePage .emby-scrollbuttons {
display: none;
}
.homePage .section0 .itemsContainer {
display: grid !important;
grid-template-columns: repeat(auto-fill, min(max(15%, 270px), 650px));
justify-content: center;
margin: 0 auto;
gap: 1em;
}
.homePage .section0 .itemsContainer .card {
width: 100% !important;
height: auto !important;
}
.homePage .section0 .itemsContainer .card .cardBox,
.homePage .section0 .itemsContainer .card .cardScalable {
width: 100% !important;
padding-bottom: 56.25% !important;
height: 0 !important;
position: relative !important;
}
.homeSectionsContainer {
display: flex;
flex-direction: column;
}
.homeSectionsContainer .section0 {
order: 0 !important;
}
.homeSectionsContainer .section1 {
order: 1 !important;
}
.homeSectionsContainer .section6 {
order: 2 !important;
}
.homeSectionsContainer .section4 {
order: 3 !important;
}
.homeSectionsContainer .section2 {
order: 4 !important;
}
.homeSectionsContainer .section3 {
order: 5 !important;
}
.homeSectionsContainer .section5 {
order: 6 !important;
}
.section6 {
display: flex;
flex-direction: column;
}
.section6 .verticalSection:nth-child(5) {
order: 1 !important;
} /* Television */
.section6 .verticalSection:nth-child(4) {
order: 2 !important;
} /* Movies */
.section6 .verticalSection:nth-child(2) {
order: 3 !important;
} /* Anime */
.section6 .verticalSection:nth-child(1) {
order: 4 !important;
} /* Animated */
.section6 .verticalSection:nth-child(3) {
order: 5 !important;
} /* Cartoons */
.sectionTitleContainer-cards {
padding-top: 0 !important;
}
.homePage .section1 .overflowBackdropCard,
.homePage .section5 .overflowBackdropCard {
max-width: max(15%, 270px) !important;
}
.homePage .section6 .card {
width: min(max(8vw, 120px), 200px) !important;
}
.homePage .padded-left,
.homePage .emby-scroller {
padding-left: 1% !important;
}
.homePage .section0 .padded-left,
.homePage .section0 .emby-scroller {
padding-left: 0 !important;
padding-right: 0 !important;
}
.homePage .section0 > .sectionTitle {
font-size: 0 !important;
line-height: 0 !important;
display: block !important;
text-align: center !important;
}
#.homePage .section0 > .sectionTitle::after {
# content: "Libraries";
# font-size: 1.35rem;
# line-height: normal;
#}
REDACTED
Linux tuning¶
nano /etc/default/jellyfin
# comment out the MALLOC_TRIM_THRESHOLD i.e. disable it
# Tweaks
MALLOC_ARENA_MAX="2"
DOTNET_gcServer = "1"
DOTNET_GCConserveMemory = "9"
DOTNET_GCHeapHardLimitPercent = "0x5a"
DOTNET_GCHighMemPercent=48
NVIDIA_DRIVER_CAPABILITIES=all
NVIDIA_VISIBLE_DEVICES=all
Important Config Notes¶
- Enable audio downmix boost, use AC4 as the algorithm
- Query what codecs your specific GPU supports
- Trickplay: use keyframes
- Networking: Known Proxies: 127.0.0.1.
- This makes it so caddy is seen as a known proxy, which makes all traffic use X-Forwarded-For, which makes it so users IPs are resolved instead of the cloudflare IP they're connecting with.
- Only use TVDB and TMDB as metadata services. Use OpenSubtitles as subtitle service, pay for a bit of VIP if you need a ton of API for a bit.
- Always save all metadata types to library folders
My Libraries
- #AutoAnime
- #AutoMovies
- #AutoTV
- Animated
- Anime
- Cartoons
- Movies
- Television
Jellyfin WebSocket "Token is required" — EnableLegacyAuthorization¶
Problem: Every WebSocket connection to Jellyfin fails immediately. Browser console spams:
WebSocket connection to 'ws://<host>:8096/socket?api_key=...' failed:
web socket closed
nulling out web socket
Server-side logs show:
[ERR] Jellyfin.Api.Middleware.ExceptionMiddleware: Error processing request: Token is required. URL GET /socket.
All HTTP API requests work fine — only WebSocket is broken.
Root cause: The bundled jellyfin-apiclient (v1.11.0, deprecated) authenticates WebSocket connections by passing api_key=<token> as a query parameter. Starting in Jellyfin 10.11, the server introduced EnableLegacyAuthorization as an opt-in flag to disable deprecated auth methods (query string api_key, old header formats). In 10.12 this defaults to false, and in 10.13 legacy auth will be removed entirely.
When EnableLegacyAuthorization is false, the server ignores the api_key query parameter on the /socket endpoint, sees no valid token, and rejects the upgrade with "Token is required." The WebSocket never establishes.
Fix: Edit /etc/jellyfin/system.xml:
sudo systemctl restart jellyfin
This is a stopgap — the real fix is for the web client to send auth via the Authorization header instead of query string, but the deprecated jellyfin-apiclient library doesn't support that for WebSocket connections.
Relevant links:
- PR #13306 — Add option to disable deprecated legacy authorization — the PR that introduced EnableLegacyAuthorization
- Issue #15962 — Default EnableLegacyAuthorization=false breaks auth in 10.12
- Issue #12990 — API Authentication documentation incorrect
- Jellyfin API Authorization gist — detailed explanation of the auth deprecation timeline
Navidrome¶
Music server. Best there is, flawless.
Clients¶
I use feishin as a desktop app.
I use subtracks on android.
Installation¶
Installer (auto grabs latest github linux amd64 release):
Downloadnavidrome_install_debian.sh
#!/usr/bin/env bash
set -euo pipefail
# === Navidrome Installer for Debian-based Systems ===
if [[ $EUID -ne 0 ]]; then
echo "This script must be run as root."
exit 1
fi
# --- Prompt for user and group ---
read -rp "Enter the Linux user to run Navidrome as: " ND_USER
read -rp "Enter the Linux group to run Navidrome as: " ND_GROUP
# Create group if it doesn't exist
if ! getent group "$ND_GROUP" > /dev/null 2>&1; then
echo "Group '$ND_GROUP' does not exist. Creating..."
groupadd "$ND_GROUP"
fi
# Create user if it doesn't exist
if ! id -u "$ND_USER" > /dev/null 2>&1; then
echo "User '$ND_USER' does not exist. Creating..."
useradd -r -g "$ND_GROUP" -s /usr/sbin/nologin -M "$ND_USER"
fi
# --- Install dependencies ---
apt update
apt upgrade -y
apt install -y ffmpeg jq curl wget
# --- Create directories ---
install -d -o "$ND_USER" -g "$ND_GROUP" /opt/navidrome
install -d -o "$ND_USER" -g "$ND_GROUP" /var/lib/navidrome
# --- Download latest Navidrome ---
LATEST_VERSION=$(curl -s https://api.github.com/repos/navidrome/navidrome/releases/latest | jq -r '.tag_name' | sed 's/^v//')
echo "Downloading Navidrome v${LATEST_VERSION}..."
wget "https://github.com/navidrome/navidrome/releases/download/v${LATEST_VERSION}/navidrome_${LATEST_VERSION}_linux_amd64.tar.gz" -O /tmp/navidrome.tar.gz
tar -xzf /tmp/navidrome.tar.gz -C /opt/navidrome/
chown -R "$ND_USER":"$ND_GROUP" /opt/navidrome
rm /tmp/navidrome.tar.gz
# --- Write navidrome.toml ---
cat > /var/lib/navidrome/navidrome.toml << 'TOML'
MusicFolder = "/shares/media/Music"
Scanner.Schedule = "0"
Scanner.WatcherWait = "1s"
Scanner.ScanOnStartup = true
Scanner.FollowSymlinks = true
Scanner.PurgeMissing = "always"
DevExternalScanner = true
# NFS share is configured with multithreading
# Otherwise, beware this setting.
DevScannerThreads = 8
DevSelectiveWatcher = true
DevOptimizeDB = true
EnableArtworkPrecache = true
ImageCacheSize = "8GB"
TranscodingCacheSize = "8GB"
RecentlyAddedByModTime = true
#LastFM.ApiKey = ""
#LastFM.Secret = ""
#Spotify.ID = ""
#Spotify.Secret = ""
# === SHARING & AUTH ===
# EnableSharing = true
# AuthWindowLength = "0s"
# AuthRequestLimit = 11111
TOML
chown "$ND_USER":"$ND_GROUP" /var/lib/navidrome/navidrome.toml
# --- Write systemd service ---
cat > /etc/systemd/system/navidrome.service << EOF
[Unit]
Description=Navidrome Music Server and Streamer compatible with Subsonic/Airsonic
After=remote-fs.target network.target
AssertPathExists=/var/lib/navidrome
[Install]
WantedBy=multi-user.target
[Service]
User=${ND_USER}
Group=${ND_GROUP}
Type=simple
ExecStart=/opt/navidrome/navidrome --configfile "/var/lib/navidrome/navidrome.toml"
WorkingDirectory=/var/lib/navidrome
TimeoutStopSec=20
KillMode=process
Restart=on-failure
DevicePolicy=closed
NoNewPrivileges=yes
PrivateTmp=yes
PrivateUsers=yes
ProtectControlGroups=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
RestrictNamespaces=yes
RestrictRealtime=yes
SystemCallFilter=~@clock @debug @module @mount @obsolete @reboot @setuid @swap
ReadWritePaths=/var/lib/navidrome
ProtectSystem=full
EOF
# --- Enable and start ---
systemctl daemon-reload
systemctl enable navidrome.service
systemctl start navidrome.service
echo ""
echo "Navidrome v${LATEST_VERSION} installed and running."
echo " User/Group: ${ND_USER}:${ND_GROUP}"
echo " Config: /var/lib/navidrome/navidrome.toml"
echo " Service: systemctl status navidrome"
echo " Web UI: http://<your-ip>:4533"
Tricks¶
Clear Playlists: