Server Requirements & Deployment
Everything you need to run TubeVendor in production: hardware, software, Nginx, queue workers, scheduled tasks, and storage.
Table of Contents
- Server Requirements
- Software Stack
- PHP Extensions
- Installation
- Environment Configuration
- Nginx — Main Application Server
- Storage Options
- Supervisor (Queue Workers)
- Scheduled Tasks
- HLS Token Authentication
- Troubleshooting
Server Requirements
| Component | Minimum | Recommended (Production) | Purpose |
|---|---|---|---|
| CPU | 2 Cores | 4+ Cores (Dedicated) | Video transcoding (FFmpeg). High-frequency cores preferred (AMD EPYC / Intel Xeon). |
| RAM | 6 GB | 16 GB+ | Redis, FFmpeg buffers, queue workers. |
| Storage | 60 GB NVMe | 500 GB+ NVMe | Temp transcoding files, thumbnails. NVMe required for concurrent read/write. |
| Network | 1 Gbps | 10 Gbps Unmetered | 100 concurrent 1080p streams ≈ 500 Mbps. |
Software Stack
| Software | Version | Purpose |
|---|---|---|
| OS | Ubuntu 22.04 / 24.04 LTS | Linux Kernel 5.15+ |
| PHP | 8.4 | Application runtime |
| MySQL | 8.0+ (or MariaDB 10.11+) | User data, video metadata, billing |
| Redis | 7.0+ | Sessions, job queues, cache |
| Node.js | 24.x LTS | Frontend build (Vite) |
| FFmpeg | 6.x+ | Transcoding, HLS, thumbnails |
| Supervisor | Latest | Queue worker process monitor |
| Nginx | Latest | Web server, media proxy, token auth |
PHP Extensions
Install and enable:
bcmath, curl, gd, imagick, mbstring, pcntl, redis, intl, exif, xml, zip, mysql
Installation
1. System dependencies
sudo apt update && sudo apt upgrade -y
sudo add-apt-repository ppa:ondrej/php
sudo apt update
sudo apt install php8.4-fpm php8.4-cli php8.4-common php8.4-mysql php8.4-zip \
php8.4-gd php8.4-mbstring php8.4-curl php8.4-xml php8.4-bcmath php8.4-redis \
php8.4-intl php8.4-imagick nginx redis-server supervisor ffmpeg unzip
2. Get the latest TubeVendor package
Do not install from a public Git repository. Use your licensed copy only:
- Log in to the Client Area at tubevendor.com.
- Open Licenses → your license → Download latest version.
- Upload and extract the package on your server (e.g.
/var/www/tubevendor).
If you do not have access or need a fresh copy, open a support ticket and our team will provide the latest release.
3. Application setup
cd /var/www/tubevendor
composer install --optimize-autoloader --no-dev
npm install
cp .env.example .env
nano .env
php artisan key:generate
php artisan storage:link
php artisan migrate --seed --force
npm run build
4. Permissions
sudo chown -R www-data:www-data /var/www/tubevendor
sudo chmod -R 775 /var/www/tubevendor/storage /var/www/tubevendor/bootstrap/cache
Replace
/var/www/tubevendorwith your actual install path everywhere in this guide.
Environment Configuration
Essential .env keys:
APP_URL=https://your-domain.com
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_DATABASE=tubevendor
DB_USERNAME=your_user
DB_PASSWORD=your_password
QUEUE_CONNECTION=redis
CACHE_STORE=redis
SESSION_DRIVER=redis
# Cloudflare R2 (see Storage Options)
CLOUDFLARE_R2_ACCESS_KEY_ID=
CLOUDFLARE_R2_SECRET_ACCESS_KEY=
CLOUDFLARE_R2_BUCKET=
CLOUDFLARE_R2_ENDPOINT=
CLOUDFLARE_R2_URL=
CLOUDFLARE_R2_TOKEN_AUTH=true
CLOUDFLARE_R2_TOKEN_KEY=
# SFTP storage (if used — support team helps with server setup)
# SFTP_HOST=
# SFTP_PUBLIC_URL=
# SFTP_TOKEN_KEY=
Nginx — Main Application Server
TubeVendor ships a reference Nginx layout for the main app server. Deploy the main config together with the conf.d/ snippets in the same directory.
What to edit before use
root— path to Laravelpublic/(e.g./var/www/tubevendor/public)fastcgi_pass— your PHP-FPM socket or TCP port (bottom of main config)conf.d/storage_proxy_target.inc— storage CDN host from.env(SFTP_PUBLIC_URLor R2 public URL)
Laravel streams large media via X-Accel-Redirect → /_tvstorage/… (no PHP body copy).
File layout
/etc/nginx/sites-available/tubevendor ← main server block (below)
/etc/nginx/conf.d/storage_proxy_target.inc ← storage origin / CDN target
/etc/nginx/conf.d/tubevendor_media_cache.inc ← media proxy headers
/etc/nginx/conf.d/tubevendor_static_image_cache.inc
SSL: The reference below is HTTP-only. In production, add Certbot or your SSL terminator in front of this block, or extend the server block with
listen 443 ssl.
Main server config
Create /etc/nginx/sites-available/tubevendor:
# TubeVendor — reference nginx (HTTP only, no SSL). Single server: nginx + PHP-FPM.
#
# Deploy with conf.d/ snippets (same paths as includes below):
# conf.d/storage_proxy_target.inc
# conf.d/tubevendor_media_cache.inc
# conf.d/tubevendor_static_image_cache.inc
#
# Edit before use:
# 1. root → Laravel public/
# 2. fastcgi_pass → PHP-FPM socket or TCP
# 3. storage_proxy_target.inc → CDN from .env SFTP_PUBLIC_URL or R2 URL
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
root /var/www/tubevendor/public;
index index.php index.html;
charset utf-8;
access_log /var/log/nginx/tubevendor-access.log;
error_log /var/log/nginx/tubevendor-error.log;
client_max_body_size 10240M;
client_body_timeout 720s;
resolver 1.1.1.1 8.8.8.8 valid=60s ipv6=off;
resolver_timeout 10s;
include /etc/nginx/conf.d/storage_proxy_target.inc;
location ~ /.well-known {
auth_basic off;
allow all;
}
# X-Accel-Redirect internal fetch (Laravel → storage CDN)
location ~ ^/_tvstorage/(?<storagereq>(?:files|thumbnails|previews|videos)/.+)$ {
internal;
proxy_pass $storage_target/$storagereq$is_args$args;
proxy_http_version 1.1;
proxy_ssl_server_name on;
proxy_set_header Host $storage_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_hide_header x-amz-id-2;
proxy_hide_header x-amz-request-id;
proxy_buffering off;
proxy_request_buffering off;
proxy_max_temp_file_size 0;
proxy_connect_timeout 720s;
proxy_send_timeout 720s;
proxy_read_timeout 720s;
}
location ^~ /build/ {
try_files $uri =404;
access_log off;
log_not_found off;
expires max;
add_header Cache-Control "public, max-age=31536000, immutable" always;
add_header Access-Control-Allow-Origin "*" always;
}
# Laravel media routes — keep on PHP (MP4 + entry M3U8 logic)
location ^~ /backend/ {
try_files $uri $uri/ /index.php?$query_string;
}
location ^~ /thumbnails/ {
try_files $uri $uri/ /index.php?$query_string;
}
location ^~ /storage/ {
try_files $uri =404;
access_log off;
log_not_found off;
expires 7d;
add_header Cache-Control "public, max-age=604800" always;
add_header Access-Control-Allow-Origin "*";
}
location ~* ^/(videos|files)/.+/preview\.mp4$ {
proxy_pass $storage_target;
proxy_http_version 1.1;
proxy_ssl_server_name on;
proxy_set_header Host $storage_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_hide_header x-amz-id-2;
proxy_hide_header x-amz-request-id;
include /etc/nginx/conf.d/tubevendor_media_cache.inc;
add_header Cache-Control "public, max-age=3600, s-maxage=86400" always;
proxy_buffering off;
proxy_request_buffering off;
proxy_max_temp_file_size 0;
proxy_connect_timeout 720s;
proxy_send_timeout 720s;
proxy_read_timeout 720s;
}
location ~* ^/(videos|files)/.+\.mp4$ {
if ($arg_md5 = "") { return 403; }
if ($arg_expires = "") { return 403; }
proxy_pass $storage_target;
proxy_http_version 1.1;
proxy_ssl_server_name on;
proxy_set_header Host $storage_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_hide_header x-amz-id-2;
proxy_hide_header x-amz-request-id;
include /etc/nginx/conf.d/tubevendor_media_cache.inc;
add_header Cache-Control "public, max-age=3600, s-maxage=86400" always;
proxy_buffering off;
proxy_request_buffering off;
proxy_max_temp_file_size 0;
proxy_connect_timeout 720s;
proxy_send_timeout 720s;
proxy_read_timeout 720s;
}
location ~* ^/files/.+/sprite\.jpg$ {
proxy_pass $storage_target;
proxy_http_version 1.1;
proxy_ssl_server_name on;
proxy_set_header Host $storage_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_hide_header x-amz-id-2;
proxy_hide_header x-amz-request-id;
include /etc/nginx/conf.d/tubevendor_media_cache.inc;
include /etc/nginx/conf.d/tubevendor_static_image_cache.inc;
proxy_buffering off;
proxy_request_buffering off;
proxy_max_temp_file_size 0;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
location ~* ^/files/.+\.(jpg|jpeg|png|gif|webp)$ {
proxy_pass $storage_target;
proxy_http_version 1.1;
proxy_ssl_server_name on;
proxy_set_header Host $storage_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_hide_header x-amz-id-2;
proxy_hide_header x-amz-request-id;
include /etc/nginx/conf.d/tubevendor_media_cache.inc;
include /etc/nginx/conf.d/tubevendor_static_image_cache.inc;
proxy_buffering off;
proxy_request_buffering off;
proxy_max_temp_file_size 0;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
location ~* ^/(previews|videos)/ {
proxy_pass $storage_target;
proxy_http_version 1.1;
proxy_ssl_server_name on;
proxy_set_header Host $storage_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_hide_header x-amz-id-2;
proxy_hide_header x-amz-request-id;
include /etc/nginx/conf.d/tubevendor_media_cache.inc;
add_header Cache-Control "public, max-age=3600, s-maxage=86400" always;
proxy_buffering on;
proxy_request_buffering on;
proxy_connect_timeout 720s;
proxy_send_timeout 720s;
proxy_read_timeout 720s;
}
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt {
access_log off;
log_not_found off;
try_files $uri /index.php?$query_string;
}
location = /sitemap.xml {
access_log off;
log_not_found off;
try_files $uri /index.php?$query_string;
}
location ~* ^/sitemap(?:-[a-z0-9_-]+)?\.xml$ {
access_log off;
log_not_found off;
try_files $uri /index.php?$query_string;
}
error_page 404 /index.php;
location ~ \.php$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_index index.php;
fastcgi_read_timeout 3600;
fastcgi_send_timeout 3600;
fastcgi_request_buffering off;
# Pick ONE (comment out the others):
fastcgi_pass unix:/var/run/php/php8.4-fpm.sock;
# fastcgi_pass 127.0.0.1:9000;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
conf.d/storage_proxy_target.inc
# Storage CDN — edit to match your storage HTTP origin.
#
# $storage_host = Host header the storage nginx expects (usually SFTP_PUBLIC_URL hostname).
# $storage_target = direct storage server IP or internal URL — avoid Cloudflare-proxied
# hostnames here or origin nginx may get intermittent 502 on /_tvstorage/ fetches.
set $storage_target "http://STORAGE_SERVER_IP";
set $storage_host "cdn.yourdomain.com";
For Cloudflare R2, point $storage_target at your R2 public URL (e.g. https://pub-xxx.r2.dev) and $storage_host at the same hostname.
The Laravel Admin Panel can update the proxy target and reload Nginx when you change storage settings.
conf.d/tubevendor_media_cache.inc
# Pass-through to storage/CDN (browser/CDN cache via add_header in vhost).
proxy_ignore_headers Set-Cookie;
proxy_hide_header Set-Cookie;
conf.d/tubevendor_static_image_cache.inc
# Long-lived browser + CDN cache for posters / sprites.
proxy_hide_header Cache-Control;
proxy_hide_header Expires;
proxy_hide_header CDN-Cache-Control;
add_header Cache-Control "public, max-age=2592000, immutable" always;
add_header CDN-Cache-Control "max-age=2592000" always;
expires 30d;
Enable the site
sudo ln -s /etc/nginx/sites-available/tubevendor /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Important rules
- Do not proxy
/backend/files/directly to storage — that bypasses Laravel media logic and breaks MP4/M3U8 behavior. - Keep
/backend/and/thumbnails/on Laravel (try_files→index.php). - Token-signed MP4 URLs require
?md5=and?expires=query params (Nginx returns 403 without them).
Storage Options
Option A — Cloudflare R2, Bunny, or other CDN (Recommended)
No separate storage server or storage Nginx config is required on your side.
- Create a bucket and public URL (R2.dev subdomain or custom domain).
- Set CORS on the bucket (GET from your site domain).
- Fill R2 (or Bunny) keys in
.env. - Point
storage_proxy_target.incat your public CDN URL.
R2 CORS example:
[
{
"AllowedOrigins": ["https://your-domain.com"],
"AllowedMethods": ["GET"]
}
]
Option B — Custom SFTP storage server
If you choose SFTP storage, you need a dedicated storage host with Nginx, token auth, and an SFTP user. That setup is not self-service.
Open a support ticket and our team will install and configure the storage server for you (SFTP user, Nginx, HLS token sync, watcher).
You still use the main server Nginx config above on your app server — it proxies media to your storage CDN hostname.
Supervisor (Queue Workers)
Queue workers handle video encoding, thumbnails, and uploads in the background.
Create /etc/supervisor/conf.d/tubevendor-worker.conf:
[program:tubevendor-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/tubevendor/artisan queue:work redis --sleep=3 --tries=3 --max-time=7200
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/tubevendor/storage/logs/worker.log
stopwaitsecs=7200
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start tubevendor-worker:*
After deploying changes to VideoService.php or job classes:
sudo supervisorctl restart tubevendor-worker:*
Verify workers are running:
supervisorctl status
ps aux | grep "queue:work" | grep -v grep
Scheduled Tasks
The Laravel scheduler is inactive until you add one crontab entry:
* * * * * cd /var/www/tubevendor && php artisan schedule:run >> /dev/null 2>&1
Use your real project path. Verify scheduled commands:
cd /var/www/tubevendor && php artisan schedule:list
| Schedule | Command | Purpose |
|---|---|---|
| Every 5 minutes | videos:retry-stuck |
Retries videos stuck in pending / processing |
| Twice daily (00:00 & 12:00) | updates:check |
Checks for TubeVendor updates |
Without the crontab, stuck-video retries and update checks never run.
HLS Token Authentication
TubeVendor uses signed URLs to protect playlists and progressive MP4:
User clicks Play
→ Laravel generates: video.m3u8?md5=…&expires=…
→ Nginx / CDN validates token
→ Valid: serve media | Invalid / expired: 403
The same path shapes apply for all backends (videos/…, files/YYYY/MM/DD/{id}/…, thumbnails/…, sprite.jpg). Only the storage origin changes (R2 URL vs SFTP host).
The HLS secret key must match between .env (CLOUDFLARE_R2_TOKEN_KEY or SFTP_TOKEN_KEY) and storage Nginx when using SFTP.
Troubleshooting
Video not playing (CORS)
- R2 / Bunny: Add CORS policy on the bucket for your domain.
- SFTP: Contact support — CORS and token rules are configured on the storage server.
Video stuck in processing
supervisorctl status
tail -f /var/www/tubevendor/storage/logs/laravel.log
php artisan videos:retry-stuck
Nginx 502 on media / _tvstorage
- Use a direct storage IP in
$storage_target, not a Cloudflare-proxied hostname. - Confirm
$storage_hostmatches what the storage server expects.
Permission errors
sudo chown -R www-data:www-data /var/www/tubevendor/storage
sudo chmod -R 775 /var/www/tubevendor/storage
License guard binary
chmod +x /var/www/tubevendor/app/Guard/license-guard
/var/www/tubevendor/app/Guard/license-guard 2>&1; echo "Exit code: $?"