# Settings (.env)

Every setting GoRunner reads from `.env`, with its default.

The setup writes the ones you need. Change the others only when a page tells you to.

To change your answers (role, address, mail, stores, license), run `./setup.sh` again. For anything else, edit `.env`, then apply it with the version you run: `./deploy.sh "$(cat VERSION)"`.

`.env`

```
PUBLIC_URL=https://automations.example.com
ENCRYPTION_KEY=…   # made by the setup: keep a copy
GORUNNER_ROLE=all
```

**Write .env by hand?**

1. Copy `.env.example` to `.env` and run `chmod 600 .env`.
2. Set `PUBLIC_URL`, `ENCRYPTION_KEY` (`openssl rand -hex 32`) and `GORUNNER_ROLE`, and the lines for your way in, mail and stores.
3. Set `COMPOSE_PROFILES`: the role, each built-in store, and `caddy` only with automatic HTTPS, such as `all,postgres,redis,storage,caddy`. Give each built-in store its passwords (`POSTGRES_PASSWORD`, `REDIS_PASSWORD`, `S3_ACCESS_KEY`, `S3_SECRET_KEY`) with `openssl rand -hex 24`.
4. Run `docker compose run --rm check`, then `docker compose up -d`, then `docker compose exec api gorunner setup-code`.

