Provisioning
What the engine installs in the Infrastructure step.
The Infrastructure step is where everything is assembled on the host. The engine installs, in sequence, with progress streamed live:
- k3s — lightweight, production-grade Kubernetes (scales to multi-node later).
- Ingress (Traefik) + cert-manager — routing and automatic TLS via Let's Encrypt.
- PostgreSQL and Redis in-cluster — the definitive database and the queue/pub-sub for logs.
- The artifact Registry and the CI Runners.
- The Deploy serverless runtime.
- The definitive api and web images (deploy + migrations + super admin seed as in-cluster Jobs).
equantic-space-* prefix (namespace equantic-space, Deployments equantic-space-api / equantic-space-platform, database equantic-space-db).CI runners
Pipeline jobs run as isolated pods on the cluster — one pod per job, with its own CPU/memory limits, so a heavy job never starves the platform or its neighbours. Capacity is derived from the cluster itself (always reserving headroom for the control plane): jobs beyond the ceiling wait in the queue, and adding a node to the fleet grows CI capacity automatically. A job the cluster still has no room for when its pod is created waits for room too, saying so in its log, instead of failing. Its timeout-minutes only starts counting once the pod exists.
Every container in a job's pod is bounded — the job itself, its services: sidecars, and the per-job Docker daemon all carry explicit ceilings, and the job's disk has a size of its own (below), so a runaway job evicts itself instead of pressuring the node. The job's timeout-minutes is also stamped onto the pod: even if the platform process dies mid-job, the cluster retires the pod on its own.
docker build (or Testcontainers) inside jobs? Pass EQS_RUNNER_DIND=true to the installer — EQS_RUNNER_DIND=true bash install.sh, on a first install or an update. Each job then gets its own private Docker daemon, never shared between jobs or workspaces. It applies to every job on the install, and the daemon is privileged, so turn it on deliberately.Tuning the capacity
The derivation is a good default, not a verdict: Settings → Platform → CI capacity lets you decide how much of the machine the runner may take, with every change previewing what it would buy on this cluster before you save it.
The panel beside the fields answers the question the numbers alone never did: why only this many? For the node it reads, it shows what the node has, what the platform takes (its measured use plus the reserve's headroom, not just the percentage), what is left for CI, each job's share, and the bound that decided the count, whether that is the target CPU per job, the memory floor, the hard cap, a fixed size or the admission floor. With more than one node, pick which one to read, and the fleet's total is shown beside it.
Two knobs matter most. The platform reserve is the slice of each node CI never touches — lowering it gives CI more in total, though more slots then divide the memory further, so each job can end up smaller. And "do not start below" is a floor for admission: a job's ceiling is frozen when its pod is created, so a brief dip in free memory can hand a two-hour build a limit it cannot finish under. Below the floor the node offers no slots and the job waits in the queue — late beats crippled.
The reserve also has an absolute floor — at least 2 cores and 3 GiB stay with the control plane whatever the percentage says (self-reducing on tiny hosts). A dedicated runner node with no platform on it can zero the floor via EQS_RUNNER_MIN_PLATFORM_RESERVE_CPU / EQS_RUNNER_MIN_PLATFORM_RESERVE_MEMORY_GI.
EQS_RUNNER_* variable only seeds an installation that has never saved here. A change takes effect within one budget refresh (30 s), with no restart. Clear a field to hand that one decision back to the platform, and the line under the label always says where the value came from.Memory that grows with the job
A job used to get one share of memory, decided when its pod was created and fixed for its whole life. That number is a guess about a curve: a build is small while it installs, big while it type-checks everything at once, and small again while it packs. Guess high and the node is wasted; guess low and a two-hour build dies at 95%.
With elastic memory on (it is, by default), the platform watches each running job and raises its memory in place when it nears its ceiling and the node has room. Request and limit move together, so the pod keeps its QoS class and the scheduler still knows the truth — and the budget sees the new size, so the next job is not admitted into memory this one just took.
It grows only into room the same arithmetic admission uses: never into the platform's reserve, never into another job's share. A growth ceiling caps one job on a big node. Both live in Settings → Platform → CI capacity.
exit 137: a single allocation bigger than the room left still kills the container between two readings, and a node with nothing free has nothing to give. It needs Kubernetes 1.33 or newer for in-place resize — an older cluster keeps the fixed share it always had.One disk per job
A job has one disk, as on a GitHub-hosted runner: its workspace, its Docker images and build cache, and everything else it writes share the same space. It is at least 14 GB, what GitHub documents for its runner, and usually more: the platform measures each node's free disk, keeps the free-space floor that CI storage defends (Settings → Platform → CI storage, 15% by default), and divides the rest among the jobs that run. When disk is what runs out first, it decides how many jobs run, and the panel beside the fields says so.
A job that writes past its disk is evicted by the node, and its log says which limit it passed and where that limit is set. Settings → Platform → CI capacity sets the floor a job is counted at, or fixes the disk every job gets.
Runs that never end
A ceiling is enforced from outside the run as well. When the cluster never has room — usually a pinned job size no node can offer — the runs behind it would queue for days, and a job whose executor has gone away would stay in progress for ever.
So the platform gives up on its own. A run waiting for a runner past the queue ceiling (24 h by default), and a run in progress past the run ceiling (12 h), are ended as timed out; so is a single job past the job ceiling (6 h), once whatever was executing it has stopped counting. A job's own timeout-minutes always outranks that last one. All three live in Settings → Platform → CI capacity, and 0 turns one off.
And when the queue is not moving because nothing may start at all, the runs screen says so, with the size a job would need.
Switching a runner machine off
A Windows or macOS runner is a VM this installation runs, and until it is asked to do something it is 8 GiB of memory doing nothing. On the install this was written for, the two of them held 14.2 GiB and around 2.4 cores for sixteen hours without a single job.
Settings → Platform → Runner machines now has a power switch for any machine the platform runs itself. Powering one off asks the guest to shut down first and only then stops the VM, which is both cleaner and safer than killing the process, and the memory goes straight back to the rest of the install.
A machine you registered by its address is untouched by this: the platform connects to it, it does not switch your hardware off.
A machine that switches itself off
Each machine the platform runs has a Power off after … min idle setting, next to its switch. 0 is the default and means never — a VM that takes minutes to boot is not one every installation wants stopped after a quiet lunch.
With a ceiling set, the platform stops the machine once nothing has run on it for that long, and starts it again by itself when a job asks for that operating system. The job waits in its own log while the machine boots ("powering it on, a cold machine takes a few minutes to answer") instead of failing on a connection to something that is not running.
Logs and retention
A single step may store up to 50,000 log lines (EQS_RUNNER_MAX_LOG_LINES_PER_STEP; 0 removes the cap). Past the cap the step keeps running to its real conclusion — a loud truncation marker is written and further output is dropped, which protects the database and the live stream from a runaway set -x.
Step logs age out after 90 days, set in the CI storage screen below (0 keeps them forever). Runs, jobs, conclusions and annotations are kept — history stays browsable, only the raw transcript expires. The registry's blob garbage collector and retention runner are also on by default (each EQS_REGISTRY_*_ENABLED=false opts out): unreferenced blobs are reclaimed on their own, and a retention policy saved in the Artifacts screen simply runs.
CI storage
The Actions cache and the run artifacts live on the platform's storage, and on a one-server install that storage is the server's own disk. Settings → Platform → CI storage keeps them to a policy, swept every hour:
- Cache: GitHub's rules. An entry nobody restored in more than 7 days goes, and a repository past 10 GB loses its least recently used entries first.
- Artifacts: kept 90 days at most (1 to 400). A workflow's
retention-daysis honoured when it asks for less, never for more. Lowering the setting shortens the artifacts already stored too. - Total limit: cache and artifacts together may hold a fifth of the disk by default (or a number of GB;
0for none). Past it, whatever was used longest ago goes first, cache or artifact, and the artifacts of a run still going are never removed. Rules by age alone cannot hold a disk: a repository that uploads a large package on every build keeps everything younger than 90 days and fills it anyway. With S3 storage there is no disk to measure, so set the limit in GB. - Keep free: 15% of the disk by default. When the disk drops below it for any reason, CI gives back exactly what is missing, before Kubernetes starts evicting pods. Checked every five minutes.
The screen shows what CI holds against the limit, who holds the most, and what the last sweeps removed, failures included with their reason; a number you type is previewed against this disk before you save it. Sweep now applies a lowered limit without waiting for the hour. The EQS_CI_CACHE_IDLE_DAYS, EQS_CI_CACHE_REPO_LIMIT_GB, EQS_CI_ARTIFACT_RETENTION_DAYS, EQS_CI_LOG_RETENTION_DAYS, EQS_CI_STORAGE_LIMIT_GB, EQS_CI_DISK_FLOOR_PERCENT variables only seed an install that never saved that screen.
The platform's own share
The platform sizes its own pods from the host — database, api and web declare resource requests derived at install time (and back-filled by updates), so under load the kernel arbitrates in the platform's favour instead of treating a CI job and the database as peers. Overridable per component via EQS_PG_CPU_REQUEST-style variables when you know your box better than the formula does.