feat: F5.6b server F as an image: the editor from F itself, one stack per environment

F hands out the editor's page and bundle (editor/ beside main.mjs, or
FILIUS_EDITOR_DIR): GET and HEAD only, one wall against leaving the folder,
F's own routes first, assets/ cached a year, the rest asked for again; the
build lays .br and .gz beside the files and F sends the better one the
browser takes (bundle 695 KB to 212 KB).

deploy/Dockerfile: two stages, Node 24, no node_modules, not root.
deploy/docker-compose.yml with an env file per environment: the file sets
the stack's name, volume, image tag and port (test 8081, production 8080).
Measured with both stacks side by side behind nginx and Chromium.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-04 17:43:51 +00:00
co-authored by Claude Opus 5.5
parent 709146b58a
commit 087721cba3
19 changed files with 714 additions and 10 deletions
+43
View File
@@ -0,0 +1,43 @@
# Server F as an image: the Filius service and the editor's page in one.
#
# Built from the repository's root (the build needs every package and the
# corpus, whose FILIUS examples the editor carries):
#
# docker compose -f deploy/docker-compose.yml --env-file deploy/test.env build
#
# Two stages. The first installs everything and builds: esbuild bundles the
# service into main.mjs and its room thread into simWorker.mjs, with every
# dependency inside; Vite builds the editor; precompressMain.mjs lays a .br
# and a .gz beside its files. The second stage takes only these files, so
# the image holds no node_modules, no sources and no compiler.
#
# Node 24 (active LTS), not the 20 of the development machines: 20 has had
# no security fixes since April 2026, and a service facing the browsers of a
# whole school should not run on it. The bundles are built for node20 and
# run unchanged on 24; the image's run is checked before each release (see
# the README).
FROM node:24-alpine AS build
WORKDIR /src
COPY . .
RUN npm ci --no-audit --no-fund
RUN npm run build -w @mdg-filius/server \
&& npm run build -w @mdg-filius/editor \
&& node packages/server/dist/precompressMain.mjs packages/editor/dist
FROM node:24-alpine
LABEL org.opencontainers.image.title="mdg-filius" \
org.opencontainers.image.description="Filius network simulation, shared editing (server F)" \
org.opencontainers.image.licenses="GPL-3.0-only"
ENV NODE_ENV=production
WORKDIR /app
COPY --from=build /src/packages/server/dist/main.mjs /src/packages/server/dist/simWorker.mjs ./
COPY --from=build /src/packages/editor/dist ./editor
COPY LICENSE README.md CHANGES.md ./
# Not root: F needs no privilege, and a hole in it should not get one.
USER node
EXPOSE 8080
# The proxy keeps no watch of its own; this one shows in `docker compose ps`.
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
CMD wget -q -O /dev/null http://127.0.0.1:8080/healthz || exit 1
CMD ["node", "main.mjs"]
+73
View File
@@ -0,0 +1,73 @@
# Server F: one stack per environment, both on the same machine.
#
# docker compose -f deploy/docker-compose.yml --env-file deploy/test.env up -d --build
# docker compose -f deploy/docker-compose.yml --env-file deploy/production.env up -d --build
#
# The environment file decides everything that tells the two apart, and it
# does so through **names**, not through the place the command runs in:
#
# * `name:` is the stack's name (`filius-test`, `filius-production`). Docker
# prefixes containers, network and volume with it, so the two stacks share
# nothing — above all not the database. Without it, compose would take the
# folder's name, and both stacks started from this folder would be **one**
# stack: the second `up` would replace the first. That is also why no
# `-p` is needed (and why forgetting one does no harm, unlike on server S).
# * `image:` carries the stack's name as its tag. A shared tag would let a
# build for the test environment change what production runs at its next
# restart.
# * The port: test 8081, production 8080, as `deploy/proxy.conf` expects.
#
# Needs Docker Compose v2 (`docker compose`; the old `docker-compose` 1.x
# does not know `name:`).
#
# The data lies in the volume `<name>_db` (documents, sessions, blobs). It is
# the one thing of a stack that has to be backed up:
#
# docker compose -f deploy/docker-compose.yml --env-file deploy/production.env \
# exec -T db pg_dump -U filius filius > filius-production.sql
name: "${FILIUS_STACK:?FILIUS_STACK missing, start with --env-file deploy/<environment>.env}"
services:
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: filius
POSTGRES_USER: filius
POSTGRES_PASSWORD: ${FILIUS_DB_PASSWORD:?FILIUS_DB_PASSWORD missing}
volumes:
- db:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U filius -d filius"]
interval: 5s
timeout: 5s
retries: 12
# No `ports:`: only F talks to its database, over the stack's network.
filius:
image: mdg-filius:${FILIUS_STACK}
build:
context: ..
dockerfile: deploy/Dockerfile
restart: unless-stopped
depends_on:
db:
condition: service_healthy
environment:
# The password goes into a URL: hex only (`openssl rand -hex 32`), a
# `@`, `/` or `:` in it would break the URL.
DATABASE_URL: postgres://filius:${FILIUS_DB_PASSWORD}@db:5432/filius
FILIUS_TICKET_SECRET: ${FILIUS_TICKET_SECRET:?FILIUS_TICKET_SECRET missing}
FILIUS_SERVICE_KEY: ${FILIUS_SERVICE_KEY:?FILIUS_SERVICE_KEY missing}
FILIUS_ORIGINS: ${FILIUS_ORIGINS:?FILIUS_ORIGINS missing}
ports:
# The address the openresty reaches F on, and the stack's port.
- "${FILIUS_BIND:?FILIUS_BIND missing}:${FILIUS_PUBLIC_PORT:?FILIUS_PUBLIC_PORT missing}:8080"
# F stops its rooms and stores every open document before it ends
# (main.ts, SIGTERM); Docker's 10 s are enough today, 30 s leave room
# for a large school day.
stop_grace_period: 30s
volumes:
db:
+23
View File
@@ -0,0 +1,23 @@
# Server F, production environment: copy to deploy/production.env and fill in the empty
# values (deploy/*.env is not in git). Started with
#
# docker compose -f deploy/docker-compose.yml --env-file deploy/production.env up -d --build
# The stack's name: containers, volume and image tag (docker-compose.yml).
FILIUS_STACK=filius-production
# Where the openresty reaches F: the address of the LXC container, and the
# port deploy/proxy.conf names for filius.hub.mdg-hamburg.de.
FILIUS_BIND=
FILIUS_PUBLIC_PORT=8080
# The origin of the page the editor runs in: this environment's own name.
FILIUS_ORIGINS=https://filius.hub.mdg-hamburg.de
# Hex only (openssl rand -hex 32): the password goes into F's database URL.
FILIUS_DB_PASSWORD=
# The same values as in the environment of dashboard (it signs the
# tickets and calls /svc/* with the key). Each environment its own.
FILIUS_TICKET_SECRET=
FILIUS_SERVICE_KEY=
+23
View File
@@ -0,0 +1,23 @@
# Server F, test environment: copy to deploy/test.env and fill in the empty
# values (deploy/*.env is not in git). Started with
#
# docker compose -f deploy/docker-compose.yml --env-file deploy/test.env up -d --build
# The stack's name: containers, volume and image tag (docker-compose.yml).
FILIUS_STACK=filius-test
# Where the openresty reaches F: the address of the LXC container, and the
# port deploy/proxy.conf names for test-filius.hub.mdg-hamburg.de.
FILIUS_BIND=
FILIUS_PUBLIC_PORT=8081
# The origin of the page the editor runs in: this environment's own name.
FILIUS_ORIGINS=https://test-filius.hub.mdg-hamburg.de
# Hex only (openssl rand -hex 32): the password goes into F's database URL.
FILIUS_DB_PASSWORD=
# The same values as in the environment of test-dashboard (it signs the
# tickets and calls /svc/* with the key). Each environment its own.
FILIUS_TICKET_SECRET=
FILIUS_SERVICE_KEY=