# Deploying the demo to IONOS
The demo (`demo/www`) is deployed to **https://mmoframework.fr**, hosted on an IONOS *Hébergement Web Premium*
webspace. That is a **managed webspace**: ssh works, and nothing else does — no `sudo`, no `systemctl`, no
vhost, no `/etc/apache2`. The only way in is to send files over ssh.
That costs nothing here, because `mmo.admin` has always deployed exactly that way: a release is one `.tar.gz`,
uploaded, checksummed *on the target*, extracted beside the running release, and switched onto with a single
atomic symlink swap. Nothing in that needs a privileged host.
Three things do, and all three are declared rather than coded — see mmo.admin's
[deploy.md](../../admin/docs/deploy.md), *Managed webspaces*, for the mechanism:
| the problem | what `deploy.json` says |
|---|---|
| no service to reload after the swap | `"reload": null`, and `"managed": true` on the server so nothing warns about it |
| `127.0.0.1` on a webspace is not this site | `"managed": true` moves the post-activation probe to **your** machine, against `https://mmoframework.fr/` |
| the document root is fixed in the IONOS panel | `"webRoot": null` — on a managed server that is `{folder}/web`, which `install` makes a symlink onto `{folder}/current` |
The server is `ionos` in `deploy.json`, the target `demo`.
## What the target looks like
`folder` is `mmoframework.fr/www`, relative to the server's `root` (the webspace, what `pwd` prints there):
```
/homepages/38/d520689115/htdocs/ the webspace: the server's `root`
mmoframework.fr/www/ ← folder: everything of this target, in one place
web -> current/demo/www ← webRoot: what the IONOS panel serves. Linked ONCE, by `install`
releases/3.1.2/ one extracted artifact, never edited in place
releases/3.1.1/ what a rollback returns to
deploy/ the journal of its deployments, and the lock
current -> releases/3.1.2 the swap. This is the deploy
```
Only what `web` leads to is served for mmoframework.fr: `releases/` and `deploy/` beside it are not.
**`demo/www` is the document root**: `"publicPath": "demo/www"`, so `web` points at `current/demo/www`, and
nothing above it — `configs/`, `packages/`, `vendor/`, `mmo.json` — is in the served tree at all.
`demo/www/.htaccess` routes every request that is not an existing file to `index.php`, and refuses any other
`.php`. It sets no `MMO_MODE_REWRITE`: `index.php` is beside it. (A working copy is still reached through the
`.htaccess` at the root of this repository, which does set it, and rewrites into `demo/www/index.php`.)
`index.php` finds the phars in `../../packages/`, the release root, and the demo's configuration in
`../configs/`: both are read from disk, never over http. `"healthFile": "demo/www/index.php"`: a release package
without that file is refused before it is uploaded.
---
## One-time setup
`php scripts/admin.php setup` asks for everything below, step by step, and writes it — the host key it reads
and pins itself. What follows is what it asks for, and what to check by hand.
### 1. How ssh reaches the webspace
In the IONOS control panel, under **SSH access**, note the host and the username (`uXXXXXXXX`). They are in
`deploy.json`, under `servers.ionos`: `host`, `user`, and `port` (`null`: 22).
The webspace takes a **password**, `"auth": "password"`. It is never in `deploy.json`: it lives in
`deploy.credentials.json` beside it — git-ignored by `*.credentials.json`, mode `0600`, per machine. Every
command refuses to start if that file is readable by anyone else, or tracked by git.
```bash
install -m 600 /dev/null deploy.credentials.json # then write {"servers": {"ionos": {"password": "..."}}}
```
To move to a key instead, register your public key in the panel and set `"auth": "agent"` (your ssh agent holds
the key) or `"auth": "key"` (its path in `deploy.credentials.json`) — see mmo.admin's *Credentials*.
### 2. Pin the server's host key
Public data, and it belongs in the repository: without it the deployment trusts whatever answers at that
address. `setup` reads it for you. By hand — MD5 or SHA1 in hex, which is what the ssh2 extension computes, not
the `SHA256:` form `ssh-keygen -l` prints by default:
```bash
ssh-keyscan home520689115.1and1-data.host 2>/dev/null | ssh-keygen -lf - -E md5
```
It goes in `servers.ionos.fingerprint`, without the `MD5:` prefix.
### 3. The webspace path
`servers.ionos.root` is the absolute path `pwd` prints on the webspace (`setup` reads it). The target's
`folder`, and `webRoot` when it is set, are relative to it.
### 4. Point the domain
In the IONOS panel, set the destination directory of `mmoframework.fr` to **`/mmoframework.fr/www/web`** —
the `webRoot` link `install` makes, written relative to the webspace, as the panel writes it.
If the panel lets you point it at `/mmoframework.fr/www/current/demo/www` instead, do that and set `"webRoot": "none"`:
one link fewer in the chain.
Either way the path Apache resolves goes through a symlink, which needs `FollowSymLinks` (on by default at
IONOS). If it turns out not to be, `Options +FollowSymLinks` in `demo/www/.htaccess` is the one place you can
say so — and the health check at the end of a release is what tells you whether it worked.
### 5. Set the PHP version
In the IONOS panel, set PHP for the webspace (or for `mmoframework.fr/`) to **8.4 or later**. The framework
uses typed class constants and property hooks; an older interpreter fails at parse time, before anything of
ours runs.
### 6. Check everything before touching the server
```bash
cd libs/client
php scripts/admin.php doctor
```
Every line should be green or grey. It checks the declaration, the credentials file, the host key, that the
target answers, and — because this is a shared host and its toolchain is the provider's — that `tar` and a
sha256 tool are actually there.
### 7. Create the layout
```bash
php scripts/admin.php install --targets=demo
```
It prints the plan first and asks before doing anything. If something is already at `web`, it is moved aside
(`web.before-mmo.`), never deleted, and the plan says so. On a managed server `install` writes no
`installTemplates` and runs no `postInstall`: there is no vhost of ours to write.
---
## Releasing
A release is built from the commit `deploy.json` names (`"ref": "develop"`), and the build refuses to run unless
your working tree is **clean and already checked out at that ref** — the `.phar` dependencies are built from
the working tree, so they have to match what is exported.
```bash
cd libs/client
git checkout develop
git status # must be empty: untracked files count too
```
Then:
```bash
php scripts/admin.php plan --targets=demo
```
This changes nothing. It reads the target and prints, step by step, what a release would do. Read it.
```bash
php scripts/admin.php release --targets=demo
```
`demo` is tagged `prod`, so it asks you to type its name before anything is changed — `--yes` does not
satisfy that, deliberately. From a script, `--yes --i-understand`.
The pipeline is: **install → build → assert → ship → activate → prune**.
- **build** exports the commit, builds the phars into it — mmo.client's `package` hook (`Theme::BuildHook`)
compiles the theme into `demo/www/bin` while it does, so the release never ships a stale one — overlays
`demo/www/bin` (generated, never committed), strips the dev-only paths `scripts/admin.php` lists, and packages
it as `build/mmo.client--.tar.gz`. It then reads the tarball back and refuses it if
`demo/www/index.php` is not in it.
- **assert** reads that tarball back and refuses to ship it if it contains `deploy.json`, a key, `src/`,
`.git*` or anything else on its deny-list. Nothing is uploaded until it passes.
- **ship** uploads it, verifies the sha256 **on the webspace**, extracts it into `releases/` beside
the running release.
- **activate** swaps `current`, then asks `https://mmoframework.fr/` — from your machine — whether it answers.
It keeps asking for three minutes (`"healthTimeout": 180`): there is no interpreter to restart on a webspace, so the swap is picked up
as each php-fpm worker's realpath cache expires (120s by default). If it never answers, the release is rolled
back automatically.
Afterwards:
```bash
php scripts/admin.php status --targets=demo # what is live, and whether the web root still points at it
php scripts/admin.php history --targets=demo # every deployment, recorded on the server itself
php scripts/admin.php rollback --targets=demo # back to the previous release, in one symlink swap
```
---
## Notes
**The demo's data.** `demo/www/personnes.json` and `demo/www/clients.json` ship in every release and are not
`shared`: each release starts from the committed copies, so a deploy resets whatever visitors added, and a
rollback returns to the data of the release it goes back to. To keep the data across releases instead, list
those two paths in the target's `shared`: they then live in `shared/`, symlinked into every release, and the
first release seeds them.
**What is not served.** The domain is served through `web`, so `releases/` and `deploy/` beside it are not
reachable from mmoframework.fr. Inside a release only `demo/www` is served: `configs/`, `packages/`,
`vendor/` and `mmo.json` sit above it. In `demo/www`, `index.php` is the one script run over http.
**Assets from `vendor/`.** The demo's config names prism as `{basepath}/../../vendor/prism/*`, not
`{vendor}/prism/*`: in a release `{vendor}` resolves inside `mmo.client.phar`, which holds `src` only, and every
page would fail on the missing folder. The files are copied into `bin/` by the theme build; the page only needs
their names, read from the release's own `vendor/`.
**Build-only tooling is not shipped.** `vendor/google` (the closure compiler: a 30 MB jar plus two zips) is
gone entirely since 3.0.13 — `MMO\client\JSCompiler` bundles the javascript in PHP, so there is no jar and no
jvm to keep off the server. `node_modules` (28 MB of jsdom, for `npm test`) is excluded instead: the tests run
here, never on the server.
**The build's warnings.** `build` warns that it overlays `demo/www/bin` with no `buildCommands` configured:
expected here, the theme is compiled by mmo.client's `package` hook instead. If it warns that the generated
assets are older than the commit being released, that hook did not write them — run
`php scripts/build-theme.php all --prod` by hand and read what it says. A "permission denied" on `demo/www/bin/*` means the dev web server wrote them as
`www-data`: `sudo chown -R $USER demo/www/bin` once, and it stops happening.
### If another domain serves the webspace root
`folder` is under `htdocs`, the webspace itself. If another domain of the webspace points at `htdocs` (or at
`mmoframework.fr/`), then `mmoframework.fr/www/releases/` and `deploy/` are reachable through it: every release,
and the journal. Guard each of them with its own deny-all — a copy of `scripts/deploy-deny.htaccess` as the
`.htaccess` of `releases/` and of `deploy/`. **Never at the root of `folder`**: Apache applies every
`.htaccess` on the path it serves, so a deny-all there makes mmoframework.fr answer 403 too.