Guide7 min read6 sections

Coolify private beta deployment

Configure and operate the Darkwind reference game on a Dusk host.

docs/COOLIFY.mdSource updated 2026-10-28
Markdown
On this page 6 sections

This deploys Darkwind, the reference game, on the Dusk engine. Settings use the DUSK_ prefix; the earlier DW_ names below remain supported, so an existing application needs no change. See Dusk compatibility for the full mapping and the storage map.

For the latest codex/engine-foundations build with the new characters and arrival art, use the current branch beta handoff. The instructions below describe the earlier beta.

The repository includes compose.coolify.yaml, which builds the production Dockerfile and declares the persistent named volume. One container serves the built game, session API and WebSocket room on port 8787. TLS terminates at Coolify's proxy; browsers connect using HTTPS and WSS on the same public origin. WebGPU requires HTTPS outside localhost.

#Configure the application

  1. Create a Coolify application from the private GitHub repository jasona/dusk-engine (formerly jasona/awaken-engine; the earlier beta used jasona/darkwind-web), branch main, using a GitHub App or deploy key with repository access.
  2. Select Docker Compose as the build pack. Use / as the base directory and /compose.coolify.yaml as the Compose file location. Load/reload the Compose configuration. Leave build and start command overrides empty.
  3. Add an HTTPS domain for the darkwind service, with internal port 8787, for example https://darkwind.example.com. In versions with a separate domain port field, set it to 8787; versions using the domain URL for the upstream port accept https://darkwind.example.com:8787. Players visit the ordinary HTTPS URL without :8787. No host port mapping is needed.
  4. Add the runtime environment variables below. Replace the example hostname with the exact public origin players will visit.
  5. Review Persistent Storage: the Compose file declares darkwind-room:/data, so Docker creates the volume automatically. Coolify may prefix its name for this application. No manual volume creation is needed. Keep this resource and its volume across redeployments. The image runs as UID/GID 1000:1000; an existing directory or restored volume must be writable by that user. Do not mount over /app.
  6. Run one instance on one server. Compose handles container replacement without Coolify's application-level rolling updates. The file already sets a 15-second stop grace period. Keep the storage declaration unchanged when updating.
  7. Deploy. Keep the Dockerfile's built-in health check; it checks /health with Node every 15 seconds. Expect healthy, then open the HTTPS domain in a desktop WebGPU browser.
Runtime variable Value
DUSK_ALLOWED_ORIGINS (legacy DW_ALLOWED_ORIGINS) https://darkwind.example.com — required; comma-separated exact origins for multiple domains
DUSK_ROOM_PASSWORD (legacy DW_ROOM_PASSWORD) A shared private playtest passphrase; recommended for an internet-accessible beta
DUSK_RELAY_PORT 8787 — fixed by the Compose file (proxy port)
DUSK_DATA_DIR /data — fixed by the Compose file; must match the volume destination
DUSK_STATIC_DIR /app/dist — fixed by the Compose file
NODE_ENV production — image default

Set DUSK_ALLOWED_ORIGINS and DUSK_ROOM_PASSWORD in Coolify (or keep existing DW_ variables); the remaining values are already set in the image/Compose file. Setting a different port, data directory or static directory under a DUSK_ name conflicts with the file and stops the container. A missing origin list now stops the server at startup (it is no longer a Compose interpolation error), so check the logs if the container is unhealthy. Coolify may not list these variables automatically; add them under Environment Variables. These are server runtime settings. Do not add VITE_ prefixes or bake the passphrase into the client. Compose rejects a missing origin list. HTTPS redirects, certificate provisioning and WebSocket forwarding are handled by the proxy; retain Coolify's generated proxy configuration.

Coolify references: Git-based Compose builds and declared storage, persistent storage, container health checks, and Compose replacement behavior.

If you already created a Dockerfile-build application, either keep that build pack and manually mount a persistent volume at /data, or configure the Compose application before saving important progress. Changing resource types does not automatically migrate an existing save; back up and restore its complete volume if needed. A Dockerfile VOLUME declaration alone does not specify the stable named mount we need across replacement containers.

#Test the deployed room

  1. Check https://YOUR_DOMAIN/health returns JSON with ready: true.
  2. Open two independent browser profiles, enter the passphrase and confirm 2 travelers · Connected. Verify moving avatars, a shared door, chat and exclusive lantern pickup using the playtest guide.
  3. Open a shared door, then redeploy the same commit in Coolify. A brief disconnect is expected. Both clients should reconnect; the door and acknowledged shared progress must survive. The same tab's session should remain valid.
  4. Repeat on two physical computers over HTTPS. Browser tests are automated; hardware performance and internet play still need this human check.

