# VPS Setup Guide (WHM / cPanel, no Docker)

Step-by-step instructions to run **Realm of Forgotten Souls** on your existing
WHM/cPanel VPS. It is written for a non-expert: copy/paste each command and
follow the notes. Nothing runs on your local Windows machine except the build +
upload (`deploy.bat`).

## 0. Your environment (detected)

These instructions are tailored to the actual server:

| Thing | Value |
|-------|-------|
| OS | AlmaLinux 9.8 (RHEL 9 family, package manager `dnf`) |
| Control panel | WHM/cPanel 11.134 (root/WHM access available) |
| cPanel user | `shader2199` (home `/home/shader2199`) |
| PHP | 8.3 (`/usr/local/bin/php`, cPanel `ea-php83`) |
| Database | MariaDB 10.11 (MySQL-compatible) on `127.0.0.1:3306` |
| Redis | `redis-cli` 6.2 present on the box |
| Go on server | not installed (not needed — we cross-compile the binary locally) |
| Project root | `/home/shader2199/realmofforgottensouls.com` |
| Server IP | `153.75.246.214` |

### How the pieces map onto cPanel

The app is three cooperating services. On cPanel we expose them through **three
subdomains**, all sharing one `.env` at the project root:

| Subdomain | Serves | Document root / target |
|-----------|--------|------------------------|
| `rofs.titanjobfinder.com` | PWA (static files) | `.../realmofforgottensouls.com/apps/web/dist` |
| `api.rofs.titanjobfinder.com` | PHP API (native cPanel PHP-FPM) | `.../realmofforgottensouls.com/apps/api/public` |
| `ws.rofs.titanjobfinder.com` | Go world server WebSocket | Apache reverse proxy to `127.0.0.1:8081` |

- MariaDB and Redis stay bound to `127.0.0.1` (never publicly exposed).
- The Go world server listens on `127.0.0.1:8081` and is fronted by Apache
  (`mod_proxy_wstunnel`) so browsers reach it over `wss://` on port 443.
- The single `.env` lives at `/home/shader2199/realmofforgottensouls.com/.env`
  and is read by both PHP (it walks up from `apps/api`) and the Go service
  (via systemd `EnvironmentFile`).

```mermaid
flowchart LR
    Browser -->|https rofs.*| DistPWA[apps/web/dist static]
    Browser -->|https api.rofs.*| PHP[apps/api/public PHP-FPM]
    Browser -->|wss ws.rofs.*| Apache[Apache proxy_wstunnel]
    Apache -->|127.0.0.1:8081| Go[Go world systemd service]
    PHP --> MariaDB[(MariaDB 127.0.0.1)]
    PHP --> Redis[(Redis 127.0.0.1)]
    Go --> Redis
    Go --> MariaDB
```

Steps marked **[WHM root]** require the root WHM login; steps marked
**[cPanel]** are done as the `shader2199` cPanel user (UI or SSH).

---

## 1. Upload the code (from your Windows machine)

You already have the deploy pipeline. From the repo root:

```bat
deploy.bat
```

This builds the PWA, cross-compiles the Go world server to a Linux binary, and
uploads everything to `/home/shader2199/realmofforgottensouls.com`. Re-run it
any time you change code. Useful flags: `-WebOnly`, `-ApiOnly`, `-WorldOnly`,
`-SkipBuild`, `-DryRun` (see `scripts/deploy.ps1`).

> Do the PWA build with production URLs baked in first — see step 7.

---

## 2. Prepare Redis [WHM root]

Redis is already present. Make sure the server is installed, running, enabled at
boot, and password-protected (important on a shared cPanel box).

```bash
# SSH in as root, then:
dnf install -y redis            # no-op if already installed
systemctl enable --now redis
systemctl status redis --no-pager

# Require a password and keep it on localhost only.
# Edit /etc/redis/redis.conf (or /etc/redis.conf):
#   bind 127.0.0.1 -::1
#   requirepass <a-long-random-password>
sed -i 's/^# *requirepass .*/requirepass CHANGE_ME_REDIS_PASS/' /etc/redis/redis.conf 2>/dev/null || true
systemctl restart redis

# Verify (use the password you set):
redis-cli -a 'CHANGE_ME_REDIS_PASS' ping   # -> PONG
```

Remember the Redis password; it goes in `.env` as `REDIS_PASSWORD`.

---

## 3. Enable Apache proxy modules for WebSockets [WHM root]

