Quick Setup (TL;DR):
# Install FFmpeg on Ubuntu VPS
sudo apt update && sudo apt install ffmpeg -y
# Create stream script
nano stream.sh
# (Paste script from "The Resilient Loop" section below)
# Make executable and run
chmod +x stream.sh
./stream.sh
This guide replaces: Upstream.so ($50/mo), Gyre ($75/mo), Live247 ($30/mo) with a $10/month VPS that runs 24/7 forever.
Why Self-Host Your 24/7 Stream?
The SaaS Cost Trap
Cloud Streaming Services (Annual Cost):
┌────────────────────────────────────┐
│ Upstream.so: $600/year │
│ Gyre.pro: $900/year │
│ Live247: $360/year │
└────────────────────────────────────┘
DIY Linux VPS (Annual Cost):
┌────────────────────────────────────┐
│ VPS (2 vCPU, 4GB): $120/year │
│ Domain (optional): $12/year │
│ Total: $132/year │
│ │
│ Savings: $228-768/year │
└────────────────────────────────────┘
More importantly: You own the infrastructure. No vendor lock-in, no feature restrictions, no "upgrade to pro" walls.
What You Can Stream 24/7
| Content Type | Use Case | Bandwidth | |--------------|----------|-----------| | LoFi Radio | Music playlist + static background | 2-4 Mbps | | Podcast Archive | Audio + album art | 1-3 Mbps | | Gaming Montage Loop | Highlight reel on repeat | 4-6 Mbps | | Security Camera | Wildlife cam, city view | 2-5 Mbps | | Server Status | Minecraft server stats (see our Minecraft guide) | 1-3 Mbps | | Educational Content | Tutorials on loop | 4-6 Mbps |
Prerequisites: VPS Selection
Minimum Requirements
| Spec | Requirement | Why | |------|-------------|-----| | CPU | 2 vCPUs (2.5+ GHz) | FFmpeg encoding uses ~60% of one core | | RAM | 2GB minimum, 4GB recommended | FFmpeg buffer + OS overhead | | Storage | 20GB (50GB+ for large libraries) | OS + video files | | Bandwidth | Unmetered or 2TB/month | 1080p @ 4Mbps = ~1.3TB/month | | Network | 100 Mbps upload minimum | Smooth 1080p delivery |
Recommended VPS Providers
| Provider | Plan | CPU | RAM | Price | Best For | |----------|------|-----|-----|-------|----------| | Space-Node | VPS-2 | 2 vCPU Ryzen | 4GB | $10/mo | Best price/performance | | Hetzner Cloud | CX21 | 2 vCPU AMD | 4GB | €4.51/mo | Europe, cheap bandwidth | | DigitalOcean | Basic Droplet | 2 vCPU | 2GB | $12/mo | Beginner-friendly UI | | Vultr | High Frequency | 2 vCPU | 4GB | $12/mo | Good uptime SLA |
Warning: Avoid shared hosting (cPanel/Plesk). You need root SSH access to install FFmpeg.
Initial VPS Setup
Step 1: OS Installation
Recommended OS: Ubuntu 24.04 LTS (Long Term Support until 2029)
Why Ubuntu?
- ✅ FFmpeg in default repos (no manual compilation)
- ✅ 5 years of security updates
- ✅ Largest community support
Alternative: Debian 12 (more stable, slightly older packages)
Step 2: SSH Connection
# From your local terminal (Mac/Linux)
ssh root@<your-vps-ip>
# Windows: Use PuTTY or Windows Terminal
First-time setup:
# Update package lists
sudo apt update
# Upgrade existing packages
sudo apt upgrade -y
# Install essential tools
sudo apt install ffmpeg nano htop curl wget -y
Verify FFmpeg installation:
ffmpeg -version
# Should show:
# ffmpeg version 6.1.x-static
The Problem with Basic FFmpeg Commands
Why Most Tutorials Fail
Standard FFmpeg command you'll find on Reddit:
ffmpeg -re -stream_loop -1 -i video.mp4 \
-c copy -f flv rtmp://a.rtmp.youtube.com/live2/YOUR_KEY
Problems:
- ❌ No auto-reconnect: If your network hiccups for 1 second, stream dies forever
- ❌ Wrong codec settings:
-c copyfails if your video isn't already YouTube-compatible - ❌ No error handling: Crashes on corrupt frames in source video
- ❌ No logging: When it breaks at 3 AM, you have no idea why
What happens in production:
Hour 1: Stream starts fine ✅
Hour 3: Network blip, FFmpeg exits ❌
Hour 4-24: Stream offline, you're asleep 😴
Morning: Viewers left angry comments 😡
The Resilient Loop: Production-Grade Script
Core Concept: The While True Loop
Bash while loop = automatic restart if FFmpeg crashes:
#!/bin/bash
while true; do
ffmpeg [your command here]
sleep 2 # Wait 2 seconds before retry
done
How it works:
- FFmpeg runs
- If FFmpeg exits (crash/disconnect), the loop continues
sleep 2prevents rapid-fire retries (which can get you IP-banned)- Loop restarts FFmpeg
- Repeat forever
Complete Production Script
Create the script:
nano ~/stream.sh
Paste this (customize the variables):
#!/bin/bash
#############################################
# 24/7 YouTube Stream - Production Script
# Created: February 2026
# License: MIT
#############################################
# CONFIGURATION
VIDEO_SOURCE="/root/content/stream.mp4"
YOUTUBE_RTMP="rtmp://a.rtmp.youtube.com/live2"
STREAM_KEY="YOUR_STREAM_KEY_HERE"
BITRATE="4500k"
LOG_FILE="/var/log/stream.log"
# ENCODING SETTINGS
PRESET="veryfast" # veryfast, fast, medium, slow (slower = better quality, more CPU)
GOP_SIZE="60" # Keyframe every 60 frames (2 seconds at 30fps)
AUDIO_BITRATE="128k"
AUDIO_RATE="44100"
# RECONNECTION SETTINGS
RETRY_DELAY="2" # Seconds to wait before retry
MAX_RETRIES="0" # 0 = infinite retries
#############################################
# DO NOT EDIT BELOW THIS LINE
#############################################
echo "Starting 24/7 stream at $(date)" | tee -a "$LOG_FILE"
retry_count=0
while [ "$MAX_RETRIES" -eq 0 ] || [ "$retry_count" -lt "$MAX_RETRIES" ]; do
echo "[$(date)] Stream attempt #$((retry_count + 1))" | tee -a "$LOG_FILE"
ffmpeg -re -stream_loop -1 -i "$VIDEO_SOURCE" \
-c:v libx264 \
-preset "$PRESET" \
-b:v "$BITRATE" \
-maxrate "$BITRATE" \
-bufsize $((${BITRATE%k} * 2))k \
-pix_fmt yuv420p \
-g "$GOP_SIZE" \
-c:a aac \
-b:a "$AUDIO_BITRATE" \
-ar "$AUDIO_RATE" \
-f flv \
"$YOUTUBE_RTMP/$STREAM_KEY" 2>&1 | tee -a "$LOG_FILE"
EXIT_CODE=$?
echo "[$(date)] FFmpeg exited with code $EXIT_CODE" | tee -a "$LOG_FILE"
retry_count=$((retry_count + 1))
if [ "$EXIT_CODE" -eq 0 ]; then
echo "[$(date)] Clean exit detected. Stopping." | tee -a "$LOG_FILE"
break
fi
echo "[$(date)] Retrying in $RETRY_DELAY seconds..." | tee -a "$LOG_FILE"
sleep "$RETRY_DELAY"
done
echo "[$(date)] Stream script terminated" | tee -a "$LOG_FILE"
Make it executable:
chmod +x ~/stream.sh
Script Breakdown: Critical Flags Explained
1. -re (Read at Native Frame Rate)
-re
What it does: Forces FFmpeg to send data at the video's actual playback speed (30fps).
Without -re:
FFmpeg reads the file as fast as possible:
500 fps → Floods YouTube's buffer → Stream crashes
With -re:
FFmpeg reads at 30 fps → Smooth delivery → Stable stream
Critical for: All 24/7 streams. Never omit this.
2. -bufsize (The Stability Secret)
-bufsize $((${BITRATE%k} * 2))k
What it does: Sets the encoder's buffer to 2x the target bitrate.
Why 2x?
- Video bitrate fluctuates based on scene complexity
- Fast motion (action scenes) need temporary bitrate spikes
- Buffer absorbs spikes without violating average bitrate limit
Example:
Bitrate: 4500k
Buffer: 9000k (2x)
Scene complexity:
- Calm scene: 2,000 kbps (under budget)
- Action scene: 6,500 kbps (spike, buffer absorbs)
- Average: 4,500 kbps (YouTube happy)
Without proper buffer: Blocky video during fast motion.
3. -ar 44100 (Prevent Audio Desync)
-ar 44100
What it does: Explicitly sets audio sample rate to 44.1 kHz.
Why needed?
- Some source videos have weird sample rates (48 kHz, 32 kHz)
- YouTube expects 44.1 kHz or 48 kHz
- Mismatched rates cause audio drift after 12+ hours
Symptom without -ar:
Hour 1: Audio synced ✅
Hour 12: Audio 3 seconds behind video ❌
Hour 24: Audio 10 seconds behind ❌❌❌
4. -g 60 (GOP Size = Keyframe Interval)
-g 60
What it does: Inserts a keyframe every 60 frames.
Math:
- 60 frames ÷ 30 fps = Keyframe every 2 seconds
Why 2 seconds?
- YouTube requires keyframes ≤ 4 seconds
- More keyframes = easier for viewers to join mid-stream
- Fewer keyframes = slightly better compression
Optimal: 2 seconds (60 frames at 30fps, 120 at 60fps)
Running the Stream
Test Run (Foreground)
Start the stream:
./stream.sh
You'll see:
Starting 24/7 stream at Thu Feb 6 14:32:01 UTC 2026
[Thu Feb 6 14:32:01 UTC 2026] Stream attempt #1
ffmpeg version 6.1 Copyright...
Input #0, mov,mp4...
Duration: 00:15:32.48, start: 0.000000, bitrate: 5432 kb/s
Stream #0:0: Video: h264, yuv420p, 1920x1080, 30 fps
Stream #0:1: Audio: aac, 44100 Hz, stereo
Output #0, flv, to 'rtmp://a.rtmp.youtube.com/live2/xxxx':
Stream #0:0: Video: h264, 1920x1080, 30 fps, 4500 kb/s
Stream #0:1: Audio: aac, 44100 Hz, 128 kb/s
frame= 456 fps= 30 q=28.0 size= 2048kB time=00:00:15.20 bitrate=1103.2kbits/s speed=1.00x
Key indicators:
- ✅
fps= 30(matches source) - ✅
speed=1.00x(real-time encoding, not faster/slower) - ✅
bitrate=fluctuates around 4500k
Stop test: Press Ctrl+C
Production Run (Background with Screen)
Problem: If you close your SSH session, the stream stops.
Solution: Use screen (terminal multiplexer).
Install screen:
sudo apt install screen -y
Start a detached screen session:
screen -S stream -dm bash -c './stream.sh'
Explanation:
-S stream: Names the session "stream"-dm: Detaches immediatelybash -c './stream.sh': Runs your script
Check if it's running:
screen -ls
# Output:
# There is a screen on:
# 12345.stream (Detached)
Attach to see live output:
screen -r stream
Detach again (leave it running): Press Ctrl+A, then D
Kill the stream:
screen -X -S stream quit
Alternative: Systemd Service (Auto-Start on Boot)
For production servers that need to survive reboots:
Create service file:
sudo nano /etc/systemd/system/youtube-stream.service
Paste:
[Unit]
Description=24/7 YouTube Live Stream
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/root
ExecStart=/root/stream.sh
Restart=always
RestartSec=10
StandardOutput=append:/var/log/stream.log
StandardError=append:/var/log/stream.log
[Install]
WantedBy=multi-user.target
Enable and start:
sudo systemctl daemon-reload
sudo systemctl enable youtube-stream.service
sudo systemctl start youtube-stream.service
Check status:
sudo systemctl status youtube-stream.service
# Should show: "active (running)"
View logs:
sudo journalctl -u youtube-stream.service -f
Now your stream auto-starts on server reboot!
Bitrate Optimization for Different Content
The Bitrate-Quality Tradeoff
| Resolution | Slow Motion (Sports) | Medium Motion (Talking) | High Motion (Gaming) | |------------|----------------------|-------------------------|----------------------| | 720p30 | 2,500 kbps | 1,500 kbps | 3,000 kbps | | 1080p30 | 4,500 kbps | 3,000 kbps | 6,000 kbps | | 1080p60 | 6,000 kbps | 4,500 kbps | 9,000 kbps |
VPS bandwidth calculator:
Bitrate × Hours × Days = Monthly bandwidth
Example (1080p30 @ 4.5 Mbps):
4.5 Mbps × 24 hours × 30 days = 3,240,000 Mb
= 3,240 Gb = 405 GB/month
Most VPS providers: Offer 1-5 TB/month (plenty for one stream).
Monitoring and Troubleshooting
Real-Time Monitoring
Install htop:
sudo apt install htop -y
htop
What to look for:
CPU: [|||||||| ] 40% ← FFmpeg using ~40% (normal)
MEM: [|||||| ] 1.2GB/4GB ← Healthy
If CPU hits 100%: Your preset is too slow. Change to ultrafast in script.
If RAM fills up: You have a memory leak. Restart the stream.
Common Errors and Fixes
Error 1: "Connection refused"
[tcp @ 0x...] Connection to tcp://a.rtmp.youtube.com:1935 failed
Cause: YouTube stream key is wrong or stream isn't enabled in YouTube Studio.
Fix:
- Go to YouTube Studio → Go Live → Stream Key
- Copy the key exactly
- Update
STREAM_KEYin script
Error 2: "Frame rate too high"
[flv @ 0x...] Frame rate 120 too high, clamping to 60
Cause: Your source video is 120fps, but YouTube only accepts up to 60fps.
Fix: Add -r 30 to force 30fps output:
ffmpeg -re -stream_loop -1 -i "$VIDEO_SOURCE" \
-r 30 \ # Add this line
-c:v libx264 ...
Error 3: "Codec not supported"
Codec for stream 0 does not use global headers but container format requires global headers
Cause: Trying to use -c copy (copy codec) with incompatible source.
Fix: Always re-encode. Never use -c copy for 24/7 streams (it's unreliable).
Error 4: "Past duration too large"
[flv @ 0x...] Past duration 0.999944 too large
Cause: Audio/video timestamps drifting (common with -stream_loop).
Fix: Add -fflags +genpts to regenerate timestamps:
ffmpeg -re -fflags +genpts -stream_loop -1 -i "$VIDEO_SOURCE" ...
Advanced: Multiple Video Playlist
Problem: One Video Gets Boring
Solution: Rotate through a folder of videos.
Create playlist file:
nano ~/playlist.txt
Add videos (absolute paths):
file '/root/content/video1.mp4'
file '/root/content/video2.mp4'
file '/root/content/video3.mp4'
Update script to use playlist:
VIDEO_SOURCE="/root/playlist.txt"
# Change FFmpeg input:
ffmpeg -re -f concat -safe 0 -stream_loop -1 -i "$VIDEO_SOURCE" \
...rest of command...
Now FFmpeg loops through all 3 videos infinitely!
Bandwidth & Cost Optimization
Reducing Data Usage
1. Lower bitrate (quality tradeoff):
BITRATE="3000k" # Instead of 4500k
# Saves: ~30% bandwidth, slight quality loss
2. Drop to 720p:
# Add scaling filter
-vf scale=1280:720 \
3. Use VP9 codec (better compression):
-c:v libvpx-vp9 \
-b:v 2500k # VP9 achieves 1080p quality at lower bitrate
Warning: VP9 encoding uses 3x more CPU than H.264. Only use on powerful VPS (4+ vCPUs).
Monthly Cost Breakdown
VPS Costs ($10/month example):
Server rental: $10.00
Domain (optional): $1.00
Bandwidth (included): $0.00
────────────────────────────
Total: $11.00/month
vs Cloud Streaming SaaS:
Upstream.so: $50.00
Gyre.pro: $75.00
Live247: $30.00
Annual savings: $228-768
Security Best Practices
1. Never Hardcode Stream Keys in Scripts
Bad:
STREAM_KEY="abc-1234-defg-5678" # Visible in script
Good (use environment variables):
# Store key in separate file
echo "export STREAM_KEY='abc-1234-defg-5678'" > ~/.stream_env
chmod 600 ~/.stream_env # Only root can read
# Load in script
source ~/.stream_env
2. Enable UFW Firewall
# Allow SSH only
sudo ufw allow 22/tcp
sudo ufw enable
# Stream doesn't need inbound ports (only outbound to YouTube)
3. Automatic Security Updates
sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure -plow unattended-upgrades
# Select "Yes"
Performance Tuning
CPU Preset Impact
| Preset | CPU Usage | Quality | Encoding Speed | |--------|-----------|---------|----------------| | ultrafast | 20% | Low | 5x real-time | | veryfast | 40% | Good | 2x real-time | | fast | 60% | Better | 1.5x real-time | | medium | 80% | High | 1x real-time | | slow | 100% | Very High | 0.5x real-time |
For 24/7 streams: Use veryfast or fast (balance of quality + CPU headroom).
For low-end VPS: Use ultrafast (prevent CPU overload).
Integrating with Minecraft Server Hosting
Use case: Stream your Minecraft server status 24/7 to attract players.
Setup:
- Create a video showing your server IP, rules, and live player count
- Use FFmpeg to overlay real-time stats (via ImageMagick + cron script)
- Stream to YouTube 24/7
Cross-link: See our Folia vs Paper guide for server optimization, then stream your optimized server!
Benefits:
- Players can find your server by searching YouTube
- 24/7 presence = appears more "active"
- Costs same $10 VPS (run Minecraft + stream on one server if under 20 players)
Troubleshooting: The 30-Second Checklist
Stream won't start?
# 1. Check FFmpeg installed
ffmpeg -version
# 2. Check video file exists
ls -lh /root/content/stream.mp4
# 3. Test YouTube stream key
curl -I rtmp://a.rtmp.youtube.com/live2
# Should return connection attempt (even if it fails, proves network works)
# 4. Check logs
tail -f /var/log/stream.log
# 5. Test with simple command first
ffmpeg -re -i /root/content/stream.mp4 -c copy -f flv rtmp://a.rtmp.youtube.com/live2/YOUR_KEY
Migration from Cloud Services
Leaving Upstream/Gyre?
Export your content:
- Download all videos from cloud service
- Upload to VPS using
scporrsync:
# From local machine
scp -r ./videos/ root@your-vps:/root/content/
Recreate playlists:
- Upstream's playlists → Create
playlist.txt(see Multiple Video section) - Scheduled streams → Use
cronto start/stop script at specific times
Example cron (stream only 9 AM - 11 PM daily):
crontab -e
# Start at 9 AM
0 9 * * * screen -S stream -dm bash -c '/root/stream.sh'
# Stop at 11 PM
0 23 * * * screen -X -S stream quit
Scaling to Multiple Streams
Can one VPS run 5 streams?
Math:
- 1 stream: ~40% CPU, 1.5GB RAM
- 5 streams: ~200% CPU (need 4 vCPUs), 7.5GB RAM
Recommended VPS for 5 streams:
- CPU: 6 vCPUs
- RAM: 12GB
- Cost: ~$40/month
- Still cheaper than 5× cloud services ($150-250/month)
Setup:
# Create separate script for each stream
cp stream.sh stream1.sh
cp stream.sh stream2.sh
...
# Edit each with different VIDEO_SOURCE and STREAM_KEY
# Run in separate screens
screen -S stream1 -dm bash -c './stream1.sh'
screen -S stream2 -dm bash -c './stream2.sh'
...
Conclusion: Freedom from SaaS
What you've built:
✅ A production-grade 24/7 streaming server
✅ Auto-reconnection on network failures
✅ Systemd service for auto-start on boot
✅ Full control over quality and bitrate
✅ $40-60/month savings vs cloud services
Next steps:
- Experiment with different bitrates for your content type
- Add multiple videos to playlist for variety
- Integrate with your Minecraft server (overlay server IP)
- Monitor for 72 hours to ensure stability
Advanced topics (future guides):
- Liquidsoap for dynamic radio stations
- SRT protocol for IRL streaming
- Multi-platform simultaneous streaming (YouTube + Twitch)
Need a powerful VPS for streaming? Space-Node offers Ryzen-based VPS with unmetered bandwidth starting at $10/month. Perfect for 24/7 streams.
Related Guides: