EasyP

Helm Values Reference

Every value of the easyp-service Helm chart 1.0.2, with the chart's own description.

Every value in values.yaml of the easyp-service chart 1.0.2 (app v1.0.2), 117 in total. Descriptions are the chart's own comments, quoted as written, which is why they are in English on both language versions of this page. What must be set and what is dangerous is summarised in Installation; config.* values map to the service configuration. The chart mostly comments whole blocks, so a key marked — is covered by the paragraph above its table or by the notes below it.

The same file, for the version you install:

helm show values oci://ghcr.io/easyp-tech/charts/easyp-service --version 1.0.2

The file opens with:

Default values for easyp-service. Three things have no working default and must be decided before the chart will install. Each is refused at install time rather than at runtime, so a missing one is a message naming it, not a pod that comes up wrong. 1. A database DSN — see secrets. There is nothing sensible to default to. 2. TLS material, because tls.enabled defaults to true. Supply one of: tls.serverSecret=<kubernetes.io/tls Secret> you bring the certificate certManager.enabled=true the chart issues it tls.enabled=false plaintext, trusted network The default stays on: a listener that speaks plaintext is the wrong thing to arrive at by saying nothing. But it does mean there is no install that consists only of a DSN, and this list said otherwise until v0.14.0. 3. ingress.host, and with it config.server.trustedProxies, if you want the gRPC API reachable from outside the cluster. Both are required together — see the comment on trustedProxies for why the second one is not optional.

replicaCount

KeyTypeDefaultDescription
replicaCountinteger1—

image

KeyTypeDefaultDescription
image.repositorystring"ghcr.io/easyp-tech/service"—
image.tagstring""Empty means Chart.appVersion. Pin an explicit tag in production: a floating tag makes a rollout non-reproducible.
image.pullPolicystring"IfNotPresent"—

imagePullSecrets

KeyTypeDefaultDescription
imagePullSecretslist[]—

nameOverride

KeyTypeDefaultDescription
nameOverridestring""—

fullnameOverride

KeyTypeDefaultDescription
fullnameOverridestring""—

serviceAccount

KeyTypeDefaultDescription
serviceAccount.createbooleantrue—
serviceAccount.annotationsmap{}—
serviceAccount.namestring""—

podAnnotations

KeyTypeDefaultDescription
podAnnotationsmap{}—

podLabels

KeyTypeDefaultDescription
podLabelsmap{}—

nodeSelector

KeyTypeDefaultDescription
nodeSelectormap{}—

tolerations

KeyTypeDefaultDescription
tolerationslist[]—

affinity

KeyTypeDefaultDescription
affinitymap{}—

topologySpreadConstraints

KeyTypeDefaultDescription
topologySpreadConstraintslist[]—

reloader

The service reads its certificate and key once, at startup, so a rotated certificate is not picked up until the pod restarts. With stakater/Reloader installed this annotation restarts the deployment when the secret changes. Without it, rotation is a manual kubectl rollout restart.

KeyTypeDefaultDescription
reloader.enabledbooleantrue—

resources

Sized against what the service is configured to do, not guessed. The plugin's output is read into memory whole, so peak usage tracks config.workerPool.maxConcurrentGenerations × config.registry.maxOutputSize — 16 × 64 MiB = 1 GiB of buffers with the defaults below, and the same bytes again once marshalled into the gRPC response. _helpers.tpl refuses an install where that product no longer fits in the limit. The earlier default was 1Gi, which the buffers alone filled. The pod was OOMKilled under load, and an OOMKill reads as a crash rather than as overload: no log line, no rejected-generation counter, no saturation alert.