The Go WebSocket endpoint is proxied by Apache, which needs the proxy modules.

- WHM -> **EasyApache 4** -> Customize -> Apache Modules, ensure these are ON:
  - `mod_proxy`
  - `mod_proxy_http`
  - `mod_proxy_wstunnel`
- Provision the profile.

Verify from SSH:

```bash
httpd -M 2>/dev/null | grep -E 'proxy_module|proxy_http_module|proxy_wstunnel_module'
```

You should see all three listed.

---

## 4. Confirm PHP version and extensions [cPanel]

The API needs PHP 8.1+ with `pdo_mysql`, `mbstring`, `curl`, `openssl`, `json`
(all standard in `ea-php83`). Redis is handled by the pure-PHP **Predis**
library, so **no `php-redis` extension is required**.

- cPanel -> **MultiPHP Manager**: set the API subdomain (created in step 6) to
  `ea-php83`.
- cPanel -> **Select PHP Version** (or **MultiPHP INI Editor**): confirm
  `pdo_mysql`, `mbstring`, `curl`, `openssl` are enabled.

Quick check over SSH:

```bash
php -m | grep -iE 'pdo_mysql|mbstring|curl|openssl'
```

---

## 5. Create the database and user [cPanel]

cPanel prefixes database and user names with your account name.

**Option A — cPanel UI (easiest):** cPanel -> **MySQL Databases**:
1. Create database: `rofs` (cPanel stores it as `shader2199_rofs`).
2. Create user: `rofs` (stored as `shader2199_rofs`) with a strong password.
3. Add the user to the database with **ALL PRIVILEGES**.

**Option B — SSH (uapi as the cPanel user):**

```bash
uapi Mysql create_database name=shader2199_rofs
uapi Mysql create_user name=shader2199_rofs password='CHANGE_ME_DB_PASS'
uapi Mysql set_privileges_on_database \
  user=shader2199_rofs database=shader2199_rofs privileges='ALL PRIVILEGES'
```

Record the final names for `.env`:
- `MYSQL_DATABASE=shader2199_rofs`
- `MYSQL_USER=shader2199_rofs`

---

## 6. Create the subdomains and document roots [cPanel]

cPanel -> **Domains** -> Create A Domain (or Subdomains), create/point:

1. `rofs.titanjobfinder.com` -> Document Root:
   `/home/shader2199/realmofforgottensouls.com/apps/web/dist`
2. `api.rofs.titanjobfinder.com` -> Document Root:
   `/home/shader2199/realmofforgottensouls.com/apps/api/public`
3. `ws.rofs.titanjobfinder.com` -> Document Root can be anything (e.g. the
   project root); its traffic is handled by the proxy in step 9.

> You originally set `rofs.titanjobfinder.com` to the project root. Change its
> Document Root to `.../apps/web/dist` so the PWA is served and the `.env` at the
> project root is **not** web-accessible.

Both `.htaccess` files ship with the code, so no manual creation is needed:
- The PWA SPA fallback lives in `apps/web/public/.htaccess`, which Vite copies
  into `apps/web/dist/` on every build.
- The API front controller lives in `apps/api/public/.htaccess` (Slim routes
  everything to `index.php`, and dotfiles are denied).

---

## 7. Build the PWA with production URLs, then re-upload

The web client bakes its API and WebSocket URLs at **build time**. Vite reads
env files from the **repo root** (`envDir` in `vite.config.ts`), so on your
Windows machine create `C:\RoFS\.env.production` (repo root, not `apps/web`):

```
VITE_API_URL=https://api.rofs.titanjobfinder.com
VITE_WORLD_WS_URL=wss://ws.rofs.titanjobfinder.com/ws
```

Then rebuild and upload just the web app:

```bat
deploy.bat -WebOnly
```

(These `VITE_*` values are public by design — they are not secrets.)

---

## 8. Create the server `.env` [cPanel/SSH]

Create `/home/shader2199/realmofforgottensouls.com/.env` (copy `.env.example`
and edit). Production values:

