# Deploying a white-label instance (Nomad Education, Tanym Education, ...)

Generic runbook for standing up a new branded copy of Axioma as a subdomain of
axioma-study.kz — e.g. `nomad.axioma-study.kz`. Not per-brand: every white-label
client follows these same steps and differs only in the values filled into
`.env` / `laravel/.env` (port, DB name, cookie names, which `MODULE_*` flags
are on). Modeled on how `demo.axioma-study.kz` was deployed on the same server.

Each instance is its **own separate git checkout** — not a second compose
file layered onto production's existing checkout — so its containers,
database, and `.env` are all independent of `/var/www/html` (production) and
of every other branded checkout. Each instance runs its own `db` (MySQL)
container, with a `mem_limit` set on every service (`docker-compose.branded.yml`)
to keep several branded instances from jointly starving the host — this
host has a documented history of memory-pressure outages from a single
extra instance, so watch actual usage (`docker stats`) as more brands are
added, and lower `mem_limit`s or move to a bigger host before that happens
again rather than after.

## 1. Checkout + PHP dependencies

```bash
git clone <this repo> /var/www/<client>
cd /var/www/<client>
docker run --rm -v "$(pwd)/laravel:/app" -w /app composer:2 install --no-interaction --optimize-autoloader
```

`laravel/vendor/` and `laravel/.env` are gitignored, so every fresh checkout
needs this — see the `srvmphtat2mec-laravel-setup-gap` note; skipping it
produces 500s on every route, not an obviously-related error.

## 2. Configure

```bash
cp .env.branded-instance.example .env
cp laravel/.env.example laravel/.env
```

Fill in `.env` (root): `APP_PORT` (pick one unused on this host — production
is 8080), `MYSQL_ROOT_PASSWORD` (a fresh random password for this instance's
own `db` container — not production's root password),
`DB_NAME`/`DB_USER`/`DB_PASS` (this client's own schema + user within that
container), and the three cookie-isolation values — `SESSION_COOKIE`,
`REMEMBER_COOKIE_NAME` (anything distinct from production's), and leave
`SESSION_COOKIE_DOMAIN` blank (host-only cookie — this instance doesn't need
cross-subdomain SSO with `game.axioma-study.kz`). Leave `DB_HOST=db` as-is.

Fill in `laravel/.env`: `APP_KEY` (`docker compose -f docker-compose.branded.yml
-p <project> run --rm app php artisan key:generate`), `DB_*`/`REDIS_*`
matching the root `.env`, and this client's `MODULE_*` flags (see
`laravel/.env.example` — uncomment and set to `false` whichever this client
doesn't get; everything else defaults to `true`).

Pick a compose **project name** for this client (e.g. `axioma-nomad`) — it
keeps containers namespaced apart from `html` (production) and any other
branded checkout on the same host; use it consistently in every command
below.

### Name and logo

`APP_NAME` is now only the **fallback** brand name, and `images/logo.png` only
the fallback logo. Once the instance is up, its admin sets both from inside the
app at `/admin/branding.php` (sidebar → Брендинг) — no file copy or `.env` edit
on the server, and the client can change them later themselves. The uploaded
logo lands in `laravel/storage/app/public/branding/`, inside the bind mount, so
it survives an image rebuild.

Still set `APP_NAME` anyway: it also names the session cookie and the Redis /
cache key prefixes (`laravel/config/session.php`, `cache.php`), and is the
mail from-name. Those deliberately do *not* follow the branding page — renaming
a live instance's session cookie would log everyone out.

## 3. Start containers + apply schema

```bash
docker compose -f docker-compose.branded.yml -p axioma-<client> up -d
scripts/init_branded_db.sh axioma-<client>
```

The first command starts `app`, `db`, and `redis` for this instance (`db`
needs to report healthy before `app` will start, per the `depends_on` in
`docker-compose.branded.yml`). The second creates this client's schema + DB
user on its own `db` container (reading `MYSQL_ROOT_PASSWORD`/`DB_NAME`/
`DB_USER`/`DB_PASS` from this checkout's own `.env`), applies
`docker/mysql/init/*.sql` (schema + migrations) but skips `999_seed.sql` —
that file is local-dev-only fake logins, not for a real client — then
prints the two commands to hash a real password and insert this client's
one admin row.

## 4. DNS + reverse proxy + TLS

A branded client's domain doesn't have to be a subdomain of
`axioma-study.kz` — confirm with the client and use whatever they've
pointed at this server (e.g. `nomad.axicrm.com`).

On the host (same ProxyPass pattern as production's own vhost — see
`srvmphtat2mec-laravel-setup-gap`; this app never runs under the host's own
PHP):

- DNS: A record for `<client's domain>` → the server's IP. Confirm with
  `dig +short <domain>` before running certbot.
- Apache vhost (port 80): `ProxyPass / http://127.0.0.1:<APP_PORT>/` +
  `ProxyPassReverse` the same.
- `certbot --apache -d <client's domain>` for TLS — this adds the `:443`
  vhost and a redirect rule on the `:80` one.
- **Then add `RequestHeader set X-Forwarded-Proto "https"` to the new
  `-le-ssl.conf`'s `<VirtualHost *:443>` block** (needs `a2enmod headers`,
  already on by default). Without it, this app's root `.htaccess`
  (`RewriteCond %{HTTPS} off` + `RewriteCond %{HTTP:X-Forwarded-Proto}
  !https`) can't tell the proxied plain-HTTP hop apart from a real insecure
  request, and force-redirects to HTTPS forever — an infinite redirect loop
  discovered deploying `nomad.axicrm.com` this way. Confirm with `curl -s -o
  /dev/null -w '%{http_code}\n' https://<domain>/login.php` — `200`, not a
  redirect chain.

## 5. Scheduler crontab entry

Laravel's scheduler needs its own per-minute cron line, naming this
instance's own app container (compose names it
`<project>-app-1` by default):

```
* * * * * docker exec axioma-<client>-app-1 php /var/www/html/laravel/artisan schedule:run >> /var/log/<client>-schedule.log 2>&1
```

## Known gap not fixed here

`AuthController::ROLE_DASHBOARDS`'s `student` role redirects unconditionally
to `https://game.axioma-study.kz/student/hero_account.php` — production's
gamification portal, a different app/DB entirely. Harmless as long as this
client doesn't create `student`-role users; if one ever needs student
logins, that redirect needs to become configurable first (same fix the
`phase-71-demo-instance` branch made via `config('app.game_url')`, never
merged to `main`).

## Verification

- Log in as the seeded admin at `https://<client>.axioma-study.kz/login.php`.
- Confirm the sidebar matches this client's `MODULE_*` flags, and — if any
  `MODULE_TEACHER_LESSON_*` / `MODULE_STUDENT_*` flag is off — that the
  teacher's attendance form, the "Добавить нового ученика" modal and the
  student profile page are trimmed to match.
- Confirm cookies in devtools use this instance's `SESSION_COOKIE` /
  `REMEMBER_COOKIE_NAME`, not production's — log in with "remember me",
  restart the browser, confirm it stays logged in without a second password
  entry, and that production (a separate tab/session) is unaffected.
