Zarif's Homelab Playbook (Debian + Docker) — Notebook Edition
Last Updated: 2026-04-21 02:50 UTC
How to use this notebook
- Markdown cells are the guide (readable + copy/paste).
- Code cells are optional checks you can run on the server (via terminal/Jupyter-on-server later).
- If you don’t run Jupyter on the server, treat this notebook like a nicely structured document.
Table of Contents
-
- Build
-
- Installation notes (Debian)
-
- TV / overscan / console cut-off fixes
-
- Users, root, sudo
-
- Networking basics (Wi-Fi / VPN vs LAN)
-
- APT sources (CD-ROM issue)
-
- SSH: enable, connect, harden
-
- Firewall (UFW)
-
- Docker: what it is
-
- Folder architecture
-
- Jellyfin (Docker)
-
- Navidrome
-
- Docker Compose
-
- Compose file: Jellyfin + Navidrome
-
- SFTP / Cyberduck workflow
-
- Jellyfin playback troubleshooting (remux)
-
- Subtitle matching + bulk renaming
-
- Operational habits
-
- Security practices
-
- Nextcloud plan
Zarif’s Homelab Playbook (Debian + Docker)
A practical, copy/paste-friendly record of what I set up and learned so far. Includes: install notes, display fixes, networking, users/sudo, firewall, Docker/Compose, Jellyfin, Navidrome, file transfer, media folder conventions, subtitle bulk renaming, and “next steps” for Nextcloud + remote access.
0) Build
Hardware: Dell OptiPlex 9020 SFF (i7, 32GB RAM, SSD)
OS: Debian 12 “Bookworm”
Service manager: systemd
App platform: Docker + Docker Compose
Running services: Jellyfin (video), Navidrome (music)
Core idea: the OS stays mostly stable; each “app” is a container with its own config stored on disk via volumes.
1) Installation notes (Debian)
Secure Boot / UEFI
- Disabled Secure Boot so the installer would boot and install cleanly. (not always necessary)
- UEFI boot remained enabled.
Make sure you have Internet Connection during install
While it's possible to connect to the internet later on, configuring wifi after installation is a nightmare. See 4) for notes on plug-and-play wifi adapters. WHile I didn't use netinst, I recommend getting the netinst iso of bookworm when installing with an internet connection.
Headless install preference
If you want a headless server, during install don’t select a desktop environment.
(If you accidentally got a GUI, you can remove it later, but it’s easier to avoid up front.)
Domain name prompt during install
Debian asks for a domain name; for a home LAN it’s fine to use:
- leave blank, or
lan(example:host.lan)
This does not “expose” you to the internet—it's just local naming.
2) TV / overscan / console cut-off fixes(optional)
At the time, I did not own a computer monitor, so I had to use my TV, my TV had some overscan issues that I couldn't fix in it's own settings. You likely won't run into this issue.
A) Simple: set a smaller console resolution
Pick a resolution that fits the TV without overscan cropping.
B) GRUB display fix (useful to remember)
Edit GRUB defaults:
sudo nano /etc/default/grub
Common options:
GRUB_GFXMODE=1024x768
GRUB_GFXPAYLOAD_LINUX=keep
If you need very safe:
GRUB_GFXMODE=640x480
Apply changes:
sudo update-grub
sudo reboot
Note: TVs vary a lot. Smaller modes are boring but reliable.
3) Users, root, sudo
Considerations
- It’s easy to end up without
sudoor without your user in thesudogroup during a headless install. - Fix is simple once you can log in as root or
su -.
Add your user to sudo group (Debian)
As root:
usermod -aG sudo username
Then log out and back in for group membership to apply.
Verify your user + groups
id username
groups username
getent group sudo
Install sudo (if missing)
su -
apt update
apt install sudo
If apt tries to use a CD/DVD only, see “APT sources” below.
4) Networking basics (Wi‑Fi / “why VPN breaks LAN access”)
Wi‑Fi issues
I bought an older machine without a built in wifi card, the usb wifi it came with had weird drivers that weren't natively supported with linux. I eventually got a plug‑and‑play adapter that created an interface like:
wlx<random>(that’s normal naming)
Confirm network interface + IP
ip a
Quick connectivity tests
ping -c 3 1.1.1.1
ping -c 3 deb.debian.org
If IP works but DNS fails, your DNS config is the issue.
VPN Note
If you want to access your device via ssh(see:6) you might run into issues if that device is connected to a VPN. When you connect to a typical commercial VPN, your device’s routes change and local traffic (your 192.168.x.x LAN) may no longer be reachable. Many VPN apps have a setting like:
- “Allow LAN access”
- “Bypass local network”
- “Split tunneling”
Power Saving Mode Note
If your server for some reason is not ethernet connected(in my case it just wasn't possible), you might run into a problem with being able to access your server via LAN. I had a scenario where my server was internet connected and the services were accessible via https, but not available through LAN. It became a recurring issue, until I ran this command:
sudo iw dev [wifiinterface] set power_save off
5) APT sources (when apt install tries to use CD-ROM)
Sometimes Debian leaves a CD-ROM entry enabled in /etc/apt/sources.list.
Note: this likely won't be an issue with the netinst ISO.
Check:
cat /etc/apt/sources.list
ls /etc/apt/sources.list.d/
If you see lines like:
deb cdrom:[Debian GNU/Linux ...]
Comment them out by adding # at the start, then ensure you have real network repos, e.g.:
deb http://deb.debian.org/debian bookworm main contrib non-free-firmware
deb http://deb.debian.org/debian-security bookworm-security main contrib non-free-firmware
deb http://deb.debian.org/debian bookworm-updates main contrib non-free-firmware
Then:
sudo apt update
6) SSH: enable, connect, and harden
Install + start SSH server
sudo apt update
sudo apt install openssh-server
sudo systemctl enable --now ssh
sudo systemctl status ssh --no-pager
Find server IP (so you can SSH in)
ip a
SSH from another machine
ssh username@SERVER_IP
Basic hardening (good early defaults)
Edit SSH config:
sudo nano /etc/ssh/sshd_config
Recommended settings (start here):
PermitRootLogin no
PasswordAuthentication yes
``
**Later**, when you set up SSH keys, switch to:
```text
PasswordAuthentication no
Restart SSH after changes:
sudo systemctl restart ssh
If you’re remote when editing SSH settings, keep a second session open so you don’t lock yourself out.
7) Firewall (UFW) – “allow SSH, deny the rest by default”
If you forward ports from your router to your server and want remote SSH access, you should configure a firewall.
Install:
sudo apt install ufw
Good baseline:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow from YOUR_IP_ADDRESS to any port 22 proto tcp
sudo ufw enable
sudo ufw status verbose
This configuration only allows SSH connections from a trusted IP address.
There are botnets that constantly scan the internet for exposed SSH services and attempt password logins.
If your IP address changes frequently, you can instead allow SSH but rate-limit connection attempts:
sudo ufw limit 22/tcp
This allows SSH from anywhere but slows brute-force attempts.
To inspect firewall logs:
sudo grep UFW /var/log/syslog
or
sudo tail -f /var/log/ufw.log
8) Docker: what it is (and why you used it)
Docker runs apps in isolated environments called containers.
- The container has its own filesystem, but you mount important data/config as volumes on the host.
- Upgrades are usually: pull new image → restart container.
There are alternatives to docker, to run apps in containers if you prefer something else.
Install Docker + Compose
On Debian 12:
sudo apt update
sudo apt install docker.io docker-compose-plugin
sudo systemctl enable --now docker
Optional: allow your user to run docker without sudo (tradeoff: convenience vs security)
sudo usermod -aG docker username
# log out/in
Then test:
docker run --rm hello-world
9) Folder architecture (host filesystem)
A clean pattern that scales:
/srv/
stack/ # compose files + .env
media/
movies/
shows/
music/
jellyfin/
config/
cache/
navidrome/
data/
Create folders:
sudo mkdir -p /srv/{stack,media/{movies,shows,music},jellyfin/{config,cache},navidrome/data}
sudo chown -R username:username /srv/stack /srv/media /srv/jellyfin /srv/navidrome
For media folders, ownership can be more flexible, but keeping it consistent avoids surprises.
10) Jellyfin (Docker)
Jellyfin is a service that allows you to access and stream media on a server. You can organize files into it's media folder and it will scrape the data of any 'dvds' you may have ripped and placed into the folders. The interface is similar to a lot of video streaming apps and you populate it with your personal collection. It has plugins, community made themes, and other customizable options. It also can stream audio, among other things, but we will be using navidrome for audio.
“One-shot” docker run (what I learned first, the method improves in 12)
docker run -d --name jellyfin --restart=unless-stopped -p 8096:8096 -v /srv/jellyfin/config:/config -v /srv/jellyfin/cache:/cache -v /srv/media:/media jellyfin/jellyfin
What each part means:
-drun in background (detached)--name jellyfinhuman-friendly container name--restart=unless-stoppedauto-start after reboot unless you stop it manually-p 8096:8096maps host port → container port-v host:containermounts persistent folders (config, cache, media)jellyfin/jellyfinis the image name
Note on backslashes mattered when typing
The \ at line end escapes the newline. If you missed a backslash while typing, Docker saw a “broken” command and threw errors like “invalid reference format”. Pasting preserved the exact syntax.
11) Navidrome (Docker)
Navidrome is a lightweight music server. There are numerous navidrome clients that you can use to stream music on your phone from with navidrome. You may also directly access the container via browser.
Recommended: run via Docker Compose (see below)
It uses:
/srv/media/musicfor your music library/srv/navidrome/datafor its internal DB/config
12) Docker Compose (better method)
Compose lets you define a whole stack in YAML:
- versionable
- reproducible for clients
- one command to bring everything up/down
Your working file location:
/srv/stack/docker-compose.yml
Bring up services:
cd /srv/stack
docker compose up -d
See status:
docker compose ps
View logs:
docker compose logs -f jellyfin
Stop services:
docker compose down
13) Current Docker Compose (Jellyfin + Navidrome)
Create/edit:
nano /srv/stack/docker-compose.yml
Example:
services:
jellyfin:
image: jellyfin/jellyfin:latest
container_name: jellyfin
restart: unless-stopped
ports:
- "8096:8096"
volumes:
- /srv/jellyfin/config:/config
- /srv/jellyfin/cache:/cache
- /srv/media:/media
navidrome:
image: deluan/navidrome:latest
container_name: navidrome
restart: unless-stopped
ports:
- "4533:4533"
volumes:
- /srv/navidrome/data:/data
- /srv/media/music:/music:ro
environment:
- ND_SCANINTERVAL=1h
Then:
cd /srv/stack
docker compose up -d
Access:
- Jellyfin:
http://SERVER_IP:8096 - Navidrome:
http://SERVER_IP:4533
14) File transfer (SFTP / Cyberduck)
Use SFTP (not FTP)
To transfer files from my laptop to the server I used an SFTP client(Cyberduck), there are other available ones, use the one that you prefer. In Cyberduck:
- Protocol: SFTP
- Server: your server IP
- Port: 22
- Username:
username - Password: your SSH password
“No sudo in Cyberduck” workaround
I ran into the issue that I couldn't transfer files to all these folders made in the root directories (/srv/media/) as these were not owned by my user, and I can't sudo in Cyberduck. Best practice is NOT logging in as root for file transfer.
Instead:
- own the media directory as your user (or group)
- write to
/srv/mediaasusername
Example permission model:
sudo chown -R username:username /srv/media
sudo chmod -R 755 /srv/media
If you want tighter permissions later, you can look into group-based access.
15) Jellyfin playback troubleshooting: “MP4 opens then closes” (Optional)
You hit:
- “Unable to find a valid media source”
- playback starts then stops immediately
- logs showed ffmpeg errors and
timescale not setwarnings for some files
Key takeaway: “MP4” doesn’t guarantee the container metadata is friendly for streaming/transcoding.
“Remux” fix (no re-encode, just rewrap)
If you ever need it again, use host ffmpeg:
sudo apt install ffmpeg
cd "/srv/media/shows/Some Show/Season 1"
ffmpeg -i "episode.mp4" -map 0:v:0 -map 0:a:0 -c copy "episode-fixed.mkv"
(We map only video+audio to avoid subtitle/art attachment stream issues when remuxing.)
Full Disclosure: While following these steps may have solved some playback issues I was having, there's a possibility that this didn't do anything at all. But in case you have some issues with certain filetypes see if this helps.
16) Subtitle matching + bulk renaming (Optional)
Jellyfin matches external subtitles when the base name matches:
VideoName.mp4
VideoName.en.ass
VideoName.zxx.ass
Problem I hit
Subtitle names didn’t match video basenames, and Windows bulk rename was untrustworthy.
Working strategy
Rename subtitle files to match the corresponding video file base name, while preserving each subtitle “variant” so they don’t overwrite each other.
Template idea:
VideoBase.en.ass(English)VideoBase.zxx.ass(signs/songs or “on-screen text”)- keep variants like
en.#4.enif you want multiple “styles”
Safe rename script (rename subs to match videos)
Run inside the season folder after videos + subs are both present:
cd "/srv/media/shows/ShowName/Season 1"
# DRY RUN (prints changes only)
for sub in *.ass; do
ep=$(echo "$sub" | sed -n 's/.* - \([0-9][0-9]\).*/\1/p')
mapfile -t vids < <(compgen -G "* - $ep"*".mp4")
if [ "${#vids[@]}" -ne 1 ]; then
echo "SKIP ep=$ep vids=${#vids[@]} :: $sub"
continue
fi
vid="${vids[0]}"
base="${vid%.mp4}"
# Variant: everything after the hash bracket in the subtitle filename
variant=$(echo "$sub" | sed -n 's/.*\]\.\(.*\)\.ass$/\1/p')
new="${base}.${variant}.ass"
echo "$sub -> $new"
done
When it looks right, replace the final echo with:
mv -- "$sub" "$new"
The
--prevents weird filenames from being treated as options bymv.
17) Operational habits
A) Keep a “stack handbook” folder
Keep copies of important configuration files and notes so you can quickly rebuild or troubleshoot your stack.
mkdir -p ~/homelab-handbook
cp /srv/stack/docker-compose.yml ~/homelab-handbook/
You can also store notes such as:
- service ports
- container names
- mounted volumes
- domain names
- configuration decisions
B) Learn the “lifecycle” commands
These are the core commands for managing Docker Compose stacks.
Start containers:
docker compose up -d
Stop containers:
docker compose down
View logs:
docker compose logs -f SERVICE
Update containers:
docker compose pull && docker compose up -d
C) Update Debian regularly
Keep the base system updated for security patches and package improvements.
sudo apt update
sudo apt upgrade
D) Prefer H.264 video encoding for maximum compatibility
When ripping or encoding media for Jellyfin, H.264 (AVC) is usually the safest choice.
Most devices support H.264 decoding natively, which allows direct play without server transcoding.
Advantages of H.264:
- widely supported by phones, TVs, browsers, and streaming devices
- reduces CPU load on the Jellyfin server
- fewer playback compatibility issues
H.265 (HEVC) provides better compression and smaller file sizes, but:
- requires more processing power to decode
- may trigger transcoding on older devices
- is less universally supported
If your playback devices support HEVC, it can be a good option to save storage space while maintaining high quality.
Summary:
- H.264 → maximum compatibility
- H.265 → smaller files but higher decoding cost
E) Monitor disk usage
sudo df -h
sudo du -sh /srv/media/*
18) Security practices
Now (simple + effective):
- Keep SSH limited to trusted IPs or enabled but rate limited
- Use a strong user password
- Avoid root SSH login
Planned:
- SSH keys, disable password auth
- Fail2ban
- Tailscale/WireGuard for remote access instead of opening ports
- Reverse proxy (Caddy/Nginx Proxy Manager) + HTTPS for web apps
19) Nextcloud plan
Nextcloud is more “app stack” than Jellyfin:
- database (Postgres or MariaDB)
- optional Redis caching
- persistent volumes
- later: HTTPS + reverse proxy
We will implement it via Docker Compose with:
nextcloudpostgresredis.envfile for passwords
Appendix: “quick reference” commands
Docker
docker ps
docker logs jellyfin --tail 100
docker restart jellyfin
Networking
ip a
ping -c 3 1.1.1.1
ping -c 3 deb.debian.org
Users/groups
id username
groups group
sudo -v
UFW
sudo ufw status verbose
sudo ufw allow 8096/tcp
sudo ufw limit 22/tcp
End of current playbook.
Appendix — Copy/Paste Task Cards
Short, practical blocks for common tasks.
Bring the stack up/down
cd /srv/stack
docker compose up -d
docker compose ps
docker compose down
Update containers (typical safe routine)
cd /srv/stack
docker compose pull
docker compose up -d
docker image prune -f
Subtitle rename DRY RUN (run inside a Season folder)
Example path shown; adjust to your show/season folder.
cd "/srv/media/shows/Show/Season 1"
for sub in *.ass; do
ep=$(echo "$sub" | sed -n 's/.* - \([0-9][0-9]\).*/\1/p')
mapfile -t vids < <(compgen -G "* - $ep"*".mp4")
if [ "${#vids[@]}" -ne 1 ]; then
echo "SKIP ep=$ep vids=${#vids[@]} :: $sub"
continue
fi
vid="${vids[0]}"
base="${vid%.mp4}"
variant=$(echo "$sub" | sed -n 's/.*\]\.\(.*\)\.ass$/\1/p')
new="${base}.${variant}.ass"
echo "$sub -> $new"
done
Subtitle rename APPLY (CAREFUL) — replace echo with mv
cd "/srv/media/shows/Show/Season 1"
for sub in *.ass; do
ep=$(echo "$sub" | sed -n 's/.* - \([0-9][0-9]\).*/\1/p')
mapfile -t vids < <(compgen -G "* - $ep"*".mp4")
if [ "${#vids[@]}" -ne 1 ]; then
echo "SKIP ep=$ep vids=${#vids[@]} :: $sub"
continue
fi
vid="${vids[0]}"
base="${vid%.mp4}"
variant=$(echo "$sub" | sed -n 's/.*\]\.\(.*\)\.ass$/\1/p')
new="${base}.${variant}.ass"
mv -- "$sub" "$new"
done
Homelab Playbook — Nextcloud Expansion & Concepts
Overview
This section documents the Nextcloud deployment, architecture, background jobs, and supporting services (Redis + PostgreSQL).
Architecture Overview
Nextcloud is running as a Docker stack composed of:
- Nextcloud App Container — Web UI + PHP application
- PostgreSQL Container — Primary database
- Redis Container — Cache and file locking
- Host Cron Job — Executes scheduled background tasks
Docker Compose Stack
services:
db:
image: postgres:15
restart: unless-stopped
env_file:
- ./nextcloud.env
volumes:
- /srv/nextcloud/db:/var/lib/postgresql/data
redis:
image: redis:7
restart: unless-stopped
volumes:
- /srv/nextcloud/redis:/data
app:
image: nextcloud:latest
restart: unless-stopped
ports:
- "8080:80"
env_file:
- ./nextcloud.env
depends_on:
- db
- redis
volumes:
- /srv/nextcloud/app:/var/www/html
- /srv/nextcloud/data:/var/www/html/data
Environment Variables
POSTGRES_DB=nextcloud
POSTGRES_USER=nextcloud
POSTGRES_PASSWORD=YOUR_PASSWORD
NEXTCLOUD_ADMIN_USER=admin
NEXTCLOUD_ADMIN_PASSWORD=YOUR_PASSWORD
REDIS_HOST=redis
Folder Structure
/srv
├── stack
│ ├── nextcloud-compose.yml
│ ├── nextcloud.env
│
└── nextcloud
├── app
├── data
├── db
└── redis
Background Jobs (Cron)
Nextcloud background jobs are configured to run via cron for reliability and performance.
Cron Entry
*/5 * * * * docker exec -u www-data nextcloud-app-1 php -f /var/www/html/cron.php > /dev/null 2>&1
What This Does
Runs every 5 minutes to:
- Process file scans
- Generate previews
- Clean temporary files
- Handle notifications
- Run scheduled app tasks
- Perform maintenance operations
Checking System Status
Containers
docker ps
Logs
docker compose -f nextcloud-compose.yml logs --tail=50
Nextcloud Status
docker exec -u www-data nextcloud-app-1 php occ status
Maintenance Commands
Add Missing Database Indices
docker exec -u www-data nextcloud-app-1 php occ db:add-missing-indices
Run Repair Tasks
docker exec -u www-data nextcloud-app-1 php occ maintenance:repair --include-expensive
Redis Explained
Redis is an in-memory data store used for:
- File locking
- Caching
- Session storage
- Performance improvements
Benefits:
- Prevents file corruption
- Reduces database load
- Speeds up UI responsiveness
PostgreSQL Explained
PostgreSQL is the primary database storing:
- User accounts
- File metadata
- Permissions
- Sharing info
- Settings
- Activity logs
Actual files are stored on disk in /srv/nextcloud/data.
Why Separate Services
This architecture provides:
- Better performance
- Easier backups
- Isolation of failures
- Easier upgrades
- Industry-standard deployment model
Current Expected Warnings
Normal for local deployment:
- HTTP instead of HTTPS
- No email configured
- Trusted domains not set
- Maintenance window not configured
- Missing indices
Future Improvements
- Reverse proxy + HTTPS
- Automated backups
- Monitoring stack
- Security hardening
- External storage mounts
- Performance tuning
Mental Model
Browser
↓
Nextcloud container
↓
Redis cache
↓
PostgreSQL database
↓
Disk storage
What Been Built
We now have a production-style self-hosted cloud stack with:
- Container orchestration
- Database backend
- Memory caching
- Scheduled maintenance
- Persistent storage
- Service isolation
Homelab Infrastructure Playbook -- Phase 2
Full Transition from Internal-Only Services to Secure External Access via Cloudflare Tunnel
Last Updated: 2026-03-02 05:56 UTC
0. Starting Environment
This phase begins with an already functioning internal homelab:
Host
- Debian headless server
- Docker installed
- Docker Compose used for orchestration
Services Running Internally
Service Port Internal URL
----------- ------ ---------------------------
Jellyfin 8096 http://SERVER_IP:8096
Navidrome 4533 http://SERVER_IP:4533
Nextcloud 8080 http://SERVER_IP:8080
Compose Layout
All compose files stored in:
/srv/stack
Service data stored in:
/srv/jellyfin
/srv/navidrome
/srv/nextcloud
My constraints: - No router access - No port forwarding - WiFi Only
Goal: Expose services via domain securely without opening inbound ports.
1. Domain & Cloudflare Preparation
1.1 Cloudflare Account Creation
- Created Cloudflare account
- Added existing domain (I already owned some domains, you may want to go and purchase a domain if you don't already have one)
- Updated nameservers at domain registrar to Cloudflare nameservers
- Waited for DNS propagation
- Verified with:
nslookup -type=ns yourdomain.com
Once nameservers resolved to Cloudflare → domain active.
1.2 Cloudflare DNS Configuration
Created proxied CNAME records:
Subdomain Target
----------- ----------------------------
media TUNNEL-ID.cfargotunnel.com
music TUNNEL-ID.cfargotunnel.com
cloud TUNNEL-ID.cfargotunnel.com
Important: - Orange cloud (Proxied) must be enabled. - Proxy ensures TLS termination and hides origin.
2. Cloudflare Zero Trust Setup
2.1 Zero Trust Initialization
- Clicked "Get Started" under Zero Trust
- Created team name
- Accessed Tunnel management under "Networks → Tunnels"
Important realization: Locally-created tunnels appear as "Locally Managed" and cannot be edited in dashboard unless migrated.
We chose local management for full Docker control.
3. Cloudflare Tunnel Creation (Docker-Based)
3.1 Directory Preparation
Created tunnel directory:
sudo mkdir -p /srv/cloudflared
Initial permissions debugging required later adjustments.
3.2 Tunnel Login
docker run -it --user 0:0 -v /srv/cloudflared:/etc/cloudflared cloudflare/cloudflared:latest tunnel login
Observed: - Cert saved inside container at
/root/.cloudflared/cert.pem - Required manual extraction via:
docker cp cloudflared-login:/root/.cloudflared/cert.pem /srv/cloudflared/cert.pem
Key lesson: --rm removes container before file retrieval if not
careful.
3.3 Create Named Tunnel
docker run --rm -it --user 0:0 -v /srv/cloudflared:/etc/cloudflared cloudflare/cloudflared:latest tunnel create homelab
Result: - JSON credentials file generated - Tunnel ID issued -
Credentials stored in /srv/cloudflared/
Security note: Keep JSON credentials secret.
4. Cloudflare Tunnel Configuration
Created:
/srv/cloudflared/config.yml
Contents:
tunnel: <TUNNEL-ID>
credentials-file: /etc/cloudflared/<TUNNEL-ID>.json
ingress:
- hostname: media.domain.com
service: http://192.168.1.204:8096
- hostname: music.domain.com
service: http://192.168.1.204:4533
- hostname: cloud.domain.com
service: http://192.168.1.204:8080
- service: http_status:404
Important rule: Fallback rule must be last.
5. Docker Compose Integration
File:
/srv/stack/cloudflared-compose.yml
Final working configuration:
services:
cloudflared:
image: cloudflare/cloudflared:latest
container_name: cloudflared
restart: unless-stopped
command: tunnel run homelab
volumes:
- /srv/cloudflared:/etc/cloudflared
Started with:
docker compose -p cloudflared -f /srv/stack/cloudflared-compose.yml up -d
6. Debugging & Lessons Learned
6.1 Permission Errors
Error observed:
open /etc/cloudflared/config.yml: permission denied
Cause: Directory permissions blocked traversal.
Fix:
sudo chmod 755 /srv/cloudflared
sudo chmod 644 /srv/cloudflared/*
Lesson: Docker containers need read access to mounted files.
6.2 Restart vs Recreate
Observed: docker restart does NOT reload compose changes.
Correct procedure:
docker compose down
docker compose up -d
6.3 Healthy Tunnel Logs
Healthy logs contain:
INF Registered tunnel connection
Multiple connections expected (connIndex 0-3).
7. Nextcloud Reverse Proxy Configuration
Issue: "Access through untrusted domain"
Resolved by editing:
config/config.php
Located via:
docker inspect nextcloud-app-1 --format '{range .Mounts}{println .Destination "->" .Source}{end}'
Updated configuration:
'trusted_domains' =>
array (
0 => '192.168.1.204:8080',
1 => 'cloud.domain.com',
),
'overwrite.cli.url' => 'https://cloud.domain.com',
'overwriteprotocol' => 'https',
No restart required.
8. Jellyfin Password Reset Mechanism
Password reset generates file:
/config/passwordreset
Host path example:
/srv/jellyfin/config/passwordreset*
Retrieve via:
sudo cat /srv/jellyfin/config/passwordreset*
File deletes automatically after successful reset.
Security consideration: Requires server access → implies broader compromise scenario.
9. Bandwidth Considerations
External access consumes upload bandwidth.
Measured upload:
~41 Mbps
Practical limit:
- 2--3 1080p streams
- High bitrate reduces concurrency
LAN traffic does not consume internet bandwidth.
10. Security Model Decisions
Chosen:
- Strong passwords
- Manual user creation
- No public registration
- Cloudflare proxy enabled
- No open ports
- Optional 2FA for Nextcloud
Rejected:
- Cloudflare Access in front of Navidrome (client compatibility risk)
- Mandatory double-login model
11. Operational Commands
Check tunnel:
docker logs cloudflared --tail 50
Restart:
docker restart cloudflared
Shutdown:
docker compose -p cloudflared -f /srv/stack/cloudflared-compose.yml down
12. Final System State
- Secure domain-based access
- No router configuration required
- Persistent volumes
- Containerized services
- External TLS handled by Cloudflare
- Fully reproducible deployment process
This concludes full Phase 2 externalization.