```ini
APP_ENV=production
APP_DEBUG=false
APP_URL=https://rofs.titanjobfinder.com
APP_NAME="Realm of Forgotten Souls"

# MariaDB (cPanel-prefixed names from step 5)
MYSQL_HOST=127.0.0.1
MYSQL_PORT=3306
MYSQL_DATABASE=shader2199_rofs
MYSQL_USER=shader2199_rofs
MYSQL_PASSWORD=CHANGE_ME_DB_PASS

# Redis (password from step 2)
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=CHANGE_ME_REDIS_PASS

# Go world server (localhost only; Apache proxies it)
WORLD_HOST=127.0.0.1
WORLD_PORT=8081
WORLD_LOG_LEVEL=info
WORLD_LOG_PATH=/home/shader2199/realmofforgottensouls.com/apps/world/logs/world.log
WORLD_TICK_MS=100

# PHP API logging
API_LOG_LEVEL=info
API_LOG_PATH=/home/shader2199/realmofforgottensouls.com/apps/api/var/log/api.log

# Security
SESSION_SECRET=GENERATE_A_LONG_RANDOM_STRING
WS_TICKET_TTL_SECONDS=60
CORS_ALLOWED_ORIGINS=https://rofs.titanjobfinder.com

# Firebase / Stripe (fill when configured — see ProjectStartupGuide.md)
FIREBASE_PROJECT_ID=
FIREBASE_API_KEY=
FIREBASE_SERVICE_ACCOUNT_PATH=
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
STRIPE_PUBLISHABLE_KEY=
```

Lock down permissions and make the log dirs:

```bash
cd /home/shader2199/realmofforgottensouls.com
chmod 600 .env
mkdir -p apps/world/logs apps/api/var/log
```

Generate a session secret:

```bash
openssl rand -hex 32
```

---

## 9. Configure the WebSocket reverse proxy [WHM root]

cPanel rebuilds Apache vhosts, so add the proxy through the include system that
survives rebuilds. Create the per-domain include for the `ws` subdomain
(replace the path if your cPanel uses a different std/ssl split):

```bash
# SSL vhost include for the ws subdomain
mkdir -p /etc/apache2/conf.d/userdata/ssl/2_4/shader2199/ws.rofs.titanjobfinder.com
cat > /etc/apache2/conf.d/userdata/ssl/2_4/shader2199/ws.rofs.titanjobfinder.com/wsproxy.conf <<'EOF'
RewriteEngine On
RewriteCond %{HTTP:Upgrade} =websocket [NC]
RewriteRule ^/ws(.*)$ ws://127.0.0.1:8081/ws$1 [P,L]
ProxyPass /ws ws://127.0.0.1:8081/ws
ProxyPassReverse /ws ws://127.0.0.1:8081/ws
ProxyPass /health http://127.0.0.1:8081/health
EOF

# Also add a plain-HTTP include (before AutoSSL issues the cert)
mkdir -p /etc/apache2/conf.d/userdata/std/2_4/shader2199/ws.rofs.titanjobfinder.com
cp /etc/apache2/conf.d/userdata/ssl/2_4/shader2199/ws.rofs.titanjobfinder.com/wsproxy.conf \
   /etc/apache2/conf.d/userdata/std/2_4/shader2199/ws.rofs.titanjobfinder.com/wsproxy.conf

# Rebuild vhosts and restart Apache
/usr/local/cpanel/scripts/rebuildhttpdconf
systemctl restart httpd
```

> If your box uses the newer `ea-apache24` paths, the base dir is the same
> (`/etc/apache2/conf.d/userdata/...`). Confirm the exact folder with
> `ls /etc/apache2/conf.d/userdata/`.

---

## 10. Run migrations and seed content [SSH]

```bash
cd /home/shader2199/realmofforgottensouls.com/apps/api
php bin/migrate.php
php bin/seed.php
```

You should see the migrations apply (schema + game systems) and the seed load
Hammerfall content. Re-running migrations is safe (they are idempotent).

---

## 11. Install the Go world server as a systemd service [WHM root]

The uploaded binary is a static Linux x86-64 ELF, so no Go toolchain is needed.

```bash
cd /home/shader2199/realmofforgottensouls.com
chmod +x apps/world/bin/world

cat > /etc/systemd/system/rofs-world.service <<'EOF'
[Unit]
Description=RoFS Go World Server
After=network.target redis.service mariadb.service

[Service]
Type=simple
User=shader2199
Group=shader2199
WorkingDirectory=/home/shader2199/realmofforgottensouls.com
EnvironmentFile=/home/shader2199/realmofforgottensouls.com/.env
ExecStart=/home/shader2199/realmofforgottensouls.com/apps/world/bin/world
Restart=on-failure
RestartSec=5
# Hardening
NoNewPrivileges=true
ProtectSystem=full

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now rofs-world
systemctl status rofs-world --no-pager
```

