* 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>
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?
- Unified environment variables. The old
PLUTO_*,IW4X_*, andALTER_*prefixes are gone. Everything is nowPLUTAINER_*and game family is derived fromPLUTAINER_GAME. - New volume layout inside
/home/plutainer/app/:
You now edit configs in one flatv1 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")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. app/logs/is now maintained bylog-watcher.sh— every*.logunderapp/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-versioncontaining2app/runtime/gamefiles/,app/runtime/plutonium/(your oldapp/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.