Introduction
Have you ever dreamt of having your own streaming server for your personal videos - family memories, home movies, or favorite content you want to preserve? In my case, I love re-living the Telugu film classic theatrical experiences that I missed during their actual releases, so I’m maintaining those vibe videos in my personal collection.
In this guide, I’ll walk you through how I set up MediaCMS - a modern, fully-featured open-source video and media CMS - on a Raspberry Pi 4B to create my own private streaming platform.
This is part of my home server series where I’m running multiple services on yashu.fun. The MediaCMS instance runs at flix.yashu.fun and handles video transcoding, streaming, and management entirely on a Raspberry Pi.
What You’ll Learn
- Setting up external storage for media files on Raspberry Pi
- Configuring persistent storage with
fstab - Deploying MediaCMS with Docker Compose on ARM64 architecture
- Brief overview of Cloudflare Tunnel for public access (optional)
- Brief overview of Gmail SMTP setup for custom domain emails (optional)
- Performance considerations for Raspberry Pi
Why MediaCMS?
MediaCMS is a Python/Django and React-based video CMS that offers:
- Open source: This is one of the main reasons why I chose MediaCMS. Fully customizable. Full control over your data.
- Modern UI: Beautiful, responsive interface
- Video transcoding: Automatic conversion to multiple formats
- RESTful API: Easy integration with other services
- User management: Roles, permissions, and access control
Alternatives to MediaCMS
While MediaCMS is great for a video-focused platform, here are two popular alternatives:
-
Plex: A polished, feature-rich media server with excellent client apps across all platforms. Best for organizing and streaming your existing media library (movies, TV shows, music). However, it’s less focused on user uploads and content management.
-
Jellyfin: The open-source alternative to Plex with no premium features for free. Great for personal media libraries with transcoding support, but lacks the video upload and CMS features that MediaCMS provides.
Why I chose MediaCMS: Unlike Plex and Jellyfin which are designed for consuming existing media libraries, lots of bloatware like live TV etc., MediaCMS is built for uploading, transcoding, and managing user-generated content - making it perfect for creating a YouTube-like platform for personal or community videos.
Prerequisites
Hardware
- Raspberry Pi 4B (4GB RAM recommended)
- 32GB SD card for OS and application
- 64GB USB drive for media storage (I’m using a pendrive, but an external SSD is better for performance)
- Stable network connection
- Active cooling (optional but recommended for transcoding workloads)
Software
- Raspberry Pi OS (official Raspberry Pi OS - I’m using the 64-bit version)
- Docker and Docker Compose
- Basic knowledge of Linux, Docker, and networking
Optional (for advanced setup)
- Domain name - If you want to host MediaCMS on your own domain (e.g.,
flix.yourdomain.com) - Cloudflare account - For setting up Cloudflare Tunnel to expose your Pi to the internet without port forwarding
Step 1: Preparing External Storage
MediaCMS stores uploaded videos, thumbnails, and transcoded files. A 32GB SD card fills up quickly, so we’ll use external storage.
1.1 Identify Your USB Device
Attach your USB drive and identify it:
lsblk
You should see output like:
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
sda 8:0 1 57.3G 0 disk
└─sda1 8:1 1 57.3G 0 part
mmcblk0 179:0 0 29.7G 0 disk
├─mmcblk0p1 179:1 0 512M 0 part /boot/firmware
└─mmcblk0p2 179:2 0 29.2G 0 part /
In my case, sda is my 64GB USB drive, and sda1 is the partition. Notice how there’s no mount point yet under MOUNTPOINTS - that’s because we just connected it and haven’t mounted it anywhere.
1.2 Check Current Filesystem
Before formatting, check what filesystem is currently on the device:
sudo blkid /dev/sda1
Output:
/dev/sda1: UUID="5304-6A0E" BLOCK_SIZE="512" TYPE="vfat"
As you can see, my USB drive currently has a vfat (FAT32) filesystem - common for USB drives since it works across different operating systems. However, for Linux servers, ext4 is better.
1.3 Format to ext4
For better performance and Linux compatibility, format it to ext4:
sudo mkfs.ext4 /dev/sda1
Warning: This will erase all data on the device!
When you run this command, you’ll see output like:
mke2fs 1.47.0 (5-Feb-2023)
/dev/sda1 contains a vfat file system
Proceed anyway? (y,N)
Type y to confirm. The formatting process will create a new ext4 filesystem on your USB drive.
1.4 Create Mount Point
Create a directory to mount the device. It’s good practice to mount external devices in /media/external_devices/:
sudo mkdir -p /media/external_devices/usb_64GB
1.5 Mount the Device
Now let’s mount the USB drive to the directory we just created:
sudo mount /dev/sda1 /media/external_devices/usb_64GB
Verify the mount worked by running lsblk again:
lsblk
Output:
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
sda 8:0 1 57.3G 0 disk
└─sda1 8:1 1 57.3G 0 part /media/external_devices/usb_64GB
mmcblk0 179:0 0 29.7G 0 disk
├─mmcblk0p1 179:1 0 512M 0 part /boot/firmware
└─mmcblk0p2 179:2 0 29.2G 0 part /
Perfect! Notice how sda1 now shows /media/external_devices/usb_64GB under MOUNTPOINTS.
Important Note: This mount is temporary and will be lost when you reboot the Raspberry Pi. That’s why we need to add it to /etc/fstab to make it permanent (covered in step 1.7).
1.6 Get the UUID
To make the mount persistent, we need the device’s UUID, whichever we want to mount:
sudo blkid /dev/sda1
Output:
/dev/sda1: UUID="e3b11f17-5401-40a5-9886-d33f155eecac" TYPE="ext4"
Copy the UUID value.
1.7 Make Mount Permanent with fstab
Edit /etc/fstab:
sudo nano /etc/fstab
Add this line at the end:
UUID=e3b11f17-5401-40a5-9886-d33f155eecac /media/external_devices/usb_64GB ext4 defaults 0 2
After adding the line, your /etc/fstab should look something like this:
proc /proc proc defaults 0 0
PARTUUID=6a3483ee-01 /boot/firmware vfat defaults 0 2
PARTUUID=6a3483ee-02 / ext4 defaults,noatime 0 1
UUID=e3b11f17-5401-40a5-9886-d33f155eecac /media/external_devices/usb_64GB ext4 defaults 0 2
# a swapfile is not a swap partition, no line here
# use dphys-swapfile swap[on|off] for that
Format explanation:
UUID=...: Device identifier (safer than/dev/sda1which can change on reboot)/media/external_devices/usb_64GB: Mount pointext4: Filesystem typedefaults: Default mount options (rw, suid, dev, exec, auto, nouser, async)0: Don’t dump (backup) this filesystem2: Check filesystem on boot (after root filesystem, which uses1)
Why use UUID instead of /dev/sda1?
Device names like /dev/sda1 can change if you connect multiple USB devices or reboot. UUIDs are unique and permanent for that specific partition.
1.8 Test the fstab Entry
Test if the entry works without rebooting:
sudo umount /media/external_devices/usb_64GB
sudo mount -a
If no errors appear, verify:
df -h | grep usb_64GB
Step 2: Installing Docker and Docker Compose
If you haven’t installed Docker yet:
# Install Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
# Add your user to docker group (to run docker without sudo)
sudo usermod -aG docker $USER
# Install Docker Compose
sudo apt update
sudo apt install docker-compose-plugin
# Verify installation
docker --version
docker compose version
Log out and back in for group changes to take effect.
Step 3: Setting Up MediaCMS
3.1 Clone the Repository
I forked the official MediaCMS repo and made few modifications:
cd /media/external_devices/usb_64GB
git clone https://github.com/yashu183/mediacms.git
cd mediacms
Why fork? I made several modifications to adapt MediaCMS for my Raspberry Pi setup:
- ARM64 FFmpeg: Changed Dockerfile to use ARM64 FFmpeg binaries instead of AMD64 (required for Pi)
- Access control: Restricted media uploads and member viewing to advanced users only
In the official repo, anyone with an account on my server can upload videos.. Free storage for everyone! 😢… Why should I give free storage for everyone 😈
- UI customization: Updated title, favicons, and defaulted to dark theme
- Configuration: Updated restart policies in docker-compose and configured frontend domain for email notifications. These containers will restart automatically if your Pi reboots. No need to start them manually.
- Removed unnecessary pages: Simplified the interface by removing unnecessary pages like
T&Cetc.
3.2 Implementation Details
For a deeper look at all the changes I made (Dockerfile modifications, docker-compose configuration, UI customizations, and access control), check out the recent commits in my fork:
https://github.com/yashu183/mediacms
P.S. I rebranded MediaCMS to “yashuFlix” for my personal deployment. If you think the name is cringy, feel free to roast me in the comments below! 😄
3.3 Building the ARM64 Image
Since the official mediacms/mediacms:latest image on Docker Hub is built for AMD64 architecture, you need to build the ARM64 version locally from my forked repo:
docker build -t mediacms/mediacms:latest .
Why build locally?
- Docker Compose references
image: mediacms/mediacms:latest - When you run
docker compose up, it looks for this image locally first - If not found locally, it pulls from Docker Hub (which won’t work on ARM)
- Building it locally with the same tag ensures Docker uses your ARM64 version instead
Note: This takes 15-30 minutes on Raspberry Pi 4. Grab a coffee ☕️!
3.4 Initial Configuration
Before starting, uncomment and set the admin password in docker-compose.yaml:
environment:
ADMIN_USER: 'admin'
ADMIN_EMAIL: '[email protected]'
ADMIN_PASSWORD: 'your-secure-password'
This creates the admin user on first run. This will be used to login to MediaCMS admin portal where u can manage everything regarding users or media or MediaCMS settings. You can visit it by naviagting to your-domain/admin.
3.5 Start MediaCMS
Make sure you’re in the MediaCMS root directory, then start all services:
cd /media/external_devices/usb_64GB/mediacms #In my case
docker compose up -d
Check logs to verify everything is running:
docker compose logs -f web
If everything looks good, you’re all set! Another way to verify is by checking running containers:
docker ps
You should see 5 containers running for MediaCMS:
CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES
2d48244d9478 mediacms/mediacms:latest "./deploy/docker/ent…" Up 2 days 0.0.0.0:80->80/tcp mediacms-web-1
bf751fa302b3 mediacms/mediacms:latest "./deploy/docker/ent…" Up 2 days 80/tcp, 9000/tcp mediacms-celery_worker-1
6aedf26fd2b8 mediacms/mediacms:latest "./deploy/docker/ent…" Up 2 days 80/tcp, 9000/tcp mediacms-celery_beat-1
27eb83f27822 postgres:17.2-alpine "docker-entrypoint.s…" Up 2 days (healthy) 5432/tcp mediacms-db-1
52c0536bfc3e redis:alpine "docker-entrypoint.s…" Up 2 days (healthy) 6379/tcp mediacms-redis-1
3.6 Access MediaCMS
Open your browser and navigate to:
http://<raspberry-pi-ip>:80
You should see the MediaCMS homepage!
Congratulations! Your MediaCMS instance is now running locally. You can access it via your Raspberry Pi’s IP address on your local network.
What’s Next?
At this point, your MediaCMS is fully functional on your local network. The following sections are optional - only proceed if you need these features:
Optional: Email Notifications
If you want MediaCMS to send you email notifications when users upload videos or users want to contact you, or if you want to email your users, continue to the Email Configuration section.
Optional: Public Access via Custom Domain
If you want to access MediaCMS from anywhere on the internet using your own domain (like flix.yourdomain.com), see the Cloudflare Tunnel section below.
Step 4: Directory Structure on External Storage
After installation, your USB drive structure looks like this:
/media/external_devices/usb_64GB/
├── mediacms/ # Main application directory
│ ├── media_files/ # Uploaded videos (original)
│ ├── docker-compose.yaml
│ └── ... # Other files & folders
└── postgres_data/ # PostgreSQL database files
All media content is stored on the USB drive, keeping your SD card free.
Step 5: Connecting to Cloudflare Tunnel (Optional)
Note: This step is only needed if you want to access MediaCMS from the internet using your own domain without port forwarding.
Since I don’t have a static IP or port forwarding capability with JioFiber, I use Cloudflare Tunnel to expose MediaCMS to the internet securely.
Requirements:
- A domain name (e.g.,
yashu.funin my case) - Cloudflare account with your domain added
- Domain nameservers pointed to Cloudflare
I’ll cover this in detail in an upcoming blog post, but here’s the quick version:
- Create a tunnel in Cloudflare Zero Trust dashboard
- Point your subdomain (e.g.,
flix.yashu.fun) tohttp://localhost:80 - Install and run the cloudflared daemon on your Raspberry Pi
This makes your local MediaCMS instance accessible at https://flix.yashu.fun with automatic HTTPS!
Benefits:
- No port forwarding needed
- Free SSL/TLS certificates
- DDoS protection from Cloudflare
- Access your Pi from anywhere
Troubleshooting
Issue: FFmpeg not found or “Exec format error”
Cause: You’re using the AMD64 image instead of ARM64.
Solution: Use my fork or rebuild with ARM64 FFmpeg:
git clone https://github.com/yashu183/mediacms.git
docker build -t mediacms/mediacms:latest .
Issue: Videos stuck in “Processing”
Cause: Transcoding worker not running or crashed.
Check worker logs:
docker compose logs celery_worker
Restart workers:
docker compose restart celery_worker
Issue: Database connection errors
Cause: PostgreSQL not ready when migrations run.
Solution: The healthcheck in docker-compose should prevent this. If it persists:
docker compose down
docker compose up -d
Issue: Out of disk space
Check disk usage:
df -h /media/external_devices/usb_64GB
Clean up old transcoded files:
cd /media/external_devices/usb_64GB/mediacms
docker compose exec web python manage.py cleanup_encoding_tasks
Issue: High CPU temperature during transcoding
Monitor temperature:
cat /sys/class/thermal/thermal_zone0/temp # This will log temperature in milli degrees.
If consistently above 70°C, consider (this was happening in my case after running MediaCMS):
- Adding a heatsink/fan (I didn’t have one until I started running MediaCMS - the transcoding workload made it necessary. I’m using this fan for Pi 4B)
- Reducing concurrent transcoding workers
Customization
Configure Email Notifications (Optional)
Note: This is optional - only configure if you want email notifications for uploads and admin alerts or when users want to contact you.
MediaCMS can send email notifications when videos are uploaded, transcoded, or when admin actions are needed. If you own a domain and manage email through Google Workspace or GCP, you can configure SMTP.
My email setup:
I maintain two separate email accounts for MediaCMS:
- Sender account:
[email protected]- Used to authenticate with SMTP and send emails to my admin account - Receiver account:
[email protected]- Where I receive all notifications from my sender account
When users receive emails from MediaCMS, they see it coming from [email protected] (even though it’s technically sent to [email protected] via [email protected]). This keeps everything professional and organized.
NOTE: Ideally we should have [email protected] but I already created [email protected], I used it. If you are creating for the first time, I would suggest you to create [email protected]
How it works:
Let’s say a user submits feedback on your MediaCMS site. Here’s the email flow:
- MediaCMS authenticates with SMTP using
[email protected](the sender account) - The user’s email is automatically added to the Reply-To header
- Notification arrives to
[email protected](your admin inbox) - When you hit “Reply” as admin, it replies directly to the user - not back to
[email protected]. It appears to come from[email protected]for the users.
This way, you maintain a professional sender address (admin@) while using a separate technical account (yashu.c@) for SMTP authentication.
Setup steps for Sender account:
- Create a Google App Password: Visit https://myaccount.google.com/apppasswords from your sender account.
- Generate an app password for “Mail” (for the sender account
[email protected]) - Use that password in
EMAIL_HOST_PASSWORD(not your regular Google password) - Update the email addresses to match your domain
Edit deploy/docker/local_settings.py (since we’re using docker compose):
Putting all these things together, my email config looks like as shown below. Add these fields to this file if not there, else update those values.
# Email settings
DEFAULT_FROM_EMAIL = "[email protected]" # Sender account (needs Google App Password)
EMAIL_HOST_USER = "[email protected]" # Authentication account
EMAIL_HOST_PASSWORD = "<your-app-password>" # Google App Password (NOT regular password). Replace with your password that you created in the above step.
EMAIL_USE_TLS = True
SERVER_EMAIL = DEFAULT_FROM_EMAIL
EMAIL_HOST = "smtp.gmail.com"
EMAIL_PORT = 587
ADMIN_EMAIL_LIST = ["[email protected]"] # Where notifications are sent
Important notes:
[email protected]is the sender account - it needs a Google App Password[email protected]is where you’ll receive notifications- MediaCMS automatically handles the Reply-To header for user communications
- This uses Google’s SMTP server (
smtp.gmail.com) - If you’re using a custom domain with Google Workspace/GCP, make sure SMTP is enabled
- Managing emails with GCP is not free - check pricing before enabling
- For personal Gmail accounts, this works too (just use your
@gmail.comaddress)
After configuration, restart the web container:
docker compose restart web
Modify Upload Limits
In cms/local_settings.py:
UPLOAD_MAX_SIZE = 2 * 1024 * 1024 * 1024 # 2GB max file size
Restart web container:
docker compose restart web
What’s Next?
In the next post, I’ll cover:
- Setting up Cloudflare Tunnel for multiple subdomains without port forwarding
- Configuring custom domains for each service that I run at yashu.fun
- Securing your home server with authentication and HTTPS
Conclusion
You now have a fully functional video streaming platform running on a Raspberry Pi 4! This setup handles:
- Video uploads and storage on external drive
- Automatic transcoding to multiple resolutions
- User management and permissions
- RESTful API for automation
- Persistent storage that survives reboots
- Production-ready with proper restart policies
Key Takeaways
- External storage is essential for media-heavy applications
- ARM64 compatibility matters when using Raspberry Pi
- Docker Compose makes complex deployments manageable
- fstab configuration ensures storage persists across reboots
- Resource constraints require thoughtful configuration (concurrent workers, resolution limits)
- Cloudflare Tunnel enables public access without port forwarding or static IP
- Email setup with Gmail SMTP allows professional communication with users using custom domain emails
Resources
- My MediaCMS Fork: github.com/yashu183/mediacms
- Official MediaCMS: mediacms.io
- MediaCMS Documentation: mediacms docs
- Docker Documentation: docs.docker.com
Feedback
Have questions or run into issues? Feel free to:
- Open an issue on my GitHub repo
- Comment below
- Reach out on my portfolio
Happy self-hosting! 🚀
This is part 1 of the “Building a Home Server on Raspberry Pi” series. Stay tuned for the next post on Cloudflare Tunnel setup!