Confirm it is listening locally:

```bash
curl -s http://127.0.0.1:8081/health   # -> {"status":"ok","service":"world"}
```

> After every `deploy.bat -WorldOnly`, restart the service:
> `sudo systemctl restart rofs-world` (and `chmod +x` the binary if needed).

---

## 12. Issue TLS certificates [cPanel]

cPanel -> **SSL/TLS Status** -> select the three subdomains -> **Run AutoSSL**.
Wait until each shows a valid certificate. HTTPS/WSS depend on this.

---

## 13. Health checks (end-to-end)

| Check | Command / URL | Expected |
|-------|---------------|----------|
| MariaDB | `mysql -u shader2199_rofs -p shader2199_rofs -e 'SELECT 1;'` | `1` |
| Redis | `redis-cli -a '<pass>' ping` | `PONG` |
| World (local) | `curl http://127.0.0.1:8081/health` | `{"status":"ok","service":"world"}` |
| World (proxied) | `curl https://ws.rofs.titanjobfinder.com/health` | `{"status":"ok","service":"world"}` |
| API | `curl https://api.rofs.titanjobfinder.com/health` | `{"status":"ok","service":"api"}` |
| PWA | open `https://rofs.titanjobfinder.com` | welcome screen loads |

A browser check: open the PWA, log in, and enter the world — the client should
open a `wss://ws.rofs.titanjobfinder.com/ws` connection (visible in DevTools ->
Network -> WS).

---

## 14. Log locations

| Service | Path |
|---------|------|
| Go world | `/home/shader2199/realmofforgottensouls.com/apps/world/logs/world.log` and `journalctl -u rofs-world -f` |
| PHP API | `/home/shader2199/realmofforgottensouls.com/apps/api/var/log/api.log` |
| Apache (this account) | `/home/shader2199/access-logs/` and `/etc/apache2/logs/error_log` |
| MariaDB | `/var/log/mariadb/mariadb.log` |
| Redis | `journalctl -u redis -f` |

To raise verbosity temporarily, set `APP_DEBUG=true`, `API_LOG_LEVEL=debug`,
`WORLD_LOG_LEVEL=debug` in `.env`, then `systemctl restart rofs-world` and
re-test. **Return to `info`/`false` in production** when done.

---

## 15. Updating the deployment

```bat
:: On Windows, from the repo root:
deploy.bat            :: full rebuild + upload
deploy.bat -WorldOnly :: just the Go server
deploy.bat -ApiOnly   :: just the PHP API
deploy.bat -WebOnly   :: just the PWA (after editing the repo-root .env.production)
```

On the server after a world-server update:

```bash
chmod +x /home/shader2199/realmofforgottensouls.com/apps/world/bin/world
sudo systemctl restart rofs-world
```

If migrations changed, re-run step 10.

---

## 16. Troubleshooting

- **PWA loads but "network error" on login:** `VITE_API_URL` in the repo-root
  `.env.production` is wrong or AutoSSL not issued for `api.*`. Rebuild with
  `deploy.bat -WebOnly` and re-run AutoSSL.
- **API 500 on every request:** check `apps/api/var/log/api.log` with
  `APP_DEBUG=true`. Most common cause is bad `MYSQL_*` values — confirm the
  cPanel-prefixed DB/user names and privileges (step 5).
- **API "could not find driver":** the API subdomain is on a PHP version without
  `pdo_mysql`. Set it to `ea-php83` in MultiPHP Manager (step 4).
- **WebSocket fails to connect (`wss` error):** verify `httpd -M` shows
  `proxy_wstunnel_module` (step 3), the include exists (step 9), Apache was
  restarted, and `curl http://127.0.0.1:8081/health` works.
- **World service won't start:** `journalctl -u rofs-world -e`. Common causes:
  binary not executable (`chmod +x`), Redis unreachable/wrong password, or a bad
  value in `.env` (systemd reads it as `EnvironmentFile`; keep values on single
  lines, quote any value containing spaces).
- **`.env` visible in a browser:** the PWA subdomain Document Root is still the
  project root. Point it at `apps/web/dist` (step 6).
- **Redis "NOAUTH"/"AUTH failed":** `REDIS_PASSWORD` in `.env` must match
  `requirepass` in `redis.conf`.

## Related

- `documentation/operations/ProjectStartupGuide.md` — owner tasks after the code
  is delivered (Firebase, Stripe, first admin account, smoke test, backups).
