MelodAI Documentation

VPS Deployment (Ubuntu + Nginx + PHP 8.3)

Full MelodAI production setup on a Linux VPS: FFmpeg via apt, Redis + Horizon, Nginx + SSL, ZIP/SQL upload from Windows, Supervisor, and troubleshooting for Contabo, Hetzner, Hostinger, and DigitalOcean.

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()BlockedFull access
FFmpeg system installNo rootapt install ffmpeg
Laravel Horizon (persistent)No systemdSupervisor
Custom php.iniLockedFull control
Redis (queues)Usually unavailableInstall freely
Cron / background workersLimitedFull cron + systemd

Recommended VPS Providers

Provider Plan Price Notes
HetznerCX22 (2 vCPU, 4 GB)~$4/moBest value, EU/US regions
ContaboVPS S (4 vCPU, 8 GB)~$5/moGood specs; common for MelodAI deploys
Hostinger VPSKVM 1 (1 vCPU, 4 GB)~$5/moSmooth if you already use Hostinger
DigitalOceanBasic Droplet (1 vCPU, 1 GB)$6/moBeginner-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 enable
sudo ufw status

Step 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 ffmpeg
ffmpeg -version

Set in .env after install:

FFMPEG_BINARIES=/usr/bin/ffmpeg
FFPROBE_BINARIES=/usr/bin/ffprobe

Step 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-redis
php -m | grep pcntl

Tune /etc/php/8.3/fpm/php.ini and /etc/php/8.3/cli/php.ini:

  • memory_limit = 512M
  • upload_max_filesize = 64M
  • post_max_size = 64M
  • max_execution_time = 300
  • Enable opcache for production
sudo systemctl restart php8.3-fpm

Step 4: Install Nginx, MySQL, Redis, Supervisor, Composer, Node

sudo apt install -y nginx mysql-server redis-server supervisor git unzip curl
sudo systemctl enable redis-server && sudo systemctl start redis-server && redis-cli ping
curl -sS https://getcomposer.org/installer | php && sudo mv composer.phar /usr/local/bin/composer
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - && sudo apt install -y nodejs

redis-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 root

At 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> melodai

Option B — Upload ZIP from Windows (see next section)

After files are in /var/www/melodai:

cd /var/www/melodai && cp .env.example .env
composer install --no-dev --optimize-autoloader
npm ci && npm run build
php artisan key:generate --force
php artisan migrate --force
php artisan storage:link
php artisan config:cache && php artisan route:cache && php artisan view:cache

Or 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/cache

Step 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.sql
sudo mysql -u melodai_user -p melodai_live < /var/www/melodai.sql

For 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.sql

Detach 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/.env

Production 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/ffprobe

Save 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/melodai
server { 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 nginx

Install SSL with Certbot

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
sudo certbot renew --dry-run

Certbot 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 +short

Only 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 nginx

Then 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=3600

Alternative — 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=3600

Ensure 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 status
sudo systemctl enable supervisor && sudo systemctl start supervisor

After 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=8080

Supervisor — 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.log
sudo supervisorctl reread && sudo supervisorctl update && sudo supervisorctl start melodai-reverb && sudo supervisorctl status melodai-reverb

Nginx — 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 nginx

After deploy

cd /var/www/melodai php artisan config:clear && php artisan view:clear && php artisan config:cache sudo supervisorctl restart melodai-reverb

Verify

  • sudo supervisorctl status melodai-reverb → RUNNING
  • curl -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>&1
sudo crontab -l -u www-data
sudo systemctl enable cron && sudo systemctl start cron
cd /var/www/melodai && php artisan schedule:list

Alternatively 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 nginx and Nginx fastcgi_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 -version and confirm FFMPEG_BINARIES=/usr/bin/ffmpeg in .env, then php artisan config:clear.
  • Horizon not starting: verify php -m | grep pcntl, Redis (redis-cli ping), and QUEUE_CONNECTION=redis.
  • Supervisor file missing: create /etc/supervisor/conf.d/melodai-horizon.conf with nano, 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 /app proxy block, and REVERB_PORT=443 with REVERB_HOST=melodai.site (no https://).
  • 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/….