From 7d965b0b71cd3322ff0732cd685ecd9fee3c5306 Mon Sep 17 00:00:00 2001 From: Natalie Rose Date: Tue, 8 Sep 2026 12:54:07 +1000 Subject: [PATCH] Volumes for handling public content --- .gitignore | 1 + .prettierrc | 11 ++++++ .vscode/settings.json | 3 ++ CLAUDE.md | 79 ++++++++++++++++++++++--------------------- caddy/Caddyfile | 55 ++++++++++++++++++++++++------ docker-compose.yml | 28 +++++++++------ package-lock.json | 29 ++++++++++++++++ package.json | 11 ++++++ 8 files changed, 159 insertions(+), 58 deletions(-) create mode 100644 .prettierrc create mode 100644 .vscode/settings.json create mode 100644 package-lock.json create mode 100644 package.json diff --git a/.gitignore b/.gitignore index 1728948..17ca2fd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .env etc/lacuna.conf data/ +node_modules/ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..10eaf2f --- /dev/null +++ b/.prettierrc @@ -0,0 +1,11 @@ +{ + "trailingComma": "all", + "arrowParens": "always", + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": true, + "jsxSingleQuote": true, + "printWidth": 100, + "proseWrap": "always" +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ad92582 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.formatOnSave": true +} diff --git a/CLAUDE.md b/CLAUDE.md index 7a2da77..e5c093e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,20 +1,21 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +This file provides guidance to Claude Code (claude.ai/code) when working with code in this +repository. ## What this repo is -Docker Compose orchestration for deploying **The Lacuna Expanse** (TLE), a browser MMO. -It contains **no application source** — every service except `logrotate` runs a -prebuilt image pulled by `:latest` tag from the private registry at `$CONTAINER_REGISTRY_URL` -(a Gitea instance). To change server, frontend, or v2 API behaviour you work in the sibling -repos (`../server` Perl backend, `../app` and `../client` frontends, `../v2-api`) and publish -new images; this repo only wires them together and supplies runtime config. +Docker Compose orchestration for deploying **The Lacuna Expanse** (TLE), a browser MMO. It contains +**no application source** — every service except `logrotate` runs a prebuilt image pulled by +`:latest` tag from the private registry at `$CONTAINER_REGISTRY_URL` (a Gitea instance). To change +server, frontend, or v2 API behaviour you work in the sibling repos (`../server` Perl backend, +`../app` and `../client` frontends, `../v2-api`) and publish new images; this repo only wires them +together and supplies runtime config. -The initial commit used a per-service `components//docker-compose.yml` layout. That has -been collapsed into the single root `docker-compose.yml` (uncommitted working tree). Some -Dockerfile comments still reference `docker/docker-compose.homelab.yml` — that path is from -the `../server` repo, not this one; the canonical compose file here is `./docker-compose.yml`. +The initial commit used a per-service `components//docker-compose.yml` layout. That has been +collapsed into the single root `docker-compose.yml` (uncommitted working tree). Some Dockerfile +comments still reference `docker/docker-compose.homelab.yml` — that path is from the `../server` +repo, not this one; the canonical compose file here is `./docker-compose.yml`. ## Common commands @@ -41,51 +42,52 @@ perl generate_captcha.pl All services share the `lacuna` Docker network; the compose project name is also `lacuna`. -**Request path.** `caddy` (official `caddy:2.8-alpine` image, config `caddy/Caddyfile` -bind-mounted at `/etc/caddy/Caddyfile`) is the front proxy. It serves static files from the -shared `./var/www/public` tree, proxies `/v2/*` to `v2-api:5999`, serves `/captcha/*` from -`./captcha`, and sends everything else to `server:5000`. `caddy` is the **public -internet-facing edge**: it is exposed directly on the open web (no Tailscale) and publishes -host ports 80, 443, and 443/udp (HTTP/3). It terminates TLS itself via automatic HTTPS — -Let's Encrypt certs for `$CADDY_DOMAIN` (set in `.env`; defaults to the prod domain), with -an automatic HTTP→HTTPS redirect. This needs the domain's DNS pointed at the deploy host and -the persistent `./data/caddy-data` volume for the cert store. `server_url` in -`etc/lacuna.conf` must match `$CADDY_DOMAIN`. For a local run without DNS/TLS, set -`CADDY_DOMAIN=http://localhost` so Caddy just listens on `:80`. +**Request path.** `caddy` (official `caddy:2.8-alpine` image, config `caddy/Caddyfile` bind-mounted +at `/etc/caddy/Caddyfile`) is the front proxy. It serves static files from the shared +`./var/www/public` tree, proxies `/v2/*` to `v2-api:5999`, serves `/captcha/*` from `./captcha`, and +sends everything else to `server:5000`. `caddy` is the **public internet-facing edge**: it is +exposed directly on the open web (no Tailscale) and publishes host ports 80, 443, and 443/udp +(HTTP/3). It terminates TLS itself via automatic HTTPS — Let's Encrypt certs for `$CADDY_DOMAIN` +(set in `.env`; defaults to the prod domain), with an automatic HTTP→HTTPS redirect. This needs the +domain's DNS pointed at the deploy host and the persistent `./data/caddy-data` volume for the cert +store. `server_url` in `etc/lacuna.conf` must match `$CADDY_DOMAIN`. For a local run without +DNS/TLS, set `CADDY_DOMAIN=http://localhost` so Caddy just listens on `:80`. **Backends.** + - `server` — Perl/Plack v1 JSON-RPC backend (`start_lacuna.sh`), image `tlecommunity/server`, listens on 5000 (host 3050). `TLE_NO_MIDDLEWARE=1` disables the prod size-limit/profiling middleware; `LACUNA_LOG_DIR` sets the log root. -- `v2-api` — Node v2 REST backend (`node index.ts`), image `tlecommunity/v2-api`, port 80 - (host 5999). +- `v2-api` — Node v2 REST backend (`node index.ts`), image `tlecommunity/v2-api`, port 80 (host + 5999). - `app` / `super-ui` — Caddy-served frontend SPA and admin UI images; no published ports. **Data + infra.** `mysql` 5.5 (host 3306, root password from `LACUNA_MYSQL_ROOT_PASSWORD`), `phpmyadmin` (host 8888), `memcached` (sessions/cache, persisted to a file under -`./data/memcached`), `beanstalk` job queue (schickling/beanstalkd, 11300) with -`beanstalk-console` (2080). +`./data/memcached`), `beanstalk` job queue (schickling/beanstalkd, 11300) with `beanstalk-console` +(2080). **Schedulers** — all reuse the `tlecommunity/server` image: -- `server-building-scheduler`, `server-ship-scheduler`, `server-captcha-scheduler` each run - one long-lived `run_scheduler.sh schedule_*.pl` daemon. -- `server-script-scheduler` runs Ofelia (`mcuadros/ofelia`), which reads `schedule.ini` and - executes the `run_{hourly,two_hourly,four_hourly,daily,weekly}.sh` batch scripts *inside* - the `server` container via the mounted `/var/run/docker.sock`. + +- `server-building-scheduler`, `server-ship-scheduler`, `server-captcha-scheduler` each run one + long-lived `run_scheduler.sh schedule_*.pl` daemon. +- `server-script-scheduler` runs Ofelia (`mcuadros/ofelia`), which reads `schedule.ini` and executes + the `run_{hourly,two_hourly,four_hourly,daily,weekly}.sh` batch scripts _inside_ the `server` + container via the mounted `/var/run/docker.sock`. **logrotate** — Alpine sidecar (`Dockerfile.logrotate`, `entrypoint-logrotate.sh`) that runs `logrotate` once a day against the shared `./log` tree using the policy in `lacuna.logrotate` -(`copytruncate`, since producers in other containers hold log handles open with no reopen -signal). +(`copytruncate`, since producers in other containers hold log handles open with no reopen signal). ## Configuration Gitignored, copy from the `*.example` sibling and fill in: + - `.env` (from `.env.example`) — `CONTAINER_REGISTRY_URL`, `LACUNA_MYSQL_ROOT_PASSWORD`, `CADDY_DOMAIN` (front-proxy public hostname). - `etc/lacuna.conf` (from `etc/lacuna.example.conf`) — the main game config: JSON-with-`#`-comments. - Covers db DSN, beanstalk/memcached endpoints, captcha fonts, map size and neutral/starter - zones, payment gateways, `server_url`/`assets_url`. + Covers db DSN, beanstalk/memcached endpoints, captcha fonts, map size and neutral/starter zones, + payment gateways, `server_url`/`assets_url`. Tracked config: `etc/log4perl.conf` (Perl logging — INFO to stderr + `log/server/lacuna.log`), `caddy/Caddyfile`, `schedule.ini` (Ofelia cron), `setup.sql` (db + user bootstrap). @@ -93,11 +95,12 @@ Tracked config: `etc/log4perl.conf` (Perl logging — INFO to stderr + `log/serv ## Host-mounted trees Created on the deploy host, mostly not in git: + - `./var` — shared runtime tree between `server` and `nginx` (generated static assets under `var/www/public`, captcha fonts under `var/fonts`). - `./log` — unified log tree (`server/`, `scheduler/`, `cron/`, `deploy/`, `caddy/`). The `logrotate` sidecar rotates all of it except `caddy/`, whose access log Caddy rolls itself. - `./tmp`, `./captcha` — server scratch and captcha image output, shared with `nginx`. -- `./data/*` — persistent volumes for `mysql`, `memcached`, the Caddy data/config dirs of - `app` and `super-ui`, and `caddy-data` / `caddy-config` for the front proxy (the former - holds the ACME account and issued TLS certs). +- `./data/*` — persistent volumes for `mysql`, `memcached`, the Caddy data/config dirs of `app` and + `super-ui`, and `caddy-data` / `caddy-config` for the front proxy (the former holds the ACME + account and issued TLS certs). diff --git a/caddy/Caddyfile b/caddy/Caddyfile index 40e6db9..3f8d1a2 100644 --- a/caddy/Caddyfile +++ b/caddy/Caddyfile @@ -18,30 +18,65 @@ defer } - redir /api /api/ permanent redir /app /app/ permanent - redir /starmap /starmap/ permanent + redir /alliance-starmap /alliance-starmap/ permanent + redir /super-ui /super-ui/ permanent + // Captcha files generated by the server handle_path /captcha/* { - root * /home/lacuna/server/captcha + root * /home/lacuna/server/public/captcha file_server } - handle /v2/* { - reverse_proxy v2-api:5999 { + // Server info such as stars.json and server_overview.json + handle_path /server/* { + root * /home/lacuna/server/public/server + file_server + } + + // Public rss feeds for the Network19 + handle_path /network19/* { + root * /home/lacuna/server/public/network19 + file_server + } + + // Public rss feeds for the mission command + handle_path /missioncommand/* { + root * /home/lacuna/server/public/missioncommand + file_server + } + + // Handle serving the app itself + handle /app/* { + reverse_proxy app:3000 { header_up X-Forwarded-Host {remote_host} } } - handle /api/* { - root * /home/lacuna/server/var/www/public - file_server { - index Intro.html + // Alliance starmap + handle /alliance-starmap/* { + reverse_proxy alliance-starmap:3000 { + header_up X-Forwarded-Host {remote_host} + } + } + + // Super UI + handle /super-ui/* { + reverse_proxy super-ui:3000 { + header_up X-Forwarded-Host {remote_host} + } + } + + // V2 proxy + handle /v2/* { + reverse_proxy v2-api:5999 { + header_up X-Forwarded-Host {remote_host} } } + // Fallback which handles serving up the handful of public files then funnels everything else into the Perl backend handle { - root * /home/lacuna/server/var/www/public + root * /home/lacuna/server/public route { try_files {path} {path}/index.html @dynamic not file diff --git a/docker-compose.yml b/docker-compose.yml index e378dd9..bebb01f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -47,11 +47,11 @@ services: container_name: memcached restart: unless-stopped image: memcached -# command: -# # Persist cache in a file to save sessions (and other data) between restarts of the docker container -# - --memory-file=/tempfs_mount/memory_file -# volumes: -# - ./data/memcached:/tempfs_mount + # command: + # # Persist cache in a file to save sessions (and other data) between restarts of the docker container + # - --memory-file=/tempfs_mount/memory_file + # volumes: + # - ./data/memcached:/tempfs_mount mysql: container_name: mysql @@ -67,7 +67,7 @@ services: healthcheck: # mysqladmin ping exits 0 once mysqld accepts connections; passing creds # makes it a truer check (it would also exit 0 on an auth failure). - test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -u root -p\"$$MYSQL_ROOT_PASSWORD\" --silent"] + test: ['CMD-SHELL', 'mysqladmin ping -h 127.0.0.1 -u root -p"$$MYSQL_ROOT_PASSWORD" --silent'] interval: 10s timeout: 5s retries: 10 @@ -98,7 +98,11 @@ services: volumes: - ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro - ./var:/home/lacuna/server/var - - ./captcha:/home/lacuna/server/captcha + - ./data/public-captcha:/home/lacuna/server/public/captcha + - ./data/public-network19:/home/lacuna/server/public/network19 + - ./data/public-missioncommand:/home/lacuna/server/public/missioncommand + - ./data/public-server:/home/lacuna/server/public/server + - ./public:/home/lacuna/server/public - ./log/caddy:/home/lacuna/server/log/caddy - ./data/caddy-data:/data - ./data/caddy-config:/config @@ -135,14 +139,17 @@ services: logging: *default-logging volumes: - ./etc:/home/lacuna/server/etc - - ./captcha:/home/lacuna/server/captcha + - ./data/public-captcha:/home/lacuna/server/public/captcha + - ./data/public-network19:/home/lacuna/server/public/network19 + - ./data/public-missioncommand:/home/lacuna/server/public/missioncommand + - ./data/public-server:/home/lacuna/server/public/server - ./tmp:/tmp - ./log:/home/lacuna/server/log - ./var:/home/lacuna/server/var healthcheck: # /starman_ping (bin/lacuna.psgi) returns "pong" only once the Lacuna app # has compiled and Starman is serving requests. curl is in the server image. - test: ["CMD", "curl", "-fsS", "http://localhost:5000/starman_ping"] + test: ['CMD', 'curl', '-fsS', 'http://localhost:5000/starman_ping'] interval: 15s timeout: 10s retries: 5 @@ -166,7 +173,8 @@ services: server-ship-scheduler: image: ${CONTAINER_REGISTRY_URL:?error}/tlecommunity/server:latest - command: '/bin/bash run_scheduler.sh schedule_ship_arrival.pl --noquiet --nodaemonize --initialize' + command: + '/bin/bash run_scheduler.sh schedule_ship_arrival.pl --noquiet --nodaemonize --initialize' container_name: server-ship-scheduler restart: unless-stopped depends_on: diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..948a8a6 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,29 @@ +{ + "name": "@tlecommunity/deployment", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@tlecommunity/deployment", + "devDependencies": { + "prettier": "^3.9.6" + } + }, + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..1ef69b1 --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ + "name": "@tlecommunity/deployment", + "private": true, + "scripts": { + "format:check": "prettier --check .", + "format:fix": "prettier --write ." + }, + "devDependencies": { + "prettier": "^3.9.6" + } +} -- 2.51.2