# Deploying FacelessShorts

Production setup for **Ubuntu 22.04 / 24.04** using **nginx + PHP-FPM**, a
**Supervisor**-managed queue worker, and **MySQL**. Adapt paths/versions as needed.

The app is not a plain web app: video generation runs as a **background queue job**
(up to a 30-minute timeout each) and shells out to **ffmpeg/ffprobe**. Without a
running queue worker, videos will stay stuck at "queued" forever.

---

## 1. Prerequisites on the server

Install PHP 8.3, extensions, nginx, MySQL, ffmpeg, Node, Composer.

```bash
sudo apt update && sudo apt upgrade -y

# PHP 8.3 (via ppa:ondrej/php on Ubuntu)
sudo add-apt-repository -y ppa:ondrej/php
sudo apt update
sudo apt install -y php8.3-fpm php8.3-cli php8.3-mysql php8.3-mbstring \
    php8.3-xml php8.3-curl php8.3-zip php8.3-gd php8.3-bcmath php8.3-intl

# Web server, DB, media tooling
sudo apt install -y nginx mysql-server ffmpeg git unzip supervisor

# Node.js 20 LTS (for building frontend assets)
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs

# Composer
curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer
```

Verify ffmpeg is on PATH (the app relies on this):

```bash
which ffmpeg ffprobe   # should print paths
```

---

## 2. Database

```bash
sudo mysql
```
```sql
CREATE DATABASE faceless_shorts CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'faceless'@'localhost' IDENTIFIED BY 'CHANGE_ME_STRONG_PASSWORD';
GRANT ALL PRIVILEGES ON faceless_shorts.* TO 'faceless'@'localhost';
FLUSH PRIVILEGES;
EXIT;
```

(Using a managed DB like DigitalOcean instead? Set `DB_SSL=true` in `.env` — this repo
already supports TLS. See `.env.example`.)

---

## 3. Get the code

```bash
sudo mkdir -p /var/www
sudo chown $USER:$USER /var/www
cd /var/www
git clone <YOUR_REPO_URL> faceless-shorts
cd faceless-shorts
```

Install PHP + JS dependencies and build assets:

```bash
composer install --no-dev --optimize-autoloader
npm ci
npm run build          # compiles Tailwind/Vite assets into public/build
```

---

## 4. Configure `.env`

```bash
cp .env.example .env
php artisan key:generate
nano .env
```

Set at minimum:

```dotenv
APP_NAME=FacelessShorts
APP_ENV=production
APP_DEBUG=false
APP_URL=https://your-domain.com

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=faceless_shorts
DB_USERNAME=faceless
DB_PASSWORD=CHANGE_ME_STRONG_PASSWORD

QUEUE_CONNECTION=database
SESSION_DRIVER=database
CACHE_STORE=database

# Pipeline API keys — REQUIRED
OPENAI_API_KEY=sk-...
ELEVENLABS_API_KEY=...

# Leave FFMPEG_DIR empty if ffmpeg/ffprobe are on PATH (they are, from step 1)
FFMPEG_DIR=
```

Run migrations and cache config:

```bash
php artisan migrate --force
php artisan storage:link
php artisan config:cache
php artisan route:cache
php artisan view:cache
```

---

## 5. Permissions

nginx/PHP-FPM run as `www-data`. Give it ownership of writable dirs:

```bash
sudo chown -R www-data:www-data /var/www/faceless-shorts/storage \
    /var/www/faceless-shorts/bootstrap/cache
sudo find /var/www/faceless-shorts/storage -type d -exec chmod 775 {} \;
```

---

## 6. nginx site

```bash
sudo nano /etc/nginx/sites-available/faceless-shorts
```

```nginx
server {
    listen 80;
    server_name your-domain.com;
    root /var/www/faceless-shorts/public;

    index index.php;
    charset utf-8;

    # Generated videos can be large; allow big responses/uploads.
    client_max_body_size 512M;

    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;
        # Video rendering responses can be slow to start; be generous.
        fastcgi_read_timeout 300;
    }

    location ~ /\.(?!well-known).* { deny all; }
}
```

Enable and reload:

```bash
sudo ln -s /etc/nginx/sites-available/faceless-shorts /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
```

---

## 7. Queue worker (CRITICAL)

Video generation is dispatched to the queue. Run a persistent worker via Supervisor.

```bash
sudo nano /etc/supervisor/conf.d/faceless-worker.conf
```

```ini
[program:faceless-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/faceless-shorts/artisan queue:work --queue=default --sleep=3 --tries=1 --timeout=1810
directory=/var/www/faceless-shorts
autostart=true
autorestart=true
user=www-data
numprocs=1
redirect_stderr=true
stdout_logfile=/var/www/faceless-shorts/storage/logs/worker.log
stopwaitsecs=1830
```

> `--timeout=1810` must exceed the job's own `$timeout` (1800s). `--tries=1` matches the
> job (rendering is expensive; it should not auto-retry).

```bash
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start faceless-worker:*
```

Restart the worker after every deploy (so it picks up new code):

```bash
php artisan queue:restart
```

---

## 8. Scheduler (optional)

If you later add scheduled tasks, add a cron entry:

```bash
sudo crontab -e -u www-data
```
```cron
* * * * * cd /var/www/faceless-shorts && php artisan schedule:run >> /dev/null 2>&1
```

---

## 9. HTTPS

```bash
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d your-domain.com
```

Then set `APP_URL=https://your-domain.com` and re-run `php artisan config:cache`.

---

## 10. Deploying updates

```bash
cd /var/www/faceless-shorts
git pull
composer install --no-dev --optimize-autoloader
npm ci && npm run build
php artisan migrate --force
php artisan config:cache && php artisan route:cache && php artisan view:cache
php artisan queue:restart          # reload the worker with new code
```

---

## Troubleshooting

- **Videos stuck at "queued"** → the worker isn't running. Check
  `sudo supervisorctl status` and `storage/logs/worker.log`.
- **"failed" videos** → check the `error` column / `storage/logs/laravel.log`.
  Usually a missing/invalid API key or ffmpeg not found.
- **ffmpeg errors** → confirm `which ffmpeg ffprobe`, or set `FFMPEG_DIR` in `.env`.
- **403 / storage not writable** → re-run the chown/chmod in step 5.
- **Assets not loading** → make sure `npm run build` ran and `public/build` exists.
