{"slug": "running-unity-6000-x-headless-builds-in-a-linux-container-a-field-report", "title": "Running Unity 6000.x Headless Builds in a Linux Container: A Field Report (Including Licensing Workarounds)", "summary": "A developer got Unity 6000.3.26f1 (LTS) running fully headless in an Ubuntu 24.04 container, using the experimental Unity Hub CLI (v1.0.0-beta.13) for installation and OAuth-based license activation, then assembled a 3D scene from a C# editor script and produced a working Linux x86_64 game build. The report documents the working path — including an LD_PRELOAD shim to work around chown restrictions and running the Editor binary directly with -batchmode -nographics -executeMethod — and the dead end of manual .alf/.ulf license activation, which failed in a login redirect loop on 2026-10-09.", "body_md": "**Status: REVISED 2026-10-10** — every technical claim verified against the actual build artifacts (install logs, auth outputs, editor scripts, binary). Hostile external AI critique applied 2026-10-10; valid points incorporated, untestable ones marked honestly. This documents a real pipeline built on a real machine, including the dead ends. It is a field report with a reproducible core — not a copy-paste guarantee: Unity's pages and CLI behavior drift, and several environment-specific behaviors are called out as such. If you follow it, verify each step's output before proceeding.\n\nOn 2026-10-09 I got **Unity 6000.3.26f1 (LTS) running fully headless in an Ubuntu 24.04 container** — no display, no human at a keyboard — and used it to assemble a 3D scene from a C# editor script and produce a working Linux x86_64 game build. The path that worked:\n\n`gcc`, `curl`) and the experimental Unity Hub CLI (` v1.0.0-beta.13`).` tar --no-same-owner` (the CLI's extractor chokes in containers, and plain `tar` fails on ownership).`unity auth login` (browser OAuth) — `unity license activate --personal --accept-eula`.` unity projects new --template com.unity.template.3d`).` chown` restrictions with an `LD_PRELOAD` shim — `Unity -batchmode -nographics -executeMethod ...`.` unity license status` as a pre-flight check, not a one-time setup.\nThe path that **did not** work: manual license activation (`.alf` → upload → `.ulf`). On 2026-10-09 the upload flow at `license.unity3d.com/manual-activation` died in a login redirect loop for my Personal seat and never produced a `.ulf` — observed behavior, not a quoted policy. Don't sink an hour into it without checking Unity's current docs first (I did, so you don't have to).\n\n**Environment:** Ubuntu 24.04.5 LTS, x86_64, unprivileged container user (non-root), ~95 GB free disk, no GPU, `xvfb` available but not required for batch mode.\n\n**Prerequisites (have these before you start):** a Unity account (free Personal is enough), a Linux x86_64 container/VM with ~15 GB free disk, and **a browser you can reach within ~5 minutes** for the one-time OAuth in Step 3.\n\n```\n# system dependencies used in this guide\nsudo apt update && sudo apt install -y gcc curl\n# gcc: only needed to compile the Step 5 LD_PRELOAD shim\n# curl: only needed for the Step 0 CLI installer\n```\n\n**Architecture (what talks to what):**\n\n``` php\nDeveloper machine (browser) -- one-time OAuth --> api.unity.com\n        |\n        |  authenticated session cached in ~/.config/unityhub/\n        v\nContainer/VM:\n  Hub CLI (~/.local/bin/unity)\n    -> downloads editor archive (no login needed)\n    -> unity license activate (needs OAuth session)\n  Editor binary (~/Unity/Hub/Editor/6000.3.26f1/Editor)\n    -> -batchmode -nographics -executeMethod ...\n  license/auth state: ~/.config/unityhub/  <- PERSIST THIS DIR\n```\n\nI wanted a CI-style pipeline: install Unity headless → create a project → assemble a scene from code → build a Linux player → verify the binary runs. No editor GUI, no clicking. This is bread-and-butter for CI/CD, but Unity 6000.x assumes an interactive user at several steps, and containers add their own permission quirks.\n\nMy operating principle for the day: **don't say \"can't\" without concrete evidence.** Every wall gets a workaround attempt first.\n\nUnity now ships an official (experimental, beta) CLI designed for terminal/CI/agent workflows:\n\n```\ncurl -fsSL https://public-cdn.cloud.unity3d.com/hub/prod/cli/install.sh \\\n  | UNITY_CLI_CHANNEL=beta bash\n# installs to ~/.local/bin/unity, version was v1.0.0-beta.13\n```\n\nKey subcommands: `unity install`, `unity build`, `unity run`, `unity license`, `unity auth`. The CLI provides higher-level `build`/` run` commands that internally launch the Editor in non-interactive mode. For custom editor-scripting workflows, I used the Editor binary directly (`Unity -batchmode -nographics -executeMethod ...`) because it exposes the full Unity command-line surface — the CLI's wrappers are convenient but thinner.\n\n```\n~/.local/bin/unity install --help   # lists versions; 6000.3.26f1 was the latest LTS\n```\n\n**With Hub CLI v1.0.0-beta.13 and Unity 6000.3.26f1, the editor archive download completed before any authentication step** (scoped claim — this is what happened on 2026-10-09, not a promise about all versions):\n\n```\n~/.local/bin/unity install 6000.3.26f1\n```\n\nIt streams JSON progress lines and pulls ~4.2 GB.\n\nThe CLI downloaded 100% successfully, then failed at extraction: `COULD_NOT_EXTRACT`. The archive itself was valid; the target directory contained a partial extraction. Manual extraction also failed:\n\n```\nCannot change ownership to uid 1000: Operation not permitted\n```\n\nRoot cause: `tar` tries to preserve the archive's original file ownership, but we're an unprivileged container user — `chown` returns `EPERM`. **Fix:** wipe partial state first, then tell tar not to preserve ownership:\n\n```\nrm -rf /home/hatch/Unity/Hub/Editor/6000.3.26f1\nmkdir -p /home/hatch/Unity/Hub/Editor/6000.3.26f1\ntar --no-same-owner -xJf ~/.config/unityhub/downloads/Unity-6000.3.26f1.tar.xz \\\n  -C /home/hatch/Unity/Hub/Editor/6000.3.26f1/\n```\n\nResult: Unity Editor 6000.3.26f1 installed (~8.2 GB). Sanity check: `<Editor>/Unity -version` → `6000.3.26f1`.\n\n**Pitfall #1:** In rootless containers, *any* tool that calls `chown` will fail — not just tar. Unity's own package manager and template installer hit the same wall later (see Step 5).\n\n`unity auth login` prints a sign-in URL and polls. Two attempts timed out (exit code 3) — **the effective window was ~5 minutes** regardless of the timeout passed. What worked: start the CLI, then *immediately* open the fresh one-time authorization URL in a browser and click \"Allow Login Request\" while the CLI is still polling.\n\n**Pitfall #2:** Each attempt generates a fresh URL with a unique `state` — you cannot reuse an old link. Have the browser ready *before* starting `unity auth login`.\n\n**Not achieved:** fully autonomous OAuth with zero browser interaction. For true CI, look at Unity Cloud **service accounts** (`--client-id` + `--client-secret`), which I didn't have.\n\n`.alf` → `.ulf`)\nThe route that **does not work** for Personal licenses as of October 2026: `<Editor>/Unity -batchmode -createManualActivationFile` produces the `.alf` fine, but uploading it at `license.unity3d.com/manual-activation` died in a login redirect loop (`ERR_TOO_MANY_REDIRECTS`) — never produced a `.ulf`. Observed behavior on 2026-10-09, not a documented policy. Plus/Pro seats might still work — unverified.\n\n```\n~/.local/bin/unity license activate --personal --accept-eula\n~/.local/bin/unity license status\n# License: active (Unity Personal, Asset Store assigned)\n```\n\n**Where the state lives (verified on disk):** `~/.config/unityhub/` — `accounts.db`, `hub.db`, and the `external-modules/licensingClient/` component. No `.ulf` in `~/.local/share/unity3d/` with Hub-CLI-managed Personal activation.\n\n**What happened to me:** activated cleanly 2026-10-09, editor ran licensed all evening. Next morning: `License: none active` while `Signed in: yes` persisted. Cause unknown — expiry, container ephemerality, or Unity reclaiming are all consistent. Do NOT read this as \"Personal licenses always expire every 24h in containers.\"\n\n**Actionable regardless of cause:**\n\n```\n# pre-flight check — run at the START of every session/job\n~/.local/bin/unity license status || \\\n  ~/.local/bin/unity license activate --personal --accept-eula\n```\n\n`~/.config/unityhub/``$HOME` changes between runs`unity license activate`.\n\n```\n~/.local/bin/unity projects new CrystalIsle \\\n  --template com.unity.template.3d \\\n  --editor-version 6000.3.26f1 \\\n  --path /path/to/parent\n```\n\n**Honesty note:** the exact invocation I ran on 2026-10-09 was not captured in my session logs. The syntax above is from `unity projects new --help` (CLI v1.0.0-beta.13, checked 2026-10-10); the resulting project's `Packages/manifest.json` confirms the official 3D template's package set. Verify against your CLI version.\n\nTemplate creation failed at first: Unity's writer calls `chown` → container `EPERM`. Workaround: a tiny `LD_PRELOAD` shim stubbing `chown`/` fchown`/` lchown` to return 0:\n\n```\n// fakechown.c — compile: gcc -shared -fPIC -o fakechown.so fakechown.c\n#define _GNU_SOURCE\n#include <sys/types.h>\n#include <unistd.h>\nint chown(const char *p, uid_t o, gid_t g) { return 0; }\nint fchown(int f, uid_t o, gid_t g) { return 0; }\nint lchown(const char *p, uid_t o, gid_t g) { return 0; }\nexport LD_PRELOAD=/path/to/fakechown.so\n# run Unity project-creation / editor commands in this environment, then unset\n```\n\n### WARNING — read before using the LD_PRELOAD shim\n\nThis is the most dangerous workaround here. `LD_PRELOAD` affects **every** dynamically linked binary in the environment; the stub **silently lies** (reports success for operations that never happened); it **hides real permission failures**; it doesn't cover static binaries or `fchownat()`. The correct implementation forwards via `dlsym(RTLD_NEXT, ...)` and only overrides the `EPERM` case (guidance, not tested here). **Rule: throwaway CI containers only. `unset LD_PRELOAD` when done.**\n\nDrop a script in `Assets/Editor/` (trimmed for clarity):\n\n```\n// Assets/Editor/BuildScene.cs\nusing UnityEngine;\nusing UnityEditor;\nusing UnityEditor.SceneManagement;\nusing UnityEngine.SceneManagement;\n\npublic class BuildScene\n{\n    public static void Build()\n    {\n        var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);\n        scene.name = \"MainScene\";\n        var islandPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(\"Assets/Models/island_v1.fbx\");\n        var island = (GameObject)PrefabUtility.InstantiatePrefab(islandPrefab, scene);\n        island.name = \"Island\";\n        // ... trees, crystals, player, camera ...\n        var sunObj = new GameObject(\"Sun\");\n        SceneManager.MoveGameObjectToScene(sunObj, scene);\n        sunObj.AddComponent<Light>().type = LightType.Directional;\n        EditorSceneManager.SaveScene(scene, \"Assets/Scenes/MainScene.unity\");\n        Debug.Log(\"SCENE_BUILD_OK\");\n    }\n}\n<Editor>/Unity -batchmode -nographics \\\n  -projectPath /path/to/CrystalIsle \\\n  -executeMethod BuildScene.Build \\\n  -logFile -\n```\n\nLogged `SCENE_BUILD_OK`; `Assets/Scenes/MainScene.unity` written. No GUI ever opened.\n\n```\n// Assets/Editor/BuildGame.cs\npublic class BuildGame\n{\n    public static void Build()\n    {\n        var report = BuildPipeline.BuildPlayer(\n            new[] { \"Assets/Scenes/MainScene.unity\" },\n            \"/path/to/build/CrystalIsle.x86_64\",\n            BuildTarget.StandaloneLinux64, BuildOptions.None);\n        Debug.Log(\"BUILD_RESULT: \" + report.summary.result + \" in \" + report.summary.totalTime);\n    }\n}\n<Editor>/Unity -batchmode -nographics -projectPath /path/to/CrystalIsle \\\n  -executeMethod BuildGame.Build -logFile -\n# BUILD_RESULT: Succeeded in 1m15.83s\n./build/CrystalIsle.x86_64 -batchmode -nographics; echo $?\n# 0\n```\n\nExit code 0. Full loop — **procedural FBX → scripted scene assembly → Linux build → launch** — works with zero human interaction after authentication.\n\n`StandaloneLinux64`. Modules for other platforms not attempted.` PlaybackEngines/LinuxStandaloneSupport`, `il2cpp/`, and Mono (verified by listing) — Linux builds worked out of the box. IL2CPP backend builds not attempted.`--client-id`/`--client-secret`) untested.` Library/`, CLI download cache. Not tested as a volume setup.\n\n```\n# Failed extraction → wipe and re-extract clean\nrm -rf ~/Unity/Hub/Editor/6000.3.26f1 && mkdir -p ~/Unity/Hub/Editor/6000.3.26f1\ntar --no-same-owner -xJf ~/.config/unityhub/downloads/Unity-6000.3.26f1.tar.xz \\\n  -C ~/Unity/Hub/Editor/6000.3.26f1/\n# Corrupt download → clear cache and re-download\nrm -rf ~/.config/unityhub/downloads/* && ~/.local/bin/unity install 6000.3.26f1\n# Broken Library cache → delete; editor reimports (slow first run)\nrm -rf <proj>/Library\n# License confusion\n~/.local/bin/unity license status || ~/.local/bin/unity license activate --personal --accept-eula\nunset LD_PRELOAD\n```\n\nThe morning started with \"the license wall is not a difficulty, it's a hard authentication barrier\" — and the day's principle was *don't declare impossibility without evidence*. Several failed OAuth attempts, one failed extraction, one dead-end activation flow, and one `EPERM` class of container problems later, the pipeline runs end to end. Most of the difficulty wasn't Unity; it was the container. If you're fighting Unity headless builds in Docker/CI, check your `chown` assumptions before you blame the engine.", "url": "https://wpnews.pro/news/running-unity-6000-x-headless-builds-in-a-linux-container-a-field-report", "canonical_source": "https://dev.to/nydjustin/running-unity-6000x-headless-builds-in-a-linux-container-a-field-report-including-licensing-519b", "published_at": "2026-10-10 03:21:17+00:00", "updated_at": "2026-10-10 03:29:03.437589+00:00", "lang": "en", "topics": ["developer-tools", "ai-agents"], "entities": ["Unity", "Unity Hub CLI", "Ubuntu", "Unity 6000.3.26f1"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/running-unity-6000-x-headless-builds-in-a-linux-container-a-field-report", "markdown": "https://wpnews.pro/news/running-unity-6000-x-headless-builds-in-a-linux-container-a-field-report.md", "text": "https://wpnews.pro/news/running-unity-6000-x-headless-builds-in-a-linux-container-a-field-report.txt", "jsonld": "https://wpnews.pro/news/running-unity-6000-x-headless-builds-in-a-linux-container-a-field-report.jsonld"}}