Skip to main content

Config file locations

ERun's configuration lives in a small number of well-known files.

Per-user (your machine)​

The per-user tree lives under the platform's config directory, with an erun/ root inside it. That directory is resolved the way the XDG base-directory spec defines it: an absolute XDG_CONFIG_HOME wins on every platform, and otherwise each platform has its own default — on macOS that default is not ~/.config:

OSPer-user config root
macOS$XDG_CONFIG_HOME/erun, else ~/Library/Application Support/erun
Linux$XDG_CONFIG_HOME/erun, else ~/.config/erun
Windows%XDG_CONFIG_HOME%\erun, else %LOCALAPPDATA%\erun

So on a Mac with no XDG_CONFIG_HOME set, ~/.config/erun does not exist and the tree is under ~/Library/Application Support/erun. Do not guess the root: erun doctor prints the one ERun actually resolved.

Elsewhere in this documentation, <config-root> stands for the per-user config root in the table above.

Every file in the tree is named config.yaml — only its directory distinguishes the global, tenant, and environment configs:

<config-root>/
├── config.yaml # global defaults (default tenant, default env)
├── config.yaml.<YYYY-MM-DD>.bak # daily backups of the global config (last 5 kept)
└── <tenant>/
├── config.yaml # tenant config (default environment, API URL)
└── <environment>/
├── config.yaml # env config (kube context, registry, runtime)
├── config.yaml.<YYYY-MM-DD>.bak # daily backups of this env's config (last 5 kept)
└── (workspace caches, idle logs, …)

Every file in the tree is named config.yaml; only the directory it sits in distinguishes the global, tenant, and environment configs.

The desktop keeps its own state beside this tree​

The config root is lowercase erun. The desktop app keeps its own per-installation state — its identity key, its log, and the orchestrator bookkeeping it restores itself from — in a separately spelled ERun directory beside it, under the same config home. The two names differ only in case, so on the case-insensitive filesystems macOS ships with by default they resolve to a single directory and the distinction is invisible; on a case-sensitive volume, and on Linux, they are two directories.

That matters when you go looking for a file by hand. A tenant's config, its secrets, the ACME state, and the port-forward records under portforward/ are all in the lowercase tree in the table above; desktopid.pub, erun-app.log, and the orchestrator files are in the capitalized one. Do not merge the two by renaming either directory: the spelling a component resolves is what points it at its state, so renaming one hides every file the other name was reading. The port-forward records are part of the config tree, not the desktop's — the desktop only reads that convention, and never writes it.

Config backups​

Both the global config and each environment's config.yaml are snapshotted before they are overwritten. The first save on any given day copies the previous file to <name>.<YYYY-MM-DD>.bak next to it; later saves the same day are no-ops, and only the five most recent dailies are kept. Backups carry no secrets the live file does not.

To roll a file back, run erun doctor: --restore-config-from-backup <date> for the global config, or --restore-env-config-from-backup <date> (with an explicit tenant and environment) for one environment's config — useful when a setting was changed or corrupted, such as an environment type that resolved to the wrong value.

Alongside the config tree, ERun keeps per-environment state under ~/.erun (always the home directory, not the XDG dir):

~/.erun/<tenant>/<environment>/
└── trace.log # rolling full-detail trace of every env-scoped command
~/.erun/timing/
└── <command>-<timestamp>.json # one step-timing record per build/release/push/deploy run

Trace log​

Every environment-scoped command (open, doctor, deploy, a scoped upgrade, and the MCP action tools inside the pod) appends its complete trace — every action and decision, at full detail regardless of -v/-vv — to trace.log, each line stamped with an RFC3339 timestamp. The file rotates to trace.log.1 once it passes 5 MB, so the pair never grows beyond ~10 MB per environment. --dry-run previews are not written. A remote environment accumulates two of these files — the host one (commands run from your machine: open, deploy, doctor, upgrade) and the in-pod one (Agent MCP actions and in-pod CLI runs). The desktop's Diagnostics console merges both into one timeline by the per-line timestamps, marking in-pod lines [pod], so diagnostics for a failure exist even when nobody was watching.

Step timing​