KeyTypeDefaultDescription
resources.requests.cpustring"500m"—
resources.requests.memorystring"1Gi"—
resources.requests.ephemeral-storagestring"1Gi"Writable layer and logs only. Both the plugin cache and the archives staged mid-download live on the plugins volume, not here — the download used to land in /tmp on the writable layer, where a few hundred megabytes times the concurrent generations was an eviction rather than an overload. With persistence disabled see persistence.ephemeralSizeLimit, which is what actually bounds that volume.
resources.limits.cpustring"4"Four cores for sixteen concurrent plugin processes. On two they each ran at an eighth speed and approached generationTimeoutSeconds, which would have surfaced as DeadlineExceeded — a broken plugin, to anyone reading — rather than as the honest ResourceExhausted the limiter returns.
resources.limits.memorystring"4Gi"—
resources.limits.ephemeral-storagestring"2Gi"—

ports

Ports the container listens on. The service defaults to 23410-23413 when no configuration is given; the chart always sets them explicitly.

KeyTypeDefaultDescription
ports.grpcinteger8080—
ports.metricinteger8081Singular, matching the service's own server.port.metric. It was metrics until v0.13.0 — one word with two spellings across three files.
ports.healthinteger8082—
ports.mcpinteger8083—

mcp

The MCP endpoint: a read-only HTTP surface AI tooling uses to read the plugin catalog and the easyp.yaml schema. It exposes nothing the anonymous gRPC reads do not, but it sits outside the gRPC interceptor chain — no TLS, no rate limit, no audit — so serving it is a deployment's explicit decision. Off: the container does not listen on ports.mcp and the Service does not publish it. Turn it on inside a trusted network or behind an ingress that terminates TLS.

KeyTypeDefaultDescription
mcp.enabledbooleanfalse—

service

KeyTypeDefaultDescription
service.typestring"ClusterIP"—
service.annotationsmap{}—

terminationGracePeriodSeconds

Seconds Kubernetes waits after SIGTERM before SIGKILL. Three numbers have to line up, and _helpers.tpl enforces the order: generationTimeoutSeconds < forceShutdownAfterSeconds < terminationGracePeriodSeconds A generation the service accepted must be able to finish; the process must be able to exit on its own after that; and Kubernetes must wait for both rather than reaching for SIGKILL first.

KeyTypeDefaultDescription
terminationGracePeriodSecondsinteger180—

config

Configuration that is not secret. Rendered into a ConfigMap and mounted at /etc/easyp/config.yml — the same config file every other deployment of this service reads. Secrets are not here; they arrive as environment variables from the secret above and override the file. The numbers below repeat the defaults compiled into the service, on purpose: this is the chart's documented interface and helm show values is where an operator looks them up. tests/render.sh checks that they still agree with the binary, so a default changed on one side and not the other fails there rather than in a cluster.

