-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathconfig.example.toml
More file actions
521 lines (481 loc) · 24.6 KB
/
Copy pathconfig.example.toml
File metadata and controls
521 lines (481 loc) · 24.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
[github]
# Authentication: personal access token OR GitHub App credentials.
# Use one approach - if app_id is set, it takes priority over token.
# token = "ghp_your_token_here"
# GitHub App authentication (recommended):
# app_id = 123456
# installation_id = 789012
# private_key_path = "/path/to/app.pem"
# Organization or user that owns the repos
owner = "your-org"
# Repos to register runners for (optional).
# If omitted, registers org-level runners available to all repos in the org.
# repos = ["repo1", "repo2"]
# OPTIONAL dispatch allowlist (defense-in-depth). This does NOT replace GitHub's
# "Require approval for all outside collaborators" setting, which is the primary
# control against fork-PR abuse of self-hosted runners — keep that enabled.
# Both fields default to empty (no restriction = today's behavior).
# [github.dispatch_policy]
# allowed_repos = ["repo1"] # only dispatch for these repos
# required_labels = ["ephemerd"] # a job must carry one of these labels to dispatch
[webhook]
# Webhook-mode reconcile sweep: how often ephemerd re-runs the catch-up poll
# as a LAST-RESORT backstop for genuinely dropped webhook deliveries. The
# common stranding case (fungible-runner churn) is healed event-drivenly on
# runner exit, so this runs at a low frequency. Default 30m; "0s" disables.
# reconcile_interval = "30m"
# By default, ephemerd polls the GitHub API every 30s for new jobs.
# No inbound ports or tunnels required.
# To use localtunnel for instant webhook delivery:
# tunnel = "localtunnel"
# tunnel_url = "http://tunnels.example.com" # self-hosted server (recommended)
# To use ngrok instead (requires free account):
# tunnel = "ngrok"
# ngrok_authtoken = "your_ngrok_token" # or set NGROK_AUTHTOKEN env var
# To use a Cloudflare Tunnel (requires a Cloudflare zone):
# Create the tunnel + DNS route in the Cloudflare dashboard (or via API),
# then give ephemerd the tunnel token and the public hostname. Ephemerd
# downloads the cloudflared binary into its data dir, runs it as a managed
# subprocess bound to ephemerd's lifetime, and auto-registers the GitHub
# webhook at https://<hostname>/webhook/github.
# tunnel = "cloudflared"
# cloudflared_token = "eyJhIjoi..." # or set CLOUDFLARE_TUNNEL_TOKEN env var
# cloudflared_hostname = "ci.example.com" # public FQDN routed to the tunnel
# cloudflared_version = "2026.6.1" # optional; pinned download version
# To use direct TLS instead of a tunnel (VPS with a public IP):
# tunnel = "none"
# tls_cert = "/etc/ephemerd/tls.crt"
# tls_key = "/etc/ephemerd/tls.key"
# secret = "your_webhook_secret" # must be stable when not using a tunnel
# port = 8080
# To use an externally-managed tunnel (e.g. a Cloudflare tunnel on another host
# that forwards a public hostname to this port). ephemerd serves the webhook
# receiver and disables polling but does not create a tunnel:
# tunnel = "external"
# secret = "your_webhook_secret" # required; must match the hook's secret
# external_url = "https://mac.example.com" # optional: public base URL of the
# # external tunnel. When set, ephemerd auto-registers each tracked repo's
# # webhook to <external_url>/webhook/<provider> on startup (idempotently) so
# # you don't have to hand-add a hook per repo. External hooks are NOT
# # removed on shutdown. Leave unset to add the hooks manually.
# port = 8080
# Pool mode: multiple ephemerd nodes sharing one public webhook URL (e.g.
# tunnel replicas behind one hostname). Registration adopts an existing
# same-URL hook instead of duplicating it, shutdown leaves the shared hook
# in place for pool-mates, and the startup stale-hook sweep is skipped.
# Every member MUST use the same explicit secret.
# pool = true
# secret = "shared_across_all_pool_members"
[network]
# Container subnet. Ephemerd auto-picks a free subnet if this conflicts with
# an existing network (e.g. Podman, Docker). You don't need to change this.
# On the Windows L2Bridge path below it means something different — see there.
# subnet = "10.88.0.0/16"
# Bridge MTU. Auto-detected from the host's default interface.
# Override if containers have connectivity issues on unusual networks.
# mtu = 1500
# --- Windows L2Bridge egress (opt-in; Windows hosts only) -------------------
# By default Windows containers use an HNS NAT network. NAT cannot
# software-filter Windows container egress (VFP does not engage on a NAT
# network), and no host-side mechanism can — so on NAT ephemerd installs
# nothing and container egress is simply UNFILTERED: job containers can reach
# your whole LAN. Setting l2bridge_egress = true instead puts containers on an
# L2Bridge with a
# VFP-managed vSwitch port, where per-endpoint ACLs actually enforce: all of
# RFC1918 (10/8, 172.16/12, 192.168/16, 169.254/16) — including the LAN router
# — is blocked, and only the internet is reachable. Leave false to keep NAT.
#
# TRADE-OFF: on L2Bridge your job containers are no longer behind NAT. They are
# peers on this host's LAN with real LAN addresses, which is exactly why the
# ACLs can enforce — and why you must reserve addresses for them (ip_pool).
# l2bridge_egress = false
#
# Host NIC the L2Bridge binds onto. REQUIRED when l2bridge_egress = true; there
# is no default (the correct adapter name is host-specific). Use the name shown
# by `Get-NetAdapter`. Ignored when l2bridge_egress is false.
#
# STRONGLY RECOMMENDED: bind a DEDICATED NIC, not the host's management NIC.
# Creating the L2Bridge builds an external Hyper-V vSwitch on this adapter and
# migrates its IP onto a "vEthernet (<name>)" adapter. On a host reached over
# that same NIC there is a brief connectivity blip during creation, and any
# failure mid-creation can leave the box unreachable — a hazard for remote
# nodes. A second NIC dedicated to container traffic keeps the management path
# untouched. (ephemerd tolerates the vSwitch rename either way, so restarts are
# safe once the network exists.)
# host_nic = "Ethernet 2"
#
# Addresses ephemerd may assign to job containers. REQUIRED when
# l2bridge_egress = true, with NO default.
#
# Why you have to set this: an L2Bridge HNS network must declare a subnet (HNS
# refuses to create one without), and left to itself HNS then picks endpoint
# addresses from anywhere inside that subnet — i.e. straight into your DHCP
# server's scope. ephemerd instead allocates from this pool and pins each
# container to the address it picked. Reserve the range on your DHCP server (or
# place it outside the DHCP scope) BEFORE enabling this, or you will get
# duplicate-address conflicts on your LAN.
#
# Either form works. Size it for at least runner.max_concurrent containers.
# The examples below use RFC 5737 documentation addresses — replace them.
# ip_pool = "192.0.2.192/27" # CIDR: .193-.222 (network/broadcast excluded)
# ip_pool = "192.0.2.200-192.0.2.230" # or an inclusive range
#
# The LAN itself. Both are auto-derived from host_nic at startup — subnet from
# the address configured on the adapter, gateway from its IPv4 default route —
# and ephemerd logs what it inferred. Set them only if the adapter has no
# default route of its own, or carries a prefix other than the LAN you want
# declared. ip_pool must lie inside subnet and must not contain either the
# host's own address or the gateway; startup refuses the config if it does.
# subnet = "192.0.2.0/24"
# gateway = "192.0.2.1"
#
# Public DNS resolvers handed to L2Bridge containers so name resolution goes to
# the internet rather than the (blocked) LAN router. Default 1.1.1.1, 8.8.8.8.
# public_dns = ["1.1.1.1", "8.8.8.8"]
#
# Extra destination CIDRs permitted through the L2Bridge egress ACLs, ABOVE the
# RFC1918 block (reserved for future use; default none — the strict posture).
# extra_allowed_destinations = []
#
# NOTE — host reachability. When dind or the Go module proxy is enabled,
# ephemerd adds a single /32 allow for its own host address, because both serve
# job containers over the network (DOCKER_HOST / GOPROXY) and would otherwise be
# blocked with the rest of RFC1918. HNS Switch ACLs cannot be port-scoped
# without blackholing the whole port, so the control-plane ports are fenced off
# at the host firewall instead. With neither feature enabled, this host is
# unreachable from job containers like every other RFC1918 address.
#
# NOTE — migration. Switching an existing Windows node from NAT to L2Bridge
# needs a REBOOT, not just a daemon restart: creating an L2Bridge network beside
# a live HNS NAT network has been observed to wedge HNS (containers start with
# no egress, runners register then go offline, service stop hangs on drain).
# Drain the node, set the keys, reboot.
# Container-registry pull-through cache.
#
# Every image pull ephemerd makes — the runner image, images a job pulls
# through the fake Docker socket, images a sibling container is created from —
# is sent to this cache instead of the origin registry. The first pull of a
# layer crosses the WAN once; every later pull of it, from any job on any node
# pointed at the same cache, is served at LAN speed. It also removes the node
# from Docker Hub's anonymous rate limit, since only the cache talks to Hub.
#
# The saving scales with how much jobs share a base image. On a node measured
# at ~71 GB/day inbound across ~80 jobs, one 1.1 GB CI image accounted for
# 163 pulls in seven days — dind pulls into a per-job containerd namespace, so
# that image crossed the WAN for essentially every job.
#
# Stand up any OCI pull-through cache on the LAN. The off-the-shelf choice is
# the upstream registry image in proxy mode:
#
# docker run -d --restart=always -p 5000:5000 \
# -v /srv/registry:/var/lib/registry \
# -e REGISTRY_PROXY_REMOTEURL=https://registry-1.docker.io \
# --name registry-cache registry:2
#
# See docs/guides/registry-cache.md for sizing, Zot/Harbor alternatives, and
# how to verify it is being used.
# [registry_mirror]
#
# Turn mirroring on. Everything else here is inert while this is false, and
# pulls behave exactly as they do with no [registry_mirror] block at all.
# enabled = true
#
# Base URL of the cache, INCLUDING the scheme. There is no default — the cache
# is site-specific. A path prefix is allowed for caches published under one
# (e.g. a Harbor proxy project).
# endpoint = "http://registry.lan:5000"
#
# Which upstream registries this endpoint serves. Defaults to ["docker.io"],
# which is where the rate limit and the large shared base images are. Add
# others only if the cache is configured to proxy them — registry:2 in proxy
# mode serves exactly one upstream per instance.
# registries = ["docker.io"]
#
# Keep the origin registry behind the mirror. DEFAULT TRUE, and you almost
# certainly want it: a cache that is down, wedged, or simply does not have the
# image then costs one failed request and the pull completes against the
# origin. The node degrades to the WAN speed it had before the mirror existed
# instead of failing every job on it.
#
# Set false to make the mirror authoritative — a deliberate egress-control
# posture (jobs can only run images the cache holds), not a performance knob.
# fallback_to_origin = true
#
# Send the credentials ephemerd would use against the origin registry to the
# mirror too. DEFAULT FALSE. 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 — in plaintext when the endpoint is http://. Turn it
# on only for a mirror you operate that requires authentication.
# forward_credentials = false
#
# Per-registry caches, for sites that cannot serve everything from one
# endpoint. An entry here wins over endpoint/registries for that host.
# [registry_mirror.mirrors]
# "ghcr.io" = "http://ghcr-cache.lan:5000"
[runner]
# Max concurrent jobs
max_concurrent = 4
# Default container image for jobs that don't specify `container:` in the workflow.
# Leave blank to auto-detect:
# Linux → ghcr.io/actions/actions-runner:latest
# Windows → mcr.microsoft.com/windows/servercore:ltsc20XX (matched to host OS)
# default_image = ""
# Extra labels applied to all runners (in addition to self-hosted, os, arch)
extra_labels = []
# Max time a job can run before forced teardown
job_timeout = "2h"
# Time to wait for running jobs during graceful shutdown
shutdown_timeout = "5m"
# Container runtime for Linux job containers.
#
# "runc" (default) runs jobs as ordinary containers on the host kernel.
# "kata" runs each job container inside its own lightweight VM with its
# own kernel (Kata Containers), which makes a kernel-level container
# escape a guest-VM escape instead of a host compromise, and brings Linux
# in line with the Hyper-V isolation Windows jobs already get.
#
# Requirements for "kata": Kata Containers installed with
# containerd-shim-kata-v2 on PATH, and a usable /dev/kvm (on a VM that
# means nested virtualization must be enabled). ephemerd REFUSES TO START
# if either is missing rather than silently falling back to runc.
#
# Costs, measured on an 8-core Proxmox guest with Kata 4.0.0 + QEMU:
# container start goes from 0.14s to 4.1s (medians), each running job
# carries ~310 MB of guest-VM memory instead of ~14 MB, CPU-bound work is
# ~35% slower, and file-heavy work is 8-40x slower because both the
# rootfs and the bind-mounted runner dir cross virtio-fs.
#
# NOT compatible with [dind] — the Docker API is passed to jobs through a
# bind-mounted unix socket, which a Kata guest cannot connect to. Enabling
# both is a config error.
# [runner.linux]
# runtime = "runc"
# Claim retry queue.
#
# GitHub does not re-deliver workflow_job webhooks. When ephemerd's
# initial claim/provision attempt fails for a transient reason
# (rate-limit exhausted, transient 5xx, network blip), the job would
# otherwise be lost. The retry queue schedules re-attempts on a
# jittered exponential backoff ladder and gives up after max_age.
#
# When the last GitHub API response reported remaining=0 with a known
# reset time, the next attempt is snapped to just after reset instead
# of falling through the ladder, so we do not burn attempts against a
# provably-exhausted budget.
#
# Enabled by default. Set enabled = false to restore the pre-existing
# "log and drop" behavior.
# [runner.claim_retry]
# enabled = true
# max_age = "90m"
# schedule = ["30s", "1m", "2m", "5m", "10m"]
# jitter = 0.2
# Orphaned-runner sweep.
#
# ephemerd registers one just-in-time runner per queued job, but GitHub
# assigns a registered runner to ANY queued job with matching labels.
# Runner teardown therefore follows the assignment GitHub reports in
# workflow_job webhooks (runner_name), not the job the runner was
# dispatched for. A runner whose intended job was picked up elsewhere
# and that never received a job of its own is destroyed and
# deregistered after the grace window.
#
# Only active in webhook mode (polling mode has no in_progress events
# to observe assignments with). Enabled by default.
# [runner.orphan_sweep]
# enabled = true
# grace = "10m"
[vm.linux]
# Enable a Linux VM for running Linux jobs on Windows/macOS hosts.
# On Windows: boots a Hyper-V Linux VM through the Host Compute Service
# (vmcompute.dll) from an embedded kernel + initrd, with an embedded
# ephemerd binary inside. Requires the Hyper-V feature to be enabled.
# On macOS: boots the VM with Apple's Virtualization.framework.
# Default is platform-dependent: ON for macOS, OFF (opt-in) for Windows.
# enabled = false
# cpus = 2
# memory_mb = 2048
# Per-job macOS VMs (Apple Virtualization.framework; darwin hosts only).
# Each macOS job boots a clone-on-write VM and waits for its runner to
# register. provision_timeout bounds that pre-registration wait: if a VM has
# not become reachable within the window it is force-stopped and its slot
# released, so a wedged VM can't hold the (often single) macOS slot and stall
# macOS CI fleet-wide. Default 5m.
# [vm.macos]
# max_concurrent = 1
# cpus = 4
# memory_mb = 8192
# provision_timeout = "5m"
# BuildKit build cache bounds.
#
# BuildKit only garbage-collects when its worker is given a GC policy.
# Without one the shared "buildkit" containerd namespace grows forever:
# every `docker build` adds cache records, snapshots and leases that nothing
# releases. These defaults keep the cache WARM BUT BOUNDED — pruning harder
# would just move the cost onto the network.
# [buildkit]
# gc_enabled = true
# gc_reserved_gb = 5 # never collected, even when idle
# gc_max_used_gb = 25 # hard ceiling
# gc_min_free_gb = 20 # collect to keep this much disk free
# gc_keep_duration = "168h" # 7 days
# gc_ephemeral_keep_duration = "48h" # build contexts, cache mounts, git checkouts
# gc_ephemeral_max_used_gb = 2
# Disk-pressure image garbage collection.
#
# Disk pressure is the TRIGGER, least-recently-used is the ORDER (kubelet's
# model). A pass starts at the high watermark and evicts LRU-first until the
# low watermark, then stops — two watermarks so it can't thrash at the line.
# The percentage and the absolute floor are independent triggers and the
# more conservative one wins: 15% of a 1 TB disk is 150 GB (pointless to
# collect), 15% of a 100 GB disk is 15 GB (three concurrent jobs can eat
# that between ticks). Size min_free_gb against max_concurrent x expected
# per-job layer size.
#
# Never evicted: images a running container references, the configured
# runner images (evicting those forces an immediate re-pull), and any live
# job's build output.
# [image_gc]
# enabled = true
# check_interval = "60s" # one statfs per tick, never a directory walk
# high_watermark_percent = 85
# low_watermark_percent = 70
# min_free_gb = 20
# target_free_gb = 40 # defaults to 2x min_free_gb
# max_age = "0" # optional age backstop; off by default
[metrics]
# Enable Prometheus metrics endpoint. Disabled by default.
# enabled = true
# port = 9090
# path = "/metrics"
# Bind address. The metrics endpoint is UNAUTHENTICATED, so it binds to
# 127.0.0.1 (loopback only) by default. To scrape from another host, set this
# to "0.0.0.0" and firewall the port and/or enable TLS below.
# bind_addr = "127.0.0.1"
# Optional TLS for the metrics endpoint
# tls_cert = "/etc/ephemerd/metrics.crt"
# tls_key = "/etc/ephemerd/metrics.key"
# [dispatch] configures the host<->VM gRPC dispatch channel used on Windows
# (Hyper-V) and macOS (Vz) hosts to run Linux jobs inside a VM. A shared bearer
# token authenticates every RPC. Leave unset: ephemerd auto-generates a token
# on first run and persists it here. Set explicitly only to pin/rotate it.
# [dispatch]
# token = "..."
# Go module caching proxy. Runs a GOPROXY on the bridge gateway and injects
# GOPROXY into every job container, so `go mod download` hits the local cache
# instead of proxy.golang.org on each build.
# [module_proxy]
# enabled = false
# port = 8082 # listen port on the bridge gateway
# upstream = "https://proxy.golang.org" # fetched from on a cache miss
# cleanup = true # wipe the cache on shutdown
# Cargo/crates caching proxy. A pull-through cache for the crates.io sparse
# index, .crate tarballs, and rustup toolchain artifacts.
#
# Jobs need no workflow changes: rustup is pointed at the proxy with
# RUSTUP_DIST_SERVER, and Cargo with a generated .cargo/config.toml that
# ephemerd bind-mounts read-only at the container's filesystem root. A repo
# that ships its own .cargo/config.toml still wins.
#
# Fails open, which matters more here than for [module_proxy]: GOPROXY has
# "|direct" to fall back on, a Cargo source replacement has nothing. So:
# - proxy does not start -> nothing injected, jobs go direct
# - upstream down, copy cached -> the stale copy is served
# - upstream down, nothing -> 307 to the real origin (index, config.json,
# crates and rustup alike); never a 5xx
# - proxy up but not answering -> a watchdog notices within ~30s and rewrites
# the mounted config to one with no source
# replacement, so cargo goes direct until it
# recovers
# [cargo_proxy]
# enabled = false
# port = 8083 # listen port on the bridge gateway
# upstream = "https://index.crates.io" # sparse registry index
# rustup_upstream = "https://static.rust-lang.org" # toolchain distribution
# index_ttl = "10m" # index revalidation interval;
# # .crate tarballs are immutable
# # and cached permanently
# cleanup = false # keep the cache across restarts
# ==============================================================================
# Language package caches
# ==============================================================================
#
# Pull-through HTTP caches for npm, pip and pub, so CI jobs stop re-downloading
# the same dependencies on every ephemeral runner. Each runs on the bridge
# gateway and injects one env var into every job container — no workflow
# changes.
#
# All three are OFF by default (same posture as [module_proxy]).
#
# Shared design, per ecosystem:
# * IMMUTABLE artifacts (npm tarballs, Python wheels/sdists, pub archives)
# are cached permanently and never revalidated. These are the bytes.
# * MUTABLE metadata (npm packuments, PEP 503 index pages, pub version
# listings) is cached for index_ttl and then revalidated with a
# conditional GET, so a package published minutes ago is still visible.
# * Download URLs inside metadata are rewritten to point back at the proxy,
# which is what makes the artifact bytes flow through the cache at all.
# Integrity hashes are left untouched and still verified by the client.
# * FAIL OPEN. A dead cache degrades to "normal speed", never "build fails":
# an unreachable upstream serves the stale copy, an uncached download is
# 307-redirected to the real origin, and a proxy that does not answer its
# health probe at startup has its env var withheld entirely.
# * BOUNDED. max_size_gb caps each cache; the least-recently-used entries are
# evicted back to 90% of the cap. Inspect with `ephemerd cache list`.
#
# Shared keys (defaults shown):
# enabled = false turn the cache on
# port = <see below on the bridge gateway>
# upstream = <the ecosystem's public registry>
# index_ttl = "5m" metadata revalidation interval; a negative value
# revalidates on every request. Artifacts ignore it.
# max_size_gb = 5 disk budget; -1 disables the bound (not advised)
# allowed_hosts = [] EXTRA hosts the proxy may fetch artifacts from.
# The ecosystem CDNs and the configured upstream are
# always allowed. This list is the SSRF fence around
# the rewritten download URLs.
# cleanup = false wipe the cache on shutdown
# npm / pnpm / Yarn. Injects npm_config_registry (npm, pnpm, Yarn Classic) and
# YARN_NPM_REGISTRY_SERVER (Yarn Berry). A repo-committed .npmrc or
# .yarnrc.yml with an explicit registry still wins. Authenticated/private
# registries are not proxied — this cache forwards no credentials.
# [npm_proxy]
# enabled = false
# port = 8084
# upstream = "https://registry.npmjs.org"
# index_ttl = "5m"
# max_size_gb = 5
# cleanup = false
# pip / Poetry / pip-tools. Injects PIP_INDEX_URL (pointing at the proxy's
# PEP 503 simple index) and PIP_TRUSTED_HOST, which is required because the
# proxy speaks plain HTTP on a private address. A pip.conf or a
# requirements.txt "--index-url" line still wins; uv reads UV_INDEX_URL rather
# than PIP_INDEX_URL and is not covered.
# [pip_proxy]
# enabled = false
# port = 8085
# upstream = "https://pypi.org"
# index_ttl = "5m"
# max_size_gb = 5
# cleanup = false
# Dart / Flutter. Injects PUB_HOSTED_URL, honoured by both `dart pub` and
# `flutter pub`. A dependency with an explicit `hosted:` URL in pubspec.yaml
# still wins, and the Flutter SDK's own artifact downloads follow
# FLUTTER_STORAGE_BASE_URL instead — those are not cached here. Publishing is
# never proxied.
# [pub_proxy]
# enabled = false
# port = 8086
# upstream = "https://pub.dev"
# index_ttl = "5m"
# max_size_gb = 5
# cleanup = false
[log]
# Log level: debug, info, warn, error
level = "info"
# Log format: text or json
format = "text"
# How long to keep job log files before automatic cleanup.
# Supports Go durations (e.g. "168h") and a "d" shorthand (e.g. "7d").
# log_retention = "7d"