build, release, push, and deploy each measure themselves as a tree of named steps — the whole run at the root, with children for each image build (further split per architecture), each chart publish, each release stage, and each deploy target — and report it two ways on completion, on both success and failure:

  1. A duration-ordered table on the normal feedback channel (so it also lands in trace.log for an environment-scoped deploy), one line per step, indented by nesting, each line reading <name> [<duration>]. Within a step, children are sorted by duration descending — the dominant cost is always the first child line — except that two children within 100ms of each other sort by name instead, so the order is the same on every run rather than flipping on scheduler jitter between two near-identical costs. A step whose children's durations don't add up to its own carries a synthetic (unaccounted) [<duration>] line for the gap (a sub-step nothing here names); a step whose children ran concurrently and so overlap carries (ran concurrently, overlap) [<duration>] instead of a misleading negative number. An image build step is annotated (cache hit) or (cache miss: <reason>) — see Agent reference · Fingerprint cache for what a hit means and why a miss happens for the reason given. A failed step is annotated (failed) and the row ends with — <error>.

  2. A JSON document at ~/.erun/timing/<command>-<timestamp>.json (<timestamp> is the run's start time, 20060102T150405.000000000Z, so a directory listing sorts oldest to newest and two runs of the same command never collide), so two runs — a normal one and a 22x-slower one — can be diffed by tooling instead of compared by eye across logs:

    {
    "command": "release", // build | release | push | deploy
    "startedAt": "2026-08-22T14:05:12.345596947Z",
    "durationSeconds": 2713.4,
    "duration": "45m13.4s", // Go's time.Duration.String()
    "failed": false,
    "error": "", // set only when failed is true; for a failed image
    // build this carries the failing step's own last
    // output plus BuildKit's headline, not just the
    // process exit status, so a detached run stays
    // diagnosable once its output is gone
    "steps": [
    {
    "name": "frs-docs", // image name, chart name ("chart <name>"),
    // release stage name, or "<tenant>/<env> <version>" for deploy
    "durationSeconds": 664.2,
    "duration": "11m4.2s",
    "failed": false,
    "cacheHit": false, // omitted entirely for a non-image step
    "cacheMissReason": "fingerprint image is missing for platforms [linux/amd64, linux/arm64]",
    "unaccountedSeconds": 0, // present only when the gap crosses the 100ms floor
    "overlapSeconds": 0, // present only when children ran concurrently
    "steps": [
    { "name": "linux/amd64", "durationSeconds": 340.1, "duration": "5m40.1s", "cacheHit": false, "cacheMissReason": "…" },
    { "name": "linux/arm64", "durationSeconds": 324.1, "duration": "5m24.1s", "cacheHit": false, "cacheMissReason": "…" }
    ]
    }
    ]
    }

    Unlike the printed table, a JSON record's steps stay in insertion order rather than duration-sorted, so the same step lands at the same array position across two runs even when a regression changed which one took longest.

~/.erun/timing/ is not tenant/environment-scoped like trace.log: build, release, and push are pure primitives that often run with no deploy target at all, so a location keyed to tenant+environment would leave most of their runs with no record. All four commands write here regardless of whether they're also inside an environment-scoped trace.log.

--dry-run does no work and skips step timing entirely, the same as it skips the ==> Building / ==> Releasing / ==> Pushing / ==> Deploying markers.

Unlike the per-user config root above, ~/.erun is always the home directory and never moves with XDG_CONFIG_HOME.

Per-project (committed in the repo)​

<repo>/.erun/
└── config.yaml # project-level config (per-env registry, k8s deploy plans)

This file is checked into the repository so that everyone on the team uses the same registry and deployment topology for shared environments.

Per-pod (inside the runtime container)​

/home/erun/ # persisted on a PVC
├── git/<repo-name>/ # your project checkout
├── .config/erun/<tenant>/<env>/ # the env's config, mirrored from your machine
├── .erun/<tenant>/<env>/bootstrap.yaml # remote-init marker
└── .erun/<tenant>/<env>/trace.log # rolling trace of in-pod erun commands

The runtime container reads the same per-user config layout as your machine (the per-user root above, resolved inside the pod, where it is ~/.config/erun/...).