| Name | Default | What it does |
| --- | --- | --- |
| **Required** |   |   |
| `PUBLIC_URL` | Set by the setup | The full address people open, such as `https://automations.example.com`, with the port when it is not 443. The links in emails and invitations are built from it. Set by the setup. |
| `ENCRYPTION_KEY` | Set by the setup | Opens every password, API key and connection GoRunner stores: 64 hexadecimal characters (`openssl rand -hex 32`). **Danger** Lose it, or change it, and nothing GoRunner stored can be read again. GoRunner refuses to start with any other key. Keep a copy of `.env` somewhere safe. The setup makes it once and never shows or replaces it. GoRunner records a check value of it. Every server of one installation has the same one. |
| `GORUNNER_ROLE` | `all` | `all`: everything on this server. `api` or `worker`: one of [several servers](https://docs.gorunner.io/self-host/several-servers). Default `all`. |
| **Generated by the setup: keep safe, never change** |   |   |
| `POSTGRES_PASSWORD` | Set by the setup | The built-in database's password. |
| `REDIS_PASSWORD` | Set by the setup | The built-in queue's password. |
| `S3_ACCESS_KEY` | Set by the setup | The keys of the built-in object storage (S3). |
| `S3_SECRET_KEY` | Set by the setup | The keys of the built-in object storage (S3). |
| **Your address and the way in** |   |   |
| `GORUNNER_PROXY_MODE` | — | How people reach GoRunner. `caddy`: automatic HTTPS on this server. `own`: [your own proxy](https://docs.gorunner.io/self-host/own-proxy) points straight at GoRunner, and no Caddy runs. A worker server has none. Set by the setup; change it by running `./setup.sh` again, which also changes `COMPOSE_PROFILES` to match. |
| `GORUNNER_APP_BIND` | — | Only with your own proxy: where it reaches GoRunner, `[IP:]PORT`. `127.0.0.1:8080` for a proxy on this server; this server's private address, such as `10.0.1.5:8080`, for one elsewhere. Never a public address. It stays open through every update; changing it closes it for a few seconds while GoRunner moves to the new one. |
| `APP_TRUSTED_PROXIES` | — | Who may tell GoRunner the caller's address. With automatic HTTPS, leave it unset: GoRunner then trusts Caddy alone. With your own proxy, the setup writes it: `gateway` for a proxy on this server (`--app-port` on `127.0.0.1`), plus any proxies in front of it; for a proxy on another machine, that proxy's own address or range. Never `private_ranges`, which would trust every container on the server. See [Which to choose](https://docs.gorunner.io/self-host/own-proxy#the-address-ranges-to-trust). |
| `GORUNNER_DOMAIN` | — | The name people open, such as `automations.example.com`. Set by the setup. |
| `GORUNNER_TLS` | `acme` | `acme` (the default): a free certificate from Let's Encrypt. `internal`: a certificate Caddy signs itself, for local testing only. |
| `GORUNNER_HSTS` | Off | on: every page tells browsers to use https for this address for a year (Strict-Transport-Security: max-age=31536000, this name only, not its subdomains). The setup writes on when it gets a certificate from Let’s Encrypt. |
| `PUBLIC_HTTP_PORT` | `80` | The server ports Caddy listens on. Default `80` and `443`. Only change them if something else must use those ports. |
| `PUBLIC_HTTPS_PORT` | `443` | The server ports Caddy listens on. Default `80` and `443`. Only change them if something else must use those ports. |
| `GORUNNER_TRUSTED_PROXIES` | — | Only when a proxy of yours stands in front of Caddy: that proxy's own address or range. Empty trusts no proxy. For a proxy on this server, with Caddy's ports on `127.0.0.1`, the setup takes `gateway`. The setup refuses `private_ranges` and any IPv4 or IPv6 range that overlaps one of Docker's networks on this server, because a container could then name any caller. |
| `GORUNNER_EDGE_NET` | — | The private networks GoRunner keeps for Caddy, as three numbers such as `172.29.253`: one joins Caddy to the app, the other carries Caddy's published ports. The setup picks a range that nothing on this server uses. Change it only if you need that range for something else, then run `./deploy.sh "$(cat VERSION)"`. |
| **Mail** |   |   |
| `RESEND_API_KEY` | — | A Resend API key. Used when set. |
| `SMTP_HOST` | — | Any SMTP server, used when there is no Resend key. Port `587` by default, or `465`. Leave the user and password out for a server that needs no sign-in. |
| `SMTP_PORT` | `587` | Any SMTP server, used when there is no Resend key. Port `587` by default, or `465`. Leave the user and password out for a server that needs no sign-in. |
| `SMTP_USER` | — | Any SMTP server, used when there is no Resend key. Port `587` by default, or `465`. Leave the user and password out for a server that needs no sign-in. |
| `SMTP_PASSWORD` | — | Any SMTP server, used when there is no Resend key. Port `587` by default, or `465`. Leave the user and password out for a server that needs no sign-in. |
| `SMTP_SECURE` | — | How the connection is encrypted. `starttls`: the server must offer STARTTLS, usually on port 587. `tls`: TLS from the first byte, usually on port 465. `none`: no encryption, refused with a user name on a server that is not on this machine. Unset (the default): TLS from the first byte on port 465, otherwise STARTTLS whenever the server offers it. |
| `MAIL_FROM` | — | The sender, such as `GoRunner <no-reply@example.com>`. Your mail service must be allowed to send for its domain. |
| **Sign-in** |   |   |
| `GORUNNER_ADMIN_EMAIL` | — | Optional. Set it, and only this address can create the first admin account, even with the setup code. It does nothing once the installation has its admin. |
| `AUTH_ENABLED` | — | `true`. Leave it: GoRunner refuses to start without sign-in on a server. |
| `ALLOW_SIGNUP` | — | `false`: nobody can sign up on their own, and people join by invitation. **Warning** Set to `true`, anyone who can reach the address can create an account. Leave it `false` unless you mean that. Leave it unless you really want anyone who can reach the address to create an account. |
| `ADMIN_EMAIL_REQUIRED` | — | `true`: with `GORUNNER_ADMIN_EMAIL` empty, nobody may create the first admin. For a deployment that writes the address later, from a secret store. |
| `PLATFORM_ADMIN_SECRET` | — | Not set by default, which keeps it closed. **Warning** It opens the operator console at `/admin`. Keep it like the encryption key, and remove it again when you are done. Your own root secret for the operator console at `/admin`: with it, someone with access to the server can name a platform admin when nobody can, or load a license by hand. Make one with `openssl rand -hex 32`, keep it like the encryption key, and remove it again when you are done. To turn off a platform admin's lost two-factor, use `gorunner reset-two-factor` instead ([First sign-in](https://docs.gorunner.io/self-host/first-sign-in#when-the-only-admin-loses-two-factor)). |
| **License** |   |   |
| `LICENSE_KEY` | — | Not needed: load the license in the dashboard instead, or paste it when the setup asks. |
| **Database** |   |   |
| `GORUNNER_POSTGRES` | — | `bundled`: the built-in database on this server. `external`: your own. Never leave it empty once written: GoRunner then refuses to start, rather than start an empty database. Your own database must be Postgres 15 or later. |
| `POSTGRES_HOST` | — | Your own database, as separate fields. Port `5432` and database `gorunner` by default. The host is a name or an IP address alone, without its port. |
| `POSTGRES_PORT` | — | Your own database, as separate fields. Port `5432` and database `gorunner` by default. The host is a name or an IP address alone, without its port. |
| `POSTGRES_DB` | — | Your own database, as separate fields. Port `5432` and database `gorunner` by default. The host is a name or an IP address alone, without its port. |
| `POSTGRES_USER` | — | Your own database, as separate fields. Port `5432` and database `gorunner` by default. The host is a name or an IP address alone, without its port. |
| `POSTGRES_PASSWORD` | — | Your own database, as separate fields. Port `5432` and database `gorunner` by default. The host is a name or an IP address alone, without its port. |
| `POSTGRES_SSLMODE` | — | `disable`, `require`, `verify-ca` or `verify-full`. Empty: `require`, or `verify-full` when `POSTGRES_CA_FILE` is set. |
| `POSTGRES_CA_FILE` | — | The certificate authority to check the database's certificate with, as a file in `certs/`: `/certs/rds.pem`. |
| `DATABASE_URL` | — | Your own database as one address instead, such as `postgresql://user:password@host:5432/gorunner?sslmode=require`. It wins over the separate fields. |
| `POSTGRES_MAX_CONNECTIONS` | `500` | The built-in database's connection limit. The setup writes `500` on a new install; one from before keeps `200` until this is raised, which restarts the database once. |
| **Redis** |   |   |
| `GORUNNER_REDIS` | — | `bundled` or `external`, as for the database. Your own Redis must be 6.2 or later, set never to delete keys (`maxmemory-policy noeviction`). |
| `REDIS_HOST` | — | Your own Redis, as separate fields. Port `6379` and database `0` by default. |
| `REDIS_PORT` | — | Your own Redis, as separate fields. Port `6379` and database `0` by default. |
| `REDIS_DB` | — | Your own Redis, as separate fields. Port `6379` and database `0` by default. |
| `REDIS_USER` | — | Your own Redis, as separate fields. Port `6379` and database `0` by default. |
| `REDIS_PASSWORD` | — | Your own Redis, as separate fields. Port `6379` and database `0` by default. |
| `REDIS_TLS` | — | `true` for an encrypted connection, as most managed Redis services need. |
| `REDIS_URL` | — | Your own Redis as one address instead, such as `redis://user:password@host:6379/0` (`rediss://` for TLS). It wins over the separate fields. |
| **File storage** |   |   |
| `GORUNNER_STORAGE` | — | `bundled`: the built-in object storage (S3). `external`: your own S3 bucket. `none`: step data stays in the database. Turning it to `none` is refused while step data still lives in storage. |
| `S3_ENDPOINT` | — | Your bucket: the store's address (such as `https://s3.eu-central-1.amazonaws.com`), the bucket (`gorunner`) and its region (`us-east-1`). |
| `S3_BUCKET` | `gorunner` | Your bucket: the store's address (such as `https://s3.eu-central-1.amazonaws.com`), the bucket (`gorunner`) and its region (`us-east-1`). |
| `S3_REGION` | `us-east-1` | Your bucket: the store's address (such as `https://s3.eu-central-1.amazonaws.com`), the bucket (`gorunner`) and its region (`us-east-1`). |
| `S3_FORCE_PATH_STYLE` | — | `true` for MinIO and SeaweedFS, `false` for AWS S3. |
| `STORAGE_INLINE_THRESHOLD_KB` | `0` | Step data larger than this many KB goes to storage; smaller stays in the database. `0` sends all of it. The setup writes `4` for your own bucket. |
| `S3_OP_TIMEOUT_SECONDS` | `15` | How long one storage call may take (15 seconds), and how often it is tried (4). |
| `S3_MAX_ATTEMPTS` | `4` | How long one storage call may take (15 seconds), and how often it is tried (4). |
| `AWS_REQUEST_CHECKSUM_CALCULATION` | — | `when_required`, for an S3-compatible store that refuses the checksums newer AWS clients add. |
| `AWS_RESPONSE_CHECKSUM_VALIDATION` | — | `when_required`, for an S3-compatible store that refuses the checksums newer AWS clients add. |
| **Workers and capacity** |   |   |
| `WORKER_REPLICAS` | `1` | How many worker containers run on this server. More workers is the safer way to grow than one large one. |
| `WORKER_CONCURRENCY` | `4` | How many runs one worker runs at once. |
| `WORKER_INTERACTIVE_CONCURRENCY` | `2` | How many **Test step** and connection tests from the editor one worker serves at once. `0`: none on this server. |
| `KEEL_CONCURRENCY` | `2` | How many Keel conversations one worker serves at once. `0` turns the assistant off on that worker. |
| `NOTIFY_CONCURRENCY` | `4` | How many notifications to your server one worker sends at once. `0`: none. |
| `WORKER_MEMORY_LIMIT` | `2g` | The memory one worker may use. Raise it together with `WORKER_CONCURRENCY`, never one alone. |
| `WORKER_STOP_GRACE` | `300s` | How long a worker may finish its runs during an update before it is stopped. |
| `API_MEMORY_LIMIT` | `1g` | The memory each of the app's two copies may use. The app always runs as two copies on one address, so an update or a crash of one is never seen. |
| `DB_MAX_CONNS` | `50` | The most database connections each GoRunner process keeps. The setup and the update size it from the check; empty means 50. |
| `DB_ADMIN_HEADROOM` | `5` | How many database connections the check keeps free beside GoRunner's own, for `pg_dump` and an administrator's session. At least 5. The pool size it recommends leaves them; one that uses them is a warning, never a refusal. On Kubernetes, `postgresql.adminHeadroom`. |
| `DB_MIN_CONNS` | `5` | The fewest each process keeps open. |
| `GOMEMLIMIT` | None | A soft memory goal for every GoRunner process, below the memory limits above, such as `900MiB`. |
| `TZ` | `UTC` | The time zone of the processes' logs, and of the time a password lockout ends. Each workflow's schedule has its own. |
| **Where workflow steps may connect** |   |   |
| `WORKFLOW_PRIVATE_NETWORKS` | — | Empty (the default): no private address. `all`: every private range. Or the ranges and addresses you trust, separated by commas: `10.0.4.0/24,192.168.1.20`. It never opens the cloud metadata service. A value the worker cannot read stops it at start, and the check names it. On Kubernetes it is `workflows.privateNetworks`. |
| **Company proxy** |   |   |
| `HTTPS_PROXY` | — | The proxy for everything GoRunner calls outside, such as `http://user:password@proxy.example.com:3128`. |
| `HTTP_PROXY` | — | The proxy for everything GoRunner calls outside, such as `http://user:password@proxy.example.com:3128`. |
| `NO_PROXY` | — | Names reached directly, separated by commas. GoRunner adds its own. |
| `EGRESS_PROXY_CONNECT` | — | `name` (the default) or `ip`: what the proxy is asked to connect to, for the addresses other people chose. |
| **Data** |   |   |
| `RUN_RETENTION_DAYS` | `90` | Runs that finished longer ago than this are deleted every hour, with their data. Each workflow keeps its newest finished run in each environment, however old. Runs still waiting or running are never deleted. |
| `AUDIT_RETENTION_DAYS` | `0` | Audit log rows older than this many days are deleted every hour. `0` keeps them forever. |
| `AUDIT_CREDENTIAL_USE_RETENTION_DAYS` | `0` | Credential use GoRunner itself recorded (a run, a schedule or a poll using a connection) older than this many days is deleted every hour. Each connection keeps its latest use, and the rest of the log stays. Empty or `0` keeps everything. |
| `EVENT_STREAMING_ENABLED` | `true` | `false` stops every organization's event streaming on this installation. |
| `MAX_STEP_OUTPUT_BYTES` | `16 MB` | The largest input or output one step may hold. |
| **Limits** |   |   |
| `API_RATE_LIMIT` | `1200` | Requests a minute to the API for one caller: an API token, a signed-in session or an embed session, else one address. Each app process counts on its own. One address may also send four times this, whatever token or session it uses. |
| `WEBHOOK_RATE_LIMIT` | `3000` | Requests a minute from one address to webhooks, and to a connection sign-in’s launch and return pages. |
| `OAUTH_RATE_LIMIT` | `120` | Requests a minute from one address to AI app sign-in: discovery, register, authorize and token. |
| `EMBED_API_RATE_LIMIT` | `3000` | Requests a minute for one embed API key, counted across every app process. |
| `EMBED_ACCOUNT_RATE_LIMIT` | `300` | Requests a minute for one embedded account, counted across every app process. |
| `EMBED_PERSON_RATE_LIMIT` | `20` | Login codes a minute for one person of one embedded account, so one page reloading in a loop stops at that person. |
| `EMBED_RENEW_RATE_LIMIT` | `4` | Renewals a minute for one embed session and the sessions renewed from it. |
| `EMBED_SESSION_MAX_HOURS` | `12` | How long the embed may keep renewing its own session after a login code, from 1 to 24 hours. Then it asks your server for a new code. |
| `API_CORS_ORIGINS` | None | Other websites allowed to call the API from a browser, separated by commas. The embed has its own list. |
| `RUNS_WAITING_PER_ORG` | `10000` | How many runs of one organization may wait in line for a free slot. Past it, new ones are refused with 429 `RUNS_WAITING_LIMIT`, “Too many runs are waiting for this organization. Try again later.” `0`: no cap. On Kubernetes, `workflows.waitingPerOrg`. |
| `RUNS_WAITING_PER_WORKFLOW` | `1000` | How many runs of one workflow may wait in line, so one flooded webhook cannot fill its organization's room. Past it, that workflow's new runs are refused with 429 `RUNS_WAITING_LIMIT`, “Too many runs of this workflow are waiting. Try again later.”, and its other doors are still open. Test runs from the editor count too. `0`: no cap of its own, the organization's still applies. On Kubernetes, `workflows.waitingPerWorkflow`. |
| `EMBED_OAUTH_REQUEST_RATE_LIMIT` | `600` | Requests a minute for one embed API key to the AI app sign-in requests (oauth-requests/*), counted across every app process, in their own window apart from EMBED_API_RATE_LIMIT. A Docker install passes it to the app from v2026.10.31; before, only the default applied. |
| `SCRIPT_MEMORY_LIMIT_MB` | `256` | Memory one Code step or {{ }} expression may use, in MB (256; 64 to 16384). |
| `API_INFLIGHT_MB` | `256` | Request bodies and whole payload reads one API holds at once, in MB (0: no limit). Past it, a request waits 2 seconds, then answers `503 BUSY` with `Retry-After`. |
| **Monitoring** |   |   |
| `LOG_LEVEL` | `info` | How much the log says: debug, info, warn or error. |
| `LOG_FORMAT` | `text` | text, or json for a log collector. |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | Not set | Your OpenTelemetry tool's address and key: metrics, traces and logs. |
| `OTEL_EXPORTER_OTLP_HEADERS` | Not set | Your OpenTelemetry tool's address and key: metrics, traces and logs. |
| `OTEL_SERVICE_NAME` | `gorunner-api, gorunner-worker` | The name GoRunner appears under in your tool. |
| `OTEL_TRACES_SAMPLER` | Every trace | Keep fewer traces on a busy installation. |
| `OTEL_TRACES_SAMPLER_ARG` | Every trace | Keep fewer traces on a busy installation. |
| `METRICS_ENABLED` | Off | A `/metrics` page for Prometheus, inside the Docker network, behind the token. |
| `METRICS_TOKEN` | Off | A `/metrics` page for Prometheus, inside the Docker network, behind the token. |
| `SENTRY_DSN` | Not set | Crashes and errors to your Sentry project. |
| `ACCESS_LOG` | Off | `true`: the app's line for each request also names the caller's address (as your proxy reported it, from a trusted proxy only) and its browser, and requests for unknown addresses are logged too. |
| `GORUNNER_ACCESS_LOG` | Off | With automatic HTTPS, `on`: Caddy writes one line for each request with the caller's address. Never a query string, a header, or more than the first 8 characters of a webhook's token. Docker Compose only: the Helm chart runs no Caddy. |
| `HEALTH_DETAILS` | Not set | `/api/health` and `/api/ready` say only `ok` or `unavailable` to anyone but the server itself. `public` shows the version and each check's result to everyone, as before v2026.10.4 ([Health checks](https://docs.gorunner.io/self-host/health-checks)). |
| `OTEL_EXPORTER_OTLP_PROTOCOL` | `http/protobuf` | http/protobuf (default) or grpc. |
| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | — | One signal elsewhere: a full address (used as given) and its own headers. |
| `OTEL_EXPORTER_OTLP_TRACES_HEADERS` | — | One signal elsewhere: a full address (used as given) and its own headers. |
| `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` | — | One signal elsewhere: a full address (used as given) and its own headers. |
| `OTEL_EXPORTER_OTLP_METRICS_HEADERS` | — | One signal elsewhere: a full address (used as given) and its own headers. |
| `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` | — | One signal elsewhere: a full address (used as given) and its own headers. |
| `OTEL_EXPORTER_OTLP_LOGS_HEADERS` | — | One signal elsewhere: a full address (used as given) and its own headers. |
| `OTEL_TRACES_EXPORTER` | `otlp` | none turns one signal off (default otlp). |
| `OTEL_METRICS_EXPORTER` | `otlp` | none turns one signal off (default otlp). |
| `OTEL_LOGS_EXPORTER` | `otlp` | none turns one signal off (default otlp). |
| `OTEL_RESOURCE_ATTRIBUTES` | — | Extra resource attributes, key=value,key=value, such as team=platform. |
| `OTEL_METRIC_EXPORT_INTERVAL` | `60000` | How often metrics are pushed, in milliseconds. |
| `OTEL_SDK_DISABLED` | `false` | true: OpenTelemetry never starts, whatever else is set. |
| `TELEMETRY_DB_SPANS` | `false` | Database and Redis calls as spans (they multiply trace volume), and a traceparent header on the HTTP calls steps make. |
| `TELEMETRY_PROPAGATE_OUTBOUND` | `false` | Database and Redis calls as spans (they multiply trace volume), and a traceparent header on the HTTP calls steps make. |
| `METRICS_ADDR` | `:9464` | A /metrics page for Prometheus on every API and worker container (port 9464 on this network, not published). |
| `METRICS_WORKFLOW_LABELS` | `false` | Add workflow_id to the run and step metrics (one series per workflow: costly). |
| `SENTRY_ENVIRONMENT` | `production` | Crashes and errors (the server's, and the dashboard's relayed through it) to Sentry. |
| `CLIENT_ERRORS_ENABLED` | `true` | false: drop the dashboard's browser error reports instead of logging them. |
| **Connections** |   |   |
| `OAUTH_MANAGED_GOOGLE_CLIENT_ID` | — | Not needed: set up the Google sign-in app under **Platform › Settings › OAuth apps** instead, which takes its place. |
| `OAUTH_MANAGED_GOOGLE_CLIENT_SECRET` | — | Not needed: set up the Google sign-in app under **Platform › Settings › OAuth apps** instead, which takes its place. |
| `OUTBOUND_ALLOW_PRIVATE` | — | Off. Notification addresses and your own OAuth apps' token addresses must be https on the public internet. `true` also allows plain http and addresses on this server and your private network. It does nothing for workflow steps: they follow `WORKFLOW_PRIVATE_NETWORKS` ([Outbound access](https://docs.gorunner.io/self-host/outbound-access)). |
| `KEEL_COMPILE` | — | Empty. `off`: the assistant builds without its compile step. Leave it empty unless the GoRunner team asks. |
| **Version and project** |   |   |
| `GORUNNER_TAG` | — | The installed version. `./deploy.sh` keeps it current. |
| `GORUNNER_REGISTRY` | `ghcr.io/ralve-org` | Where the images come from, such as `registry.example.com/ralve-org`. Set only when not the default, `ghcr.io/ralve-org`. |
| `COMPOSE_PROFILES` | — | What runs on this server: the role, one name for each built-in store (`postgres`, `redis`, `storage`), and `caddy` with automatic HTTPS. The setup and the update write it, so that `docker compose ps`, `logs` and `up` cover GoRunner. |
| `GORUNNER_GATE_TAG` | — | Written by the setup and the update (the relay's tag by the update only) for two small containers that must never restart during an update: the one that holds the app's address open, and the one in front of the object storage. Each keeps the version it was created with. |
| `GORUNNER_GATE_SYSCTL` | `net.ipv4.tcp_migrate_req=1` | The kernel setting of the container that holds the app’s address open. The update writes it. |
| `GORUNNER_RELAY_TAG` | — | Written by the setup and the update (the relay's tag by the update only) for two small containers that must never restart during an update: the one that holds the app's address open, and the one in front of the object storage. Each keeps the version it was created with. |
| `COMPOSE_PROJECT_NAME` | — | The prefix of the container and volume names. Set only when not `gorunner`. |
| **Other** |   |   |
| `WORKER_RUN_GATE` | — | off: a worker takes runs while it has slots, whatever its memory and sandbox say. |
| `GORUNNER_JSBOX_SEAL` | — | The sandbox around Code steps and expressions. Empty: its seal is applied where the kernel allows it. `off`: no seal. `required`: a script that can’t be sealed doesn’t start. |
| `GORUNNER_JSBOX_SHIP_ALL` | — | 1: hand every script every earlier step, instead of only the ones it reads. |
| `SYNC_WAITS_MAX` | `4096` | Synchronous callers (webhooks, forms, ?wait=) one API waits for at once, one organization's share of them (default an eighth), and the MB of their answers it builds at once. |
| `SYNC_WAITS_PER_ORG` | `512` | Synchronous callers (webhooks, forms, ?wait=) one API waits for at once, one organization's share of them (default an eighth), and the MB of their answers it builds at once. |
| `SYNC_ANSWER_MB` | `128` | Synchronous callers (webhooks, forms, ?wait=) one API waits for at once, one organization's share of them (default an eighth), and the MB of their answers it builds at once. |
| `AUDIT_IP_RETENTION_DAYS` | `0` | The client IP address on audit rows older than this many days is removed hourly; the rows stay. |
| `OAUTH_PUBLIC_CORS` | `on` | AI apps that sign in with OAuth (ChatGPT, Claude, VS Code, Cursor…): their discovery documents, /oauth/register, /oauth/token and /oauth/revoke answer any website (CORS *, no cookies), so an app running in a browser can sign in. |
| `OAUTH_EXTRA_REDIRECT_URIS` | — | More app callbacks AI apps may register: whole URIs, comma-separated, no query, for an app whose callback GoRunner does not know (https, loopback, Cursor's and Raycast's need nothing here). |

## Setup flags

Every question of the setup is also a flag, so `./setup.sh` can run without questions (`-y`). On an existing installation, only what you pass changes. Give a secret as `-` to read it from standard input, so it never shows in the server’s process list. `./setup.sh --help` prints this list.

### This server

| Flag | Writes | What it does |
| --- | --- | --- |
| `--role all\|api\|worker` | `GORUNNER_ROLE` | all: GoRunner on this one server (the default). Several servers: api on the main one (the dashboard and the API), worker on the others (they run the workflows). Several servers need your own database, Redis and storage (or --storage none) |
| `--encryption-key KEY` | `ENCRYPTION_KEY` | the key every server of one installation shares (64 hex characters): required for role api and worker, the same on each (make it once: openssl rand -hex 32). Role all makes one. Never changed once .env has it. The installation records a check value of its key, and a server with any other key is refused by the check and does not start |

### The way in

| Flag | Writes | What it does |
| --- | --- | --- |
| `--domain NAME` | `GORUNNER_DOMAIN` | automatic HTTPS: the address people will open, e.g. automations.example.com |
| `--behind-proxy URL` |   | your own proxy: the https address it serves; GoRunner listens on --app-port for it |
| `--public-url URL` | `PUBLIC_URL` | a worker server: the address people open (the links in emails) |
| `--app-port [IP:]PORT` | `GORUNNER_APP_BIND` | your own proxy: where it reaches GoRunner (127.0.0.1:8080). Use a private address the proxy can reach when it runs on another machine |
| `--trusted-proxies RANGES` |   | the addresses your proxy connects from, as addresses or CIDRs. Your own proxy: with --app-port on 127.0.0.1 a proxy on this server is trusted already (the word gateway: Docker's published port delivers from there), so this adds only proxies in front of it; on another address it is required (your proxy's own address). Never private_ranges, which would trust every container on this server. With Caddy: the proxies in front of Caddy, if any. Written as APP_TRUSTED_PROXIES (your own proxy) or GORUNNER_TRUSTED_PROXIES (Caddy) |
| `--tls acme\|internal` | `GORUNNER_TLS` | acme: a Let's Encrypt certificate (default) internal: Caddy's own CA — for testing on this machine only |
| `--http-port [IP:]PORT` | `PUBLIC_HTTP_PORT` | Caddy's host port for HTTP (80) |
| `--https-port [IP:]PORT` | `PUBLIC_HTTPS_PORT` | Caddy's host port for HTTPS (443) |

### Mail

| Flag | Writes | What it does |
| --- | --- | --- |
| `--mail resend\|smtp\|none` |   | which one (--resend-key and --smtp-host imply it) |
| `--resend-key KEY` | `RESEND_API_KEY` | send mail through Resend… |
| `--smtp-host HOST` | `SMTP_HOST` | …or through any SMTP server |
| `--smtp-port PORT` | `SMTP_PORT` | 587 by default |
| `--smtp-secure MODE` | `SMTP_SECURE` | starttls, tls (port 465) or none; by default tls on 465, else starttls |
| `--smtp-user USER` | `SMTP_USER` |   |
| `--smtp-password PASS` | `SMTP_PASSWORD` |   |
| `--mail-from ADDRESS` | `MAIL_FROM` | the sender, on a domain your mail service may send for |

### The stores

| Flag | Writes | What it does |
| --- | --- | --- |
| `--postgres bundled\|external` | `GORUNNER_POSTGRES` |   |
| `--database-url URL` | `DATABASE_URL` | postgresql://user:password@host:5432/gorunner?sslmode=require |
| `--postgres-host HOST` | `POSTGRES_HOST` |   |
| `--postgres-port PORT` | `POSTGRES_PORT` | 5432 |
| `--postgres-db NAME` | `POSTGRES_DB` | gorunner |
| `--postgres-user USER` | `POSTGRES_USER` |   |
| `--postgres-password PASS` | `POSTGRES_PASSWORD` |   |
| `--postgres-sslmode MODE` | `POSTGRES_SSLMODE` | disable, require (default), verify-ca or verify-full |
| `--postgres-ca-file /certs/FILE` | `POSTGRES_CA_FILE` | a .pem file you put in./certs, for verify-ca/-full |
| `--redis bundled\|external` | `GORUNNER_REDIS` |   |
| `--redis-url URL` | `REDIS_URL` | redis://user:password@host:6379/0 (rediss:// for TLS) |
| `--redis-host HOST` | `REDIS_HOST` |   |
| `--redis-port PORT` | `REDIS_PORT` | 6379 |
| `--redis-db N` | `REDIS_DB` | 0 |
| `--redis-user USER` | `REDIS_USER` |   |
| `--redis-password PASS` | `REDIS_PASSWORD` |   |
| `--redis-tls true\|false` | `REDIS_TLS` |   |
| `--storage bundled\|external\|none` | `GORUNNER_STORAGE` | none: every step's data stays in the database |
| `--s3-endpoint URL` | `S3_ENDPOINT` | AWS: https://s3.\<region>.amazonaws.com |
| `--s3-bucket NAME` | `S3_BUCKET` | gorunner |
| `--s3-region REGION` | `S3_REGION` | us-east-1 |
| `--s3-access-key KEY` | `S3_ACCESS_KEY` | empty on AWS when the server has a role |
| `--s3-secret-key KEY` | `S3_SECRET_KEY` |   |
| `--s3-path-style true\|false` | `S3_FORCE_PATH_STYLE` | true for MinIO |
| `--confirm-store-change` |   | move an existing installation between a built-in store and your own. The other one does not hold this installation's data: move the data first |

### Also

| Flag | Writes | What it does |
| --- | --- | --- |
| `--license-key KEY` | `LICENSE_KEY` | a license you already have (or load it in the dashboard later) |
| `--admin-email ADDRESS` | `GORUNNER_ADMIN_EMAIL` | only this address can be the first admin (optional) |
| `--https-proxy URL` | `HTTPS_PROXY` | a company proxy for everything GoRunner calls outside (this shell’s own variables are not read). Extra certificates to trust go in./certs as .pem files |
| `--http-proxy URL` | `HTTP_PROXY` | a company proxy for everything GoRunner calls outside (this shell’s own variables are not read). Extra certificates to trust go in./certs as .pem files |
| `--no-proxy LIST` | `NO_PROXY` | a company proxy for everything GoRunner calls outside (this shell’s own variables are not read). Extra certificates to trust go in./certs as .pem files |
| `--proxy-connect name\|ip` | `EGRESS_PROXY_CONNECT` | what the proxy is asked to connect to for addresses workflows and partners chose: the site's name (default; proxies that allow sites by name need it) or the address GoRunner checked (no DNS rebinding at the proxy; such proxies refuse it) |
| `--registry PREFIX` | `GORUNNER_REGISTRY` | where the images come from |
| `--project NAME` | `COMPOSE_PROJECT_NAME` | compose project name (gorunner), for two installs on one server |
| `--skip-check` |   | start even when the check finds a problem (only when you are sure); never when it finds a risk to data |
| `--no-start` |   | write .env and stop |
| `-y, --non-interactive` |   | never ask: take the answers from flags, the environment and .env |

## Update flags

`./deploy.sh` takes these, and the same variables. The variables also work with a `deploy.sh` from v2026.10.0 to v2026.10.2, which doesn’t know the flags: `GORUNNER_SKIP_CHECK=1./deploy.sh v2026.10.30`. See [Update](https://docs.gorunner.io/self-host/update).

| Flag | Same as | What it does |
| --- | --- | --- |
| `--skip-check` | `GORUNNER_SKIP_CHECK=1` | Goes on when the new version’s check finds problems. Never past a risk to stored data (exit 4). |
| `--confirm-store-change` | `GORUNNER_CONFIRM_STORE_CHANGE=1` | Says you changed the database, Redis or storage settings in `.env` on purpose. |
| `--replace-gate` | `GORUNNER_REPLACE_GATE=1` | Moves the API’s gate to the new image too. The address closes for a few seconds, so pick a quiet moment. |

Related: [Install with Docker](https://docs.gorunner.io/self-host/install-docker) · [Helm values](https://docs.gorunner.io/self-host/helm-values) · [The preflight check](https://docs.gorunner.io/self-host/preflight-check) · [Your own database, Redis and storage](https://docs.gorunner.io/self-host/own-stores)

Source: https://docs.gorunner.io/self-host/settings
