16 KiB
TrueNAS deployment
Healer Man uses the same local-Gitea deployment pattern as the previous
i-want-to-heal-mmo game: clone the Gitea bare repository through the TrueNAS
filesystem, mount the runnable checkout into one Node container, and update it
with a local fast-forward pull plus an app restart.
For the repeatable Git, TrueNAS, signed-APK, and Obtainium release checklist, follow RELEASE_RUNBOOK.md.
The public URL remains https://iwanttoheal.phenomrom.com. The old game and
Healer Man cannot both own host port 4173; stop the old app before the final
cutover. Keep its checkout and database until the replacement is verified.
What the TrueNAS server does
The healer-man TrueNAS app is the browser host and online account server. One
Node 24 process serves the production Vite bundle and authenticated /api
routes on port 4173. Its SQLite database stores accounts, hashed credentials,
sessions, and cloud rosters under /app/runtime-data/game.db.
The source checkout and persistent data are separate mounts. Replacing the
container, rebuilding dist, or pulling source must not replace the database.
TrueNAS Gitea stores the source repository and APK release attachments; it is
not the game database.
Large runtime assets are published separately under /app/content. Production
browser and Android clients download verified content objects from /content;
browsers retain them in an app-managed cache and Android retains them in
app-private storage for offline play. See CONTENT_UPDATES.md for the manifest,
atomic activation, compatibility, and rollback contract.
Paths
Local Gitea bare repository:
/mnt/.ix-apps/app_mounts/gitea/data/git/repositories/phenom/healer-man.git
Runnable working checkout:
/mnt/usbssds/apps/healer-man/app
Persistent game data:
/mnt/usbssds/apps/healer-man/data
Published downloadable content:
/mnt/usbssds/apps/healer-man/content
Database backups:
/mnt/usbssds/apps/healer-man/backups
Public URL:
https://iwanttoheal.phenomrom.com
Container/host port:
4173
The Gitea bare repository is not runnable. Clone it with Git; never copy it
with cp. The checkout contains several gigabytes of content-source GLB assets,
so the initial clone and first content publication will take substantially
longer than later fast-forward updates. Production browser and Android bundles
stage only their compact required-content subset.
Do not turn the GLBs into Git LFS pointers without also changing this deployment procedure. Gitea keeps LFS objects outside the bare repository, so a bare-path clone alone cannot retrieve them.
First installation
Create an empty Gitea repository named phenom/healer-man, then initialize and
push the local F:\Projects\HealerMan project to its main branch.
The project is already connected to Gitea. Verify the checkout and both remote URLs from PowerShell with these exact commands:
Set-Location F:\Projects\HealerMan
git status --short --branch
git remote -v
git branch --show-current
If the repository slug differs, change both the remote URL here and every
healer-man.git bare-repository path below. Do not commit .env files,
keystores, API tokens, content, dist, dist-android, .web-public, or
.android-public.
On TrueNAS, confirm the bare repository path. If the first command fails, use the search command:
sudo test -d /mnt/.ix-apps/app_mounts/gitea/data/git/repositories/phenom/healer-man.git
sudo find /mnt -type d -name "healer-man.git" -prune -print 2>/dev/null
Clone entirely through the local filesystem:
sudo mkdir -p /mnt/usbssds/apps/healer-man/{app,data,content,backups}
sudo git config --global --add safe.directory \
/mnt/.ix-apps/app_mounts/gitea/data/git/repositories/phenom/healer-man.git
sudo git clone \
/mnt/.ix-apps/app_mounts/gitea/data/git/repositories/phenom/healer-man.git \
/mnt/usbssds/apps/healer-man/app
sudo chown -R truenas_admin:truenas_admin /mnt/usbssds/apps/healer-man
Verify the checkout:
git -C /mnt/usbssds/apps/healer-man/app remote -v
git -C /mnt/usbssds/apps/healer-man/app branch --show-current
ls /mnt/usbssds/apps/healer-man/app/package.json
ls /mnt/usbssds/apps/healer-man/app/server/server.mjs
Expected branch: main. Expected origin: the local Gitea path above.
Install as a TrueNAS app
- Stop the old
iwanttoheal-mmoapp so port4173is free. - Open Apps and then Discover.
- Open the three-dot menu and select Install via YAML.
- Name the new app
healer-man. - Paste the following YAML:
services:
healerman:
image: node:24-bookworm-slim
command: >-
sh -lc "apt-get update && apt-get install -y --no-install-recommends python3 && rm -rf /var/lib/apt/lists/* && python3 --version && npm ci --include=dev && npm run db:init && npm run content:publish && npm run build && npm start"
environment:
BALANCE_LAB_MAX_CONCURRENT: "4"
BALANCE_LAB_MAX_QUEUED: "100"
BALANCE_LAB_PYTHON: "python3"
CORS_ORIGINS: "https://iwanttoheal.phenomrom.com,capacitor://localhost,http://localhost,https://localhost"
CONTENT_DIR: /app/content
DATA_DIR: /app/runtime-data
HOST: 0.0.0.0
HEALER_MAN_ENABLE_SHELVED_MODES: "0"
NODE_OPTIONS: "--max-old-space-size=8192"
PORT: "4173"
SESSION_TTL_DAYS: "30"
STATIC_DIR: /app/dist
TRUST_PROXY: "true"
VITE_API_BASE_URL: "https://iwanttoheal.phenomrom.com"
init: true
ports:
- "4173:4173"
restart: unless-stopped
volumes:
- /mnt/usbssds/apps/healer-man/app:/app
- /mnt/usbssds/apps/healer-man/data:/app/runtime-data
- /mnt/usbssds/apps/healer-man/content:/app/content
working_dir: /app
Do not remove either persistent mount. /app/runtime-data owns
account/cloud-save data; /app/content owns downloadable manifests and
immutable content objects. Never mount a volume at /app/data, because that
would hide the checkout's required /app/data/loot/schema.sql. The
startup command installs locked dependencies, initializes the schema, publishes
new or changed content objects, builds the compact required-content browser
bundle, and starts the combined static/API/content server. Existing content
objects are reused.
Required GM Balance Lab server dependency
Balance Lab uses the repository's Python controller to orchestrate the live
TypeScript combat runtime. The canonical YAML above therefore installs Python
3 before installing/building the app and explicitly selects python3 with
BALANCE_LAB_PYTHON.
If an existing app reports Could not start python3, edit that app's YAML to
use this command:
command: >-
sh -lc "apt-get update && apt-get install -y --no-install-recommends python3 && rm -rf /var/lib/apt/lists/* && python3 --version && npm ci --include=dev && npm run db:init && npm run content:publish && npm run build && npm start"
and add or correct these environment values:
BALANCE_LAB_PYTHON: "python3"
BALANCE_LAB_MAX_CONCURRENT: "4"
BALANCE_LAB_MAX_QUEUED: "100"
The app stores job reports in
/app/runtime-data/balance-lab/jobs/<job-id>/, so the existing persistent
runtime-data mount also preserves generated reports and the GM app's run
history across container/server restarts. Each directory includes job.json
metadata used to rebuild the newest 100 history entries; do not exclude those
files when migrating the runtime-data dataset. An interrupted queued/running
job returns as failed after restart, while any job with a complete
suite-report.json returns as complete. Keep concurrency at one
unless the server has enough spare CPU for multiple full combat sweeps. All
Balance Lab routes independently require a server-authorized GM account.
Balance Lab's one-shot vite-node workers run in the dedicated balance-lab
Vite mode, which disables the public directory and ignores all filesystem-watch
paths. This is required for parallel sweeps: without it, concurrent workers can
exhaust Linux's file-watcher limit while scanning the shipped asset tree and
fail immediately with ENOSPC. Do not use a larger inotify allowance as the
primary fix; deploy the current runtime configuration.
After saving the YAML, restart the TrueNAS app and watch its startup log for a
line beginning with Python 3.. Reopen Balance Lab; the setup panel must show
Server runtime ready before Run on server becomes available. The server
now performs this preflight before queueing, so a missing runtime cannot create
another doomed job.
After startup, test on the LAN:
curl -I http://TRUENAS-IP:4173
curl http://TRUENAS-IP:4173/api/health
curl http://TRUENAS-IP:4173/content/manifest.json
Expected results are HTTP 200 and a JSON response containing
"service":"healer-man". Keep the existing reverse proxy for
iwanttoheal.phenomrom.com pointed at TRUENAS-IP:4173; no DNS change is
required if that proxy target stays the same.
Verify account creation, sign-out/sign-in, character creation, and a second login before deleting the old TrueNAS app. The old game's database is not schema-compatible with Healer Man and must not be copied over the new one.
Update workflow
The visual release manager described in RELEASE_RUNBOOK.md defaults to **Git
- TrueNAS only**, which commits and pushes a server update without changing the
Android version or publishing an APK. Clear that toggle when a code update also
requires a new APK. For a manual APK release, choose the next version first.
The example below upgrades
0.1.2to0.1.3; replace1003if it is not greater than every prior AndroidversionCode. Then run these commands in Windows PowerShell on the development PC. Stop if any test or build fails:
Set-Location F:\Projects\HealerMan
npm run release:check
$VersionName = '0.1.3'
$VersionCode = 1003
npm version $VersionName --no-git-tag-version
$ReleaseVersion = [ordered]@{
versionName = $VersionName
versionCode = $VersionCode
} | ConvertTo-Json
[IO.File]::WriteAllText(
(Join-Path $PWD 'release-version.json'),
"$ReleaseVersion`n",
[Text.UTF8Encoding]::new($false)
)
npm run content:test
npm run server:test
npm test
npm run content:publish
npm run build
npm test -- --run src/avatar
npx vitest run src/game/inputManager.test.ts src/game/inputMath.test.ts
npm run release:check
git status --short
git add -A
git status --short
git diff --cached --stat
git diff --cached --check
git commit -m "Release Healer Man $VersionName"
git push origin main
git rev-parse HEAD
content:publish is a fail-closed pre-build release check. It must report all
54 WoW campaign dungeon packs and refuses to replace the manifest when a pack,
dependency, or catalog-referenced asset is missing. Its local content/ output
is ignored and must not be staged.
Before git commit, read the staged file list printed by git status --short.
Do not commit a .env file, password, API token, keystore, APK, database,
content/, dist/, dist-android/, .web-public/, or .android-public/
output. The final command prints the exact deployed commit ID.
Next, open the TrueNAS Apps UI and stop healer-man. Open a TrueNAS shell
and run these exact commands:
sudo mkdir -p /mnt/usbssds/apps/healer-man/backups
if sudo test -f /mnt/usbssds/apps/healer-man/data/game.db; then
sudo cp -p \
/mnt/usbssds/apps/healer-man/data/game.db \
/mnt/usbssds/apps/healer-man/backups/game-$(date +%Y%m%d-%H%M%S).db
fi
sudo git config --global --add safe.directory \
/mnt/usbssds/apps/healer-man/app
sudo git -C /mnt/usbssds/apps/healer-man/app pull --ff-only \
/mnt/.ix-apps/app_mounts/gitea/data/git/repositories/phenom/healer-man.git \
main
sudo chown -R truenas_admin:truenas_admin \
/mnt/usbssds/apps/healer-man/app
sudo git -C /mnt/usbssds/apps/healer-man/app rev-parse HEAD
The TrueNAS commit ID must match the one printed on the development PC. Start
healer-man in the TrueNAS Apps UI afterward. Container startup
publishes immutable content objects and replaces manifest.json only after all
objects exist, then rebuilds the browser client and starts the server. Browser
and Android clients discover the small new manifest and download only missing
or changed files for packs the player installed. Asset-only updates do not
require a new APK. A failed fast-forward pull deliberately leaves the running
checkout unchanged; resolve diverged history on the development machine rather
than forcing the TrueNAS checkout.
After the logs say the server is listening on port 4173, run on TrueNAS:
curl -fI http://127.0.0.1:4173/
curl -fsS http://127.0.0.1:4173/api/health
curl -fsS http://127.0.0.1:4173/content/manifest.json | head -c 500
To publish content manually without restarting the app container, run inside a Node 24 environment that mounts the checkout and content volume:
CONTENT_DIR=/mnt/usbssds/apps/healer-man/content \
npm --prefix /mnt/usbssds/apps/healer-man/app run content:publish
Database backup
Create a consistent SQLite backup before migrations or destructive maintenance:
DATA_DIR=/mnt/usbssds/apps/healer-man/data \
BACKUP_DIR=/mnt/usbssds/apps/healer-man/backups \
npm --prefix /mnt/usbssds/apps/healer-man/app run db:backup
Back up the whole /mnt/usbssds/apps/healer-man/data dataset with TrueNAS
snapshots as well. The script backup is convenient for point-in-time database
copies; it does not replace dataset-level protection.
APK releases and Obtainium
Keep APK binaries out of the Git branch. The Android build uses dist-android,
which contains the game runtime, UI, starter equipment, basic Human character,
and Wailing Caverns starter content. Optional character/skin packs, original
spell media, dungeons, and other large content remain outside the APK.
Downloaded packs survive APK updates.
Install JDK 21 and Android SDK Platform 36 on the development PC and set
JAVA_HOME plus ANDROID_SDK_ROOT. The build wrapper also recognizes the
project's existing portable JDK/SDK layout when present.
For each Android version, complete the exact update workflow above first. Then set the permanent signing-key secrets and Gitea token in PowerShell:
$env:ANDROID_KEYSTORE_FILE = 'F:\secure\healer-man-release.jks'
$env:ANDROID_KEYSTORE_PASSWORD = '...'
$env:ANDROID_KEY_ALIAS = 'healer-man'
$env:ANDROID_KEY_PASSWORD = '...'
$env:GITEA_TOKEN = '...'
Do not commit those values or the keystore. Build, sign, checksum, create the Gitea release/tag, and upload both attachments with:
powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts/publish-gitea-release.ps1 `
-VersionName 0.1.3 `
-VersionCode 1003 `
-ReleaseNotes 'Healer Man client update.'
The result contains:
Tag: v0.1.3
APK: healer-man-0.1.3-release.apk
Checksum: healer-man-0.1.3-release.apk.sha256
Obtainium should monitor:
https://git.whoagland.com/phenom/healer-man/releases
Generic Gitea is not listed as a dedicated Obtainium source. Add the public releases URL above; if automatic detection does not select it correctly, set the source override to HTML and filter APK links with:
healer-man-.*-release\.apk$
The repository or a separate release-only repository must be publicly readable for players. Never distribute a privileged Gitea token in the app or Obtainium configuration.
Use one permanent Android release-signing key and increase versionCode for
every release. Losing or changing that key prevents installed copies from
updating in place. If the source repository is private, expose a public
read-only release repository or download page rather than storing a privileged
Gitea token on the game device.
Asset, texture, audio, and compatible data updates use content:publish and do
not need an Obtainium release. JavaScript/React gameplay changes, loader or
shader changes, native Android changes, permissions, or manifest-schema changes
require a new signed APK with a higher versionCode.