Configuration reference
Every setting you can change on a Marshal install, with its default and when you would change it. Marshal installs as one Helm chart. You set it up in one of two ways:
- Terraform installer (EKS, GKE,
single-node k3s). You write
terraform.tfvars. The Terraform variables below are the settings each install target declares. - ArgoCD (GitOps path). You write chart values in the Application’s
valuesObject. Use the Chart values section and the chart value named next to each Terraform variable.
Any chart value also works on the Terraform path: put it in
values_overrides. This page covers the Marshal settings. Cluster, node
pool, network and database settings that belong to one cloud are on that cloud’s install page.
What you must set
Five settings have no default, on every target:
| Setting | What it is |
|---|---|
installation_jwt | Your license. |
deployment_id | Your deployment ID (dep_...). It is also the registry username. |
registry_pull_token | Your registry pull token (mrpt_...). |
chart_version | The Marshal version to install. |
ingress_host | The hostname people use to open Marshal, for example marshal.acme.com. |
The first three come with your onboarding. See
Onboarding credentials. Each cloud adds its
own: cluster_name and region on EKS, project_id and location on GKE.
Terraform variables
The Targets column shows which install targets declare the variable: EKS (aws-eks),
GKE (gcp-gke) and k3s (single-node-k3s). A variable a target does not declare is not
a tfvar there. Terraform warns about it and ignores the value. Set the chart value through
values_overrides instead.
Release and chart
| Variable | Default | Targets | What it does, and when to change it |
|---|---|---|---|
release_name | marshal | EKS, GKE, k3s | The Helm release name. Change it to run a second install (for example staging) on the same cluster, together with namespace. |
namespace | marshal | EKS, GKE, k3s | Namespace for the Marshal services. Spaces run in their own namespace, <release_name>-spaces. |
chart | oci://registry.marshal.codes/marshal-charts/marshal | EKS, GKE, k3s | Where the chart comes from. Keep the default. |
chart_version | none, required | EKS, GKE, k3s | The product version. Each upgrade changes this one value. See Day-2 operations. |
Credentials
| Variable | Default | Targets | What it does, and when to change it |
|---|---|---|---|
installation_jwt | none, required | EKS, GKE, k3s | Your license. The installer stores it in the marshal-license Secret. Change it when you renew your license. |
deployment_id | none, required | EKS, GKE, k3s | Your deployment ID. Used as the registry username. |
registry_pull_token | none, required | EKS, GKE, k3s | Read-only registry password. One token pulls the chart, the product images and the tool images that spaces use. |
Images and registry
All images resolve under one registry prefix. Point it at your image cache for production. See Registry options.
| Variable | Default | Targets | What it does, and when to change it |
|---|---|---|---|
image_cache_registry | empty | EKS, k3s | Your own pull-through cache, for example harbor.acme.internal/marshal. When set, every image pull goes through it. Set it when you run one cache for all your installs. Chart value: global.registry. |
registry | registry.marshal.codes/marshal | EKS, GKE, k3s | The image prefix when no cache is set. On GKE, set it to an Artifact Registry mirror you run yourself. Chart value: global.registry. |
tools_registry | empty | EKS, GKE | Prefix for the tool images that spaces pull. Empty means the registry prefix with /marshal swapped for /marshal-tools. Set it when your own mirror does not follow that naming. Chart value: toolsRegistryBase. |
registry_auth_mode | secret | EKS, GKE, k3s | How nodes authenticate to the registry. secret: a pull Secret built from your token, stamped on every pod. workloadIdentity: the node identity (GKE Workload Identity or EKS IRSA) pulls from a cloud cache. none: an open cache in your network. Chart value: registryAuth.mode. |
Each cloud target can also build the cache for you. On GKE, create_registry_cache (default
true) creates an Artifact Registry mirror and sets registry and registry_auth_mode for you.
On EKS, create_harbor_cache (default false) runs Harbor in the cluster as the cache. See
GKE and EKS.
Datastores
Postgres holds everything durable: spaces, organizations, credentials, manifests and the audit log. Redis holds only short-lived state that Marshal can rebuild. So the recommended setup is your own managed Postgres and the in-cluster Redis. See Datastore options.
| Variable | Default | Targets | What it does, and when to change it |
|---|---|---|---|
database_mode | external | EKS, GKE | external: Marshal uses the Postgres at database_url. bundled: an in-cluster Postgres for evaluation only. |
database_url | empty | EKS, GKE, k3s | Postgres 16 connection string, for example postgres://marshal:PW@HOST:5432/marshal?sslmode=require. Required with database_mode = "external". Stored in a Secret, never in Helm values. On k3s it is used when provision_datastores = false. On EKS, create_rds = true fills it for you. |
redis_mode | bundled | EKS, GKE | bundled: Redis runs in the cluster. external: Marshal uses the Redis at redis_url. Change it only if you already run managed Redis and want to use it. |
redis_url | empty | EKS, GKE, k3s | Redis 7 connection string, for example rediss://default:PW@HOST:6380. Required with redis_mode = "external". On k3s it is used when provision_datastores = false. |
On k3s, provision_datastores (default true) runs Postgres and Redis on the same VM.
Ingress & TLS
| Variable | Default | Targets | What it does, and when to change it |
|---|---|---|---|
ingress_host | none, required | EKS, GKE, k3s | The main hostname. The web app, the API and the gateway share it. Plans need it before DNS exists, so pick the name first and add the DNS record when the load balancer has an address. Chart value: ingress.host. |
extra_ingress_hosts | [] | EKS, GKE, k3s | More hostnames that reach the same install, for example a Tailscale MagicDNS name. Each one needs its own DNS record. Chart value: ingress.extraHosts. |
ingress_class_name | EKS alb, GKE gce, k3s traefik | EKS, GKE, k3s | The IngressClass. Change it when you run your own controller, such as ingress-nginx. On GKE it applies when internal_ingress = false. Chart value: ingress.className. |
ingress_annotations | {} | EKS, GKE, k3s | Extra annotations on the Ingress, for example a cert-manager issuer. Chart value: ingress.annotations. |
internal_ingress | true | EKS, GKE | true gives an internal load balancer, reachable only inside your VPC. Set false only to expose Marshal on the internet. |
ingress_mode | ingress | GKE | gateway serves Marshal through a Gateway API Gateway instead of the Ingress, on the same internal IP. Use it for a Google-managed certificate. See Google-managed certificate. Chart value: ingress.mode. |
create_managed_certificate | false | GKE | With ingress_mode = "gateway", creates a Google-managed certificate for ingress_host and *.<preview_domain>. Google issues and renews it. |
gateway_certificate_names | [] | GKE | With ingress_mode = "gateway", Certificate Manager certificates you already have, by name. Chart value: ingress.gateway.certManagerCerts. |
gateway_class_name | gke-l7-rilb | GKE | The GatewayClass for ingress_mode = "gateway". The default is the regional internal load balancer. Chart value: ingress.gateway.className. |
tls_mode | self_signed | EKS, GKE, k3s | Where the certificate comes from. self_signed: Terraform creates a placeholder certificate that covers every host, so HTTPS works at once with a browser warning. inline: you paste a real certificate in tls_cert_pem and tls_key_pem. secret: you manage a TLS Secret yourself (for example with cert-manager) and name it in tls_secret_name. none: HTTP only. Move from self_signed to inline or secret before people use the install. |
tls_cert_pem, tls_key_pem | empty | EKS, GKE, k3s | PEM certificate and key, for tls_mode = "inline". |
tls_secret_name | marshal-tls | EKS, GKE, k3s | Your TLS Secret, for tls_mode = "secret". |
ingress_tls | [] | EKS, GKE, k3s | Advanced. Raw TLS blocks for the Ingress. When set, it replaces what tls_mode would do. Use it only for host and Secret combinations tls_mode cannot express. Chart value: ingress.tls. |
preview_domain | empty | EKS, GKE, k3s | Base domain for port previews from spaces. Spaces get *.<preview_domain>. Empty turns previews off. Chart value: ingress.preview.domain. |
preview_scheme | https | EKS, GKE, k3s | Scheme of preview URLs. Use http only for a test without TLS. Chart value: ingress.preview.scheme. |
With preview_domain set, the certificate must cover *.<preview_domain> as well as
ingress_host. The self_signed mode covers it for you. For a certificate you bring, issue a
wildcard certificate. See Troubleshooting.
Integrations
| Variable | Default | Targets | What it does, and when to change it |
|---|---|---|---|
webhook_relay_enabled | false | EKS, GKE, k3s | true turns webhooks on for every organization whose admin has not set the Webhooks switch in Settings > Platform yet. Webhooks open Settings > Webhooks and Personal settings > Webhooks. External systems then deliver events through the relay, and your cluster needs no inbound access. Needs installation_jwt. Turn it on to start deputy missions from events. Chart value: webhookRelay.enabled. |
webhook_relay_url | empty, which means https://hooks.marshal.codes | EKS, GKE, k3s | The relay endpoint. Keep the default unless Marshal gives you another one. An admin can set an organization’s own Webhook Relay address in Settings > Platform. Chart value: webhookRelay.url. |
auth_broker_url | empty | EKS, GKE, k3s | Setting it turns the Slack and OAuth sign-in switch in Settings > Platform on for every organization whose admin has not set it yet, and makes this URL the Auth Broker address. The Auth Broker lets an org connect a Slack workspace without registering its own Slack app. With it empty, an admin can still turn the switch on, and Marshal uses https://auth.marshal.codes. Needs installation_jwt. Chart value: authBroker.url. |
An organization’s admin turns these features on or off in Settings > Platform at any time, with no restart, and a value saved there always wins over these variables. Both need outbound HTTPS to the host they point at. See Networking.
Object storage
| Variable | Default | Targets | What it does, and when to change it |
|---|---|---|---|
create_spaces_persistence_bucket | false | EKS, GKE | Creates a bucket for space storage and grants Marshal access with no keys (IRSA or Workload Identity). Then an admin selects it in Settings > Space Storage. |
create_audit_persistence_bucket | false | EKS, GKE | Creates a bucket for audit export, with the same keyless access. Then an admin selects it in Settings > Auditing. |
See Object storage for the second step.
values_overrides
| Variable | Default | Targets | What it does, and when to change it |
|---|---|---|---|
values_overrides | [] | EKS, GKE, k3s | Raw chart values, as a list of YAML strings. They are applied last, so they win over anything the installer sets. Use it for every chart value that has no variable on your target. |
values_overrides = [
yamlencode({
api = {
extraEnv = [{ name = "MARSHAL_REGISTRATION_INVITE_ONLY", value = "true" }]
}
license = { enforce = true }
})
]Chart values
These are chart values with no Terraform variable on any target. Set them in
values_overrides (Terraform) or valuesObject (ArgoCD).
Sign-in and license
| Chart value | Default | What it does, and when to change it |
|---|---|---|
auth.basic.enabled | true | Email and password sign-in to the web app. Turn it off only when you put your own SSO or identity proxy in front of Marshal. |
license.enforce | false | false: Marshal logs the license state and blocks nothing. true: new spaces are paused once the install is no longer licensed and the grace period has ended. |
license.jwksURL | https://keys.marshal.codes/.well-known/jwks.json | Where Marshal reads the public keys that verify your license. Change it only when Marshal tells you to. |
license.refreshURL | https://tokens.marshal.codes/v1/license | Where Marshal exchanges your license for a short-lived one. Set "" to turn the exchange off for a fully offline license. An install with no refresh address sends no usage report. |
license.telemetryURL | "" | Where the hourly usage report is sent. Empty uses your refresh address with the path /v1/telemetry. |
license.telemetryInterval | "" (every hour) | How often the install sends the usage report. |
Marketplace and toolchains
| Chart value | Default | What it does, and when to change it |
|---|---|---|
marketplace.upstream | https://marketplace.marshal.codes | The package catalog your install reads. |
marketplace.tokenExchangeURL | https://tokens.marshal.codes | Where your install exchanges its license for a marketplace token. |
marketplace.customerRepos | [] | Your own package sources, merged on top of the catalog. Each entry: { url, authSecret: { name, key } }. Package names must carry your namespace (acme/foo) and cannot replace official packages. |
toolchainMirror.baseUrl | https://assets.marshalstatic.com/toolchains | Fallback download location for toolchains. Spaces download from the toolchain’s own host first and use this mirror only when that fails. Each download is checked against a known hash. Point it at an internal host with the same layout, or set "" to turn it off. |
Price list
The Compute tab of the Costs page estimates your monthly cost from public list prices for Google Cloud and AWS. Your install downloads the latest price list once a day and keeps a copy built into every release. It needs no cloud credentials.
| Chart value | Default | What it does, and when to change it |
|---|---|---|
pricing.baseUrl | https://assets.marshalstatic.com/pricing/v1 | Where your install downloads the price list. Set "" for an air-gapped install: Marshal uses the price list built into the release, and admins can still enter their own prices on the Compute tab of Costs. Or point it at an internal host that holds the same file layout. |
Admins see the date of the price list in use on the Compute tab. See Costs.
Secrets management
Two app secrets live in one Kubernetes Secret: JWT_SECRET signs sign-in and API tokens, and
KMS_LOCAL_KEY encrypts every stored credential. You choose how the chart gets them.
secrets.provider | What it means | Use it for |
|---|---|---|
generated (default) | The chart creates both values on the first install and keeps them on every upgrade. | Terraform installs. Terraform runs helm upgrade, which reads the existing Secret back. |
existing | You create the Secret once and name it in secrets.existingSecret. The chart only reads it. | ArgoCD and any GitOps tool. They never run helm upgrade, so generated would create new values on every sync. |
externalSecrets | The External Secrets Operator fills the Secret from your cloud secret manager. Configure secrets.externalSecrets. | GitOps installs that already run External Secrets Operator. |
| Chart value | Default | What it does |
|---|---|---|
secrets.existingSecret | empty | With existing: the name of your Secret, with keys JWT_SECRET and KMS_LOCAL_KEY. |
secrets.externalSecrets | see the chart | With externalSecrets: the secretStoreRef and the remote keys for both values. |
kms.backend | local | The key that encrypts credentials comes from KMS_LOCAL_KEY. Keep the default. |
Back up the app secret. Never switch a running install back to generated, and never delete
the Secret. If KMS_LOCAL_KEY is lost, stored credentials cannot be decrypted. A new
JWT_SECRET signs everyone out and ends every API token. See
Troubleshooting.
Namespaces, registry details and capacity
| Chart value | Default | What it does, and when to change it |
|---|---|---|
spacesNamespace | empty, which means <release>-spaces | The namespace spaces run in. Set it to use a namespace you already govern, for example with your own NetworkPolicies. |
manageSpacesNamespace | true | Set false when spacesNamespace names a namespace you created yourself, so Helm neither creates nor deletes it. |
ingress.enabled | true | Set false only when your own gateway or load balancer routes to the Marshal Services. |
registryAuth.secret.server | Terraform derives it from the image registry | With registryAuth.mode: secret: the registry host the pull credential is for. Set it only when the credential belongs to a different host than the images. On ArgoCD, set it to your registry host. |
resources.*, autoscaling.* | see Sizing | Requests, limits and replica counts of the Marshal services. |
The MCP settings mcp.extraHosts, mcp.cimdEnabled and tailscaleIngress.* are explained on
Connect an AI agent (MCP).
API database pool and readiness
The API keeps serving under heavy load. Its readiness probe checks the database on a connection of
its own, outside the pool, so a busy pool that keeps completing queries stays in rotation. A pod
leaves rotation only when its pool is fully in use and has completed nothing for staleAfter. At
startup the API waits up to 60 seconds for Postgres and Redis to answer, so a fresh install comes
up without a restart.
| Chart value | Default | What it does, and when to change it |
|---|---|---|
api.db.maxConns | unset: the pool_max_conns in database_url, else 10 or the pod’s CPU count, whichever is larger | Database connections per API pod. Keep API replicas × maxConns under your database’s connection limit. |
api.readiness.path | /readyz | Set /healthz to keep pods ready while the database is unreachable. |
api.readiness.initialDelaySeconds | 5 | Seconds before the first probe. |
api.readiness.periodSeconds | 5 | Seconds between probes. |
api.readiness.failureThreshold | 6 | Failed probes in a row before a pod leaves rotation. With the defaults that is about 30 seconds, longer than a Cloud SQL failover. |
api.readiness.budget | 1s | Time limit on the probe’s database check. |
api.readiness.staleAfter | 30s | How long a fully used pool may complete nothing before the pod reports unready. 0 turns this check off, so readiness means only that the database is reachable. |
Two request limits are set through api.extraEnv. Each takes a duration such as 45s, and 0
turns the limit off. Streaming routes, such as a run’s live events, and a few long-running
actions, such as purging a deputy, are exempt. The configuration routes are limited like any
other.
| Variable | Default | What it does |
|---|---|---|
API_REQUEST_TIMEOUT | 60s | The longest an ordinary API request may take. |
API_AUTH_REQUEST_TIMEOUT | 10s | The limit for sign-in routes, so the sign-in page answers quickly. |
A request that reaches its limit answers 503 with {"error":"service unavailable"} and
Retry-After: 2. Routes behind a per-organization switch (runs and deputies, the Webhook Relay,
the Auth Broker, Slack) answer with the same 503, error and header when the API cannot read the
organization’s settings within 5 seconds, and add a message that says to try again. A slow
database never shows a feature as switched off. A switch that is off still answers 503 with feature_disabled. API clients
should treat service unavailable as retryable: earlier releases answered an unreadable switch
with feature_disabled and reason unavailable.
Upgrades without failed requests
When an API or web console pod stops, during an upgrade, a rollout or a node drain, it keeps
serving for a short drain window first. The API also reports itself not ready in that window,
so Kubernetes, your ingress controller and any load balancer that checks pods directly move
traffic to the other pods before this one closes. In-flight requests then finish and the pod
exits on its own.
With api.readiness.path set to /healthz, the API still drains but does not report itself
not ready.
| Chart value | Default | What it does, and when to change it |
|---|---|---|
api.shutdown.drainSeconds | 10 | Seconds an API pod keeps serving after it is told to stop. Raise it when a cloud load balancer in front of the cluster takes longer to stop sending traffic to a pod. 0 stops the pod at once. |
api.terminationGracePeriodSeconds | 60 | Time Kubernetes gives an API pod to stop. It must be at least drainSeconds plus 35, the time the API needs to finish requests and background jobs. The chart refuses a smaller value. |
web.shutdown.drainSeconds | 10 | Seconds a web console pod keeps serving after it is told to stop. 0 stops the pod at once. |
web.terminationGracePeriodSeconds | 30 | Time Kubernetes gives a web console pod to stop. It must be at least drainSeconds plus 5. The chart refuses a smaller value. |
A longer drain makes each upgrade a little slower: every stopping pod waits its drain window once.
Node tuning
Spaces watch many files, and the kernel’s inotify limits are shared by every container on a node.
The kernel default of 128 instances runs out after a handful of spaces, and the next space fails
to start with failed to create fsnotify watcher: too many open files.
These limits are node settings, so no pod can raise them for itself. The chart runs a small DaemonSet that raises them on every node, including nodes an autoscaler adds later. It only ever raises a value, so a node you tuned higher yourself keeps its value.
| Chart value | Default | What it does, and when to change it |
|---|---|---|
nodeTuner.enabled | true | Set false if your policy forbids privileged pods. Then raise the same limits in your node image or node configuration. |
nodeTuner.sysctls | fs.inotify.max_user_instances: "8192", fs.inotify.max_user_watches: "1048576" | Any node-level sysctl works, not only these two. |
nodeTuner.image | the gateway image | Reuses an image your cluster already pulls, so nothing new goes through your image cache. |
The write happens in a short-lived privileged init container. The container that stays running has no privileges, runs as uid 65534 with a read-only root filesystem, and requests 10m CPU.
Optional features
Some features are off until you turn them on. This table lists each one, where it is switched on, and the default.
The first three are switches each organization’s owners and admins control themselves, under Settings > Platform. A change applies right away, with no restart. Webhooks and the Slack connection need the installation JWT, because they connect to Marshal’s hosted services.
| Feature | How to turn it on | Default |
|---|---|---|
| Runs and deputies, with their schedules and triggers (one switch) | Deputies and runs in Settings > Platform. | On |
| Webhooks | Webhooks in Settings > Platform. Terraform webhook_relay_enabled = true or chart webhookRelay.enabled: true turns it on for every organization that has not set the switch. | Off |
| Slack connection | Slack and OAuth sign-in in Settings > Platform. Terraform auth_broker_url or chart authBroker.url turns it on for every organization that has not set the switch. Turn on Webhooks as well, so people can answer with the buttons in Slack. | Off |
| Space storage (stop, resume, fork and archive) | An admin picks a backend in Settings > Space Storage. | Disabled |
| Audit export | An admin picks a backend in Settings > Auditing. | Database only. Events are stored in the database, with no archive files in your own bucket. |
| MCP access for AI agents | Every install serves /mcp. An org admin turns it on in Organization settings > General, card MCP access. | Off in each org |
| Live resize of a running space | The cluster needs the Kubernetes feature gates InPlacePodLevelResourcesVerticalScaling and NodeDeclaredFeatures, on the API server and on every kubelet. Turn both on together. Size changes applied at the next restart work on any cluster. | Depends on your cluster |
| Password sign-in | auth.basic.enabled | On |
| Open registration (anyone can sign up) | auth.openRegistration: true | Off. People join by invitation. |
| Invite-only registration (blocks sign-up even when open registration is on) | Set MARSHAL_REGISTRATION_INVITE_ONLY to "true" on the API through api.extraEnv. | Off |
| More than one organization | auth.disableOrgCreation: false | Off. The first organization is not affected. |
Example of api.extraEnv in chart values:
api:
extraEnv:
- name: MARSHAL_REGISTRATION_INVITE_ONLY
value: "true"Notes:
- Runs and deputies. One switch controls both, for one organization. With it off, the
Deputies and Runs entries in the web app show as not enabled, the run and deputy API answers
503withfeature_disabled, andmarshal runfails. Other organizations on the same install keep their own setting. An install that setMARSHAL_DEPUTIES_ENABLEDon the API keeps that value as the starting point for every organization that has not set the switch. - First-run setup token. The chart stores it in the app secret under the key
SETUP_TOKEN. Read it withkubectl get secret <release>-app-secrets -n <namespace> -o jsonpath='{.data.SETUP_TOKEN}' | base64 -d. Withsecrets.provider: existing, use your own Secret’s name, and add aSETUP_TOKENkey to it, or first-run setup stays off.