cd /news/developer-tools/running-unity-6000-x-headless-builds… Β· home β€Ί topics β€Ί developer-tools β€Ί article
[ARTICLE Β· art-148595] src=dev.to β†— pub= topic=developer-tools verified=true sentiment=Β· neutral

Running Unity 6000.x Headless Builds in a Linux Container: A Field Report (Including Licensing Workarounds)

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.

by read8 min views2 publishedOct 10, 2026

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.

On 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:

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. The 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).

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.

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.

sudo apt update && sudo apt install -y gcc curl

Architecture (what talks to what):

Developer machine (browser) -- one-time OAuth --> api.unity.com
        |
        |  authenticated session cached in ~/.config/unityhub/
        v
Container/VM:
  Hub CLI (~/.local/bin/unity)
    -> downloads editor archive (no login needed)
    -> unity license activate (needs OAuth session)
  Editor binary (~/Unity/Hub/Editor/6000.3.26f1/Editor)
    -> -batchmode -nographics -executeMethod ...
  license/auth state: ~/.config/unityhub/  <- PERSIST THIS DIR

I 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.

My operating principle for the day: don't say "can't" without concrete evidence. Every wall gets a workaround attempt first.

Unity now ships an official (experimental, beta) CLI designed for terminal/CI/agent workflows:

curl -fsSL https://public-cdn.cloud.unity3d.com/hub/prod/cli/install.sh \
  | UNITY_CLI_CHANNEL=beta bash

Key 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.

~/.local/bin/unity install --help   # lists versions; 6000.3.26f1 was the latest LTS

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):

~/.local/bin/unity install 6000.3.26f1

It streams JSON progress lines and pulls ~4.2 GB.

The 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:

Cannot change ownership to uid 1000: Operation not permitted

Root 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:

rm -rf /home/hatch/Unity/Hub/Editor/6000.3.26f1
mkdir -p /home/hatch/Unity/Hub/Editor/6000.3.26f1
tar --no-same-owner -xJf ~/.config/unityhub/downloads/Unity-6000.3.26f1.tar.xz \
  -C /home/hatch/Unity/Hub/Editor/6000.3.26f1/

Result: Unity Editor 6000.3.26f1 installed (~8.2 GB). Sanity check: <Editor>/Unity -version β†’ 6000.3.26f1.

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).

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.

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.

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.

.alf β†’ .ulf) The route that does not work for Personal licenses as of October 2026: <Editor>/Unity -batchmode -createManualActivationFile produces the .alf fine, but up 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.

~/.local/bin/unity license activate --personal --accept-eula
~/.local/bin/unity license status

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.

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."

Actionable regardless of cause:

~/.local/bin/unity license status || \
  ~/.local/bin/unity license activate --personal --accept-eula

~/.config/unityhub/``$HOME changes between runsunity license activate.

~/.local/bin/unity projects new CrystalIsle \
  --template com.unity.template.3d \
  --editor-version 6000.3.26f1 \
  --path /path/to/parent

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.

Template creation failed at first: Unity's writer calls chown β†’ container EPERM. Workaround: a tiny LD_PRELOAD shim stubbing chown/ fchown/ lchown to return 0:

// fakechown.c β€” compile: gcc -shared -fPIC -o fakechown.so fakechown.c
#define _GNU_SOURCE
#include <sys/types.h>
#include <unistd.h>
int chown(const char *p, uid_t o, gid_t g) { return 0; }
int fchown(int f, uid_t o, gid_t g) { return 0; }
int lchown(const char *p, uid_t o, gid_t g) { return 0; }
export LD_PRELOAD=/path/to/fakechown.so

WARNING β€” read before using the LD_PRELOAD shim

This 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.

Drop a script in Assets/Editor/ (trimmed for clarity):

// Assets/Editor/BuildScene.cs
using UnityEngine;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine.SceneManagement;

public class BuildScene
{
    public static void Build()
    {
        var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
        scene.name = "MainScene";
        var islandPrefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Models/island_v1.fbx");
        var island = (GameObject)PrefabUtility.InstantiatePrefab(islandPrefab, scene);
        island.name = "Island";
        // ... trees, crystals, player, camera ...
        var sunObj = new GameObject("Sun");
        SceneManager.MoveGameObjectToScene(sunObj, scene);
        sunObj.AddComponent<Light>().type = LightType.Directional;
        EditorSceneManager.SaveScene(scene, "Assets/Scenes/MainScene.unity");
        Debug.Log("SCENE_BUILD_OK");
    }
}
<Editor>/Unity -batchmode -nographics \
  -projectPath /path/to/CrystalIsle \
  -executeMethod BuildScene.Build \
  -logFile -

Logged SCENE_BUILD_OK; Assets/Scenes/MainScene.unity written. No GUI ever opened.

// Assets/Editor/BuildGame.cs
public class BuildGame
{
    public static void Build()
    {
        var report = BuildPipeline.BuildPlayer(
            new[] { "Assets/Scenes/MainScene.unity" },
            "/path/to/build/CrystalIsle.x86_64",
            BuildTarget.StandaloneLinux64, BuildOptions.None);
        Debug.Log("BUILD_RESULT: " + report.summary.result + " in " + report.summary.totalTime);
    }
}
<Editor>/Unity -batchmode -nographics -projectPath /path/to/CrystalIsle \
  -executeMethod BuildGame.Build -logFile -
./build/CrystalIsle.x86_64 -batchmode -nographics; echo $?

Exit code 0. Full loop β€” procedural FBX β†’ scripted scene assembly β†’ Linux build β†’ launch β€” works with zero human interaction after authentication.

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.

rm -rf ~/Unity/Hub/Editor/6000.3.26f1 && mkdir -p ~/Unity/Hub/Editor/6000.3.26f1
tar --no-same-owner -xJf ~/.config/unityhub/downloads/Unity-6000.3.26f1.tar.xz \
  -C ~/Unity/Hub/Editor/6000.3.26f1/
rm -rf ~/.config/unityhub/downloads/* && ~/.local/bin/unity install 6000.3.26f1
rm -rf <proj>/Library
~/.local/bin/unity license status || ~/.local/bin/unity license activate --personal --accept-eula
unset LD_PRELOAD

The 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.

── more in #developer-tools 4 stories Β· sorted by recency
── more on @unity 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/running-unity-6000-x…] indexed:0 read:8min 2026-10-10 Β· β€”