KeyTypeDefaultDescription
config.logLevelstring"info"—
config.server.trustedProxieslist[]CIDRs whose X-Forwarded-For and X-Real-IP headers may be believed. Required once ingress.enabled is true, and the chart refuses the install without it. Behind a proxy every request arrives from the proxy's address, so with this empty the rate limit and the per-caller concurrency limit become one bucket shared by every client at once, and the audit log records the ingress instead of who acted. None of that fails visibly. Set it to the range your ingress controller's pods run in — the pod CIDR of that node pool, not the whole cluster: anything listed here can claim to be any client.
config.server.maxRecvMsgSizeinteger67108864Largest single gRPC message accepted and sent. gRPC's own defaults are 4 MiB in — which a request carrying a large proto tree exceeds — and effectively unlimited out. maxSendMsgSize must be at least config.registry.maxOutputSize or the service refuses to start: a plugin allowed to produce more than can be sent does its work for nothing.
config.server.maxSendMsgSizeinteger67108864—
config.server.maxConcurrentStreamsinteger256Concurrent streams one connection may hold. gRPC defaults to unlimited, which lets a single caller allocate goroutines and buffers until the pod is out of memory, before any limiter in the chain sees the requests.
config.forceShutdownAfterSecondsinteger150Hard exit this long after SIGTERM, if graceful shutdown has not finished. Expressed in seconds so the chart can compare it with the two numbers on either side of it.
config.workerPool.workersinteger4Concurrent plugin lookups: a database read, plus a download and unpack from object storage on a cache miss.
config.workerPool.queueSizeinteger16Waiting slots, applied to lookups and to generations alike. Beyond this the answer is ErrServerOverloaded rather than a longer queue.
config.workerPool.maxConcurrentGenerationsinteger16Concurrent plugin processes. Separate from workers on purpose: a worker is released once the plugin is located, and execution runs after that.
config.workerPool.generationTimeoutSecondsinteger120Expressed in seconds (not "120s") so the chart can compare it against terminationGracePeriodSeconds. Rendered as a Go duration for the service.
config.workerPool.maxRetriesinteger2—
config.workerPool.shutdownTimeoutstring"30s"—
config.rateLimit.requestsPerSecondinteger10—
config.rateLimit.burstinteger20—
config.rateLimit.cleanupIntervalstring"10m"—
config.rateLimit.maxConcurrentPerIPinteger2Requests one client may have in flight at once. Rate alone does not bound this: a caller staying under the rate can still hold every generation slot with long requests. 0 disables the check.
config.audit.bufferSizeinteger1000—
config.audit.batchSizeinteger100—
config.audit.flushIntervalstring"1s"—
config.audit.maxSaveRetriesinteger3—
config.audit.enqueueTimeoutstring"1s"How long an operation waits for room in the audit queue. This is the only place audit can slow a request down, and it takes a backed-up queue to get there: a healthy writer drains batchSize/flushInterval — a hundred entries a second — so this expires only when the database has stopped keeping up. An entry that never finds room is dropped and counted under easyp_audit_events_lost_total{reason="enqueue_timeout"}; the operation itself still succeeds.
config.audit.flushTimeoutstring"5s"Bound on one write to storage, retries included. A batch that misses it is lost rather than holding the writer up behind a database that has stopped answering.
config.audit.retentionMonthsinteger12Partitions older than this are dropped on a schedule — a real delete, so afterwards the only copy of that month is in a backup taken while it still existed. Backup retention has to exceed this rather than match it: matching means the month leaves the database and the archive at about the same time, leaving it nowhere.
config.audit.preCreateMonthsinteger3—
config.audit.partitionCheckIntervalstring"6h"—
config.audit.partitionOpTimeoutstring"30s"—
config.registry.pluginsDirstring"/plugins"—
config.registry.maxOutputSizeinteger67108864—
config.registry.cacheMaxBytesinteger21474836480Bound on the unpacked plugins on disk. Least recently used ones are dropped once this is exceeded; the archive in object storage is left alone, so an evicted plugin is one download away rather than lost. Keep it below persistence.size — eviction starts at the limit, and the volume needs room to reach it. 0 disables eviction. 20 GiB
config.registry.s3.endpointstring""—
config.registry.s3.bucketstring""—
config.registry.s3.regionstring"us-east-1"—
config.registry.s3.prefixstring""—
config.registry.s3.forcePathStylebooleanfalse—
config.telemetry.otlpEndpointstring""Empty means no collector, and the service then builds no exporter at all. These carry no fallback on purpose: the OTLP connection is lazy, so an endpoint nobody is listening on does not fail — it retries forever, and a pod with no observability stack would fill its log with connection errors while otherwise working.
config.telemetry.pyroscopeEndpointstring""—
config.telemetry.serviceTierstring""Tags traces and profiles with the licence tier this release serves. Worth setting only where community and enterprise run side by side and their signals land in the same backends; on a cluster running one tier it would distinguish nothing. Metrics carry the same distinction already, so use the same word here that the metric label uses.
config.license.publicKeysmap{"2026-08":"81322461987167d5cfd529e9cb8b96f4797f12fce6be4399a0866e250c9b6bb5"}Public halves of the keys licence tokens are signed with, keyed by the key id that appears in the token footer. The token names one; the rest are here so that a signing key can be rotated without every deployment having to change key on the same day. These are the trust anchor: whoever sets them decides which authority may issue licences for this installation. The default below is easyp.tech's own published key, so a customer holding a LICENSE_KEY needs nothing else. Replace it only if you issue your own licences. Source of truth is keys/ in the licence registry (easyp-tech/licenses); these must be kept in step with it. Without a key, LICENSE_KEY is ignored and the service runs in community mode.
  • config.registry.s3: Leaving bucket empty disables S3 and makes pluginsDir the only source of plugin binaries.

