Deploy the puzzle relay alongside the container stack

Adds a puzzle-relay service (docker-compose.yml) running the same
image as php but executing app:puzzle-relay instead of php-fpm, kept
off the host network - only nginx can reach it. nginx proxies
wss://dmtools.fvandenberg.nl/puzzle-ws/ through to it
(docker/nginx/default.conf), with the public URL set via a new
.env.prod. setup.sh/restart.sh now rebuild, restart, and clean up the
new container alongside the existing ones.

Still needs "Websockets Support" enabled on the NPM proxy host for
this domain, or the upgrade headers never reach the container.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Frank
2026-09-12 13:36:38 +02:00
co-authored by Claude Sonnet 5
parent 2ccbf4ca67
commit efba34a1e2
5 changed files with 49 additions and 5 deletions
+8
View File
@@ -0,0 +1,8 @@
# Committed prod defaults (see .env for the full explanation of the cascade).
# The relay itself still binds 0.0.0.0:8091 inside its own container (see
# PUZZLE_RELAY_LISTEN in .env) - it's never published to the host or the
# internet directly. Browsers instead reach it through this same domain, via
# the /puzzle-ws/ proxy in docker/nginx/default.conf, which forwards to the
# puzzle-relay container over the docker network.
PUZZLE_RELAY_PUBLIC_URL=wss://dmtools.fvandenberg.nl/puzzle-ws/
+16
View File
@@ -33,6 +33,21 @@ services:
condition: service_healthy condition: service_healthy
restart: unless-stopped restart: unless-stopped
puzzle-relay:
build:
context: .
dockerfile: docker/php/Dockerfile
container_name: dmtools-puzzle-relay
command: ["php", "bin/console", "app:puzzle-relay"]
env_file:
- path: .env
- path: .env.local
required: false
volumes:
- .:/var/www/html:cached
- php_var:/var/www/html/var
restart: unless-stopped
nginx: nginx:
image: nginx:1.30-alpine image: nginx:1.30-alpine
container_name: dmtools-nginx container_name: dmtools-nginx
@@ -43,6 +58,7 @@ services:
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
depends_on: depends_on:
- php - php
- puzzle-relay
restart: unless-stopped restart: unless-stopped
database: database:
+18
View File
@@ -48,6 +48,24 @@ server {
return 404; return 404;
} }
# Proxies the puzzle relay's WebSocket connections (src/Command/
# PuzzleRelayCommand.php, a separate long-running container - see
# docker-compose.yml) through this same domain, so the browser can use a
# plain wss://dmtools.fvandenberg.nl/puzzle-ws/ URL instead of a second
# exposed port. NPM must also have "Websockets Support" enabled for this
# proxy host, or the Upgrade header never reaches here.
location /puzzle-ws/ {
resolver 127.0.0.11 valid=10s;
set $puzzle_relay dmtools-puzzle-relay:8091;
proxy_pass http://$puzzle_relay/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 1h;
proxy_send_timeout 1h;
}
error_log /var/log/nginx/dmtools_error.log; error_log /var/log/nginx/dmtools_error.log;
access_log /var/log/nginx/dmtools_access.log; access_log /var/log/nginx/dmtools_access.log;
+1 -1
View File
@@ -23,7 +23,7 @@ done
echo "Restarting stack: $PROJECT" echo "Restarting stack: $PROJECT"
(cd "$ROOT_DIR" && docker compose -p "$PROJECT" -f docker-compose.yml down --remove-orphans) || true (cd "$ROOT_DIR" && docker compose -p "$PROJECT" -f docker-compose.yml down --remove-orphans) || true
docker rm -f dmtools-php dmtools-nginx dmtools-db 2>/dev/null || true docker rm -f dmtools-php dmtools-puzzle-relay dmtools-nginx dmtools-db 2>/dev/null || true
for net in $(docker network ls -q --filter "name=^${PROJECT}_" 2>/dev/null); do for net in $(docker network ls -q --filter "name=^${PROJECT}_" 2>/dev/null); do
docker network rm "$net" || true docker network rm "$net" || true
done done
+6 -4
View File
@@ -65,7 +65,7 @@ fi
# docker-compose v1 can choke recreating a container in place on any config # docker-compose v1 can choke recreating a container in place on any config
# change; removing them first sidesteps that. Safe: state lives in named volumes. # change; removing them first sidesteps that. Safe: state lives in named volumes.
dc rm -fs php nginx database 2>/dev/null || true dc rm -fs php puzzle-relay nginx database 2>/dev/null || true
BUILD_ARGS=() BUILD_ARGS=()
[ "$REBUILD" -eq 1 ] && BUILD_ARGS+=("--build") [ "$REBUILD" -eq 1 ] && BUILD_ARGS+=("--build")
@@ -97,7 +97,7 @@ if grep -q '^APP_SECRET=$' "$ROOT_DIR/.env" 2>/dev/null \
&& ! grep -q '^APP_SECRET=' "$ROOT_DIR/.env.local" 2>/dev/null; then && ! grep -q '^APP_SECRET=' "$ROOT_DIR/.env.local" 2>/dev/null; then
echo "Generating APP_SECRET in .env.local..." echo "Generating APP_SECRET in .env.local..."
printf 'APP_SECRET=%s\n' "$(openssl rand -hex 16)" >> "$ROOT_DIR/.env.local" printf 'APP_SECRET=%s\n' "$(openssl rand -hex 16)" >> "$ROOT_DIR/.env.local"
dc up -d php # pick up the new env value dc up -d php puzzle-relay # pick up the new env value
fi fi
echo "Creating database if it doesn't exist..." echo "Creating database if it doesn't exist..."
@@ -116,8 +116,10 @@ pexec chown -R www-data:www-data var
# php.ini sets opcache.validate_timestamps=0, so php-fpm keeps serving the # php.ini sets opcache.validate_timestamps=0, so php-fpm keeps serving the
# old compiled code/templates until it restarts. Bounce it so a --no-build # old compiled code/templates until it restarts. Bounce it so a --no-build
# run (git pull + this script) actually picks up the new cache. # run (git pull + this script) actually picks up the new cache. puzzle-relay
dc restart php # is a single long-running process, not php-fpm workers, but it's exactly as
# stale otherwise: it keeps running whatever code was loaded when it started.
dc restart php puzzle-relay
# Make sure Nginx Proxy Manager can reach this stack's nginx by name. # Make sure Nginx Proxy Manager can reach this stack's nginx by name.
# Harmless (and a no-op) if already connected; NPM keeps it across restarts. # Harmless (and a no-op) if already connected; NPM keeps it across restarts.