From e8cd7991d0449783d348f04f80738820b90ab698 Mon Sep 17 00:00:00 2001 From: AuthorTom <63931206+authorTom@users.noreply.github.com> Date: Thu, 23 Jul 2026 08:21:20 +0100 Subject: [PATCH 1/3] feat: replace docker-compose.yml with hardened compose.yaml Adopt the canonical single compose.yaml filename (shipping both it and docker-compose.yml triggers a "multiple config files" warning) and harden it: - pull the published GHCR image (lowercase owner) instead of building; keep a documented build-from-source option - add init: true for signal forwarding / zombie reaping on graceful shutdown - add a healthcheck using Node's built-in fetch (alpine has no curl/wget), in exec/list form so the "://" and colon-space in the script aren't misread by YAML as a mapping - keep named volume, restart: unless-stopped, quoted port; no obsolete version: - document that compose auto-reads a sibling .env Co-Authored-By: Claude Opus 4.8 --- compose.yaml | 59 ++++++++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 30 ----------------------- 2 files changed, 59 insertions(+), 30 deletions(-) create mode 100644 compose.yaml delete mode 100644 docker-compose.yml diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..170a378 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,59 @@ +# SimHub -- departmental deployment. +# +# docker compose up -d start (pulls the published image) +# docker compose pull && docker compose up -d update to the latest image +# docker compose exec simhub node seed.js load the example scenario +# docker compose ps check status (look for "healthy") +# +# Compose automatically reads a sibling ".env" file, so copy ".env.example" +# to ".env" and edit it to override any of the settings below. +# +# To build from local source instead of pulling the published image, comment +# out the `image:` line and uncomment the `build: .` line. Building needs the +# full source tree checked out next to this file; pulling does not. + +services: + simhub: + # Published automatically to GitHub Container Registry on every push to + # main. GHCR forces the owner/repo to lowercase, hence "authortom". + image: ghcr.io/authortom/simhub:latest + # build: . + container_name: simhub + + # Restart on failure and on daemon/host reboot, but not if an operator + # deliberately stopped it. + restart: unless-stopped + + # PID 1 that forwards signals and reaps zombies, so SIGTERM reaches node + # and the graceful-shutdown handler can flush state on `compose down`. + init: true + + ports: + # "host:container" -- change the LEFT number to serve on a different host + # port. Quoted so YAML never reinterprets it (e.g. 22:22 as base-60). + - "3000:3000" + + environment: + # Production defaults. Override in .env if needed. + NODE_ENV: production + # TRUST_PROXY: "1" # set when behind a reverse proxy (see .env.example) + + volumes: + # All scenarios, programmes, users and sessions live here. A named volume + # survives image updates and rebuilds; back it up regularly. + - simhub-data:/app/data + + healthcheck: + # Alpine/slim runtime images ship no curl or wget, so probe with Node's + # built-in fetch (Node 18+). Exec (list) form is used deliberately: it + # avoids a shell AND keeps YAML from misreading the "://" / colon-space + # in the script as a nested mapping. A bare `test: node -e "a: b"` string + # containing a colon-space WOULD need quoting; the list form sidesteps it. + test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3000/api/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"] + interval: 30s + timeout: 5s + start_period: 10s + retries: 3 + +volumes: + simhub-data: diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 1ccbe84..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,30 +0,0 @@ -# SimHub — departmental deployment. -# -# docker compose up -d start (pulls the published image) -# docker compose pull && docker compose up -d update to latest -# docker compose exec simhub node seed.js load the example scenario -# -# To build from local source instead of pulling the published image, -# comment out `image:` and uncomment `build: .` - -services: - simhub: - image: ghcr.io/authortom/simhub:latest - # build: . - container_name: simhub - restart: unless-stopped - ports: - # host:container — change the left side to serve on a different port - - "3000:3000" - volumes: - # All scenarios, programmes, users and sessions live here. - # A named volume survives image updates; back it up regularly. - - simhub-data:/app/data - environment: - # Uncomment when running behind a reverse proxy (nginx, Caddy, Traefik) - # so login rate-limiting sees real client IPs: - # TRUST_PROXY: "1" - NODE_ENV: production - -volumes: - simhub-data: From 2577714c1a21f00a713bd4247be2d29bc183050d Mon Sep 17 00:00:00 2001 From: AuthorTom <63931206+authorTom@users.noreply.github.com> Date: Thu, 23 Jul 2026 08:21:43 +0100 Subject: [PATCH 2/3] chore: add tracked .env.example config template Ship a documented template of the real runtime knobs (NODE_ENV, TRUST_PROXY, PORT). Compose auto-reads a sibling .env; real .env files stay gitignored. Notes that the admin password is set in-app at first sign-in, not via env. Co-Authored-By: Claude Opus 4.8 --- .env.example | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f9b81ff --- /dev/null +++ b/.env.example @@ -0,0 +1,29 @@ +# SimHub configuration template. +# +# Copy this file to ".env" next to compose.yaml and edit as needed: +# +# cp .env.example .env +# +# Docker Compose reads a sibling ".env" automatically, so any values set here +# are picked up by `docker compose up -d` with no extra flags. The real ".env" +# is gitignored -- never commit it. +# +# NOTE ON PASSWORDS: SimHub has no admin-password environment variable. It seeds +# a provisional admin account (admin@simhub.local / admin123) that you are +# forced to re-password in the browser at first sign-in. There is no secret to +# put in this file for that -- set it in the app after the first `up -d`. + +# Runtime environment. Leave as "production" for real deployments. +NODE_ENV=production + +# Set to "1" (or the number of proxy hops) ONLY when running behind a reverse +# proxy (nginx, Caddy, Traefik) so login rate-limiting sees real client IPs. +# Leave unset otherwise -- trusting proxy headers without a proxy lets clients +# spoof their IP. +# TRUST_PROXY=1 + +# Port SimHub listens on INSIDE the container. Changing this also requires +# updating the right-hand side of the port mapping in compose.yaml and the +# healthcheck URL, so most deployments leave it at 3000 and change only the +# host (left) side of the compose port mapping instead. +# PORT=3000 From bd1fb1cf0a948019992ca54e3dff98b0c7b1d357 Mon Sep 17 00:00:00 2001 From: AuthorTom <63931206+authorTom@users.noreply.github.com> Date: Thu, 23 Jul 2026 08:23:00 +0100 Subject: [PATCH 3/3] docs: pull-based Compose deploy steps; point README at compose.yaml - Deploy by curling compose.yaml (+ .env.example) onto the server; no source checkout needed since compose pulls the published image - Document the "(healthy)" check, first-sign-in password, and TRUST_PROXY via .env - Update all docker-compose.yml references and project-layout listing Co-Authored-By: Claude Opus 4.8 --- README.md | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index fe4a6f3..4acfa23 100644 --- a/README.md +++ b/README.md @@ -148,15 +148,27 @@ docker exec simhub node seed.js ### Recommended — Docker Compose -The repository ships a ready-made [docker-compose.yml](docker-compose.yml). Copy it (or clone the repo) onto the server, then: +The repository ships a ready-made [compose.yaml](compose.yaml) that **pulls the published image** — you do **not** need the source checked out on the server. Grab just that one file onto the server and start it: ```bash -docker compose up -d # start +# Download the compose file next to where your data volume will live +curl -fsSL -o compose.yaml \ + https://raw.githubusercontent.com/authorTom/simhub/main/compose.yaml + +# Optional: configuration overrides (proxy, port). Compose auto-reads a sibling .env +curl -fsSL -o .env.example \ + https://raw.githubusercontent.com/authorTom/simhub/main/.env.example +cp .env.example .env # then edit .env if you need TRUST_PROXY, etc. + +docker compose up -d # start (pulls the image) +docker compose ps # confirm STATUS shows "(healthy)" docker compose exec simhub node seed.js # optional: load the example scenario docker compose logs -f simhub # watch the logs ``` -Compose gives you a declarative record of your deployment (port, volume, environment) that you can keep in your team's documentation. Uncomment `TRUST_PROXY: "1"` in the file when running behind a reverse proxy. +Then open `http://:3000` and sign in as `admin@simhub.local` / `admin123`; you are forced to set a real password at first sign-in. + +Compose gives you a declarative record of your deployment (port, volume, environment) that you can keep in your team's documentation. To run behind a reverse proxy, set `TRUST_PROXY=1` in `.env` (or uncomment it in `compose.yaml`). ### Your data lives in the volume @@ -173,7 +185,7 @@ docker run --rm -v simhub-data:/data -v "$PWD":/backup alpine \ docker compose pull && docker compose up -d ``` -That's it — the new container starts against the same data volume, and persisted sessions mean your faculty aren't even signed out. To be able to roll back, deploy a pinned tag (`ghcr.io/authortom/simhub:sha-` or a release version) instead of `latest`, and change the tag in `docker-compose.yml` when you upgrade. +That's it — the new container starts against the same data volume, and persisted sessions mean your faculty aren't even signed out. To be able to roll back, deploy a pinned tag (`ghcr.io/authortom/simhub:sha-` or a release version) instead of `latest`, and change the tag in `compose.yaml` when you upgrade. ### Building the image yourself @@ -246,7 +258,8 @@ simhub/ ├── CHANGELOG.md # Full change history ├── CONTRIBUTING.md # Contribution guide ├── Dockerfile # Production container image -├── docker-compose.yml # Departmental deployment recipe +├── compose.yaml # Departmental deployment recipe (pull-based) +├── .env.example # Config template -> copy to .env ├── LICENSE # MIT licence ├── scenario_template.md # ASPiH scenario blueprint (reference) ├── seed.js # Example dataset generator