compose.yaml / compose.override.yaml:
- container_name is now ${STACK_NAME:-escapepage}-* (STACK_NAME is a plain var,
not COMPOSE_PROJECT_NAME, so prod keeps its escapepage-* names with no config change)
- every published host port is ${*_PORT:-<current default>}, so prod is unchanged
and a second stack can bind its own (localhost-only) ports
- nginx joins the external nginx_default network so Nginx Proxy Manager can
forward to <stack>-nginx by name
restart.sh:
- scoped to STACK_NAME / COMPOSE_PROJECT_NAME read from docker/.env, so running it
from the test checkout can't touch the prod stack
- host-wide `docker system prune` / `docker builder prune` moved behind --prune-all
Adds docker/.env.test.example and doc/test-environment.md (separate checkout,
env layers, NPM proxy host + Access List IP allowlist, Mercure on test).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
140 lines
5.7 KiB
Markdown
140 lines
5.7 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.sh
|
|
```
|
|
Builds `escapepage-test-*` images, starts the containers, creates + migrates
|
|
`escapepage_test`, builds assets. Re-run any time; `--no-build` skips the image
|
|
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 /opt/escapepage-test
|
|
git fetch && git checkout <branch> && git pull
|
|
docker compose -f docker/compose.yaml -f docker/compose.override.yaml \
|
|
exec php composer install
|
|
docker compose ... exec php php bin/console doctrine:migrations:migrate -n
|
|
docker compose ... exec php php bin/console cache:clear
|
|
docker compose ... exec php npm ci && ... npm run build
|
|
docker restart escapepage-test-php-worker
|
|
```
|
|
(Or just `./docker/setup.sh --no-build`.)
|
|
|
|
## Safety notes
|
|
|
|
- **`docker/restart.sh` is now scoped to `STACK_NAME` / `COMPOSE_PROJECT_NAME`**
|
|
from `docker/.env` — running it in the test checkout only touches
|
|
`escapepage-test-*`. The host-wide
|
|
`docker system prune` / `docker builder prune` it used to always run are now
|
|
opt-in via `./docker/restart.sh --prune-all`; don't use that flag while the
|
|
other stack shares the host.
|
|
- 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.
|