- Prerequisites
- VPS Setup (Hetzner)
- Domain & DNS (Cloudflare)
- Server Initial Setup
- Install Dependencies
- Clone & Build Project
- Environment Variables
- Nginx Reverse Proxy
- SSL Certificate (Let's Encrypt)
- PM2 Process Manager
- Firewall & Security
- Verify Deployment
- Maintenance & Updates
- Troubleshooting
Internet → Cloudflare DNS → blacklyrics.tech
│
┌──────▼──────┐
│ Nginx │ ← SSL termination
│ :80/:443 │
└──────┬──────┘
┌──────────────┼──────────────┐
│ │ │
┌──────▼──┐ ┌──────▼──┐ ┌──────▼──┐
│Frontend │ │ AI │ │ Render │
│React │ │ Backend │ │ Server │
│Router SSR│ │ FastAPI │ │Remotion │
│ :5173 │ │ :3000 │ │ :8000 │
└──────────┘ └─────────┘ └─────────┘
Routing:
| URL | Proxied To | Service |
|---|---|---|
blacklyrics.tech/* |
localhost:5173 |
Frontend (React Router SSR) |
blacklyrics.tech/ai/* |
localhost:3000 |
AI Backend (FastAPI) |
blacklyrics.tech/render/* |
localhost:8000 |
Render Server (Remotion + media) |
blacklyrics.tech/media/* |
localhost:8000/media/ |
Media file serving |
Before starting, you need:
- Domain:
blacklyrics.techpurchased (Namecheap, Cloudflare Registrar, etc.) - VPS: Hetzner CCX33 or better (8+ dedicated vCPU, 16+ GB RAM)
- Cloudflare account (free tier is fine)
- GitHub repo access to your project
- Groq API key for AI backend
- Google OAuth credentials (for login)
- PostgreSQL database (Supabase or self-hosted)
- Go to Hetzner Cloud Console
- Create a new project → "Aladdin"
- Add a new server:
| Setting | Value |
|---|---|
| Location | Falkenstein (EU) or Ashburn (US) |
| Image | Ubuntu 24.04 LTS |
| Type | CCX33 (8 dedicated vCPU, 32 GB RAM, 240 GB NVMe) |
| SSH Key | Add your public key |
| Name | aladdin-prod |
- Click Create & Buy (~€51/month)
After creation, note your server's IP address (e.g., 65.21.xxx.xxx). You'll need this for DNS.
- Go to Cloudflare Dashboard
- Add a site → Enter
blacklyrics.tech - Choose Free plan
- Cloudflare will show you nameservers (e.g.,
aria.ns.cloudflare.com,leo.ns.cloudflare.com) - Go to your domain registrar → Update nameservers to Cloudflare's
In Cloudflare DNS settings, add:
| Type | Name | Content | Proxy | TTL |
|---|---|---|---|---|
| A | @ |
65.21.xxx.xxx (your VPS IP) |
☁️ Proxied | Auto |
| A | www |
65.21.xxx.xxx (your VPS IP) |
☁️ Proxied | Auto |
- Go to SSL/TLS → Set mode to Full (Strict)
- Go to SSL/TLS → Edge Certificates → Enable Always Use HTTPS
- Go to Speed → Optimization → Enable Auto Minify (JS, CSS, HTML)
| Rule | Setting |
|---|---|
blacklyrics.tech/media/* |
Cache Level: Standard, Edge TTL: 7 days |
blacklyrics.tech/api/* |
Cache Level: Bypass |
ssh root@65.21.xxx.xxxadduser aladdin
usermod -aG sudo aladdin
# Copy SSH keys
rsync --archive --chown=aladdin:aladdin ~/.ssh /home/aladdinsu - aladdinsudo apt update && sudo apt upgrade -y
sudo apt install -y build-essential curl wget git unzip software-properties-commonsudo timedatectl set-timezone UTCsudo hostnamectl set-hostname aladdin-prodcurl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.bashrc
nvm install 20
nvm use 20
nvm alias default 20
node -v # Should show v20.x.xnpm install -g pnpm
pnpm -v # Should show 9.x.xsudo apt install -y python3.12 python3.12-venv python3-pip
# Install uv (fast Python package manager)
curl -LsSf https://astral.sh/uv/install.sh | sh
source ~/.bashrc
uv --versionsudo apt install -y ffmpeg
ffmpeg -version# Install Chromium dependencies (Remotion will download its own, but system deps are needed)
sudo apt install -y \
libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 \
libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libgbm1 \
libpango-1.0-0 libcairo2 libasound2t64 libxshmfence1 \
fonts-liberation fonts-noto-color-emoji
# Verify
npx remotion browser ensurenpm install -g pm2
pm2 --versionsudo apt install -y nginx
sudo systemctl enable nginx
sudo systemctl start nginxcd /home/aladdin
git clone https://github.com/YOUR_USERNAME/videoeditor.git aladdin
cd aladdinpnpm installpnpm buildThis outputs to build/ directory (React Router production build).
cd backend
uv sync
cd ..mkdir -p out
chmod 755 out# Ensure public/fonts directory has all required fonts
ls public/fonts/
# Should see: PakNastaleeq.ttf, NotoNastaliqUrdu.ttf, etc.nano /home/aladdin/aladdin/.envPaste:
# ==========================================
# Aladdin Production Environment Variables
# ==========================================
# Node
NODE_ENV=production
# Database (PostgreSQL — Supabase or self-hosted)
DATABASE_URL=postgresql://postgres:[PASSWORD]@db.[PROJECT_REF].supabase.co:6543/postgres?pgbouncer=true
# Google OAuth (for login)
GOOGLE_CLIENT_ID=your-google-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-google-client-secret
# Auth Configuration
AUTH_BASE_URL=https://blacklyrics.tech
AUTH_TRUSTED_ORIGINS=https://blacklyrics.tech,https://www.blacklyrics.tech
AUTH_COOKIE_DOMAIN=blacklyrics.tech
# Groq AI (backend)
GROQ_API_KEY=gsk_your_groq_api_key_here
# Render Server (internal communication)
UPLOAD_SERVICE_URL=http://localhost:8000/upload
RENDER_SERVICE_URL=http://localhost:8000
# Supabase (if using for waitlist on landing page)
VITE_SUPABASE_URL=https://your-project.supabase.co
VITE_SUPABASE_ANON_KEY=your-supabase-anon-keynano /home/aladdin/aladdin/backend/.envPaste:
GROQ_API_KEY=gsk_your_groq_api_key_herechmod 600 /home/aladdin/aladdin/.env
chmod 600 /home/aladdin/aladdin/backend/.envsudo nano /etc/nginx/sites-available/blacklyrics.techPaste the full configuration:
# Redirect www to non-www
server {
listen 80;
listen [::]:80;
server_name www.blacklyrics.tech;
return 301 https://blacklyrics.tech$request_uri;
}
# Main server block
server {
listen 80;
listen [::]:80;
server_name blacklyrics.tech;
# Max upload size (500MB for video uploads)
client_max_body_size 500M;
# Increase timeouts for video rendering (can take 15+ minutes)
proxy_connect_timeout 60s;
proxy_send_timeout 900s;
proxy_read_timeout 900s;
send_timeout 900s;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# ─── AI Backend (FastAPI) ───
# /ai/* → localhost:3000
location /ai/ {
proxy_pass http://127.0.0.1:3000/;
proxy_http_version 1.1;
proxy_set_header Host $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;
# CORS headers for AI endpoint
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
if ($request_method = 'OPTIONS') {
return 204;
}
}
# ─── Render Server & Media ───
# /render/* → localhost:8000
location /render/ {
proxy_pass http://127.0.0.1:8000/;
proxy_http_version 1.1;
proxy_set_header Host $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;
# Long timeouts for rendering
proxy_read_timeout 900s;
proxy_send_timeout 900s;
# Allow large video uploads
client_max_body_size 500M;
}
# ─── Media Files (with caching) ───
# /media/* → localhost:8000/media/
location /media/ {
proxy_pass http://127.0.0.1:8000/media/;
proxy_http_version 1.1;
proxy_set_header Host $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;
# Cache media files aggressively
proxy_cache_valid 200 7d;
add_header Cache-Control "public, max-age=604800, immutable";
add_header Access-Control-Allow-Origin "*" always;
}
# ─── Frontend (React Router SSR) ───
# Everything else → localhost:5173 (production: 3000 via react-router-serve)
location / {
proxy_pass http://127.0.0.1:4173;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $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;
}
# ─── Static Assets ───
location /assets/ {
proxy_pass http://127.0.0.1:4173/assets/;
proxy_http_version 1.1;
add_header Cache-Control "public, max-age=31536000, immutable";
}
# ─── Fonts ───
location /fonts/ {
proxy_pass http://127.0.0.1:4173/fonts/;
proxy_http_version 1.1;
add_header Cache-Control "public, max-age=31536000, immutable";
}
# Health check
location /health {
proxy_pass http://127.0.0.1:8000/health;
proxy_http_version 1.1;
}
}sudo ln -s /etc/nginx/sites-available/blacklyrics.tech /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default # Remove default sitesudo nginx -t
# Should say: syntax is ok, test is successful
sudo systemctl reload nginxIf using Cloudflare Proxy (☁️ orange cloud): Cloudflare handles SSL automatically. Skip to Option A. If not using Cloudflare Proxy: Use Let's Encrypt. Use Option B.
- In Cloudflare Dashboard → SSL/TLS → Origin Server
- Click Create Certificate
- Keep defaults (15 years, covers
blacklyrics.techand*.blacklyrics.tech) - Copy the certificate and private key
sudo mkdir -p /etc/ssl/cloudflare
sudo nano /etc/ssl/cloudflare/blacklyrics.tech.pem
# Paste the certificate
sudo nano /etc/ssl/cloudflare/blacklyrics.tech.key
# Paste the private key
sudo chmod 600 /etc/ssl/cloudflare/blacklyrics.tech.keyUpdate Nginx to listen on 443:
sudo nano /etc/nginx/sites-available/blacklyrics.techChange the main server block:
server {
listen 80;
listen [::]:80;
server_name blacklyrics.tech;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name blacklyrics.tech;
ssl_certificate /etc/ssl/cloudflare/blacklyrics.tech.pem;
ssl_certificate_key /etc/ssl/cloudflare/blacklyrics.tech.key;
# ... rest of config (client_max_body_size, locations, etc.) ...
}sudo nginx -t && sudo systemctl reload nginxsudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d blacklyrics.tech -d www.blacklyrics.tech
# Follow prompts, enter email, agree to ToS
# Auto-renewal (already set up by certbot, verify):
sudo certbot renew --dry-runnano /home/aladdin/aladdin/ecosystem.config.cjsPaste:
module.exports = {
apps: [
// ─── Frontend (React Router SSR production server) ───
{
name: "frontend",
script: "node_modules/.bin/react-router-serve",
args: "./build/server/index.js",
cwd: "/home/aladdin/aladdin",
env: {
NODE_ENV: "production",
PORT: 4173,
},
instances: 2, // 2 instances for load balancing
exec_mode: "cluster", // Cluster mode for multi-core
max_memory_restart: "1G",
watch: false,
autorestart: true,
max_restarts: 10,
restart_delay: 5000,
},
// ─── Render Server (Remotion + Express) ───
{
name: "render-server",
script: "node_modules/.bin/tsx",
args: "app/videorender/videorender.ts",
cwd: "/home/aladdin/aladdin",
env: {
NODE_ENV: "production",
PORT: 8000,
},
instances: 1, // Single instance (Remotion manages its own concurrency)
exec_mode: "fork",
max_memory_restart: "8G", // Render server can use lots of RAM
watch: false,
autorestart: true,
max_restarts: 5,
restart_delay: 10000, // Wait 10s before restarting (rebundling takes time)
kill_timeout: 30000, // Give 30s to finish current render before kill
},
// ─── AI Backend (FastAPI via uvicorn) ───
{
name: "ai-backend",
script: "uv",
args: "run uvicorn main:app --host 127.0.0.1 --port 3000 --workers 2",
cwd: "/home/aladdin/aladdin/backend",
interpreter: "none", // uv is the command, not a script
env: {
GROQ_API_KEY: "gsk_your_groq_api_key_here",
},
instances: 1,
exec_mode: "fork",
max_memory_restart: "2G",
watch: false,
autorestart: true,
max_restarts: 10,
restart_delay: 3000,
},
],
};cd /home/aladdin/aladdin
pm2 start ecosystem.config.cjspm2 statusExpected output:
┌────┬──────────────────┬──────┬─────┬───────────┬──────────┬────────┐
│ id │ name │ mode │ ↺ │ status │ cpu │ memory │
├────┼──────────────────┼──────┼─────┼───────────┼──────────┼────────┤
│ 0 │ frontend │ cluster │ 0 │ online │ 0% │ 80MB │
│ 1 │ frontend │ cluster │ 0 │ online │ 0% │ 80MB │
│ 2 │ render-server │ fork │ 0 │ online │ 0% │ 300MB │
│ 3 │ ai-backend │ fork │ 0 │ online │ 0% │ 120MB │
└────┴──────────────────┴──────┴─────┴───────────┴──────────┴────────┘
pm2 save
pm2 startup
# Run the command it outputs (sudo env PATH=... pm2 startup systemd ...)pm2 logs # View all logs
pm2 logs frontend # View frontend logs only
pm2 logs render-server # View render server logs
pm2 logs ai-backend # View AI backend logs
pm2 monit # Real-time monitoring dashboard
pm2 restart all # Restart all services
pm2 restart render-server # Restart only render server
pm2 reload frontend # Zero-downtime reload frontend
pm2 stop all # Stop all servicessudo ufw default deny incoming
sudo ufw default allow outgoing
# Allow SSH
sudo ufw allow 22/tcp
# Allow HTTP and HTTPS
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Enable firewall
sudo ufw enable
sudo ufw statusDo NOT expose ports 3000, 4173, 8000 externally. Nginx proxies all traffic.
sudo nano /etc/ssh/sshd_configSet:
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
sudo systemctl restart sshdsudo apt install -y fail2ban
sudo systemctl enable fail2ban
sudo systemctl start fail2bansudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
# Select "Yes"# PM2 status
pm2 status
# Test frontend
curl -I http://localhost:4173
# Test render server
curl http://localhost:8000/health
# Test AI backend
curl http://localhost:3000/docs# Test from outside via domain
curl -I https://blacklyrics.tech
# Should return 200 OK
curl -I https://blacklyrics.tech/health
# Should return render server health JSON
curl https://blacklyrics.tech/ai/docs
# Should return FastAPI OpenAPI docscurl -X POST https://blacklyrics.tech/render/upload \
-F "media=@test-video.mp4"
# Should return JSON with filename and URLcurl -vI https://blacklyrics.tech 2>&1 | grep -E "SSL|certificate|subject"
# Should show valid SSL certificate| Test | URL | Expected |
|---|---|---|
| Landing page | https://blacklyrics.tech |
Landing page loads |
| Login | https://blacklyrics.tech/login |
Google OAuth works |
| Editor | https://blacklyrics.tech/home |
Editor loads with timeline |
| AI Chat | Send message in chat | AI responds with tool call |
| Upload video | Drop video in media bin | Upload succeeds |
| Render video | Click render button | Render starts and completes |
| Media playback | Play video in timeline | Video plays smoothly |
Before deploying, update the production URLs in your code:
The URLs are already configured with a production/dev toggle. Update the production URLs:
// Current (needs update):
return isProduction ? "https://tryAladdin.com/ai/api" : "http://127.0.0.1:3000";
return isProduction ? "https://tryAladdin.com/render" : "http://localhost:8000";
// Change to:
return isProduction ? "https://blacklyrics.tech/ai" : "http://127.0.0.1:3000";
return isProduction ? "https://blacklyrics.tech/render" : "http://localhost:8000";Update trusted origins:
// Add to defaultTrustedOrigins:
"https://blacklyrics.tech",
"https://www.blacklyrics.tech",Update the production upload URL:
// Change from:
? process.env.UPLOAD_SERVICE_URL || "https://localhost:8000/upload"
// Change to:
? process.env.UPLOAD_SERVICE_URL || "http://localhost:8000/upload" // internal, no HTTPS neededUpdate in Google Cloud Console:
| Setting | Value |
|---|---|
| Authorized JavaScript origins | https://blacklyrics.tech |
| Authorized redirect URIs | https://blacklyrics.tech/api/auth/callback/google |
# SSH into server
ssh aladdin@65.21.xxx.xxx
cd /home/aladdin/aladdin
# Pull latest code
git pull origin main
# Install any new dependencies
pnpm install
# Rebuild frontend
pnpm build
# Install backend deps if changed
cd backend && uv sync && cd ..
# Restart services
pm2 restart frontend --update-env
pm2 restart render-server --update-env
pm2 restart ai-backend --update-env# Uses PM2 cluster mode — reloads one instance at a time
pm2 reload frontend# All logs
pm2 logs
# Follow specific service
pm2 logs render-server --lines 50
# Nginx access logs
sudo tail -f /var/log/nginx/access.log
# Nginx error logs
sudo tail -f /var/log/nginx/error.log# Delete temp render files older than 24 hours
find /home/aladdin/aladdin/out -name "*.jpeg" -mtime +1 -delete
find /tmp/remotion-* -mtime +1 -exec rm -rf {} +
find /tmp/react-motion-* -mtime +1 -exec rm -rf {} +
# Check disk usage
df -h
du -sh /home/aladdin/aladdin/out/crontab -eAdd:
# Clean temp render files every 6 hours
0 */6 * * * find /tmp/remotion-* -mmin +360 -exec rm -rf {} + 2>/dev/null
0 */6 * * * find /tmp/react-motion-* -mmin +360 -exec rm -rf {} + 2>/dev/null
# Clean old media files older than 30 days
0 3 * * * find /home/aladdin/aladdin/out -name "TimelineComposition.mp4" -mtime +30 -delete 2>/dev/null
# PM2 log rotation (keep last 10MB per service)
0 0 * * * pm2 flush# Real-time dashboard
pm2 monit
# Memory usage
free -h
# CPU usage
htop
# Disk usage
df -h# Check if frontend is running
pm2 logs frontend --lines 20
# Check if port 4173 is listening
ss -tlnp | grep 4173
# Rebuild if needed
pnpm build
pm2 restart frontend# Check logs
pm2 logs render-server --lines 50
# Common issue: Chromium deps missing
sudo apt install -y libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 \
libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libgbm1 \
libpango-1.0-0 libcairo2 libasound2t64
# Common issue: Out of memory
free -h
# If RAM is low, reduce Remotion concurrency in videorender.ts
# Restart
pm2 restart render-server# Check logs
pm2 logs ai-backend --lines 20
# Test directly
curl http://localhost:3000/docs
# Check Python env
cd /home/aladdin/aladdin/backend
uv run python -c "import fastapi; print(fastapi.__version__)"# Nginx can't reach backend — check if services are running
pm2 status
# Check nginx error log
sudo tail -20 /var/log/nginx/error.log
# Restart everything
pm2 restart all
sudo systemctl reload nginx# Check Cloudflare SSL mode is "Full (Strict)"
# Check origin certificate is installed correctly
sudo nginx -t
openssl x509 -in /etc/ssl/cloudflare/blacklyrics.tech.pem -text -noout | head -20# Check CPU usage during render
htop # Look for Chromium processes
# Check if swap is needed
free -h
# If no swap and RAM is maxing out:
sudo fallocate -l 4G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile swap swap defaults 0 0' | sudo tee -a /etc/fstabSave this as deploy.sh on the server for easy updates:
#!/bin/bash
set -e
echo "🚀 Deploying Aladdin to blacklyrics.tech..."
cd /home/aladdin/aladdin
echo "📥 Pulling latest code..."
git pull origin main
echo "📦 Installing frontend dependencies..."
pnpm install --frozen-lockfile
echo "🔨 Building frontend..."
pnpm build
echo "📦 Installing backend dependencies..."
cd backend && uv sync --frozen && cd ..
echo "♻️ Restarting services..."
pm2 reload frontend --update-env
pm2 restart render-server --update-env
pm2 restart ai-backend --update-env
echo "✅ Deployment complete!"
echo "🌐 https://blacklyrics.tech"
pm2 statuschmod +x deploy.sh
./deploy.sh| Step | Status |
|---|---|
| VPS ordered (Hetzner CCX33+) | ☐ |
| Domain DNS pointed to VPS IP | ☐ |
| Cloudflare SSL configured | ☐ |
| Node.js 20 + pnpm installed | ☐ |
| Python 3.12 + uv installed | ☐ |
| FFmpeg + Chromium deps installed | ☐ |
| Project cloned and built | ☐ |
.env files created |
☐ |
| Nginx configured and running | ☐ |
| SSL certificate installed | ☐ |
| PM2 running all 3 services | ☐ |
| PM2 startup on boot configured | ☐ |
| Firewall (UFW) enabled | ☐ |
| SSH hardened (no root, no password) | ☐ |
| API URLs updated for production | ☐ |
| Google OAuth redirect updated | ☐ |
| Frontend loads at https://blacklyrics.tech | ☐ |
| AI chat works | ☐ |
| Video upload works | ☐ |
| Video render completes | ☐ |
| Cron cleanup jobs configured | ☐ |
Document generated: February 8, 2026 Aladdin Video Editor — blacklyrics.tech Deployment Guide