#Saves, updates and recovery

/data/room.json contains shared progress and /data/session-key signs traveler sessions. Stop the application before backing up the whole volume. Restore it to a volume owned by UID/GID 1000:1000 with the same build/content version. Never commit this data or include it in an image. Client settings and position remain browser-local.

This beta has one coordinator and a local JSON save. It needs stop-then-start deployment, with a brief outage. A kernel file lock on the mounted volume rejects a second writer with exit code 75; this is a safeguard, not support for rolling updates or multiple replicas. Use a local Docker volume on one server, not a network filesystem or multi-server deployment. Never delete the lock file to bypass a running process.

On shutdown, the development relay stops new admissions and upgrades, terminates room sockets, and gives active HTTP responses up to two seconds to drain. Remaining sockets—including partial requests and stalled downloads—are then destroyed. This keeps a slow client from holding the process open until the ten-second watchdog fails. Acknowledged room changes are already fsynced before this drain; an unfinished admission cannot create a new session after shutdown starts. Docker's fifteen-second grace period still leaves room for the watchdog and process cleanup.

For a fresh test, stop the application and attach a new named volume while retaining the old one. If a future content update is incompatible, startup refuses the save; restore the previous image or use an explicit migration. Existing local saves from before the porch fix need the migration described in the playtest guide before importing them.

Troubleshooting:

  • Unhealthy / exited: inspect application logs for missing origins, unwritable /data, incompatible content, or another active writer. Correct the configuration and redeploy.
  • Page loads but admission fails: check the browser's exact HTTPS origin appears in DW_ALLOWED_ORIGINS, and verify the passphrase.
  • Room disconnects: confirm the proxy forwards /room as a WebSocket and only one instance is active.
  • Progress resets: check that the same persistent volume is mounted at /data after replacement.
  • No WebGPU: use HTTPS and a supported desktop browser with hardware acceleration; inspect its GPU support.

#Local Docker verification

Terminal
docker compose up --build -d
# Open http://localhost:8787 in two browser profiles.
docker compose logs -f
docker compose down

The local compose file binds only to localhost and preserves its named volume on down. If port 8787 is occupied, run DUSK_HOST_PORT=8790 DUSK_ALLOWED_ORIGINS=http://localhost:8790 npm run compose -- up --build -d; plain docker compose reads the legacy DW_HOST_PORT. .env.example shows production settings; copying it unchanged would reject localhost admission.

With Node 22.23.4 and Docker available, npm ci && npm run test:container builds the actual image and verifies non-root execution, the packaged isolated authoring compiler, static assets, origin/passphrase admission, two WebSocket clients sharing a door, exclusive storage locking, graceful shutdown, saved progress/session recovery and Docker health. It creates and removes only its own uniquely named image, container and volume. GitHub Actions runs this alongside the engine and browser suites.

The image also contains the packaged authoring CLI and build worker at /app/author-dist. These are file tools for explicit operator use; they do not add a public build endpoint, automatically change room content or require another persistent volume.

#Published engines across redeployments

The image includes engine-dist; newly published invitations retain their player and multiplayer executable in /data/engine-builds with bindings in /data/engine-pins. The existing persistent volume is sufficient. Back up these directories with the world and room data. Keep the pinned Node image version: cross-Node migration is not supported. See engine pinning for the two-deployment test and legacy release behavior.

#Experimental owned-physics shared room

On codex/engine-foundations, a separate test application can set DW_PHYSICS_BACKEND=owned; both Compose files pass this variable through and retain rapier by default. Give the owned test application its own persistent volume. The room records its backend and refuses to reinterpret an existing Rapier save as owned physics. This setting applies to relay, editor-development and newly published invitation rooms; persistent/trusted and regional deployments also accept the setting in their respective Compose profiles, using the schema-8 setup in PERSISTENT_ROOMS.md.

Open the player with ?physics=owned (or add &physics=owned to an invitation) in two browser profiles. Both the HTTP admission and WebSocket handshake require the matching backend. A server restart retains owned shared bodies and sessions. Older pinned invitations keep their older executable, so publish a new invitation to test this implementation. See owned room testing.

End of Coolify deployment