Files
Escapepage/doc/test-environment.md
T
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

6.5 KiB

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

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

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

./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:

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

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

./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.