Limited Offer Apply V8WPL9Y9 to get 30% discount!

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

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:

  1. Log in to the Client Area at tubevendor.com.
  2. Open Licenses → your license → Download latest version.
  3. 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/tubevendor with 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

  1. root — path to Laravel public/ (e.g. /var/www/tubevendor/public)
  2. fastcgi_pass — your PHP-FPM socket or TCP port (bottom of main config)
  3. conf.d/storage_proxy_target.inc — storage CDN host from .env (SFTP_PUBLIC_URL or 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_filesindex.php).
  • Token-signed MP4 URLs require ?md5= and ?expires= query params (Nginx returns 403 without them).

Storage Options

No separate storage server or storage Nginx config is required on your side.

  1. Create a bucket and public URL (R2.dev subdomain or custom domain).
  2. Set CORS on the bucket (GET from your site domain).
  3. Fill R2 (or Bunny) keys in .env.
  4. Point storage_proxy_target.inc at 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_host matches 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: $?"
Last updated: 5 days ago