extraEnv

Extra environment variables, appended last. Use for settings the chart does not model yet.

KeyTypeDefaultDescription
extraEnvlist[]—

secrets

Secrets The pod loads them with envFrom, so the KEYS OF THE SECRET ARE THE ENVIRONMENT VARIABLE NAMES. A secret you bring yourself must use exactly these keys: DB_POSTGRES_DSN required REGISTRY_S3_ACCESS_KEY_ID required when config.registry.s3.bucket is set REGISTRY_S3_SECRET_ACCESS_KEY required when config.registry.s3.bucket is set LICENSE_KEY optional; absent means community mode The single-key LICENSE_PUBLIC_KEY was removed in v0.13.0. The trust anchor is config.license.publicKeys alone: an entry per key id, or one under "*" to verify any key id. Without a public key, LICENSE_KEY counts for nothing. AUTH_WRITE_TOKENS optional; absent means no writes are allowed AUTH_WRITE_TOKENS holds sha256 digests, never the tokens: "ci=<64 hex>,me=<64 hex>". Generate a token with easyp-svc auth new-token --name ci.

KeyTypeDefaultDescription
secrets.existingSecretstring""Bring your own secret (recommended). Values put in data below end up in Helm release history and in helm get values, which is why create defaults to false.
secrets.createbooleanfalse—
secrets.datamap{}—

tls

Transport security When enabled, the gRPC listener serves TLS. Adding clientCASecret turns it into mutual TLS: only callers holding a certificate signed by that CA get in, which is what keeps the listener private when the ingress is the only party meant to reach it.

KeyTypeDefaultDescription
tls.enabledbooleantrue—
tls.serverSecretstring""Secret of type kubernetes.io/tls holding tls.crt and tls.key for the server.
tls.clientCASecretstring""Secret holding ca.crt used to verify client certificates. Empty falls back to ca.crt inside serverSecret, which is what cert-manager writes for a CA issuer. Set to "-" to serve plain server-side TLS with no client check.
tls.mountPathstring"/certs"—

certManager

Optional: let cert-manager issue the certificates instead of supplying them.

KeyTypeDefaultDescription
certManager.enabledbooleanfalse—
certManager.issuerRef.namestring""—
certManager.issuerRef.kindstring"ClusterIssuer"—
certManager.issuerRef.groupstring"cert-manager.io"—
certManager.durationstring"2160h"—
certManager.renewBeforestring"360h"—
certManager.extraDnsNameslist[]Extra SANs for the server certificate. The in-cluster service name is always included.
certManager.clientCertificate.enabledbooleantrue—
certManager.clientCertificate.commonNamestring"easyp-ingress"—
  • certManager.clientCertificate: Issues the client certificate the ingress presents to the service.

ingress

Ingress. Defaults target Traefik because it is the only controller that can express a mutual-TLS backend leg declaratively, via ServersTransport.

KeyTypeDefaultDescription
ingress.enabledbooleanfalse—
ingress.classNamestring"traefik"—
ingress.hoststring""—
ingress.annotationsmap{}—
ingress.tlsSecretstring""Secret holding the certificate served to external clients.
ingress.serversTransport.enabledbooleantrue—
ingress.serversTransport.clientSecretstring""Secret of type kubernetes.io/tls with the client certificate the router presents to the service.
ingress.serversTransport.serverNamestring""Name the router expects on the service certificate.
ingress.serversTransport.insecureSkipVerifybooleanfalse—
  • ingress.serversTransport: Traefik ServersTransport describing how the router reaches the pod. Required whenever tls.enabled is true, otherwise the backend leg would be plaintext against a listener that demands a client certificate.

