Cleaning up my Media Server: Complete Cleanup and Automated Updates with Watchtower
I recently found myself in a situation many self-hosters face: my media server was a mess. I had duplicate files everywhere, old TV shows I'd never watch again, and I was manually updating Docker containers whenever I remembered (which wasn't often). I decided it was time for a complete fresh start. Here's how I cleaned everything up and automated my updates.
The Problem I Faced
My media server had grown out of control. I thought I had hardlinks set up properly between my downloads and media folders, but when I started investigating, I discovered I'd been creating copies instead. This meant every movie and TV show was taking up twice the space it should. Additionally, I had accumulated years of content I no longer wanted, and updating my containers was a manual chore I kept putting off.
Part 1: Setting Up Automated Updates with Watchtower
First, I tackled the update problem. I was tired of manually checking for updates and running docker-compose pull for each service. I needed automation.
Adding Watchtower to My Stack
I added Watchtower to my docker-compose.yml. Here's what worked for me:
watchtower:
container_name: watchtower
image: containrrr/watchtower:latest
restart: unless-stopped
environment:
- TZ=Europe/London # My timezone
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_INCLUDE_RESTARTING=true
- WATCHTOWER_SCHEDULE=0 0 3 * * 1 # Monday 3 AM - when I'm definitely asleep
- WATCHTOWER_TIMEOUT=60s
volumes:
- /var/run/docker.sock:/var/run/docker.sock
command: jellyfin radarr sonarr prowlarr # Only update my media containers
networks:
- default
The Email Notification Challenge
I wanted to know when updates happened, but I don't have an SMTP server. I use Apple's email service with my custom domain through Cloudflare. Setting this up was trickier than expected.
First, I needed an app-specific password from Apple:
- Went to appleid.apple.com
- Generated an app-specific password
- Created a secure file for it:
mkdir -p /opt/mediaserver/secrets
echo "my-apple-app-password" > /opt/mediaserver/secrets/apple_mail_password.txt
chmod 600 /opt/mediaserver/secrets/apple_mail_password.txt
Then I updated my Watchtower configuration:
environment:
# ... other variables ...
- WATCHTOWER_NOTIFICATIONS=email
- [email protected]
- [email protected]
- WATCHTOWER_NOTIFICATION_EMAIL_SERVER=smtp.mail.me.com
- WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT=587
- [email protected] # This was the tricky part!
- WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD_FILE=/run/secrets/apple_mail_password
secrets:
- apple_mail_password
The key discovery: I needed to use my actual Apple ID for authentication, not my custom domain email.
Testing Was Essential
I ran several tests before trusting it with my production containers:
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
-e WATCHTOWER_NOTIFICATIONS=email \
-e [email protected] \
-e [email protected] \
-e WATCHTOWER_NOTIFICATION_EMAIL_SERVER=smtp.mail.me.com \
-e WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT=587 \
-e [email protected] \
-e WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD=actual-app-password \
containrrr/watchtower \
--run-once \
--monitor-only
Part 2: The Great Media Cleanup
With updates automated, I turned to my storage disaster. I had only 11 movies I actually wanted to keep, but my media folders were full of content I'd never watch again.
The Hardlink Revelation
I always assumed my files were hardlinked. When I checked:
ls -la /data/media/movies/
I was shocked to see individual files with link counts of 1. My "hardlinks" were just copies! This explained why my storage was disappearing so quickly.
My Cleanup Strategy
I decided on a complete fresh start. Here's what I did:
- Created a list of movies to keep - I had 11 specific files I wanted to preserve
- Stopped all services to prevent any issues:
docker-compose stop qbittorrent radarr sonarr jellyfin
- Wrote a cleanup script because doing this manually would be error-prone
- Deleted everything except my chosen files
The scariest part was running rm -rf commands on my media. I triple-checked my keep list before executing.
Part 3: The Jellyfin Ghost Problem
After deleting all my files and restarting Jellyfin, I hit an unexpected issue: all the deleted shows still appeared in the interface! They couldn't play (obviously, the files were gone), but they cluttered my library.
Investigating the Problem
I realized Jellyfin was holding onto database entries for non-existent files. Simply rescanning the library wasn't enough.
My Solution: Direct Database Surgery
This felt risky, but I needed those entries gone:
# Accessed the Jellyfin container
docker exec -it jellyfin bash
# Installed SQLite (it wasn't included)
apt update && apt install -y sqlite3
# Found the right database
find /config -name "*.db" -type f
I discovered the database was at /config/data/data/library.db, not where I initially looked.
-- Connected to the database
sqlite3 /config/data/data/library.db
-- Found the ghost entries
SELECT Name, Path FROM TypedBaseItems WHERE Path LIKE '%/tv/%' LIMIT 10;
-- Took a deep breath and deleted them
DELETE FROM TypedBaseItems WHERE Path LIKE '%/tv/%';
After restarting Jellyfin, finally, success! My library showed only my 11 movies.
Lessons Learned
- Hardlinks aren't automatic - I should have verified this years ago
- Test everything - Especially before running deletion commands
- Databases remember everything - File deletion doesn't mean library cleanup
- Automation saves time - Watchtower now handles what I always forgot to do
- Document your setup - This blog post serves as my personal reference too
Current Status
My media server is now:
- Clean: Only the content I actually want
- Automated: Updates happen every Monday at 3 AM without my intervention
- Monitored: I get emails when containers update
- Documented: I know exactly how everything is configured
Moving Forward
I've learned to:
- Enable hardlinks properly in Radarr/Sonarr (Settings → Media Management → Use Hardlinks)
- Regularly review my content instead of hoarding
- Trust automation but verify with logs
- Keep better documentation of my setup
This cleanup was overdue, but I'm glad I finally did it. If you're facing similar issues, I hope my experience helps you avoid some of the pitfalls I encountered. Sometimes you need to burn it down and start fresh.