Files
Escapepage/doc/test-environment.md
FrankandClaude Sonnet 5 97d35f8fcb Add dedicated setup.test.sh / restart.test.sh for the staging stack
Both refuse to run unless docker/.env names a non-prod COMPOSE_PROJECT_NAME and
scope every compose call to that project.

setup.test.sh: like setup.sh but runs the DB/cache/console steps as www-data and
repairs var/cache|log|sessions ownership at the end, so php-fpm can read its own
compiled cache (bare `docker exec` runs as root -> silent 500s). Never touches
var/volumes/db. Test-appropriate final message (NPM upstream, local curl check).

restart.test.sh: keeps the database by default (--fresh-db to wipe + re-init),
never runs a host-wide docker prune, and only chowns var/cache|log|sessions
(chowning var/volumes/db is what corrupted the MySQL data dir).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-31 00:19:13 +02:00

153 lines
6.5 KiB
Markdown

# Test / staging environment (`test.escapepage.com`)
Runs a second copy of the full Docker stack on the same server as production,
behind the same Nginx Proxy Manager (NPM). Production is untouched: with no
`docker/.env` overrides, `compose.yaml` behaves exactly as before.
## How isolation works
`docker/compose.yaml` derives everything instance-specific from `docker/.env`:
| Concern | Mechanism | prod (no overrides) | test |
|---|---|---|---|
| Container names | `${STACK_NAME:-escapepage}-*` | `escapepage-php` … | `escapepage-test-php` … |
| Compose project (networks, labels) | `COMPOSE_PROJECT_NAME` | `docker` (unchanged) | `escapepage-test` |
| Published ports | `${NGINX_HTTP_PORT:-8080}` etc. | `0.0.0.0:8080/8443/3306/8090/8025` | `127.0.0.1:8081/8444/3307/8091/8026` |
| Database files | bind mount `../var/volumes/db` | per checkout | per checkout |
| Web reachability | `nginx` joined to external `nginx_default` | via NPM | via NPM |
`STACK_NAME` is a plain variable (not the Compose-managed `COMPOSE_PROJECT_NAME`),
so its `:-escapepage` default is reliable and **production needs no `docker/.env`
change** to keep its `escapepage-*` container names.
`nginx`, `mercure` and `mailer` all sit on the external `nginx_default` network,
so NPM forwards straight to `escapepage-test-nginx` / `-mercure` by name — no
public host port needed.
## Production checkout — optional
Nothing is required. Two optional tidy-ups, each needing a `docker compose up -d`
to recreate the affected container:
- The `nginx` service now also attaches to `nginx_default`. If you'd rather have
NPM forward to `escapepage-nginx` by name instead of `host:8080`, recreate it
and repoint the NPM proxy host. Otherwise the published `8080/8443` still work
as before and you can ignore this.
- Add `STACK_NAME=escapepage` and `COMPOSE_PROJECT_NAME=escapepage` to the
production `docker/.env` to move it off the implicit `docker` project name.
Cosmetic; do it only if you want the two stacks named symmetrically.
## Standing up the test stack
### 1. DNS
`test.escapepage.com` → server IP. (`mercure-test.escapepage.com` too if you want
live game features.) On Cloudflare, DNS-only ("grey cloud") keeps it low-profile.
### 2. Separate checkout
```bash
git clone <repo> /opt/escapepage-test
cd /opt/escapepage-test
git checkout <branch-to-test>
```
Use a **separate clone**, not `git worktree` — the Docker build context is the
checkout directory and full isolation avoids surprises.
### 3. Compose env
```bash
cp docker/.env.test.example docker/.env
# edit: real passwords, fresh MERCURE_JWT_SECRET (openssl rand -hex 32), reCAPTCHA keys
```
### 4. Symfony env overrides
Create `/opt/escapepage-test/.env.local` in the checkout (git-ignored):
```
APP_SECRET=<openssl rand -hex 16>
# Behind NPM the forwarded client IP lands from a Docker-private range; trust them
# all on staging so URL generation / HTTPS detection work.
TRUSTED_PROXIES=127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
```
`APP_ENV=prod`, `DATABASE_URL`, `SITE_BASE_URL`, `MERCURE_*` and `MAILER_DSN` are
already supplied to the containers by `docker/.env`.
### 5. Build & start
```bash
./docker/setup.test.sh
```
The test-specific script (not `setup.sh`): it refuses to run unless
`COMPOSE_PROJECT_NAME` in `docker/.env` is a non-prod value, scopes every compose
call to that project, runs the DB/cache/console steps as `www-data`, and repairs
`var/cache` / `var/log` / `var/sessions` ownership at the end (bare `docker exec`
runs as root, which otherwise leaves php-fpm unable to read its own cache — a
silent 500). It never touches `var/volumes/db`.
Builds `escapepage-test-*` images, starts the containers, creates + migrates
`escapepage_test`, builds assets. Re-run any time; `--no-build` skips the rebuild.
### 6. Nginx Proxy Manager — proxy host
- Domain: `test.escapepage.com`
- Forward to: `escapepage-test-nginx`, port **443**, scheme **https**
(the app nginx force-redirects 80→443 and serves a self-signed cert; leave
"Verify SSL" off — same arrangement as production)
- SSL: request a Let's Encrypt cert, Force SSL, HTTP/2
- Optional: add response header `X-Robots-Tag: noindex`
If you want live game screens, add a second proxy host
`mercure-test.escapepage.com` → `escapepage-test-mercure` port `80` (http).
### 7. Restrict access to your IP — NPM Access List
A host firewall on 80/443 can't help: prod and test share those ports on NPM.
Restrict at the proxy instead.
- **NPM → Access Lists → Add Access List**
- Name: e.g. `staging-allowlist`
- Authorisation: leave empty
- Access: `Allow <your.public.ip>` then a final `Deny all`
- Satisfy: **Any**
- Edit the `test.escapepage.com` proxy host → **Access List** → select it.
- Do the same on `mercure-test.escapepage.com` if you added it.
Everyone else gets `403` at the proxy. Update the IP in one place when it changes.
Defence in depth (optional): in `/opt/escapepage-test/docker/nginx/default.conf`,
inside the `server { listen 443 ... }` block:
```nginx
set_real_ip_from 172.16.0.0/12; # NPM's docker network
real_ip_header X-Forwarded-For;
allow <your.public.ip>;
deny all;
```
## Deploying a new version to test
```bash
cd /var/sites/escapepage-test
git fetch && git checkout <branch> && git pull
./docker/setup.test.sh --no-build # composer install, migrate, build assets, fix perms
```
`--no-build` skips the image rebuild; drop it if the Dockerfile changed.
## Restarting
```bash
./docker/restart.test.sh # down + up, keeps the DB and images
./docker/restart.test.sh --build # also rebuild images
./docker/restart.test.sh --fresh-db # also wipe var/volumes/db and re-init MySQL
```
## Safety notes
- Use the **`*.test.sh`** scripts on this checkout, not `setup.sh` / `restart.sh`.
Both refuse to run unless `docker/.env` names a non-prod
`COMPOSE_PROJECT_NAME`, and both scope every action to that project — they
can't reach the production stack.
- `restart.test.sh` **keeps the database** by default (you loaded prod data into
it); `--fresh-db` is the only thing that wipes it. It never runs a host-wide
`docker system prune` / `docker builder prune`, and it only repairs ownership
of `var/cache` / `var/log` / `var/sessions` — **never `var/volumes/db`**
(chowning the MySQL data dir is what corrupted it earlier this build).
- The test `docker/.env` uses its own `DB_NAME` and passwords so a config slip
can't reach the production database.
- `MAILER_DSN=smtp://mailer:1026` keeps staging mail inside Mailpit instead of
sending through Mailgun.