Why VPS vs Shared Hosting
MelodAI video features (music videos, video clips, lyrics burn-in) need FFmpeg, long-running queue workers, and shell access. A VPS removes the workarounds required on shared hosting.
| Feature | Shared Hosting | VPS |
|---|---|---|
| exec() / shell_exec() | Blocked | Full access |
| FFmpeg system install | No root | apt install ffmpeg |
| Laravel Horizon (persistent) | No systemd | Supervisor |
| Custom php.ini | Locked | Full control |
| Redis (queues) | Usually unavailable | Install freely |
| Cron / background workers | Limited | Full cron + systemd |
Recommended VPS Providers
| Provider | Plan | Price | Notes |
|---|---|---|---|
| Hetzner | CX22 (2 vCPU, 4 GB) | ~$4/mo | Best value, EU/US regions |
| Contabo | VPS S (4 vCPU, 8 GB) | ~$5/mo | Good specs; common for MelodAI deploys |
| Hostinger VPS | KVM 1 (1 vCPU, 4 GB) | ~$5/mo | Smooth if you already use Hostinger |
| DigitalOcean | Basic Droplet (1 vCPU, 1 GB) | $6/mo | Beginner-friendly docs and UI |
Use Ubuntu 22.04 or 24.04 LTS. Minimum 2 GB RAM recommended; 4 GB+ is better for video jobs.
Step 1: Provision and Secure the VPS
- Connect via SSH:
ssh root@YOUR_VPS_IP - Create a deploy user with sudo for daily tasks (optional but recommended)
- Open firewall ports 22, 80, and 443
sudo ufw allow OpenSSH && sudo ufw allow 80 && sudo ufw allow 443 && sudo ufw enablesudo ufw statusStep 2: Install FFmpeg (No Workarounds)
On a VPS you install FFmpeg with apt — no static builds, Python extraction, or custom paths in your home directory.
sudo apt update && sudo apt install -y ffmpegffmpeg -versionSet in .env after install:
FFMPEG_BINARIES=/usr/bin/ffmpegFFPROBE_BINARIES=/usr/bin/ffprobeStep 3: PHP 8.3 and Extensions
Do not install php8.3-pcntl — it is not a separate package.
pcntl is compiled into PHP core on Linux and required for Laravel Horizon. Install the packages below,
then verify with php -m | grep pcntl.
sudo apt install -y php8.3-fpm php8.3-cli php8.3-mysql php8.3-curl php8.3-xml php8.3-mbstring php8.3-zip php8.3-gd php8.3-bcmath php8.3-intl php8.3-fileinfo php8.3-redisphp -m | grep pcntlTune /etc/php/8.3/fpm/php.ini and /etc/php/8.3/cli/php.ini:
memory_limit = 512Mupload_max_filesize = 64Mpost_max_size = 64Mmax_execution_time = 300- Enable
opcachefor production
sudo systemctl restart php8.3-fpmStep 4: Install Nginx, MySQL, Redis, Supervisor, Composer, Node
sudo apt install -y nginx mysql-server redis-server supervisor git unzip curlsudo systemctl enable redis-server && sudo systemctl start redis-server && redis-cli pingcurl -sS https://getcomposer.org/installer | php && sudo mv composer.phar /usr/local/bin/composercurl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - && sudo apt install -y nodejsredis-cli ping should return PONG.
Step 5: Create MySQL Database
Run SQL inside MySQL — not in the bash shell.
If you type CREATE DATABASE ... in bash you will get CREATE: command not found.
sudo mysql -u rootAt the mysql> prompt:
CREATE DATABASE melodai_live CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'melodai_user'@'localhost' IDENTIFIED BY 'your_strong_password_here';
GRANT ALL PRIVILEGES ON melodai_live.* TO 'melodai_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;sudo mysql -u melodai_user -p -e 'USE melodai_live; SHOW TABLES;'Step 6: Deploy MelodAI (Git or ZIP)
Option A — Git clone
cd /var/www && sudo git clone <repo-url> melodaiOption B — Upload ZIP from Windows (see next section)
After files are in /var/www/melodai:
cd /var/www/melodai && cp .env.example .envcomposer install --no-dev --optimize-autoloadernpm ci && npm run buildphp artisan key:generate --forcephp artisan migrate --forcephp artisan storage:linkphp artisan config:cache && php artisan route:cache && php artisan view:cacheOr import a SQL dump instead of migrate — see SQL import section.
sudo chown -R www-data:www-data /var/www/melodai && sudo chmod -R 755 /var/www/melodai && sudo chmod -R 775 /var/www/melodai/storage /var/www/melodai/bootstrap/cacheStep 7: Upload ZIP from Windows 10
Use SCP from PowerShell or Command Prompt. Wrap the Windows path in quotes — the colon in C: breaks unquoted paths.
scp "C:\Users\YOUR_USER\Downloads\melodai.zip" root@YOUR_VPS_IP:/var/www/Or change directory first:
cd "C:\Users\YOUR_USER\Downloads"
scp melodai.zip root@YOUR_VPS_IP:/var/www/GUI alternatives: WinSCP (winscp.net) or FileZilla (SFTP, port 22). Drag the ZIP to /var/www/.
On the VPS, extract:
sudo apt install -y unzip && cd /var/www && sudo unzip melodai.zip -d melodai && ls melodai/You should see app, bootstrap, config, public, routes, etc.
Step 8: Import SQL Dump (Instead of Migrate)
scp "C:\Users\YOUR_USER\Downloads\melodai.sql" root@YOUR_VPS_IP:/var/www/melodai.sqlsudo mysql -u melodai_user -p melodai_live < /var/www/melodai.sqlFor large dumps, use screen so SSH disconnect does not kill the import:
sudo apt install -y screen && screen -S import && sudo mysql -u melodai_user -p melodai_live < /var/www/melodai.sqlDetach with Ctrl+A then D. Reattach with screen -r import.
Step 9: Configure .env on the VPS
List hidden files (including .env):
ls -la /var/www/melodai/cp /var/www/melodai/.env.example /var/www/melodai/.env && nano /var/www/melodai/.envProduction highlights:
APP_ENV=production
APP_DEBUG=false
APP_URL=https://yourdomain.com
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=melodai_live
DB_USERNAME=melodai_user
DB_PASSWORD=your_strong_password_here
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_CLIENT=phpredis
QUEUE_CONNECTION=redis
HORIZON_PREFIX=melodai_
FFMPEG_BINARIES=/usr/bin/ffmpeg
FFPROBE_BINARIES=/usr/bin/ffprobeSave in nano: Ctrl+O, Enter, Ctrl+X.
Step 10: Nginx Virtual Host and SSL
Point your domain A record to the VPS IP. Nginx document root must be /var/www/melodai/public.
sudo nano /etc/nginx/sites-available/melodaiserver {
listen 80;
listen [::]:80;
server_name yourdomain.com www.yourdomain.com;
root /var/www/melodai/public;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
index index.php;
charset utf-8;
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; }
error_page 404 /index.php;
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.(?!well-known).* {
deny all;
}
}sudo ln -s /etc/nginx/sites-available/melodai /etc/nginx/sites-enabled/ && sudo rm -f /etc/nginx/sites-enabled/default && sudo nginx -t && sudo systemctl reload nginxInstall SSL with Certbot
sudo apt install -y certbot python3-certbot-nginxsudo certbot --nginx -d yourdomain.com -d www.yourdomain.comsudo certbot renew --dry-runCertbot fails with IPv6 / AAAA record (common on Contabo)
If Let's Encrypt shows an error from an IPv6 address like 2a02:4780:..., your domain has an AAAA record
pointing to IPv6 that is not configured on the VPS. Delete AAAA records for @ and www at your registrar.
dig yourdomain.com +short && dig AAAA yourdomain.com +shortOnly the A record (IPv4) should remain. Then retry Certbot.
Standalone Certbot fallback
sudo systemctl stop nginx && sudo certbot certonly --standalone -d yourdomain.com -d www.yourdomain.com && sudo systemctl start nginxThen point Nginx at the certificate paths manually:
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name yourdomain.com www.yourdomain.com;
root /var/www/melodai/public;
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
index index.php;
charset utf-8;
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; }
error_page 404 /index.php;
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.(?!well-known).* {
deny all;
}
}Step 11: Supervisor, Horizon, and Queue Workers
Create the config file with nano — do not run the path as a shell command.
Wrong: /etc/supervisor/conf.d/melodai-horizon.conf as a command.
Right: sudo nano /etc/supervisor/conf.d/melodai-horizon.conf
Recommended — Horizon only (uses Redis + config/horizon.php)
[program:melodai-horizon]
process_name=%(program_name)s
command=php /var/www/melodai/artisan horizon
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/www/melodai/storage/logs/horizon.log
stopwaitsecs=3600Alternative — Horizon plus dedicated queue:work programs
Use this if you want explicit workers for music-generation and video-processing,video-clips in addition to Horizon, or while debugging queue throughput:
[program:melodai-horizon]
process_name=%(program_name)s
command=php /var/www/melodai/artisan horizon
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/www/melodai/storage/logs/horizon.log
stopwaitsecs=3600
[program:melodai-music-generation]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/melodai/artisan queue:work redis --queue=music-generation --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/melodai/storage/logs/music-generation.log
stopwaitsecs=3600
[program:melodai-video-processing]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/melodai/artisan queue:work redis --queue=video-processing,video-clips --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/melodai/storage/logs/video-processing.log
stopwaitsecs=3600Ensure config/horizon.php production supervisors include video-clips if you use AI video clips:
// config/horizon.php — supervisor-video
'queue' => ['video-processing', 'video-clips'],sudo supervisorctl reread && sudo supervisorctl update && sudo supervisorctl start all && sudo supervisorctl statussudo systemctl enable supervisor && sudo systemctl start supervisorAfter each deploy: php artisan queue:restart and php artisan horizon:terminate if using Horizon.
Step 12: Laravel Reverb (Real-Time Chat & Notifications)
Chat and live updates use WebSockets. The browser connects to wss://yourdomain.com/app/….
That requires Reverb running locally and an Nginx reverse proxy — without both, the console shows
WebSocket connection to 'wss://…' failed.
Generate credentials
php artisan tinker --execute "echo 'KEY=' . bin2hex(random_bytes(16)) . PHP_EOL . 'SECRET=' . bin2hex(random_bytes(32));".env (production — hostname only, no https:// prefix)
BROADCAST_CONNECTION=reverb
REVERB_APP_ID=melodai
REVERB_APP_KEY=your-generated-key
REVERB_APP_SECRET=your-generated-secret
REVERB_HOST=melodai.site
REVERB_PORT=443
REVERB_SCHEME=https
REVERB_SERVER_HOST=0.0.0.0
REVERB_SERVER_PORT=8080Supervisor — Reverb process
sudo nano /etc/supervisor/conf.d/melodai-reverb.conf[program:melodai-reverb]
process_name=%(program_name)s
command=php /var/www/melodai/artisan reverb:start
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/www/melodai/storage/logs/reverb.logsudo supervisorctl reread && sudo supervisorctl update && sudo supervisorctl start melodai-reverb && sudo supervisorctl status melodai-reverbNginx — proxy WebSockets to Reverb
Add this inside your HTTPS server { … } block (before the PHP location):
location /app {
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header Scheme $scheme;
proxy_set_header SERVER_PORT $server_port;
proxy_set_header REMOTE_ADDR $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_read_timeout 86400;
proxy_pass http://127.0.0.1:8080;
}sudo nginx -t && sudo systemctl reload nginxAfter deploy
cd /var/www/melodai
php artisan config:clear && php artisan view:clear && php artisan config:cache
sudo supervisorctl restart melodai-reverbVerify
sudo supervisorctl status melodai-reverb→ RUNNINGcurl -I https://melodai.site→ 200- Browser DevTools → Network → WS → connection to
wss://melodai.site/app/…should stay open (101 Switching Protocols)
404 on avatar.webp / cover.png
Media URLs must use /storage/media/…. If you imported the database from local dev, also copy
storage/app/public (or your R2 bucket) to the VPS, then run
php artisan storage:link.
Step 13: Laravel Scheduler (Cron)
Edit the www-data crontab (runs as the web server user):
sudo crontab -e -u www-data* * * * * cd /var/www/melodai && php artisan schedule:run >> /dev/null 2>&1sudo crontab -l -u www-datasudo systemctl enable cron && sudo systemctl start croncd /var/www/melodai && php artisan schedule:listAlternatively use HTTP cron URLs from Admin → Cron & Queue if you prefer curl-based scheduling.
Troubleshooting (VPS)
- 502 Bad Gateway: check
sudo systemctl status php8.3-fpm nginxand Nginxfastcgi_pass unix:/var/run/php/php8.3-fpm.sock. - Permission denied on storage:
sudo chown -R www-data:www-data /var/www/melodai/storage /var/www/melodai/bootstrap/cache. - Video jobs fail: run
ffmpeg -versionand confirmFFMPEG_BINARIES=/usr/bin/ffmpegin.env, thenphp artisan config:clear. - Horizon not starting: verify
php -m | grep pcntl, Redis (redis-cli ping), andQUEUE_CONNECTION=redis. - Supervisor file missing: create
/etc/supervisor/conf.d/melodai-horizon.confwithnano, not by executing the path. - Certbot unauthorized / 404 on acme-challenge: delete AAAA DNS records, confirm A record points to VPS IP, open ports 80/443, retry Certbot.
- SCP from Windows fails (Could not resolve hostname c:): quote the local path:
scp "C:\path\file.zip" root@IP:/var/www/. - WebSocket failed (chat not live): confirm Reverb Supervisor program is RUNNING, Nginx has the
location /appproxy block, andREVERB_PORT=443withREVERB_HOST=melodai.site(nohttps://). - 404 on avatar/cover images: copy media files from local dev to VPS and run
php artisan storage:link; URLs must be/storage/media/users/…not/storage/users/….