Fill in your details up top and get customized, copy-pasteable configs for a media server setup.
Gluetun (VPN) → qBittorrent → Soulseek → Jellyfin → Tailscale
These values get plugged into all the configs below. Change them and everything updates live.
After installing Ubuntu Server (or Debian) and logging in:
1. Update everything:
sudo apt update && sudo apt upgrade -y
2. Install essentials:
sudo apt install -y curl wget git nano htop tree net-tools
3. Set your timezone:
sudo timedatectl set-timezone America/Chicago
4. Set a hostname:
sudo hostnamectl set-hostname myserver
Run these on your laptop/desktop (not the server):
# Generate a key pair (press Enter through the prompts) ssh-keygen # Copy your public key to the server ssh-copy-id youruser@192.168.0.100
Now ssh youruser@192.168.0.100 should log in without a password.
Optional — disable password login (only after confirming key auth works!):
# Edit SSH config sudo nano /etc/ssh/sshd_config # Find and change: PasswordAuthentication no # Restart SSH sudo systemctl restart sshd
Don't use the docker.io apt package (outdated) or the Snap version (permission quirks). Use Docker's official repository:
# Remove old versions sudo apt remove -y docker docker-engine docker.io containerd runc 2>/dev/null # Add Docker's official GPG key and repository sudo apt update sudo apt install -y ca-certificates curl gnupg sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpg echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null # Install Docker sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin # Add yourself to the docker group (no more sudo for docker commands) sudo usermod -aG docker $USER
Log out and back in for the group change to take effect. Then verify:
docker run hello-world docker compose version
Create the directory structure for media and Docker configs. If you have a separate drive for media, mount it first and add it to /etc/fstab so it mounts on boot.
Create everything at once (uses your paths from the form above):
sudo mkdir -p /docker
sudo mkdir -p /media/{movies,tv,music,downloads/{complete/{movies,tv},incomplete}}
sudo chown -R 1000:1000 /docker /media
sudo chmod -R 775 /docker /media
This gives you:
/docker/ ← Docker compose files and service configs
/media/ ← Media storage
├── movies/
├── tv/
├── music/
└── downloads/
├── complete/
│ ├── movies/
│ └── tv/
└── incomplete/
Ubuntu has ufw (Uncomplicated Firewall). Allow SSH first so you don't lock yourself out, then allow service ports as you deploy them.
# Allow SSH first! sudo ufw allow ssh # Allow service ports sudo ufw allow 8096 # Jellyfin sudo ufw allow 8080 # qBittorrent (via Gluetun) sudo ufw allow 5030 # slskd (via Gluetun) # Enable the firewall sudo ufw enable # Check status sudo ufw status
┌──────────────────────────────────────┐
│ Gluetun (VPN) │
│ All download traffic exits here │
│ │
│ ┌─────────────┐ ┌──────────────┐ │
│ │ qBittorrent │ │ slskd │ │
│ │ (torrents) │ │ (Soulseek) │ │
│ └──────┬──────┘ └──────┬───────┘ │
└──────────┼────────────────┼──────────┘
│ │
▼ ▼
downloads/ + movies/ music/
│
▼
┌──────────────────────┐
│ Jellyfin │
│ (streams it all) │
└──────────────────────┘
Gluetun creates a VPN tunnel. qBittorrent and slskd share its network, so their traffic is always encrypted.
If the VPN disconnects, traffic stops — built-in kill switch.
qBittorrent downloads torrents. You organize completed downloads into your media folders.
slskd searches and downloads music from Soulseek.
Jellyfin reads your media folders and streams to any device.
Gluetun is a VPN client container. Other containers connect through it by sharing its network namespace, which forces all their traffic through the VPN tunnel. This is important for torrenting — your ISP can see unencrypted torrent traffic, and your IP is visible to other peers. A VPN prevents both.
Note: The environment variables below work for most providers, but some (like Mullvad or ProtonVPN) need different ones. Check Gluetun's provider docs for yours.
Setup: mkdir -p /docker/gluetun && nano /docker/gluetun/docker-compose.yml
Start: cd /docker/gluetun && docker compose up -d
Verify VPN works: docker exec gluetun wget -qO- https://ifconfig.me — should show a different IP than your home connection
Key detail: The ports section is on Gluetun, not on the individual services. Since qBittorrent and slskd share Gluetun's network, their ports are exposed through here.
A torrent client with a web UI. All traffic goes through Gluetun's VPN tunnel via network_mode: "container:gluetun". Gluetun must be running first.
Setup: mkdir -p /docker/qbittorrent && nano /docker/qbittorrent/docker-compose.yml
Start: cd /docker/qbittorrent && docker compose up -d
Access: http://your-server-ip:8080
Default password: Check docker logs qbittorrent 2>&1 | grep -i password
Recommended settings after login:
/media/downloads/incompletemovies → save path /media/downloads/complete/moviestv → save path /media/downloads/complete/tv
Your own streaming server — like Plex but free, no accounts, no phoning home. Web browser, phone apps, TV apps, everything.
Runs on network_mode: "host" (port 8096) for best streaming and DLNA performance. Does not need the VPN.
Setup: mkdir -p /docker/jellyfin/{config,cache} && nano /docker/jellyfin/docker-compose.yml
Start: cd /docker/jellyfin && docker compose up -d
Access: http://your-server-ip:8096
First-time setup wizard: Create admin account, add libraries:
/media/movies/media/tv/media/musicHardware transcoding (optional): If your CPU supports Intel Quick Sync, add devices: ["/dev/dri:/dev/dri"] to the compose file and enable VA-API in Jellyfin's transcoding settings.
Soulseek is a peer-to-peer network for sharing music — great for finding rare albums, live recordings, and lossless (FLAC) files. slskd is a modern web-based client. Like qBittorrent, it routes through Gluetun's VPN.
Setup: mkdir -p /docker/slskd && nano /docker/slskd/docker-compose.yml
Start: cd /docker/slskd && docker compose up -d
Access: http://your-server-ip:5030 — default login: slskd / slskd (change it!)
You'll also need a Soulseek config file at /docker/slskd/slskd.yml:
soulseek:
username: your-soulseek-username
password: your-soulseek-password
listen_port: 50300
shares:
directories:
- /music
directories:
downloads: /downloads
incomplete: /downloads/incomplete
Create a free Soulseek account at slsknet.org if you don't have one.
Gluetun needs to be running before qBittorrent and slskd. Start in this order:
After first start, Docker handles this on reboot automatically (restart: unless-stopped).
movies or tv category/media/downloads/complete/movies/ or .../tv//media/movies/ or /media/tv/Jellyfin matches files to metadata based on names. These conventions work best:
Movies: /media/movies/The Matrix (1999)/The Matrix (1999).mkv TV Shows: /media/tv/Breaking Bad/Season 01/Breaking Bad - S01E01 - Pilot.mkv Music: /media/music/Artist/Album (Year)/01 - Track Name.flac
Tailscale lets you access your server from anywhere — phone on cellular, laptop at a coffee shop — as if you were on your home network. No port forwarding, no exposing anything to the internet. It creates a private encrypted network between your devices using WireGuard.
curl -fsSL https://tailscale.com/install.sh | sh sudo tailscale up
This prints a login URL. Open it, sign in with Google/Microsoft/GitHub, and authorize the device. Then check your Tailscale IP:
tailscale ip -4 # Something like: 100.100.82.4
Phone: Install "Tailscale" from App Store / Google Play, sign in with the same account.
Laptop: Download from tailscale.com/download, sign in.
Use the Tailscale IP instead of the local IP. Works from anywhere:
| Service | URL |
|---|---|
| Jellyfin | http://100.x.y.z:8096 |
| qBittorrent | http://100.x.y.z:8080 |
| slskd | http://100.x.y.z:5030 |
The Tailscale IP works at home too, so you can just use it everywhere.
Makes your entire home network accessible via Tailscale, not just devices running Tailscale:
# Enable IP forwarding echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf echo 'net.ipv6.conf.all.forwarding = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf sudo sysctl -p /etc/sysctl.d/99-tailscale.conf # Advertise your subnet (change to match your network) sudo tailscale up --advertise-routes=192.168.0.0/24
Then approve the route in Tailscale admin console.
Expose a single service publicly through Tailscale's infrastructure (HTTPS, no home IP exposed):
# Expose Jellyfin publicly sudo tailscale funnel 8096 # Turn it off sudo tailscale funnel --off 8096
sudo tailscale up --advertise-exit-nodessh youruser@100.x.y.z| Command | What it does |
|---|---|
pwd | Print where you are right now |
ls -la | List everything including hidden files with details |
cd /some/path | Move to a directory |
cd .. | Go up one level |
cd - | Go back to wherever you just were |
mkdir -p path/to/folder | Create nested directories |
cp -r src/ dest/ | Copy a directory recursively |
mv old new | Move or rename |
rm -r folder/ | Delete a directory (no undo!) |
nano file.txt | Edit a file (Ctrl+O save, Ctrl+X exit) |
cat file.txt | Print file contents |
less file.txt | View with scrolling (q to quit) |
When you run ls -la you see: drwxrwxr-x 2 leah leah
d = directory, - = filerwx = owner permissionsrwx = group permissionsr-x = everyone elseNumeric: 7=rwx, 6=rw, 5=rx, 4=r, 0=none. Three digits = owner, group, others.
| Command | What it does |
|---|---|
chmod 775 folder/ | Set permissions (rwxrwxr-x) |
chmod -R 775 folder/ | Set permissions recursively |
chown user:group file | Change owner and group |
chown -R user:group folder/ | Change owner recursively |
id | Show your UID, GID, and groups |
For Docker: Make sure directories your containers write to are owned by the same UID/GID the container runs as (PUID/PGID in compose files).
Compose commands (run from the directory with your compose file):
| Command | What it does |
|---|---|
docker compose up -d | Start containers in background |
docker compose down | Stop and remove containers |
docker compose pull | Pull latest images |
docker compose logs -f | Follow logs in real time |
docker compose logs -f --tail 50 | Logs, last 50 lines first |
docker compose restart | Restart containers |
General commands (work from anywhere):
| Command | What it does |
|---|---|
docker ps | List running containers |
docker ps -a | List all containers (incl. stopped) |
docker exec -it name bash | Shell into a running container |
docker logs name | View container logs |
docker system prune | Clean up unused resources |
docker image prune -a | Remove unused images |
Update a service:
docker compose pull && docker compose up -d
| Command | What it does |
|---|---|
ssh user@ip | Connect to a server |
ssh-keygen | Generate SSH key pair |
ssh-copy-id user@ip | Copy public key (passwordless login) |
scp file user@ip:/path/ | Copy file to server |
scp -r folder/ user@ip:/path/ | Copy directory to server |
SSH config shortcut — add to ~/.ssh/config on your local machine:
Host myserver
HostName 192.168.0.100
User youruser
Then just ssh myserver.
| Command | What it does |
|---|---|
df -h | Disk space usage |
du -sh /path | Size of a directory |
htop | Interactive process viewer |
free -h | Memory usage |
ip a | Network interfaces and IPs |
hostname -I | Quick IP check |
uptime | How long the server has been up |
sudo reboot | Reboot |
Package management (Ubuntu/Debian):
| Command | What it does |
|---|---|
sudo apt update | Refresh package list |
sudo apt upgrade | Install updates |
sudo apt install pkg | Install a package |
sudo apt autoremove | Clean up unused dependencies |
| Shortcut | What it does |
|---|---|
Tab | Autocomplete file/directory names |
Ctrl+C | Kill current command |
Ctrl+L | Clear the screen |
Ctrl+R | Search command history |
!! | Repeat last command (sudo !! to re-run as root) |
Up arrow | Cycle previous commands |
Once you're comfortable, there's a whole ecosystem of automation called the *arr stack:
Together these create a pipeline: request something → it gets searched, downloaded, organized, and shows up in Jellyfin automatically.
Resources: