# NGINX Deployment — Kapitano API (AlmaLinux / RHEL 9)

Target: a **VPS/Dedicated Alma or RHEL 9** box running **Nginx + PHP-FPM 8.3** directly (no Apache),
**local Redis** for cache/queue/session, **Let's Encrypt** TLS, queue worker + scheduler, and the
production caches. This replaces the HostGator shared-hosting approach (no custom `index.php`, the
document root is Laravel's real `public/`).

Companion files in this repo:

- `deploy/nginx.conf` — virtual host for `logistic.kapitano.shop`
- `deploy/php-fpm-kapitano.conf` — PHP-FPM pool (dedicated `kapitano` user, limits, OPcache)

---

## 1. Prerequisites & base firewall

```sh
sudo dnf update -y
sudo dnf install -y epel-release git

# open only what is needed (add 22 for SSH, 80/443 for web)
sudo firewall-cmd --permanent --add-service=ssh
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload
```

## 2. PHP 8.3 (Remi), Nginx, Redis

```sh
sudo dnf install -y https://rpms.remirepo.net/enterprise/remi-release-9.rpm

sudo dnf module reset php -y
sudo dnf module enable php:remi-8.3 -y
sudo dnf install -y php php-cli php-fpm php-mysqlnd php-pdo php-mbstring php-xml \
    php-curl php-zip php-intl php-gd php-bcmath php-opcache php-pecl-redis

sudo dnf install -y nginx redis
```

## 3. Dedicated app user + directory

```sh
sudo useradd -r -s /sbin/nologin -d /var/www/kapitano kapitano
sudo mkdir -p /var/www/kapitano
git clone https://gitlab.com/alihytham97/kapitano_logistic.git /var/www/kapitano/api
cd /var/www/kapitano/api
git checkout Captain-Approval-And-Authentication
sudo chown -R kapitano:kapitano /var/www/kapitano/api
```

## 4. Composer dependencies (no dev)

```sh
cd /var/www/kapitano/api
curl -sS https://getcomposer.org/installer -o composer-setup.php
php composer-setup.php --install-dir=/usr/local/bin --filename=composer
sudo -u kapitano composer install --no-dev --optimize-autoloader --no-interaction
```

## 5. PHP-FPM pool

```sh
sudo cp deploy/php-fpm-kapitano.conf /etc/php-fpm.d/kapitano.conf
sudo systemctl enable --now php-fpm
sudo systemctl restart php-fpm
```

Verify the socket appears:

```sh
ls -l /run/php-fpm/
```

## 6. Nginx virtual host

```sh
sudo cp deploy/nginx.conf /etc/nginx/conf.d/logistic.kapitano.shop.conf
sudo rm -f /etc/nginx/conf.d/default.conf        # avoid a competing default server
sudo nginx -t
sudo systemctl enable --now nginx
```

## 7. SELinux (keep it enforcing)

```sh
# php-fpm/nginx must write runtime dirs
sudo semanage fcontext -a -t httpd_sys_rw_t "/var/www/kapitano/api/storage(/.*)?"
sudo semanage fcontext -a -t httpd_sys_rw_t "/var/www/kapitano/api/bootstrap/cache(/.*)?"
sudo restorecon -Rv /var/www/kapitano/api/storage /var/www/kapitano/api/bootstrap/cache

# the app calls out to external services (SMS OTP, routing engine…)
sudo setsebool -P httpd_can_network_connect on
```

## 8. `.env` (production, Redis-backed)

```sh
cp .env.example .env
```

```dotenv
APP_NAME="Kapitano"
APP_ENV=production
APP_KEY=
APP_DEBUG=false
APP_URL=https://logistic.kapitano.shop
LOG_LEVEL=error
LOG_CHANNEL=stack

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=kapitano
DB_USERNAME=kapitano
DB_PASSWORD=<strong-random>

CACHE_STORE=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis

REDIS_CLIENT=phpredis        # php-pecl-redis is installed above
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
# REDIS_PASSWORD=<set one in /etc/redis.conf and mirror it here>

FILESYSTEM_DISK=public
```

```sh
sudo -u kapitano php artisan key:generate
sudo -u kapitano php artisan migrate --force
sudo -u kapitano php artisan storage:link
sudo -u kapitano php artisan config:cache
sudo -u kapitano php artisan route:cache
sudo -u kapitano php artisan view:cache
sudo -u kapitano php artisan l5-swagger:generate --all
```

(`storage:link` works here because the doc root is the real `public/` — no manual symlink needed.)

**`--all` is required.** The API has two OpenAPI documentations (`config/l5-swagger.php`): *captain*
and *dashboard*, and `default` is `dashboard`. A bare `l5-swagger:generate` builds only the
dashboard spec, so `/api/documentation/captain` opens but its `docs?captain-docs.json` answers
**404**. The generated files (`storage/api-docs/*-docs.json`) are not committed — they must be
built on every deploy.

Optional demo data: `sudo -u kapitano php artisan db:seed --force`.

## 9. Redis service

```sh
sudo systemctl enable --now redis
sudo systemctl status redis --no-pager
sudo -u kapitano php artisan dispatch:redis-check
```

Every row must say `OK`.

## 10. Queue worker (systemd)

```sh
sudo tee /etc/systemd/system/kapitano-queue.service > /dev/null << 'EOF'
[Unit]
Description=Kapitano queue worker
After=network.target redis.service

[Service]
User=kapitano
Group=kapitano
WorkingDirectory=/var/www/kapitano/api
ExecStart=/usr/bin/php /var/www/kapitano/api/artisan queue:work redis --sleep=3 --tries=1 --timeout=60 --max-time=3600
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now kapitano-queue
```

Two flags matter for push notifications (`docs/runbook.md` §1):

- **`--tries=1`** — FCM has no idempotency key. A notification job that reached Firebase and then
  failed would be delivered **again** on every retry, so a captain gets the same order twice. With
  one attempt a failure lands in `failed_jobs`, where the back office retries it deliberately
  (`/api/dashboard/push/failed-jobs`).
- **`--timeout=60`** — must stay below the queue connection's `retry_after` (90 s). A timeout equal
  to it lets a second worker claim a job the first is still running, which again pushes twice.

Every deploy must run `php artisan queue:restart`: a running worker keeps the old code in memory.

## 11. Scheduler (GPS history flush, notifications)

```sh
sudo crontab -u root -e
```
Add one line (adjust php path via `which php`):

```cron
* * * * * /usr/bin/php /var/www/kapitano/api/artisan schedule:run >> /dev/null 2>&1
```

## 12. TLS — Let's Encrypt

```sh
sudo dnf install -y certbot python3-certbot-nginx
sudo certbot --nginx -d logistic.kapitano.shop
```

Renewal is automatic: `systemctl enable --now certbot-renew.timer` (package provides the timer).
On RHEL the post-hook is auto-added; confirm with `certbot renew --dry-run`.

## 13. Verify

```sh
curl -I http://logistic.kapitano.shop                     # expect 301 to https
curl --http2 -I https://logistic.kapitano.shop            # expect HTTP/2 200, nosniff, HSTS headers
curl -X POST https://logistic.kapitano.shop/api/driver/auth/login \
  -H "Accept: application/json" -H "Accept-Language: en" \
  -d '{"phone":"+966500000008"}'
# -> 200 {"Model":null,"Status":true,"Message":"We have sent your verification code!"...}

/usr/bin/php /var/www/kapitano/api/artisan dispatch:redis-check   # all OK
/usr/bin/php /var/www/kapitano/api/artisan about                    # APP_ENV=production, Redis connected

# Both API specs must load — a 404 on either means l5-swagger:generate ran without --all
curl -s -o /dev/null -w "%{http_code}\n" "https://logistic.kapitano.shop/api/documentation/captain/docs?captain-docs.json"     # 200
curl -s -o /dev/null -w "%{http_code}\n" "https://logistic.kapitano.shop/api/documentation/dashboard/docs?dashboard-docs.json" # 200
```

Error paths to look at: `/var/log/nginx/api-kapitano-shop.error.log`, `/var/log/php-fpm/kapitano-slow.log`,
`/var/www/kapitano/api/storage/logs/laravel.log`.

---

## Hardening checklist

- [ ] `APP_DEBUG=false`, `APP_ENV=production`, a real random `APP_KEY`.
- [ ] `.env` and `.git` are **outside** the web root (they are — root is `.../public`).
- [ ] Files owned by `kapitano:kapitano`, nothing world-writable: `sudo -u kapitano chmod -R 775 storage bootstrap/cache` (never `777`).
- [ ] `listen.mode 0660` + `listen.owner/group nginx` (already in the pool) — only nginx reaches the socket.
- [ ] SSH: `PermitRootLogin no`, key-only auth, change the SSH port optional; block with `firewalld` (22/80/443 only).
- [ ] `server_tokens off` (already), `display_errors=Off` (already in pool).
- [ ] HSTS active on HTTPS; do NOT add `preload` until you have verified every subdomain is HTTPS.
- [ ] TLS 1.2+ only (already); tighten ciphers to the list in `deploy/nginx.conf`.
- [ ] Set a Redis password (`requirepass` in `/etc/redis/redis.conf`), or keep `bind 127.0.0.1` and never expose 6379 in the firewall.
- [ ] Database: use the `kapitano` user with least privileges on only the `kapitano` DB, strong password.
- [ ] Pull security updates weekly: `sudo dnf update`; watch-announce via Alma/CVE RSS.
- [ ] Add **fail2ban** for SSH (and nginx later if you log 401 sprees).
- [ ] App secret hygiene: never commit `.env`, revoke any leaked tokens; rotate OTP/SMS and cloud keys.
- [ ] Backups: nightly `mysqldump` + `rsync` of `storage/app` to another host; restore tested.
- [ ] Log rotation: nginx/php-fpm/laravel logs rotate (logrotate default covers nginx; add php-fpm + laravel entries).
- [ ] `php artisan optimize` is on; OPcache `validate_timestamps=1` with `revalidate_freq=60` — set `0` only when deploys always run `config:cache`/`optimize:clear`.
- [ ] On deploy: `git pull && composer install --no-dev && php artisan migrate --force && php artisan optimize:clear && php artisan optimize && php artisan l5-swagger:generate --all && php artisan queue:restart`.

---

## Moving from HostGator (optional, keeps your data)

- Dump DB on HostGator: `mysqldump -u <user> -p <db> > kapitano.sql` → import on the VPS:
  `mysql -u kapitano -p kapitano < kapitano.sql`.
- Copy media: `rsync -av ~/apps/captain-api/storage/app/ /var/www/kapitano/api/storage/app/`.
- Switch `APP_URL` and DNS `A` record when ready; keep both hosts up during the cut-over.