Documentation
¶
Index ¶
- Constants
- func NormalizeRegistryHost(s string) string
- type BuildKitConfig
- func (b *BuildKitConfig) BuildKitGCEnabled() bool
- func (b *BuildKitConfig) BuildKitGCEphemeralKeepDuration() time.Duration
- func (b *BuildKitConfig) BuildKitGCEphemeralMaxUsedBytes() int64
- func (b *BuildKitConfig) BuildKitGCKeepDuration() time.Duration
- func (b *BuildKitConfig) BuildKitGCMaxUsedBytes() int64
- func (b *BuildKitConfig) BuildKitGCMinFreeBytes() int64
- func (b *BuildKitConfig) BuildKitGCReservedBytes() int64
- type CargoProxyConfig
- type ClaimRetryToml
- type Config
- type ContainerdConfig
- type DindConfig
- type DispatchConfig
- type DispatchPolicyConfig
- type ForgejoConfig
- type GitHubConfig
- type GitLabConfig
- type GiteaConfig
- type ImageGCConfig
- func (i *ImageGCConfig) ImageGCCheckInterval() time.Duration
- func (i *ImageGCConfig) ImageGCEnabled() bool
- func (i *ImageGCConfig) ImageGCHighWatermarkPercent() float64
- func (i *ImageGCConfig) ImageGCLowWatermarkPercent() float64
- func (i *ImageGCConfig) ImageGCMaxAge() time.Duration
- func (i *ImageGCConfig) ImageGCMinFreeBytes() uint64
- func (i *ImageGCConfig) ImageGCTargetFreeBytes() uint64
- type LinuxRunnerToml
- type LinuxVMToml
- type LogConfig
- type MacOSVMToml
- type MetricsConfig
- type ModuleProxyConfig
- type NetworkConfig
- type OrphanSweepToml
- type PkgProxyConfig
- type RegistryMirrorConfig
- type RunnerConfig
- func (r *RunnerConfig) ClaimRetryEnabled() bool
- func (r *RunnerConfig) ClaimRetryJitter() float64
- func (r *RunnerConfig) ClaimRetryMaxAge() time.Duration
- func (r *RunnerConfig) ClaimRetrySchedule() []time.Duration
- func (r *RunnerConfig) ImageForRepo(repo string) string
- func (r *RunnerConfig) ImageForRepoOS(repo, os string) string
- func (r *RunnerConfig) OrphanSweepEnabled() bool
- func (r *RunnerConfig) OrphanSweepGrace() time.Duration
- func (r *RunnerConfig) ParsedJobTimeout() time.Duration
- func (r *RunnerConfig) ParsedShutdownTimeout() time.Duration
- type RuntimeConfig
- type RuntimeRlimits
- type VMConfig
- type WebhookConfig
- type WindowsRunnerToml
- type WoodpeckerConfig
Constants ¶
const ( // LinuxRuntimeRunc is the default: an ordinary OCI container sharing // the host kernel, run by io.containerd.runc.v2. LinuxRuntimeRunc = "runc" // LinuxRuntimeKata runs each job container inside its own lightweight // VM with its own kernel, via io.containerd.kata.v2. LinuxRuntimeKata = "kata" )
Container runtimes selectable for Linux job containers via [runner.linux] runtime. These are the TOML values, not the containerd runtime handler names — see LinuxRunnerToml.ContainerdRuntime.
Variables ¶
This section is empty.
Functions ¶
func NormalizeRegistryHost ¶ added in v0.2.2
NormalizeRegistryHost reduces an upstream registry name to the form containerd resolves references under: no scheme, no path, lowercase, and Docker Hub's several spellings folded to "docker.io". Exported because the pull paths have to look up a mirror by the host containerd hands them.
Types ¶
type BuildKitConfig ¶ added in v0.1.8
type BuildKitConfig struct {
// GCEnabled toggles BuildKit's cache garbage collection. Nil = default
// true. Setting false restores the old unbounded behavior and is only
// sensible while debugging a cache-correctness problem.
GCEnabled *bool `toml:"gc_enabled"`
// GCReservedGB is build cache, in GiB, that is never collected even
// when idle. The warm floor. Default 5.
GCReservedGB uint64 `toml:"gc_reserved_gb"`
// GCMaxUsedGB is the hard ceiling, in GiB, on total build cache.
// Anything above it is collected regardless of age. Default 25.
GCMaxUsedGB uint64 `toml:"gc_max_used_gb"`
// GCMinFreeGB makes BuildKit collect whatever it must to keep at least
// this much free space, in GiB, on the filesystem — overriding
// GCReservedGB. This is the arm that rescues a node whose disk is
// being consumed by something other than the build cache. Default 20,
// matching [image_gc].min_free_gb so the two collectors agree on when
// the node is tight.
GCMinFreeGB uint64 `toml:"gc_min_free_gb"`
// GCKeepDuration is the age past which cache records are collected
// once usage exceeds GCReservedGB. Default 168h (7 days). BuildKit's
// own default is 60 days, which is far too long for a CI node that
// rebuilds the same images many times a day.
GCKeepDuration time.Duration `toml:"gc_keep_duration"`
// GCEphemeralKeepDuration and GCEphemeralMaxUsedGB bound the cheaply
// reproducible record types (local build contexts, RUN --mount=cache
// mounts, git checkouts). Re-creating those costs a local copy rather
// than a network round trip, so they are collected far more eagerly.
// Defaults 48h and 2 GiB.
GCEphemeralKeepDuration time.Duration `toml:"gc_ephemeral_keep_duration"`
GCEphemeralMaxUsedGB uint64 `toml:"gc_ephemeral_max_used_gb"`
}
BuildKitConfig bounds the embedded BuildKit solver's on-disk build cache.
WHY THIS TABLE EXISTS: BuildKit only garbage-collects when its worker is given a GC policy, and ephemerd never gave it one. Every `docker build` run by every CI job therefore added cache records, snapshots and `containerd.io/gc.flat` leases to the shared "buildkit" containerd namespace that nothing ever released. A production node accumulated 76 image records, 302 snapshots and 481 leases — about 44 GB of a 116 GB disk — spanning 49 dead jobs over two and a half weeks, which is what actually filled the disk and froze the VM.
The defaults aim for a WARM BUT BOUNDED cache. Pruning hard on every build would trade the disk problem for a network problem (re-downloading and re-building layers on every job), which is the opposite of what these nodes need. So there is a floor that is never collected, a ceiling that always is, and a free-space guard that overrides the floor when the node is genuinely tight.
func (*BuildKitConfig) BuildKitGCEnabled ¶ added in v0.1.8
func (b *BuildKitConfig) BuildKitGCEnabled() bool
BuildKitGCEnabled reports whether the build cache is bounded. Default true.
func (*BuildKitConfig) BuildKitGCEphemeralKeepDuration ¶ added in v0.1.8
func (b *BuildKitConfig) BuildKitGCEphemeralKeepDuration() time.Duration
BuildKitGCEphemeralKeepDuration returns the ephemeral-record age limit, default 48h.
func (*BuildKitConfig) BuildKitGCEphemeralMaxUsedBytes ¶ added in v0.1.8
func (b *BuildKitConfig) BuildKitGCEphemeralMaxUsedBytes() int64
BuildKitGCEphemeralMaxUsedBytes returns the ephemeral-record ceiling, default 2 GiB.
func (*BuildKitConfig) BuildKitGCKeepDuration ¶ added in v0.1.8
func (b *BuildKitConfig) BuildKitGCKeepDuration() time.Duration
BuildKitGCKeepDuration returns the record age limit, default 7 days. A negative value disables the age arm (size bounds still apply).
func (*BuildKitConfig) BuildKitGCMaxUsedBytes ¶ added in v0.1.8
func (b *BuildKitConfig) BuildKitGCMaxUsedBytes() int64
BuildKitGCMaxUsedBytes returns the hard ceiling, default 25 GiB.
func (*BuildKitConfig) BuildKitGCMinFreeBytes ¶ added in v0.1.8
func (b *BuildKitConfig) BuildKitGCMinFreeBytes() int64
BuildKitGCMinFreeBytes returns the free-space guard, default 20 GiB.
func (*BuildKitConfig) BuildKitGCReservedBytes ¶ added in v0.1.8
func (b *BuildKitConfig) BuildKitGCReservedBytes() int64
BuildKitGCReservedBytes returns the never-collected floor, default 5 GiB.
type CargoProxyConfig ¶ added in v0.2.2
type CargoProxyConfig struct {
// Enabled turns the proxy on. Default false.
Enabled bool `toml:"enabled"`
// Port is the listen port on the bridge gateway. Default 8083.
Port int `toml:"port"`
// Upstream is the sparse registry index base URL.
// Default "https://index.crates.io".
Upstream string `toml:"upstream"`
// RustupUpstream is the toolchain distribution server.
// Default "https://static.rust-lang.org".
RustupUpstream string `toml:"rustup_upstream"`
// IndexTTL is how long a cached sparse-index entry is served before a
// conditional revalidation. Default 10m. Crate tarballs ignore this —
// they are immutable and cached permanently.
IndexTTL time.Duration `toml:"index_ttl"`
// Cleanup wipes the cache on shutdown. Default FALSE, unlike the Go
// module proxy: the whole point of a pull-through cache is to survive
// restarts, and wiping it on every shutdown is what made the module
// proxy's cache worthless.
Cleanup *bool `toml:"cleanup"`
}
CargoProxyConfig configures the Cargo/crates caching proxy.
When enabled, ephemerd runs a pull-through cache for the crates.io sparse index, .crate tarballs, and rustup toolchain artifacts on the bridge gateway. Job containers are pointed at it automatically: rustup via RUSTUP_DIST_SERVER, and Cargo via a generated .cargo/config.toml that is bind-mounted read-only at the container's filesystem root (Cargo ignores CARGO_SOURCE_* environment variables, so a file is the only mechanism).
func (*CargoProxyConfig) CleanupEnabled ¶ added in v0.2.2
func (c *CargoProxyConfig) CleanupEnabled() bool
CleanupEnabled reports whether the Cargo cache is wiped on shutdown. Defaults to false — see the field comment.
type ClaimRetryToml ¶
type ClaimRetryToml struct {
// Enabled toggles the retry queue. Nil = default true; operators
// disable by explicitly setting enabled = false.
Enabled *bool `toml:"enabled"`
// MaxAge is the total time budget from first failure to giving up.
// Accepts Go duration strings ("90m", "1h30m"). Default 90m.
MaxAge string `toml:"max_age"`
// Schedule is the ordered backoff ladder. Each entry is the base
// delay for one attempt; jitter is applied on top. Default is
// {30s, 1m, 2m, 5m, 10m}. Use a shorter schedule for shorter
// MaxAge, or a longer one to let jobs marinate through extended
// outages.
Schedule []string `toml:"schedule"`
// Jitter is the +/- fraction applied to each delay (0.0-1.0).
// Default 0.2 (+/-20%). Set 0 to disable jitter (useful in tests,
// rarely useful in production).
Jitter *float64 `toml:"jitter"`
}
ClaimRetryToml configures the scheduler's claim-retry queue.
Enabled defaults to true when the [runner.claim_retry] table is omitted (see RunnerConfig.ClaimRetryEnabled). Losing queued jobs to transient errors is almost never what an operator wants; set enabled = false to restore the pre-existing "log and drop" behavior.
type Config ¶
type Config struct {
GitHub GitHubConfig `toml:"github"`
GitHubExtra []GitHubConfig `toml:"github_extra"` // additional GitHub owners, each with its own auth ([[github_extra]])
Forgejo ForgejoConfig `toml:"forgejo"`
Gitea GiteaConfig `toml:"gitea"`
GitLab GitLabConfig `toml:"gitlab"`
Woodpecker WoodpeckerConfig `toml:"woodpecker"`
Webhook WebhookConfig `toml:"webhook"`
Containerd ContainerdConfig `toml:"containerd"`
Network NetworkConfig `toml:"network"`
VM VMConfig `toml:"vm"`
Dind DindConfig `toml:"dind"`
BuildKit BuildKitConfig `toml:"buildkit"`
ImageGC ImageGCConfig `toml:"image_gc"`
ModuleProxy ModuleProxyConfig `toml:"module_proxy"`
CargoProxy CargoProxyConfig `toml:"cargo_proxy"`
NpmProxy PkgProxyConfig `toml:"npm_proxy"`
PipProxy PkgProxyConfig `toml:"pip_proxy"`
PubProxy PkgProxyConfig `toml:"pub_proxy"`
// RegistryMirror routes container image pulls through a LAN pull-through
// cache instead of the origin registry. See RegistryMirrorConfig.
RegistryMirror RegistryMirrorConfig `toml:"registry_mirror"`
Runtime RuntimeConfig `toml:"runtime"`
Runner RunnerConfig `toml:"runner"`
Metrics MetricsConfig `toml:"metrics"`
Dispatch DispatchConfig `toml:"dispatch"`
Log LogConfig `toml:"log"`
}
func (*Config) EnsureDispatchToken ¶ added in v0.1.4
EnsureDispatchToken guarantees c.Dispatch.Token is set, minting a fresh token and persisting it into the config file at path when it is currently empty. The generated token is appended as a "[dispatch]" block so the operator's existing content (and comments) are preserved; the persisted file then rides into the Linux VM through the normal config-delivery channel, giving the host client and the in-VM dispatch server the same shared secret.
It is a no-op when a token is already set (whether from config or a prior mint). Only the host that boots the VM needs to call this; the in-VM daemon simply reads the token that was delivered inside config.toml.
gen supplies the token bytes (crypto/rand-backed in production); injecting it keeps this package free of a scheduler import and makes the persistence path testable.
func (*Config) GitHubTargets ¶ added in v0.1.3
func (c *Config) GitHubTargets() []GitHubConfig
GitHubTargets returns every configured GitHub target: the primary [github] (when set) followed by any [[github_extra]] entries. Each target carries its own owner + auth, so ephemerd can serve multiple owners at once (e.g. an org via a GitHub App and a personal account via a PAT).
func (*Config) PinnedRunnerImages ¶ added in v0.1.8
PinnedRunnerImages returns every image ref this node is configured to run runners from: the [runner] default, every per-repo [runner.images.<repo>] entry, and each provider's per-OS defaults.
The image GC treats these as never-evictable. Dropping one guarantees a re-pull on the very next job of that shape, which is exactly the network thrash pressure-triggered GC exists to avoid; they are also the images most likely to look "stale" to an LRU sweep on a node that has been busy with third-party container: images.
Refs are returned in a stable order with duplicates removed.
type ContainerdConfig ¶
type ContainerdConfig struct {
}
type DindConfig ¶
type DindConfig struct {
Enabled bool `toml:"enabled"` // mount /var/run/docker.sock with a fake Docker API
// CachePruneInterval is how often the per-repo image cache pruner runs.
// Accepts standard Go duration strings ("24h", "30m"). Set to 0 to
// disable pruning entirely. Default 24h.
CachePruneInterval time.Duration `toml:"cache_prune_interval"`
// CacheMaxAge is an OPTIONAL age backstop for cached image records:
// any record whose ephemerd.io/last-accessed label (or UpdatedAt as
// fallback) is older than this gets removed on the next prune pass.
// Containerd's content GC then reclaims the unreferenced blobs.
//
// BEHAVIOR CHANGE: this used to default to 168h (7 days) and was the
// only image eviction mechanism ephemerd had. It now defaults to 0
// (disabled), because disk pressure — not age — is the correct
// trigger: evicting a warm cache while the disk is half empty just
// forces re-downloads. See [image_gc], which supersedes this for
// both the dind cache namespaces and the main runtime namespace.
//
// An explicit value is still honored, and still applies only to the
// ephemerd-dind-cache-* namespaces. Empty cache namespaces are
// reaped on every prune pass regardless of this setting.
CacheMaxAge time.Duration `toml:"cache_max_age"`
// AllowPrivileged controls whether `docker run --privileged` (or
// HostConfig.Privileged=true / HostConfig.CapAdd, or
// `--security-opt seccomp=unconfined` / `apparmor=unconfined`) from
// inside a job is honored. When true, a sibling container can request
// the full elevation stack (all caps, all devices, seccomp/apparmor off,
// writable sysfs/cgroupfs) — needed for KIND clusters, nested
// containerd, /dev/fuse-style mounts, etc. When false, such requests
// are rejected with HTTP 403.
//
// SECURITY: a privileged sibling container is effectively root on
// whatever host runs the containerd that backs dind. On Windows and
// macOS hosts that backing containerd lives inside a managed Linux
// VM (WSL2 / Hyper-V / Vz), so an escape only reaches the VM. On a
// Linux host with no VM fence, an escape reaches the bare-metal host
// — set this to false unless every workload is trusted.
//
// Use the pointer form so an empty/missing TOML key is
// distinguishable from an explicit `allow_privileged = false`. See
// ResolvedAllowPrivileged for the default policy.
AllowPrivileged *bool `toml:"allow_privileged"`
}
DindConfig configures the fake Docker daemon mounted into job containers.
func (*DindConfig) DindCacheMaxAge ¶
func (d *DindConfig) DindCacheMaxAge() time.Duration
DindCacheMaxAge returns the optional age backstop for dind cache namespaces. Zero means disabled, which is now the default — see DindConfig.CacheMaxAge for why. A negative value is also treated as disabled so a typo cannot evict everything.
func (*DindConfig) DindCachePruneInterval ¶
func (d *DindConfig) DindCachePruneInterval() time.Duration
DindCachePruneInterval returns the prune interval with the default applied when unset (or set to 0).
func (*DindConfig) ResolvedAllowPrivileged ¶
func (d *DindConfig) ResolvedAllowPrivileged() bool
ResolvedAllowPrivileged returns whether privileged dind sibling containers are allowed, applying the secure default when the operator hasn't set the key explicitly.
Default policy: false on ALL platforms. Privileged is opt-in everywhere. A privileged sibling container is effectively root on whatever host runs the backing containerd, so shipping it on-by-default is unsafe even where a VM fence limits the blast radius (Windows/macOS) — an operator that needs it (KIND clusters, nested containerd, /dev/fuse) opts in explicitly with `allow_privileged = true`.
type DispatchConfig ¶ added in v0.1.4
type DispatchConfig struct {
// Token is the shared bearer token the host client presents and the in-VM
// dispatch server requires on every RPC (constant-time compared). It is
// delivered to the VM inside config.toml through the existing config-share
// channel, so both sides converge on the same value with no extra plumbing.
//
// Leave empty for the daemon to mint a 256-bit token on first run and
// persist it back into config.toml (see EnsureDispatchToken) — operators
// normally never set this by hand. Set it explicitly only to pin a known
// value (e.g. to rotate, or in a config baked read-only).
Token string `toml:"token"`
}
DispatchConfig configures the host<->VM dispatch gRPC channel. On Windows (Hyper-V) and macOS (Vz) hosts, Linux jobs run inside a long-lived Linux VM and the host dispatches Create/Wait/Destroy over gRPC. That surface can spawn and kill jobs with a caller-supplied image, so it is authenticated with a shared bearer token.
type DispatchPolicyConfig ¶ added in v0.1.4
type DispatchPolicyConfig struct {
// AllowedRepos, when non-empty, restricts dispatch to jobs whose repository
// name is in this list. Empty means "all tracked repos" (current behavior).
// Repo names are matched exactly against the webhook's repository name (the
// same value used by [github].repos).
AllowedRepos []string `toml:"allowed_repos"`
// RequiredLabels, when non-empty, requires an incoming job to carry at
// least one of these labels (beyond the mandatory "self-hosted" gate)
// before it dispatches. Empty means "no extra label requirement". Use this
// to pin dispatch to jobs that opt in with a distinctive label an outside
// fork is unlikely to request (e.g. "ephemerd").
RequiredLabels []string `toml:"required_labels"`
}
DispatchPolicyConfig is an opt-in allowlist restricting which webhook jobs may dispatch a runner. All fields default to "no restriction"; setting any field narrows dispatch. It is a safety net layered under GitHub's outside- collaborator approval gate, not a substitute for it.
func (DispatchPolicyConfig) IsZero ¶ added in v0.1.4
func (d DispatchPolicyConfig) IsZero() bool
IsZero reports whether the policy imposes no restrictions (the default).
type ForgejoConfig ¶
type ForgejoConfig struct {
InstanceURL string `toml:"instance_url"` // Forgejo instance URL (e.g., "https://codeberg.org")
Token string `toml:"token"` // runner registration token from Forgejo admin
Owner string `toml:"owner"` // org or user (empty = instance-level runner)
Repos []string `toml:"repos"` // limit to specific repos (empty = all)
Labels []string `toml:"labels"` // runner labels (default: ["ubuntu-latest:docker://<job_image>"])
DefaultImage string `toml:"default_image"` // runner daemon image (default: "data.forgejo.org/forgejo/runner:12")
DefaultImageLinux string `toml:"default_image_linux"` // per-OS Linux runner image (wins over default_image when set)
DefaultImageWindows string `toml:"default_image_windows"` // per-OS Windows runner image
JobImage string `toml:"job_image"` // job execution image (default: "gitea/runner-images:ubuntu-24.04")
}
ForgejoConfig configures the Forgejo Actions provider. Set instance_url and token to enable Forgejo instead of GitHub. Uses forgejo-runner binary with one-job --handle mode.
Forgejo's runner daemon (DefaultImage) is Linux-only; setting DefaultImageWindows is allowed for completeness but no upstream Windows build of forgejo-runner exists today.
func (*ForgejoConfig) DefaultImageFor ¶
func (f *ForgejoConfig) DefaultImageFor(os string) string
DefaultImageFor returns the provider-level default for the given OS.
type GitHubConfig ¶
type GitHubConfig struct {
// Authentication: either a PAT or GitHub App
Token string `toml:"token"`
AppID int64 `toml:"app_id"`
InstallationID int64 `toml:"installation_id"`
PrivateKeyPath string `toml:"private_key_path"`
// Which org/user and repos to register runners for
Owner string `toml:"owner"`
Repos []string `toml:"repos"`
// DispatchPolicy is an OPTIONAL defense-in-depth allowlist that gates which
// incoming workflow_job webhook events are allowed to dispatch a runner.
// It is IN ADDITION to (not a replacement for) GitHub's own "require
// approval for outside collaborators" setting, which remains the primary
// control against fork-PR abuse. Empty (the default) preserves today's
// behavior: any tracked repo with a self-hosted label dispatches.
DispatchPolicy DispatchPolicyConfig `toml:"dispatch_policy"`
// Job discovery: polling interval (default "30s")
PollInterval string `toml:"poll_interval"`
// DefaultImage is the legacy single-image override (Linux only).
// Kept for backward compatibility — prefer DefaultImageLinux /
// DefaultImageWindows for new configs. When DefaultImageLinux is empty
// and this is set, it's treated as the Linux default.
// Linux fallback (when nothing is set): "ghcr.io/actions/actions-runner:latest".
DefaultImage string `toml:"default_image"`
// DefaultImageLinux is the provider-level default image for Linux jobs.
// Per-repo entries in [runner.images.<repo>].linux win over this.
DefaultImageLinux string `toml:"default_image_linux"`
// DefaultImageWindows is the provider-level default image for Windows
// jobs. Per-repo entries in [runner.images.<repo>].windows win over
// this. Falls through to the runtime's host-matched servercore default
// (pkg/runtime/image_windows.go) when unset.
DefaultImageWindows string `toml:"default_image_windows"`
}
func (*GitHubConfig) DefaultImageFor ¶
func (g *GitHubConfig) DefaultImageFor(os string) string
DefaultImageFor returns the provider-level default image for the given OS. Resolution: per-OS field → legacy DefaultImage (Linux only) → empty. Empty means "no provider default — let the runtime pick its OS-native fallback (e.g. mcr.microsoft.com/windows/servercore:ltsc20XX on Windows)".
func (*GitHubConfig) ParsedPollInterval ¶
func (g *GitHubConfig) ParsedPollInterval() time.Duration
ParsedPollInterval returns the poll interval as a time.Duration.
type GitLabConfig ¶
type GitLabConfig struct {
InstanceURL string `toml:"instance_url"` // GitLab instance URL (e.g., "https://gitlab.com")
Token string `toml:"token"` // runner authentication token (glrt-xxx for GitLab 16+)
Tags []string `toml:"tags"` // runner tags for job matching
DefaultImage string `toml:"default_image"` // runner image (default: "ghcr.io/ephpm/runner-gitlab:latest")
DefaultImageLinux string `toml:"default_image_linux"` // per-OS Linux runner image (wins over default_image when set)
DefaultImageWindows string `toml:"default_image_windows"` // per-OS Windows runner image
}
GitLabConfig configures the GitLab CI provider. Set instance_url and token to enable GitLab instead of GitHub.
func (*GitLabConfig) DefaultImageFor ¶
func (g *GitLabConfig) DefaultImageFor(os string) string
DefaultImageFor returns the provider-level default for the given OS.
type GiteaConfig ¶
type GiteaConfig struct {
InstanceURL string `toml:"instance_url"` // Gitea instance URL (e.g., "https://gitea.example.com")
Token string `toml:"token"` // runner registration token from Gitea admin
Owner string `toml:"owner"` // org or user (empty = instance-level runner)
Repos []string `toml:"repos"` // limit to specific repos (empty = all)
Labels []string `toml:"labels"` // runner labels (default: ["ubuntu-latest:docker://<job_image>"])
DefaultImage string `toml:"default_image"` // runner daemon image (default: "docker.io/gitea/act_runner:latest")
DefaultImageLinux string `toml:"default_image_linux"` // per-OS Linux runner image (wins over default_image when set)
DefaultImageWindows string `toml:"default_image_windows"` // per-OS Windows runner image
JobImage string `toml:"job_image"` // job execution image (default: "gitea/runner-images:ubuntu-24.04")
}
GiteaConfig configures the Gitea Actions provider. Set instance_url and token to enable Gitea instead of GitHub. Uses act_runner binary with --ephemeral mode.
func (*GiteaConfig) DefaultImageFor ¶
func (g *GiteaConfig) DefaultImageFor(os string) string
DefaultImageFor returns the provider-level default for the given OS.
type ImageGCConfig ¶ added in v0.1.8
type ImageGCConfig struct {
// Enabled toggles collection. Nil = default true; operators disable
// by explicitly setting enabled = false.
Enabled *bool `toml:"enabled"`
// CheckInterval is how often disk usage is sampled. The sample is one
// statfs-class syscall (microseconds), so this can be short. Default
// 60s. Set to 0 to disable the periodic sweep — the pre-pull headroom
// check still runs.
CheckInterval time.Duration `toml:"check_interval"`
// HighWatermarkPercent is the disk used-percentage at which a
// collection pass triggers. Default 85. Set to 0 to disable the
// percentage arm and rely on min_free_gb alone.
HighWatermarkPercent float64 `toml:"high_watermark_percent"`
// LowWatermarkPercent is the used-percentage a triggered pass evicts
// down to. Default 70. Must be below HighWatermarkPercent; a value at
// or above it degrades to single-threshold behavior.
LowWatermarkPercent float64 `toml:"low_watermark_percent"`
// MinFreeGB is the absolute free-space floor, in GiB, below which a
// pass triggers regardless of percentage. Default 20. Set to 0 to
// disable the absolute arm.
MinFreeGB uint64 `toml:"min_free_gb"`
// TargetFreeGB is the free space, in GiB, a pass triggered by
// MinFreeGB evicts back up to. Defaults to twice MinFreeGB, mirroring
// the default 85%/70% percentage gap. Values below MinFreeGB are
// clamped up to it.
TargetFreeGB uint64 `toml:"target_free_gb"`
// MaxAge is an OPTIONAL age backstop applied to every collected
// namespace: records idle longer than this are evicted whether or not
// the disk is under pressure. Default 0 (disabled) — age is
// deliberately NOT the primary mechanism, because evicting a warm
// cache while the disk is half empty just forces re-downloads.
MaxAge time.Duration `toml:"max_age"`
}
ImageGCConfig configures disk-pressure-triggered container image garbage collection.
Model (kubelet's): disk pressure is the TRIGGER, least-recently-used is the ORDER. Collection starts when usage crosses a high watermark and evicts LRU-first until a distinctly lower low watermark is reached, then stops. Two watermarks rather than one line is what prevents thrashing at the boundary.
Two independent trigger arms exist and the more conservative one wins: a percentage (high_watermark_percent) and an absolute floor (min_free_gb). Neither is safe alone — 15% free of a 1 TB node is 150 GB and evicting there is pointless, while 15% free of a 100 GB node is 15 GB, which three concurrent jobs writing ~5 GB of container layers each can eat between ticks. Size min_free_gb relative to runner.max_concurrent times the expected per-job writable layer.
Scope is the main "ephemerd" runtime namespace AND the per-repo "ephemerd-dind-cache-*" namespaces. Images referenced by an existing container, and the node's configured runner images, are never evicted.
func (*ImageGCConfig) ImageGCCheckInterval ¶ added in v0.1.8
func (i *ImageGCConfig) ImageGCCheckInterval() time.Duration
ImageGCCheckInterval returns the sampling interval, defaulting to 60s. A negative value is treated as 0 (periodic sweep off).
func (*ImageGCConfig) ImageGCEnabled ¶ added in v0.1.8
func (i *ImageGCConfig) ImageGCEnabled() bool
ImageGCEnabled reports whether image garbage collection runs. Default true.
func (*ImageGCConfig) ImageGCHighWatermarkPercent ¶ added in v0.1.8
func (i *ImageGCConfig) ImageGCHighWatermarkPercent() float64
ImageGCHighWatermarkPercent returns the trigger percentage, defaulting to 85. Out-of-range values fall back to the default rather than failing startup — a misconfigured watermark should not stop the node collecting.
func (*ImageGCConfig) ImageGCLowWatermarkPercent ¶ added in v0.1.8
func (i *ImageGCConfig) ImageGCLowWatermarkPercent() float64
ImageGCLowWatermarkPercent returns the stop percentage, defaulting to 70. A value at or above the high watermark is clamped to it by the collector.
func (*ImageGCConfig) ImageGCMaxAge ¶ added in v0.1.8
func (i *ImageGCConfig) ImageGCMaxAge() time.Duration
ImageGCMaxAge returns the optional age backstop. Zero (the default) and any negative value mean disabled.
func (*ImageGCConfig) ImageGCMinFreeBytes ¶ added in v0.1.8
func (i *ImageGCConfig) ImageGCMinFreeBytes() uint64
ImageGCMinFreeBytes returns the absolute floor in bytes, defaulting to 20 GiB.
func (*ImageGCConfig) ImageGCTargetFreeBytes ¶ added in v0.1.8
func (i *ImageGCConfig) ImageGCTargetFreeBytes() uint64
ImageGCTargetFreeBytes returns the absolute target in bytes, defaulting to twice the floor.
type LinuxRunnerToml ¶ added in v0.2.2
type LinuxRunnerToml struct {
// Runtime selects the container runtime for Linux job containers:
// "runc" (default) or "kata". Empty means "runc".
Runtime string `toml:"runtime"`
}
LinuxRunnerToml configures how Linux job containers are isolated.
Linux is the weakest of the three platforms today: Windows jobs get Hyper-V isolation and macOS jobs get a full VM, but Linux jobs are ordinary containers on the host kernel, so a kernel-level escape is a host compromise. Setting runtime = "kata" gives each job container its own kernel in a lightweight VM, which makes isolation uniform across platforms.
Default is "runc" — Kata is opt-in. Measured on an 8-core amd64 node with Kata 4.0.0 + QEMU, it costs seconds of extra container start latency (0.14s -> 4.1s median), ~310 MB of guest memory per running job instead of ~14 MB, ~35% on CPU-bound work and 8-40x on file-heavy work.
[dind] works under Kata. The Docker API cannot be handed over as a bind-mounted unix socket — the guest has its own kernel, so the socket inode arrives with no endpoint behind it and connect(2) returns ECONNREFUSED — so those jobs get the same DOCKER_HOST=tcp:// transport dind has always used for Hyper-V-isolated Windows containers, with the port firewalled to the owning container's address. The transport is chosen from this key at container-create time; see runtime.resolveDindTransport.
func (LinuxRunnerToml) ContainerdRuntime ¶ added in v0.2.2
func (l LinuxRunnerToml) ContainerdRuntime() string
ContainerdRuntime returns the containerd runtime handler name for the configured runtime — the string passed to containerd's WithRuntime.
func (LinuxRunnerToml) ResolvedRuntime ¶ added in v0.2.2
func (l LinuxRunnerToml) ResolvedRuntime() string
ResolvedRuntime returns the configured Linux job-container runtime, applying the default when the key is unset. Always returns one of the LinuxRuntime* constants; validate() rejects anything else at load time.
type LinuxVMToml ¶
type LinuxVMToml struct {
// Enabled controls whether this host serves Linux jobs through the VM.
// Pointer so "not set" is distinguishable from an explicit choice: the
// platform default is preserved when omitted — ON for darwin (macOS has
// always booted the VM unconditionally), OFF for windows (the WSL VM has
// always been opt-in). Set `enabled = false` on a darwin host to stop it
// claiming Linux jobs — e.g. when a dedicated Linux box now serves the
// same labels and dual-claiming would orphan a runner per job. (The VM
// itself still boots on darwin for now; skipping the boot entirely is a
// separate change.)
Enabled *bool `toml:"enabled"`
CPUs uint `toml:"cpus"` // virtual CPUs (default: 2)
MemoryMB uint64 `toml:"memory_mb"` // memory in MB (default: 2048)
DiskSizeGB uint64 `toml:"disk_size_gb"` // sparse disk size in GB (default: 50)
}
LinuxVMToml configures the long-running Linux VM for Linux jobs on Windows (Hyper-V) and macOS (Virtualization.framework) hosts.
func (*LinuxVMToml) ResolvedEnabled ¶ added in v0.1.5
func (l *LinuxVMToml) ResolvedEnabled() bool
ResolvedEnabled returns whether the host should serve Linux jobs via the Linux VM, defaulting per platform when the key is not set: darwin true (historical always-on), everything else false (windows is opt-in; on a linux host the field is meaningless — jobs run natively).
type LogConfig ¶
type LogConfig struct {
Level string `toml:"level"`
Format string `toml:"format"` // "text" or "json"
LogRetention string `toml:"log_retention"` // max age for job log files (e.g. "7d", "24h"); default "7d"
// Writer overrides the log output destination. When nil, logs go to
// stderr. Set by the Windows Service handler to route logs to the
// Windows Event Log.
Writer io.Writer `toml:"-"`
}
func (LogConfig) LogRetentionDuration ¶
LogRetentionDuration returns the parsed log retention duration. Supports Go duration strings (e.g. "168h") and a "d" suffix for days (e.g. "7d"). Returns 7 days if the value is empty or invalid.
type MacOSVMToml ¶
type MacOSVMToml struct {
// DiskImage is an optional path to a pre-installed macOS VM disk
// (produced by `ephemerd vm setup-macos` or an operator-supplied
// restore of an Apple IPSW). If empty, ephemerd downloads the latest
// Apple-signed IPSW on first boot and installs stock macOS into
// <data_dir>/vm/macos/base.img. Distinct from the OCI base image
// overlaid per job — that's fetched from the job's image label.
DiskImage string `toml:"disk_image"`
CPUs uint `toml:"cpus"` // CPUs per VM (default: 4)
MemoryMB uint64 `toml:"memory_mb"` // memory per VM in MB (default: 8192)
MaxConcurrent int `toml:"max_concurrent"` // max simultaneous macOS VMs (default: auto-detected from host CPUs)
// ProvisionTimeout bounds the pre-registration provisioning phase of a
// macOS VM job (boot + wait for the runner to become reachable). If a VM
// has not registered its runner within this window it is force-stopped and
// its concurrency slot released, so a wedged VM cannot hold the (often
// single) macOS slot indefinitely. Empty applies a 5m default.
ProvisionTimeout string `toml:"provision_timeout"`
}
MacOSVMToml configures per-job macOS VMs. macOS jobs always run in a per-job VM on darwin hosts — there's no other way on Apple Silicon — so there's no enable/disable toggle. On non-darwin hosts this block is ignored.
func (*MacOSVMToml) ParsedProvisionTimeout ¶ added in v0.2.5
func (m *MacOSVMToml) ParsedProvisionTimeout() time.Duration
ParsedProvisionTimeout returns the macOS VM provisioning timeout as a time.Duration, defaulting to 5m when unset or unparseable.
type MetricsConfig ¶
type MetricsConfig struct {
Enabled bool `toml:"enabled"` // enable /metrics endpoint (default false)
Port int `toml:"port"` // listen port (default 9090)
Path string `toml:"path"` // metrics path (default "/metrics")
TLSCert string `toml:"tls_cert"` // TLS certificate path (optional)
TLSKey string `toml:"tls_key"` // TLS private key path (optional)
// BindAddr is the interface the metrics listener binds to. The endpoint is
// unauthenticated, so it defaults to "127.0.0.1" (loopback only). Set to
// "0.0.0.0" to scrape from another host, and firewall the port and/or set
// tls_cert/tls_key when you do.
BindAddr string `toml:"bind_addr"`
// ContainerStatsInterval is how often per-container resource samples are
// taken (CPU, memory). Used both by the host's local sampler ticker and as
// the cadence the host requests from the in-VM Dispatch StreamContainerStats
// stream. Default 10s.
ContainerStatsInterval string `toml:"container_stats_interval"`
}
MetricsConfig configures the Prometheus metrics endpoint. Disabled by default. Set enabled = true to expose /metrics.
func (MetricsConfig) ParsedContainerStatsInterval ¶
func (m MetricsConfig) ParsedContainerStatsInterval() time.Duration
ParsedContainerStatsInterval returns the configured per-container sampling interval, applying the default (10s) when unset. Falls back to the default on parse error rather than failing the daemon — the metric series simply won't update if this is misconfigured, which is not worth aborting startup.
type ModuleProxyConfig ¶
type ModuleProxyConfig struct {
Enabled bool `toml:"enabled"` // enable Go module caching proxy
Port int `toml:"port"` // listen port on bridge gateway (default 8082)
Upstream string `toml:"upstream"` // upstream proxy URL (default "https://proxy.golang.org")
Cleanup *bool `toml:"cleanup"` // wipe cache on shutdown (default true)
// MaxCacheGB is the ceiling, in GiB, on the on-disk module cache.
// A prune pass evicts least-recently-used files until the directory is
// back under it. Default 20.
//
// 20 GiB is chosen to sit alongside the node's other disk bounds
// rather than compete with them: [buildkit].gc_max_used_gb defaults to
// 25 and [image_gc].min_free_gb to 20, so on the ~100 GB CI nodes this
// runs on the three together still leave headroom. It is also far
// larger than any single repo's module closure (a big Go service is a
// few GB with all its versions), so the cache stays warm in normal
// operation and the bound only bites when something pathological —
// or hostile — is filling it.
MaxCacheGB uint64 `toml:"max_cache_gb"`
// PruneInterval is how often the eviction pass runs. Default 1h.
// A pass is a directory walk plus a stat per file, so it is cheap;
// hourly is frequent enough that a job downloading modules in a loop
// cannot sit far above the cap for long. Set to a negative value to
// disable periodic pruning entirely (the cache is then unbounded
// again — only do this while debugging).
PruneInterval time.Duration `toml:"prune_interval"`
}
ModuleProxyConfig configures the Go module caching proxy. When enabled, ephemerd runs a local GOPROXY on the bridge gateway that caches module downloads. Containers receive GOPROXY env var automatically.
The cache is SHARED by every job on the node — that is the point of it, and it is safe because jobs only ever speak HTTP to the proxy (they cannot write the cache), because the proxy stores upstream's response under the key derived from the same path it fetched, and because the `go` client authenticates modules itself via go.sum and the checksum database. What sharing does expose is DISK: any job can ask for arbitrarily many module versions, so the cache needs a bound of its own. See MaxCacheGB.
func (*ModuleProxyConfig) CleanupEnabled ¶ added in v0.2.2
func (m *ModuleProxyConfig) CleanupEnabled() bool
CleanupEnabled reports whether the Go module cache is wiped on shutdown. Defaults to true, preserving the historical behavior.
Pointer-typed so "unset" is distinguishable from an explicit false. The previous plain bool could not express that: the call site coerced any false value back to true, which silently ignored `cleanup = false`.
Now that the cache is size-bounded (see MaxCacheGB), `cleanup = false` is the better setting for a long-lived node — it keeps the cache warm across restarts and version bumps, which is the whole point of a pull-through cache. Flipping the default silently would change every existing node's disk profile, so it stays opt-in.
func (*ModuleProxyConfig) ModuleProxyMaxCacheBytes ¶ added in v0.2.2
func (m *ModuleProxyConfig) ModuleProxyMaxCacheBytes() int64
ModuleProxyMaxCacheBytes returns the module cache ceiling in bytes, default 20 GiB.
func (*ModuleProxyConfig) ModuleProxyPruneInterval ¶ added in v0.2.2
func (m *ModuleProxyConfig) ModuleProxyPruneInterval() time.Duration
ModuleProxyPruneInterval returns the eviction interval, default 1h. A negative value means disabled and is returned as 0, matching ImageGCConfig.ImageGCCheckInterval.
type NetworkConfig ¶
type NetworkConfig struct {
// Subnet is the container subnet.
//
// Linux (CNI bridge): auto-selected when empty, avoiding ranges already in
// use on the host.
//
// Windows L2Bridge (l2bridge_egress = true): the CIDR of the LAN the bridge
// is attached to — containers are peers on it, not behind NAT — declared as
// the HNS network's Ipam subnet. Auto-derived from the address configured on
// host_nic when empty, which is the expected setting. Pin it only when the
// adapter carries a prefix that differs from the LAN you want declared.
//
// Windows NAT (the default): not consulted; the HNS NAT network always uses
// the built-in 10.88.0.0/16.
Subnet string `toml:"subnet"`
MTU int `toml:"mtu"` // bridge MTU (auto-detected from host if 0)
// L2BridgeEgress opts a Windows pool into L2Bridge container networking
// with VFP-enforced egress filtering, instead of the default HNS NAT.
// NAT cannot software-filter Windows container egress (VFP does not engage
// on a NAT network); L2Bridge puts the container on a VFP-managed vSwitch
// port so per-endpoint ACLs actually enforce. Windows only; ignored on
// Linux/macOS. Default false — NAT stays the default and this flag flips
// nothing until an operator opts a pool in.
L2BridgeEgress bool `toml:"l2bridge_egress"`
// HostNIC is the host network adapter name the L2Bridge binds onto
// (e.g. "Ethernet"). REQUIRED when L2BridgeEgress is true — the bridge
// has no uplink without it. There is no default: the correct NIC name is
// host-specific (do not assume "Ethernet 2", which was a spike's
// hot-added test NIC). Ignored when L2BridgeEgress is false.
HostNIC string `toml:"host_nic"`
// IPPool is the range of LAN addresses ephemerd may assign to job
// containers on the L2Bridge path. REQUIRED when L2BridgeEgress is true,
// with no default, and validated at load time.
//
// Why it cannot be inferred: an L2Bridge network must declare a subnet (HNS
// rejects a subnet-less one outright), and once it has one HNS will assign
// endpoint addresses from anywhere inside it — on a real LAN, straight into
// the site DHCP server's scope. ephemerd therefore allocates addresses
// itself, and only the operator knows which slice of their LAN the DHCP
// server is configured never to lease.
//
// Accepts a CIDR ("192.0.2.192/27" — network and broadcast excluded) or an
// inclusive range ("192.0.2.200-192.0.2.230"). Must lie inside Subnet and
// must not contain the host's own address or the LAN gateway. Size it for
// at least runner.max_concurrent addresses. Ignored when L2BridgeEgress is
// false.
IPPool string `toml:"ip_pool"`
// Gateway is the LAN router the L2Bridge default route points at (the HNS
// Ipam route next hop). Auto-derived from the default route on HostNIC when
// empty, which is the expected setting; pin it only when the adapter has no
// default route of its own or carries more than one.
//
// Containers route THROUGH this address while the egress ACLs stop them
// ADDRESSING it — the gateway gets no allow rule. Windows L2Bridge only.
Gateway string `toml:"gateway"`
// PublicDNS is the DNS resolver list handed to L2Bridge containers. Public
// resolvers keep container DNS off the LAN router (which the egress ACLs
// block along with the rest of RFC1918). Empty falls back to a built-in
// public default (1.1.1.1, 8.8.8.8). Only consulted on the L2Bridge path.
PublicDNS []string `toml:"public_dns"`
// ExtraAllowedDestinations are additional CIDRs permitted through the
// L2Bridge egress ACLs at a precedence ABOVE the RFC1918 block (so a
// listed destination wins over the block). Reserved for future use —
// default empty, which reproduces the strict Linux end-state (no RFC1918
// carve-outs at all). Only consulted on the L2Bridge path.
ExtraAllowedDestinations []string `toml:"extra_allowed_destinations"`
}
NetworkConfig configures container networking.
type OrphanSweepToml ¶
type OrphanSweepToml struct {
// Enabled toggles the sweep. Nil = default true; operators disable
// by explicitly setting enabled = false.
Enabled *bool `toml:"enabled"`
// Grace is how long a dispatched runner may sit without being
// assigned a job before it is destroyed and deregistered. Accepts
// Go duration strings ("10m", "1h"). Default 10m.
Grace string `toml:"grace"`
}
OrphanSweepToml configures the scheduler's orphaned-runner sweep.
Enabled defaults to true when the [runner.orphan_sweep] table is omitted. The sweep only acts in webhook mode (in polling mode there are no in_progress events, so ephemerd cannot tell an orphaned runner from a busy one) and only for providers that report runner assignments (GitHub).
type PkgProxyConfig ¶ added in v0.2.2
type PkgProxyConfig struct {
// Enabled turns the proxy on. Default false.
Enabled bool `toml:"enabled"`
// Port is the listen port on the bridge gateway. Zero takes the
// per-ecosystem default (npm 8084, pip 8085, pub 8086).
Port int `toml:"port"`
// Upstream overrides the registry to pull through to (npm:
// https://registry.npmjs.org, pip: https://pypi.org, pub:
// https://pub.dev). The upstream's own host is always permitted to
// serve artifacts, so an override needs no matching allowed_hosts entry.
Upstream string `toml:"upstream"`
// IndexTTL is how long cached MUTABLE metadata (an npm packument, a PEP
// 503 index page, a pub version listing) is served before it is
// revalidated with a conditional GET. Zero takes the 5m default; a
// negative value revalidates on every request.
//
// Immutable artifacts — tarballs, wheels, sdists, archives — ignore
// this entirely: they are cached permanently and never revalidated.
IndexTTL time.Duration `toml:"index_ttl"`
// MaxSizeGB is the cache's disk budget in GiB. When it is exceeded, the
// least-recently-used entries are evicted until the cache is back to
// 90% of the budget. Zero takes the 5 GiB default; a NEGATIVE value
// disables the budget entirely.
//
// There is no "unlimited by default" option on purpose: an unbounded
// package cache is how a node fills its disk (see [image_gc] and
// [buildkit] for the two previous instances of that lesson).
MaxSizeGB int64 `toml:"max_size_gb"`
// AllowedHosts extends the set of hosts the proxy will fetch package
// ARTIFACTS from. Metadata documents carry absolute download URLs which
// the proxy rewrites to point at itself; this list is what stops a job
// from hand-crafting such a URL and using the daemon as an open relay
// into the host's network. Entries match a host exactly or as a parent
// domain. The ecosystem's own CDNs and the configured upstream are
// always allowed.
AllowedHosts []string `toml:"allowed_hosts"`
// Cleanup wipes the cache directory on shutdown. Default false.
Cleanup bool `toml:"cleanup"`
}
PkgProxyConfig configures one language package caching proxy. The same shape serves [npm_proxy], [pip_proxy] and [pub_proxy]: all three are pull-through HTTP caches with an immutable-artifact half and a mutable- metadata half, and differ only in their upstream and their defaults.
Disabled by default, matching [module_proxy]'s opt-in posture.
Unlike [module_proxy], `cleanup` defaults to FALSE. A pull-through cache that empties itself on every daemon restart saves nothing, and these are bounded by max_size_gb rather than by being thrown away.
func (*PkgProxyConfig) ProxyIndexTTL ¶ added in v0.2.2
func (p *PkgProxyConfig) ProxyIndexTTL() time.Duration
ProxyIndexTTL returns the metadata revalidation interval, defaulting to 5 minutes. A negative value is preserved: it means "always revalidate".
func (*PkgProxyConfig) ProxyMaxBytes ¶ added in v0.2.2
func (p *PkgProxyConfig) ProxyMaxBytes() int64
ProxyMaxBytes returns the cache disk budget in bytes: 5 GiB by default, and a negative value (meaning unbounded) passed through as-is.
func (*PkgProxyConfig) ProxyPort ¶ added in v0.2.2
func (p *PkgProxyConfig) ProxyPort(def int) int
ProxyPort returns the configured port, or def when unset.
type RegistryMirrorConfig ¶ added in v0.2.2
type RegistryMirrorConfig struct {
// Enabled turns mirroring on. Everything else in this block is inert
// when false, and the pull path is exactly what it was before this
// feature existed.
Enabled bool `toml:"enabled"`
// Endpoint is the base URL of the pull-through cache, including the
// scheme — "http://registry.lan:5000" or "https://cache.example.com".
// A path prefix is allowed ("https://harbor.lan/v2/dockerhub-proxy")
// and is joined ahead of the /v2 API root.
//
// It serves every host listed in Registries. Use Mirrors instead (or
// as well) when different registries need different caches.
Endpoint string `toml:"endpoint"`
// Registries are the upstream registry hosts Endpoint mirrors.
// Defaults to ["docker.io"] when Endpoint is set and this is empty —
// Docker Hub is where the rate limit and the big shared base images
// are. Add "ghcr.io" etc. when the cache is configured to proxy them.
//
// Values are normalized: a scheme is stripped, and "index.docker.io" /
// "registry-1.docker.io" both fold to "docker.io" (the name containerd
// resolves references under).
Registries []string `toml:"registries"`
// Mirrors maps a single upstream registry host to its own cache URL,
// for setups where one endpoint cannot serve everything:
//
// [registry_mirror.mirrors]
// "ghcr.io" = "http://ghcr-cache.lan:5000"
//
// An entry here wins over Endpoint/Registries for that host.
Mirrors map[string]string `toml:"mirrors"`
// FallbackToOrigin keeps the origin registry in the host list behind
// the mirror, so a cache that is down, wedged, or missing the image
// costs a failed request and not a failed job. Pointer so an explicit
// `fallback_to_origin = false` is distinguishable from the key being
// absent; the default is TRUE — fail open. See
// ResolvedFallbackToOrigin.
//
// Setting false makes the mirror authoritative: a job whose image the
// cache cannot serve fails instead of reaching the WAN. That is a
// deliberate egress-control posture, not a performance setting.
FallbackToOrigin *bool `toml:"fallback_to_origin"`
// ForwardCredentials sends the credentials ephemerd would have used
// against the origin registry to the mirror as well. Off by default —
// see the SECURITY note on the type. Turn it on only for a mirror you
// operate that requires authentication (Harbor with a robot account,
// a Zot instance behind htpasswd).
ForwardCredentials bool `toml:"forward_credentials"`
}
RegistryMirrorConfig points container image pulls at a pull-through registry cache on the LAN instead of the origin registry.
Every pull ephemerd performs — the runner image, images a job pulls through the fake Docker daemon (dind), and images a sibling container is created from — is routed through the mirror when the reference's registry host is one of the mirrored ones. The first pull of a given layer crosses the WAN once; every later pull of the same layer, from any job on any node pointed at the same cache, is served at LAN speed. It also takes the node out of Docker Hub's anonymous rate limit, since only the cache talks to Hub.
The mirror is a read path only. Pushes (docker push from a job) always go to the origin registry: a pull-through cache is not a place to publish, and containerd's own host model marks mirrors pull-only for the same reason.
SECURITY: credentials are NOT sent to the mirror unless forward_credentials is set. A pull-through cache normally holds its own upstream credentials and needs none from the client, and a mirror that answered with a Basic challenge would otherwise harvest the registry PAT a job just logged in with — over plaintext when the endpoint is http://.
func (*RegistryMirrorConfig) ResolvedFallbackToOrigin ¶ added in v0.2.2
func (r *RegistryMirrorConfig) ResolvedFallbackToOrigin() bool
ResolvedFallbackToOrigin reports whether the origin registry stays in the pull host list behind the mirror. Defaults to true: a dead cache must degrade a node to today's WAN pull speed, never break every job on it.
func (*RegistryMirrorConfig) ResolvedMirrors ¶ added in v0.2.2
func (r *RegistryMirrorConfig) ResolvedMirrors() map[string]string
ResolvedMirrors flattens Endpoint/Registries and Mirrors into a single upstream-host -> mirror-URL table with both sides normalized. Per-host Mirrors entries override the Endpoint/Registries default.
Returns nil when mirroring is disabled or nothing is mapped, which every consumer treats as "no mirror configured" and leaves the pull untouched. Only call after validate has accepted the config — it assumes the URLs parse.
type RunnerConfig ¶
type RunnerConfig struct {
MaxConcurrent int `toml:"max_concurrent"`
ExtraLabels []string `toml:"extra_labels"`
DefaultImage string `toml:"default_image"`
// Images maps repo → OS → image. TOML shape:
//
// [runner.images.ephemerd]
// linux = "ephpm/ephemerd:runner-ci-linux-amd64"
// windows = "ephpm/ephemerd:runner-ci-windows"
//
// A repo can specify just one OS — the others fall through to the
// provider per-OS default and then the runtime fallback.
Images map[string]map[string]string `toml:"images"`
JobTimeout string `toml:"job_timeout"`
ShutdownTimeout string `toml:"shutdown_timeout"`
Windows WindowsRunnerToml `toml:"windows"`
Linux LinuxRunnerToml `toml:"linux"`
// ClaimRetry controls the in-memory retry queue for jobs whose
// initial claim / provision attempt fails with a transient error
// (rate limit, 5xx, network). GitHub does not re-deliver
// workflow_job webhooks, so without this queue any queued job that
// hit an API blip at claim time would be lost until human
// intervention.
ClaimRetry ClaimRetryToml `toml:"claim_retry"`
// OrphanSweep controls teardown of runners that were dispatched for
// a job but never observed picking one up. GitHub assigns JIT
// runners to ANY queued job with matching labels, so runner
// lifecycle is keyed to the observed assignment; a runner whose
// intended job went elsewhere and that never got a job of its own is
// destroyed after the grace window.
OrphanSweep OrphanSweepToml `toml:"orphan_sweep"`
}
func (*RunnerConfig) ClaimRetryEnabled ¶
func (r *RunnerConfig) ClaimRetryEnabled() bool
ClaimRetryEnabled reports whether the claim retry queue should run. Defaults to true (retries on) when the table is omitted or Enabled is nil; operators opt out with `enabled = false`.
func (*RunnerConfig) ClaimRetryJitter ¶
func (r *RunnerConfig) ClaimRetryJitter() float64
ClaimRetryJitter returns the retry backoff jitter fraction. Defaults to 0.2 (+/-20%). Values outside [0,1] are clamped to the default.
func (*RunnerConfig) ClaimRetryMaxAge ¶
func (r *RunnerConfig) ClaimRetryMaxAge() time.Duration
ClaimRetryMaxAge returns the retry queue give-up threshold. Defaults to 90 minutes when unset or unparseable.
func (*RunnerConfig) ClaimRetrySchedule ¶
func (r *RunnerConfig) ClaimRetrySchedule() []time.Duration
ClaimRetrySchedule returns the retry backoff ladder. Defaults to {30s, 1m, 2m, 5m, 10m} when unset. Unparseable entries are dropped; if all entries fail to parse, the default is returned.
func (*RunnerConfig) ImageForRepo ¶
func (r *RunnerConfig) ImageForRepo(repo string) string
ImageForRepo is the legacy helper kept for callers that haven't migrated. Prefer ImageForRepoOS. Returns the Linux image override (if set), then DefaultImage. Empty when neither is set.
func (*RunnerConfig) ImageForRepoOS ¶
func (r *RunnerConfig) ImageForRepoOS(repo, os string) string
ImageForRepoOS returns the per-repo, per-OS image override, or empty if no override is configured for that combination. Caller falls back to the provider default and then the runtime default.
func (*RunnerConfig) OrphanSweepEnabled ¶
func (r *RunnerConfig) OrphanSweepEnabled() bool
OrphanSweepEnabled reports whether the orphaned-runner sweep should run. Defaults to true when the table is omitted or Enabled is nil; operators opt out with `enabled = false`.
func (*RunnerConfig) OrphanSweepGrace ¶
func (r *RunnerConfig) OrphanSweepGrace() time.Duration
OrphanSweepGrace returns how long a dispatched runner may remain unassigned before the sweep destroys it. Defaults to 10 minutes when unset or unparseable.
func (*RunnerConfig) ParsedJobTimeout ¶
func (r *RunnerConfig) ParsedJobTimeout() time.Duration
ParsedJobTimeout returns the job timeout as a time.Duration.
func (*RunnerConfig) ParsedShutdownTimeout ¶
func (r *RunnerConfig) ParsedShutdownTimeout() time.Duration
ParsedShutdownTimeout returns the shutdown timeout as a time.Duration.
type RuntimeConfig ¶
type RuntimeConfig struct {
Rlimits RuntimeRlimits `toml:"rlimits"`
// AllowNewPrivileges controls whether a process in the runner
// container may gain privileges through execve — i.e. whether setuid
// binaries and file capabilities take effect. When true the OCI spec
// carries NoNewPrivileges=false, which is what makes `sudo` work.
//
// Jobs generally need this: `sudo apt-get install` is routine in CI,
// and the stock actions-runner image expects it. That is why the
// default is true.
//
// SECURITY: the runner container already runs as root with a trimmed
// capability set (see runtime.containerCapabilities), so with
// NoNewPrivileges=false the seccomp and AppArmor profiles carry
// nearly all of the containment on their own. A pool that builds
// untrusted code — fork PRs especially — should set this to false and
// accept that `sudo` and `apt-get install` stop working there.
//
// This is NOT the same knob as dind's allow_privileged: that one
// gates what a *sibling* container launched through the fake Docker
// API may request, while this one applies to the runner container
// itself. See DindConfig.AllowPrivileged.
//
// Use the pointer form so a missing TOML key is distinguishable from
// an explicit `allow_new_privileges = false`. See
// ResolvedAllowNewPrivileges for the default policy.
AllowNewPrivileges *bool `toml:"allow_new_privileges"`
}
RuntimeConfig configures behavior of the per-job container runtime — things that apply to the OCI spec rather than to a specific subsystem like dind or networking.
func (RuntimeConfig) ResolvedAllowNewPrivileges ¶ added in v0.1.5
func (r RuntimeConfig) ResolvedAllowNewPrivileges() bool
ResolvedAllowNewPrivileges returns whether the runner container may gain privileges through execve, applying the default when the operator hasn't set the key explicitly.
Default policy: true on all platforms — this preserves the behavior ephemerd has always had. Unlike dind's allow_privileged, a secure-by -default of false would break `sudo apt-get install` in every existing workflow, so tightening it is an explicit operator decision per pool.
type RuntimeRlimits ¶
type RuntimeRlimits struct {
// Nofile is RLIMIT_NOFILE (max open file descriptors). Both soft
// and hard get set to this value. Default 1024 (containerd default).
Nofile int64 `toml:"nofile"`
// Nproc is RLIMIT_NPROC (max processes/threads for the container's
// user). Both soft and hard get set to this value. Default 1024.
Nproc int64 `toml:"nproc"`
}
RuntimeRlimits sets POSIX resource limits (RLIMIT_*) on each runner container's OCI spec. Defaults match containerd's built-in OCI spec (nofile=1024, nproc=1024) so an empty config is a no-behavior-change.
Set higher when CI workloads need more file descriptors or processes than containerd's defaults allow. Common case: a build tool calling `ulimit -n 2048` to raise its open-file ceiling. That fails with "Operation not permitted" if the container's hard limit is 1024 — raising the hard limit needs CAP_SYS_RESOURCE, which we deliberately don't grant. Setting nofile higher here lets the same `ulimit` call succeed without granting the capability, because lowering is always allowed and the OCI hard limit is now generous.
func (RuntimeRlimits) Resolved ¶
func (r RuntimeRlimits) Resolved() RuntimeRlimits
Resolved returns the rlimits with defaults filled in for any unset (zero or negative) field. Always returns positive values so callers can blindly emit OCI rlimit entries.
type VMConfig ¶
type VMConfig struct {
// CrossPlatform enables macOS and Windows VM support. Default true.
// Set to false for platforms like Gitea/Forgejo that only support
// Linux runners — this skips macOS image pulls and Windows VM setup.
CrossPlatform *bool `toml:"cross_platform"`
Linux LinuxVMToml `toml:"linux"`
MacOS MacOSVMToml `toml:"macos"`
}
VMConfig configures virtual machines for cross-OS job execution.
func (*VMConfig) CrossPlatformEnabled ¶
CrossPlatformEnabled returns whether macOS/Windows VM support is enabled. Defaults to true when not set.
type WebhookConfig ¶
type WebhookConfig struct {
Secret string `toml:"secret"` // webhook HMAC secret (auto-generated for managed tunnels; required for "external")
Port int `toml:"port"` // listen port for health endpoint (default 8080)
TLSCert string `toml:"tls_cert"` // TLS certificate path (direct TLS, no tunnel)
TLSKey string `toml:"tls_key"` // TLS private key path
Tunnel string `toml:"tunnel"` // "none" (default, polling), "external" (unmanaged ingress), "localtunnel", "ngrok", or "cloudflared"
// ExternalURL is the public base URL of the externally-managed tunnel
// (e.g. https://mac.tricorder.cc). When set with tunnel="external",
// ephemerd registers each tracked repo's webhook to
// <external_url>/webhook/<provider> using the secret. Ignored for managed
// tunnels (they use the tunnel's own URL) and for polling.
ExternalURL string `toml:"external_url"`
TunnelURL string `toml:"tunnel_url"` // localtunnel: self-hosted server URL
NgrokAuthtoken string `toml:"ngrok_authtoken"` // ngrok auth token (or use NGROK_AUTHTOKEN env)
TunnelMaxRetries int `toml:"tunnel_max_retries"` // max consecutive reconnect failures before falling back to polling (default 5)
// cloudflared: tunnel = "cloudflared". Ephemerd runs cloudflared as a
// managed subprocess bound to its own lifetime (child gets SIGTERM on
// parent exit). The tunnel and its DNS record must be provisioned in
// Cloudflare beforehand — ephemerd only runs the client. The token
// authenticates and identifies which tunnel to connect.
CloudflaredToken string `toml:"cloudflared_token"` // tunnel run token (or use CLOUDFLARE_TUNNEL_TOKEN env)
CloudflaredHostname string `toml:"cloudflared_hostname"` // public FQDN of the tunnel (e.g. "runner.example.com"); required for GitHub webhook registration
CloudflaredVersion string `toml:"cloudflared_version"` // pinned cloudflared release (e.g. "2026.6.1"); defaults to a known-good version if empty
// Pool marks this instance as one member of a pool of ephemerd nodes
// sharing a single public webhook URL (e.g. cloudflared tunnel replicas
// behind one hostname). In pool mode webhook registration is
// adopt-or-create (an existing hook with the same URL is converged, not
// duplicated), the hook is never deregistered on shutdown (pool-mates
// still need it), and the startup stale-hook sweep is skipped (it cannot
// tell a pool-mate's live hook from a stale one). Requires an explicit
// shared webhook.secret — every pool member must present the same one.
Pool bool `toml:"pool"`
// ReconcileInterval controls the webhook-mode reconcile sweep: how often
// ephemerd re-runs the catch-up poll to pick up jobs that got stranded.
//
// This is a LAST-RESORT backstop only for genuinely DROPPED webhook
// deliveries (network/tunnel loss where GitHub's own redelivery also
// missed us). The common stranding case — a fungibly-reassigned runner
// leaving its dispatched job queued — is now healed instantly and
// event-drivenly by the scheduler on runner exit, without polling. So this
// runs at a low frequency: empty = default 30m; a zero/negative duration
// disables it entirely (relying purely on the event-driven path + GitHub's
// delivery retries).
ReconcileInterval string `toml:"reconcile_interval"`
}
WebhookConfig configures webhook delivery and tunnel providers. By default, ephemerd uses polling (tunnel = "none"). Set tunnel = "localtunnel", "ngrok", or "cloudflared" for ephemerd to create and manage a tunnel and auto-register the GitHub webhook. Set tunnel = "external" when a tunnel is provided by something else: ephemerd then serves the webhook receiver and disables polling, but does not create a tunnel or register the webhook — that is owned externally, so a matching secret is required.
func (*WebhookConfig) ResolvedReconcileInterval ¶
func (w *WebhookConfig) ResolvedReconcileInterval() time.Duration
ResolvedReconcileInterval returns the webhook-mode reconcile sweep interval: 30m by default (empty or unparseable), the parsed value otherwise, and 0 (disabled) only when explicitly set to a zero/negative duration.
type WindowsRunnerToml ¶
type WindowsRunnerToml struct {
MemoryMB uint64 `toml:"memory_mb"` // memory in MB (default: 4096)
CPUs uint64 `toml:"cpus"` // virtual CPUs (default: 2)
}
WindowsRunnerToml configures resource limits for Hyper-V isolated Windows runner containers. Without limits Hyper-V containers default to ~1 GB RAM, which is too small for MSVC + parallel cl.exe builds.
func (WindowsRunnerToml) CPUCount ¶
func (w WindowsRunnerToml) CPUCount() uint64
CPUCount returns the CPU count, applying the default if unset.
func (WindowsRunnerToml) MemoryBytes ¶
func (w WindowsRunnerToml) MemoryBytes() uint64
MemoryBytes returns the memory limit in bytes, applying the default if unset.
type WoodpeckerConfig ¶
type WoodpeckerConfig struct {
ServerURL string `toml:"server_url"` // Woodpecker server gRPC URL (e.g., "woodpecker.example.com:9000")
AgentSecret string `toml:"agent_secret"` // shared secret for agent authentication
}
WoodpeckerConfig configures the Woodpecker CI provider. Set server_url and agent_secret to enable Woodpecker instead of GitHub. Woodpecker requires a forge backend (Gitea/Forgejo) for repo management; ephemerd manages the agent lifecycle, not the server.