# 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 /opt/escapepage-test cd /opt/escapepage-test git checkout ``` 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= # 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 ` 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 ; deny all; ``` ## Deploying a new version to test ```bash cd /var/sites/escapepage-test git fetch && git checkout && 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.