persistence

Storage for the plugin cache. Plugin archives are downloaded lazily and unpacked here, and the directory is pruned: once it passes config.registry.cacheMaxBytes the least recently used plugin versions are removed. So size this for the working set you want resident, not for every plugin that could ever be requested. Eviction only ever removes local files. The archive stays in object storage, so an evicted plugin costs one download on its next request rather than being lost — which is why the cache can be sized to taste. Whichever of the two sizes below applies has to exceed cacheMaxBytes, and the chart refuses to install when it does not: eviction starts at the limit, so storage sized exactly to it is already full by the time the cache first needs headroom.

KeyTypeDefaultDescription
persistence.enabledbooleantrue—
persistence.existingClaimstring""—
persistence.storageClassstring""—
persistence.accessModestring"ReadWriteOnce"—
persistence.sizestring"25Gi"—
persistence.ephemeralSizeLimitstring"25Gi"Used when enabled=false, where the cache lives in an emptyDir on the node instead of a volume of its own. Without a limit here the cache grows into the node's ephemeral storage, and disk pressure there is resolved by the kubelet evicting pods — not necessarily this one. With it, an overrun stops at the pod that caused it. Remember this is per replica: with persistence off, every pod keeps its own copy, so plan for replicaCount × this much on the nodes.

serviceMonitor

KeyTypeDefaultDescription
serviceMonitor.enabledbooleanfalse—
serviceMonitor.intervalstring"30s"—
serviceMonitor.scrapeTimeoutstring"10s"—
serviceMonitor.labelsmap{}—

prometheusRule

Alerting rules. Needs the Prometheus Operator CRDs, same as serviceMonitor, which is why this defaults to off rather than failing the install where they are absent. Turn it on wherever you actually run this. The rules worth having on day one are the licence ones: a licence that lapses breaks nothing loudly. The tier drops to community, audit stops, and the plugin limit starts rejecting registrations — all silently.

KeyTypeDefaultDescription
prometheusRule.enabledbooleanfalse—
prometheusRule.labelsmap{}Extra labels, usually what your Prometheus selects rules by.
prometheusRule.runbookBaseUrlstring"https://easyp.tech/docs/api-service/runbooks"Where each alert's runbook_url points. Every alert gets one, anchored on its own name. Point this at your own copy if you keep runbooks internally — an alert that arrives at 3am with no procedure attached is where most of the time goes.
prometheusRule.licenceExpiryWarningDaysinteger14How long before expiry to start warning. The token also carries its own grace period, which runs after this; both exist so a renewal that slips does not take a customer's pipeline down.
prometheusRule.cacheUsageWarningRationumber0.95Fraction of config.registry.cacheMaxBytes that counts as full.
prometheusRule.generationErrorRationumber0.05Fraction of generations allowed to fail before alerting.
prometheusRule.authFailureRatenumber0.5Rejected write credentials per second before alerting.

podDisruptionBudget

KeyTypeDefaultDescription
podDisruptionBudget.enabledbooleanfalse—
podDisruptionBudget.minAvailableinteger1—

networkPolicy

On by default. This pod's whole job is running third-party binaries, so the set of places it can reach should be stated rather than inherited. If your database, object storage or collector is not on one of the ports below, add it here or the pod goes quiet in a way that looks like a hang. That is the one thing to check first after enabling this.

KeyTypeDefaultDescription
networkPolicy.enabledbooleantrue—
networkPolicy.ingressNamespaceSelectormap{}Selector matching the namespace your ingress controller runs in. Empty means the gRPC port accepts connections from anywhere in the cluster — set it once you know which namespace your controller runs in.
networkPolicy.egressPortslist[5432,443,4317]Ports the pod is allowed to reach outbound, beyond DNS.

On this page