og-cod-plutainer/MIGRATION.md
Amos 4ce33c637e
V2 layout (#4)
* v2: unify PLUTAINER_* env vars, restructure volume layout, harden entries

Volume layout (breaking — v1 volumes refused on start, see migrate-v1-to-v2.sh):
  app/configs/         user-facing real *.cfg files (flat); SOT
  app/logs/            stable symlinks (unchanged)
  app/runtime/gamefiles/, app/runtime/plutonium/
  app/.plutainer-version=2

Each entry script fans out symlinks from app/configs/*.cfg into the engine's
expected config dir, so games read configs through symlinks back to the
user-facing dir. RCON writeconfig writes back transparently.

Env vars unified: PLUTAINER_GAME/CONFIG_FILE/PORT/SERVER_NAME/MOD/AUTO_UPDATE/
HEALTHCHECK/SKIP_SEED/EXTRA_ARGS. Old PLUTO_/IW4X_/ALTER_ prefixes still work
via shim_env_vars in game-config.sh, emit [DEPRECATED] warning. PLUTO_SERVER_KEY,
PLUTO_MAX_CLIENTS, IW4X_NET_LOG_IP keep original names (single-family vars).

Other fixes folded in:
- Symlink existence guards via new link_files helper (eliminates dangling
  symlinks when host gamefiles are missing — same class as the prior t7x '*'
  bug).
- set -euo pipefail in all entry scripts.
- wget -N for t7x.exe (skip redownload when unchanged).
- HEALTHCHECK start-period: 1m -> 5m for slow first-run downloads.
- migrate-v1-to-v2.sh handles the volume restructure (supports --dry-run).
- .gitattributes pins shell/python/Dockerfile to LF.

* Fix shim_env_vars + detect_game_type under set -u

Indirect expansion ${!var} and bare ${VAR} both error out under set -u when
the referenced variable is unset. Add :- default so the strict mode in entry
scripts works when canonical PLUTAINER_* vars (or any old-prefix ones) are
absent.

* CI: build :v2 tag from v2-layout branch, keep separate from :latest

Workflow now triggers on pushes to main and v2-layout. Tag rules:
- :latest only when is_default_branch (= main).
- :v2    only when github.ref == refs/heads/v2-layout.

These conditions are mutually exclusive, so v2 builds never touch :latest
and main builds never touch :v2. Both branches still get a :sha-<short> tag.

Doc references updated from :main to :v2.

* v2: drop env-var shim; add raw-configs mode, auto-lift, hold/throttle, mod fan-out

Breaking (only on :v2 — :latest unaffected):
* shim_env_vars removed. PLUTO_*/IW4X_*/ALTER_* prefix env vars no longer
  recognised. PLUTAINER_* is the only accepted form (plus the three unique
  vars PLUTO_SERVER_KEY, PLUTO_MAX_CLIENTS, IW4X_NET_LOG_IP).

New:
* PLUTAINER_USE_RAW_CONFIGS=true opts out of the configs/ symlink system;
  engine path becomes the SOT directly. Default off.
* ensure_config_present: auto-lifts CONFIG_FILE from engine path to configs/
  if found as a real file there; refuses startup with case-insensitive
  find-iname hint if absent everywhere.
* resolve_mod_config_dir + link_configs variadic: when PLUTAINER_MOD is set,
  cfg symlinks fan out into BOTH the base engine config dir AND the
  mod-scoped dir (plutonium fs_game / iw4x mod path). Alterware skipped
  (Workshop ID, not a path).
* link_configs refuses to overwrite a real file at engine path; warns
  instead. Prevents the silent-replace failure mode.
* hold_indefinitely: on startup validation failure, exec sleep infinity so
  the container stays Up and docker doesn't loop restart it. User fixes
  the config and runs docker restart.
* launch_game: 30s sleep after wine exits before container exits, so
  restart policies (no min-delay knob in compose) throttle to ~1 restart
  per 30s on real crashes.
* Improved extract_rcon_password: handles single-quoted and unquoted
  forms; on failure emits a structured WARN telling the user the accepted
  forms and not to set rcon_password via PLUTAINER_EXTRA_ARGS. Doesn't
  block startup.

Docs updated (README + CLAUDE.md): removed deprecated-prefix table,
documented PLUTAINER_USE_RAW_CONFIGS, auto-lift, refuse behavior, restart
throttle, and the rcon parser accepted forms.

* Split auto-lift from ensure_config_present so seed can't pre-empt user file

Original order: seed_configs (cp -n) before ensure_config_present meant a
user's real cfg at engine path got bypassed because seed had already
filled configs/ with a default basename, satisfying the SOT check.

Now auto_lift_user_config runs before seed: if user has a real
(non-symlink) cfg at engine path, mv -f it to configs/ unconditionally.
Then seed gap-fills (cp -n preserves the user file). Then link_configs
fans out. Then ensure_config_present only does the presence-or-refuse
check.

Three callers updated: alterentry, plutoentry, iw4xentry.

* log-watcher: strip stray dirs at stable paths; reap orphan temp symlinks

A sidecar that bind-mounts an individual log file (e.g. games_zm.log) before
the file exists causes Docker to auto-create an empty root-owned directory at
that host path. place_symlink's mv -Tf then fails every poll and litters
logs/ with .XXX temp symlinks.

Fix both reactively in place_symlink and on startup heal. Try rmdir first
(empty dir, plutainer owns parent), fall back to rm -rf; if both fail (non-
empty root-owned dir), log a clear error pointing at the sidecar config.

* extract_rcon_password: accept bare 'rcon_password' (no set/seta prefix)

T6/T7 community cfgs commonly write `rcon_password "value"` without the
`set` keyword. Engine accepts both forms; parser only accepted the prefixed
form, so healthcheck and rcon-cli silently failed on stock t6zm seeds.

Make the `set[a]?` prefix optional and adjust the unquoted fallback to pick
the right awk column whether or not the prefix is present.

* Arm64 v2 (#3)

* Add ARM64 image variant with native Wine 11 via Hangover

New Dockerfile.arm64 targets aarch64 (Pi 5+, Ampere, Apple silicon under
Linux). Multi-stage build:

  1. rust-builder    -> native aarch64 plutonium-updater (upstream ships
                       no aarch64-linux release).
  2. cpp-builder     -> native aarch64 iw4x-launcher via build2 0.18.1
                       compiled from source.
  3. runtime         -> Debian 13 trixie + Hangover 11.4 (.deb bundle:
                       Wine 11 + FEX 2603 + box64 as Wine CPU plug-ins).

x86_64 Windows PE binaries (Plutonium/IW4x/T7x server exes) run under
Wine 11 thunked WoW64 with libarm64ecfex.dll as the CPU backend. No
user-space x86 emulator is required because the helper CLIs are native
aarch64.

scripts/ unchanged - the entrypoints call /home/plutainer/.plutainer/
{plutonium-updater,iw4x-launcher} by fixed path, and we drop a native
aarch64 ELF at exactly that path. The amd64 Dockerfile and its
workflow are untouched.

CI: .github/workflows/docker-publish-arm64.yml builds via QEMU on
ubuntu-latest and pushes ghcr.io/<repo>:arm64. Comment in the file
notes the ubuntu-24.04-arm runner swap for a 3-5x speedup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add curl to arm64 cpp-builder stage so build2 installer runs

The build2-install-*.sh script unconditionally `check_cmd curl`s before
fetching the toolchain source tarball; the stage shipped only wget, so
the installer exited 1 on first ARM64 build. Add curl to the apt list
and use it for the script download too for consistency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Switch arm64 CI to native ubuntu-24.04-arm runner

Plutainer is a public repo, so ARM-hosted runners are free. The
Multi-Stage Dockerfile compiles build2 from source and runs full
cargo/build2 builds; QEMU-emulated on ubuntu-latest that runs 60-90 min
and exposes flaky edge cases. Native aarch64 should land at 20-30 min
and removes QEMU as a failure surface.

setup-qemu-action removed (not needed on native arm runner).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Adapt arm64 image to v2-layout: add migrate-v1-to-v2.sh, retag

Two narrow tweaks on top of the v2 scripts:

* Dockerfile.arm64: include migrate-v1-to-v2.sh in the chmod list so the
  v2 entrypoint can find it executable (was missing because the original
  arm64 commit predated v2).
* CI workflow: trigger on `arm64-v2` and `v2-layout` too, and emit a
  clean `:arm64-v2` tag for those branches instead of the v1-style
  `arm64-{{branch}}` pattern which would produce `arm64-arm64-v2`.

scripts/ themselves come unchanged from upstream v2-layout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* v2: combined v1-deployment refusal + MIGRATION.md + workflow for :latest promotion

Pre-promotion to :latest, the v2 image needs to refuse v1 deployments
cleanly. Previously the volume check printed its own refusal block but the
env-var side was silent — a user pulling :latest (post-promotion) onto
their v1 compose would have hit "PLUTAINER_CONFIG_FILE not set" with no
hint that the rename was the cause.

Changes:

- detect_legacy_env_vars: scans PLUTO_*/IW4X_*/ALTER_* names, populates
  LEGACY_ENVS_FOUND. Returns non-zero if any are set.
- check_volume_version: still validates marker / inits fresh / detects v1,
  but no longer prints the v1 block inline. Sets V1_VOLUME_DETECTED=true
  on v1 detection and returns. Marker-mismatch errors still print.
- print_v1_migration_block: combined refusal block, adapts to what was
  detected (env, volume, or both). Shows both paths side by side —
  pin :v1-final or migrate via the bundled tool + env rename.
- entrypoint.sh: runs both detectors before deciding to hold, so a single
  block surfaces every reason for refusal.
- MIGRATION.md: full env mapping table, step-by-step migration, rollback.
- README.md: top banner rewritten for the post-promotion tag layout.
  :latest = v2, :v1-final = frozen v1.
- docker-publish.yml + docker-publish-arm64.yml: :v2 / :arm64-v2 enable
  predicates now also fire on main, so the post-merge build publishes
  both the canonical (:latest / :arm64) and the alias tags.

---------

Co-authored-by: nellydocs <71311423+nellydocs@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 11:56:13 +01:00

7.3 KiB

Migration: Plutainer v1 → v2

Plutainer v2 is now published as ghcr.io/ayymoss/plutainer:latest (and :v2 as a permanent alias). The previous v1 image is frozen at ghcr.io/ayymoss/plutainer:v1-final and :v1.

This page covers everything you need to migrate an existing v1 deployment.

If you do not want to migrate right now, pin v1-final instead and come back when you are ready. v1-final will continue to start, but it shows a deprecation banner every time and receives no further updates.


What changed?

  1. Unified environment variables. The old PLUTO_*, IW4X_*, and ALTER_* prefixes are gone. Everything is now PLUTAINER_* and game family is derived from PLUTAINER_GAME.
  2. New volume layout inside /home/plutainer/app/:
    v1 layout                    v2 layout
    ─────────                    ─────────
    app/gamefiles/               app/configs/        ← your *.cfg files (flat)
    app/plutonium/               app/runtime/gamefiles/
    app/logs/                    app/runtime/plutonium/
                                 app/logs/           ← stable symlinks to active logs
                                 .plutainer-version  ← layout marker (contains "2")
    
    You now edit configs in one flat app/configs/ directory. The entrypoint places relative symlinks from there into wherever the game engine expects them, so a single edit reaches both the engine path and any mod path.
  3. app/logs/ is now maintained by log-watcher.sh — every *.log under app/ is surfaced as a stable relative symlink in this directory, so sidecars (IW4MAdmin) can read from one predictable path regardless of which mod dir the game is writing to.

Path A — Stay on v1

# docker-compose.yml
services:
  myserver:
    image: ghcr.io/ayymoss/plutainer:v1-final   # was: :latest
    # ... everything else stays as-is

Then:

docker compose pull
docker compose up -d

A deprecation banner appears in docker logs on every start. The server otherwise runs unchanged.


Path B — Migrate to v2

Step 1 — Stop the container

docker compose down

Step 2 — Migrate the volume layout

Run the bundled migration tool against your existing volume. The tool moves files in place, replacing engine-path config files with relative symlinks back into the new app/configs/ tree. No data is deleted.

docker run --rm \
  -v <YOUR_APP_VOLUME>:/home/plutainer/app \
  --entrypoint /home/plutainer/.plutainer/migrate-v1-to-v2.sh \
  ghcr.io/ayymoss/plutainer:v2

<YOUR_APP_VOLUME> is whatever host path is bound to /home/plutainer/app in your compose. For a directory-style mount like ./t6zm-1:/home/plutainer/app, that is ./t6zm-1.

Preview first by appending --dry-run — the tool prints every move it would make without touching the filesystem.

After the tool runs, your volume contains:

  • app/configs/<your *.cfg> (real files, moved from old engine paths)
  • Symlinks from the old engine paths back into app/configs/ so the game still finds them
  • app/.plutainer-version containing 2
  • app/runtime/gamefiles/, app/runtime/plutonium/ (your old app/gamefiles/, app/plutonium/ moved here)

Step 3 — Rename your environment variables

Anything not listed in this table keeps its old name (notably PLUTO_SERVER_KEY, PLUTO_MAX_CLIENTS, IW4X_NET_LOG_IP — these are unchanged).

v1 name v2 name
PLUTO_GAME PLUTAINER_GAME
PLUTO_CONFIG_FILE PLUTAINER_CONFIG_FILE
PLUTO_PORT PLUTAINER_PORT
PLUTO_HEALTHCHECK PLUTAINER_HEALTHCHECK
PLUTO_MOD PLUTAINER_MOD
PLUTO_SKIP_SEED PLUTAINER_SKIP_SEED
PLUTO_AUTO_UPDATE PLUTAINER_AUTO_UPDATE
PLUTO_SERVER_NAME PLUTAINER_SERVER_NAME
PLUTO_EXTRA_ARGS PLUTAINER_EXTRA_ARGS
IW4X_GAME PLUTAINER_GAME=iw4x
IW4X_CONFIG_FILE PLUTAINER_CONFIG_FILE
IW4X_PORT PLUTAINER_PORT
IW4X_HEALTHCHECK PLUTAINER_HEALTHCHECK
IW4X_MOD PLUTAINER_MOD
IW4X_AUTO_UPDATE PLUTAINER_AUTO_UPDATE
IW4X_SERVER_NAME PLUTAINER_SERVER_NAME
IW4X_EXTRA_ARGS PLUTAINER_EXTRA_ARGS
ALTER_GAME PLUTAINER_GAME (e.g. t7x)
ALTER_CONFIG_FILE PLUTAINER_CONFIG_FILE
ALTER_PORT PLUTAINER_PORT
ALTER_HEALTHCHECK PLUTAINER_HEALTHCHECK
ALTER_MOD PLUTAINER_MOD
ALTER_SKIP_SEED PLUTAINER_SKIP_SEED
ALTER_AUTO_UPDATE PLUTAINER_AUTO_UPDATE
ALTER_SERVER_NAME PLUTAINER_SERVER_NAME
ALTER_EXTRA_ARGS PLUTAINER_EXTRA_ARGS

Update the image: line in your compose:

image: ghcr.io/ayymoss/plutainer:latest   # or :v2 (alias)

Step 4 — Start

docker compose up -d
docker compose logs -f

You should see a fresh boot ending in Starting Plutonium <game> Server: ... (or the equivalent for IW4x/Alterware) followed by the game process. Healthcheck flips to healthy once RCON responds.


Troubleshooting

Container holds with a v1 deployment refusal block. You started v2 against either a v1 volume, v1 env vars, or both. The block lists exactly what was detected. Fix what it lists, then docker compose up -d. The container does not restart-loop — it stays Up with sleep infinity so you can read the message in docker logs.

Container holds with "Config file not found". Your PLUTAINER_CONFIG_FILE does not match a file in app/configs/. The block prints a case-insensitive hint (Did you mean: ...?) — filenames on Linux are case-sensitive.

RCON / healthcheck warns "Could not parse rcon_password". The parser accepts set rcon_password "...", seta rcon_password '...', unquoted values, and the bare rcon_password "..." form. It strips // line comments. Do not set rcon_password via PLUTAINER_EXTRA_ARGS — it cannot be read back.

I want the old behaviour where I edit configs directly in the engine path. Set PLUTAINER_USE_RAW_CONFIGS=true. The fan-out symlinks are skipped and the engine path becomes the source of truth.


Rolling back

If something goes wrong, you can return to v1 immediately:

image: ghcr.io/ayymoss/plutainer:v1-final

docker compose up -d. The v2 migration tool only moved files inside the bind-mounted volume — it did not delete anything. v1-final will read the v1 layout it remembers, ignore the new app/configs/ and app/runtime/ dirs, and start as before.

If you need to fully restore the pre-migration state (so the dirs from v2 are removed), back up first and then mv the contents of app/runtime/gamefiles/ back to app/gamefiles/ and app/runtime/plutonium/ back to app/plutonium/. Symlinks at engine paths can be replaced with the real files from app/configs/. Most users will not need this.