> ## Documentation Index
> Fetch the complete documentation index at: https://nango.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Self-Managed

> You deploy, you operate.

With Self-Managed, you deploy Nango yourself and are responsible for operating it — upgrades, scaling, and on-call — on your own infrastructure, any cloud or on-prem. This gives you the most flexibility over infrastructure, at the cost of running it.

See [Self-Hosting overview](/docs/guides/platform/self-hosting) for the architecture, and [BYOC](/docs/guides/platform/self-hosting/byoc) if you'd rather have Nango deploy and operate it inside your cloud account for you.

## Quickstart

<CardGroup cols={3}>
  <Card title="Helm chart" icon="dharmachakra" href="https://github.com/NangoHQ/nango-helm-charts">
    Deploy to your Kubernetes cluster.
  </Card>

  <Card title="Managed image releases" icon="tags" href="https://github.com/NangoHQ/managed-image-releases">
    Versioned, pinned Docker images.
  </Card>

  <Card title="nangoctl" icon="flask" href="https://github.com/NangoHQ/nangoctl">
    Try Nango locally in a `kind` cluster first.
  </Card>
</CardGroup>

Custom deployments (e.g., ECS instead of Kubernetes) are possible with our guidance — reach out if the Helm chart isn't the right fit for your infrastructure.

## What leaves your account

**Nothing.** Self-hosted instances don't send any telemetry, logs, or data back to Nango automatically. You're the only one with visibility into your instance, unless you choose to export metrics to your own monitoring tool, or share something with us directly when asking for support.

## Where your data lives

Everything runs on your infrastructure, configured by you:

| Store | Holds |
| - | - |
| Postgres | Control-plane data, your end users' connection credentials, scheduled tasks, synced records |
| Object storage (S3, GCS, or Azure) | Compiled integration code for the Runner |
| ElasticSearch / OpenSearch | Execution logs |
| Redis / Valkey | Cache — socket info, token refresh locks, rate limits |

See [Data storage](/docs/guides/platform/self-hosting#data-storage) on the overview page for more, and [Security](/docs/guides/platform/security) for how credentials are encrypted at rest.

## Who's responsible for what

| Area | Nango | You |
| - | - | - |
| Managed image releases | Publishes them, on a two-week cadence plus hotfixes | Pull and apply them |
| Helm chart | Maintains it | Deploy and configure it |
| Infrastructure provisioning | — | Provision it — VPC, database, cache, cluster, on any cloud or on-prem |
| Application deployment & updates | — | Deploy and update it yourself |
| Scaling | Advises on request | Handle it |
| Incident response / on-call | — | Handle it |
| Support | Per your contract's SLA | Reach out via your support channel |

In short: budget for a platform team that can run Kubernetes in production. Nango ships the images and the chart; you run them.

## Encryption & credentials

Encryption is entirely in your hands. At minimum, set [`NANGO_ENCRYPTION_KEY`](#encryption-key) yourself; if you'd rather not manage it as a flat secret, you can wrap it with your own KMS instead.

The most sensitive thing Nango stores is your end users' connection credentials (OAuth tokens, API keys) for the integrations you connect — encrypted at rest once you've set a key, inside your own Postgres. See [Security](/docs/guides/platform/security) for the full model.

## Observability & support

Everything is visible to you, and only you — the dashboard, the audit trail, your logs, your metrics. Nango has no visibility into your instance unless you choose to share something with us.

Support follows your contract's SLA. Reach out through your support channel with what you're seeing (logs, dashboard state, error messages) — we can't look at your instance ourselves.

## Updates

You pull and apply updates yourself. Notifications about new releases are available by subscribing to the [`NangoHQ/managed-image-releases`](https://github.com/NangoHQ/managed-image-releases) repository:

* **GitHub:** watch the repository, select **Custom**, and enable **Releases**.
* **RSS/Atom:** subscribe to `https://github.com/NangoHQ/managed-image-releases/releases.atom`, which can be wired into Slack, Microsoft Teams, RSS readers, or internal automation.

Each release includes the managed image tag, application version, source commit hash, release date, Docker pull reference, a GitHub compare link to the previous managed release, and a generated changelog of changes since the previous managed release.

### Image tags

Managed image tags follow this format:

```
nangohq/nango:managed-{managed-release-version}-{application-version}-{commit-sha}
```

* `managed-release-version`: Semantic version for the managed image lifecycle (major = breaking, minor/patch = features/fixes)
* `application-version`: Semantic version of the Nango application baked into the image
* `commit-sha`: Full Git commit hash of the released source

For example, pull a release from Docker Hub with:

```bash theme={null}
docker pull nangohq/nango:managed-1.5.4-0.70.4-0fe07a6d83aea0becc4cea382c5cead2f555718d
```

Pin your CLI version to the `application-version` specified in the tag for compatibility.

### Versions and policy

You can find the latest version in [`managed-manifest.json`](https://github.com/NangoHQ/managed-image-releases/blob/main/managed-manifest.json) on `managed-image-releases` (mirrored from [`NangoHQ/nango`](https://github.com/NangoHQ/nango/blob/master/managed-manifest.json) on each release). The in-repo [`CHANGELOG.md`](https://github.com/NangoHQ/managed-image-releases/blob/main/CHANGELOG.md) tracks release history.

Each managed release maps to a specific source commit, and published image tags are never changed after release. Stay reasonably current with managed image releases. See the full [changelog](/docs/updates/changelog) for details on each release.

## Cloud provider support

Self-Managed supports all major cloud providers (AWS, GCP, Azure) and on-prem Kubernetes, since you're the one operating it.

## Configuration reference

Everything below is what you configure directly to run a production instance — env vars, secrets, and the hardening options available to you.

### Minimum environment variables

| Variable | Example | Purpose |
| - | - | - |
| `NODE_ENV` | `production` | Runtime mode |
| `AUTH_ALLOW_SIGNUP` | `false` | Disable open signup |
| `FLAG_AUTH_ROLES_ENABLED` | `true` | Enable role-based access |
| `SERVER_PORT` | `8080` | Port the server listens on |
| `DEFAULT_RATE_LIMIT_PER_MIN` | `200` | Default API rate limit |
| `NANGO_SERVER_URL` | `https://your-api-url` | Your API URL |
| `NANGO_PUBLIC_SERVER_URL` | `https://your-app-url` | Your app URL |
| `NANGO_PUBLIC_CONNECT_URL` | `https://your-connect-url` | Your Connect UI URL |
| `PERSIST_SERVICE_URL` | `http://nango-persist-svc.nango` | Internal URL of the persist service |
| `ORCHESTRATOR_SERVICE_URL` | `http://nango-orchestrator-svc.nango` | Internal URL of the orchestrator service |
| `RUNNER_SERVICE_URL` / `RUNNER_URL` | `http://nango-runner-svc.nango` | Internal URL of the runner service |
| `SERVER_SERVICE_URL` | `http://nango-server-svc.nango` | Internal URL of the server service |
| `RUNNER_NODE_ID` | `1` | Identifier for this runner node |
| `RUNNER_TYPE` | `REMOTE` | Runner runs as its own service |
| `NANGO_LOGS_ENABLED` | `true` | Enable the logs UI |
| `NANGO_LOGS_PROVIDER` | `opensearch` | `opensearch` or `elasticsearch` |
| `NANGO_LOGS_ES_URL` | `https://your-opensearch-url` | Your OpenSearch/Elasticsearch URL |
| `NANGO_LOGS_ES_PREFIX` | `cust` | Index prefix |
| `NANGO_LOGS_ES_INDEX_OPERATIONS` | `cust-operations` | Operations index name |
| `NANGO_LOGS_ES_INDEX_MESSAGES` | `cust-messages` | Messages index name |
| `NANGO_ADMIN_UUID` | `3fa85f64-5717-4562-b3fc-2c963f66afa6` | A new UUID identifying your admin account |
| `NANGO_CLOUD` | `false` | Marks this as a self-hosted instance |
| `NANGO_ENTERPRISE` | `true` | Enables Enterprise features |
| `NANGO_DB_SSL` | `true` | Require SSL to Postgres |

### Minimum secrets

| Secret | Example | Purpose |
| - | - | - |
| `NANGO_ENCRYPTION_KEY_WRAPPED` | `AQICAHjK...` (KMS-wrapped, base64) | Your data-encryption key, wrapped |
| `NANGO_KMS_KEY_ARN` | `arn:aws:kms:us-west-2:123456789012:key/abcd-1234-efgh-5678` | KMS key used to unwrap it (on GCP or Azure, see [Encryption key](#encryption-key)) |
| `NANGO_INTERNAL_API_KEY` | *(generated, 32+ char random string)* | Internal service-to-service auth |
| `NANGO_ADMIN_KEY` | *(generated, random string)* | Admin API access |
| `NANGO_ADMIN_UUID` | `3fa85f64-5717-4562-b3fc-2c963f66afa6` | Matches the env var above |
| `NANGO_DATABASE_URL` | `postgresql://nango:••••@db.internal:5432/nango` | Postgres connection string |
| `NANGO_REDIS_URL` | `rediss://:••••@redis.internal:6379` | Redis / Valkey connection string |
| `NANGO_ADMIN_INVITE_TOKEN` | *(generated, single-use token)* | Token for inviting the first admin |
| `AWS_INTEGRATIONS_BUCKET_NAME` | `my-company-nango-integrations` | S3 bucket for compiled integration files |
| `GCS_INTEGRATIONS_BUCKET_NAME` | `my-company-nango-integrations` | GCS bucket for compiled integration files |
| `AZURE_INTEGRATIONS_ACCOUNT_NAME` | `mycompanynango` | Azure storage account for compiled integration files |
| `AZURE_INTEGRATIONS_CONTAINER_NAME` | `integrations` | Azure blob container for compiled integration files |
| `AZURE_INTEGRATIONS_ACCOUNT_KEY` | `••••••••` | Optional Azure shared key. Omit to use `DefaultAzureCredential` |
| `MAILGUN_API_KEY` | `key-••••••••••••••••` | Transactional email |
| `NANGO_LOGS_ES_USER` | `nango_logs` | OpenSearch/Elasticsearch username |
| `NANGO_LOGS_ES_PWD` | `••••••••` | OpenSearch/Elasticsearch password |

<Note>
  Set one object store. On AWS, set `AWS_INTEGRATIONS_BUCKET_NAME`. On Google Cloud Storage, set `GCS_INTEGRATIONS_BUCKET_NAME`; Nango authenticates with Application Default Credentials, so the workload needs permission to read and write objects in that bucket. On Azure Blob Storage, set `AZURE_INTEGRATIONS_ACCOUNT_NAME` and `AZURE_INTEGRATIONS_CONTAINER_NAME` together. Set `AZURE_INTEGRATIONS_ACCOUNT_KEY` to authenticate with a shared key, or omit it to use `DefaultAzureCredential`. S3, GCS, and Azure cannot be configured together.
</Note>

<Tip>
  This is the *minimum* set to get a production instance running. The sections below cover what each optional subsystem (logs, encryption, audit trail, Redis, hardening) additionally needs.
</Tip>

### Encryption key

You must provide your own encryption key via the `NANGO_ENCRYPTION_KEY` environment variable to enable encryption at rest. It encrypts credentials in the control-plane database as well as data in the records cache. Without this key, credentials are stored unencrypted.

The records cache always requires this key. If you run sync functions that persist records, the persist and records services fail to store or retrieve records when `NANGO_ENCRYPTION_KEY` is not set—they do not fall back to plaintext.

```sh theme={null}
openssl rand -base64 32
```

<Note>
  The encryption key must be a base64-encoded 256-bit (32-byte) key. Key rotation is not supported yet—changing the key after initial setup will cause decryption failures. Plan your key management accordingly.
</Note>

If you'd rather not manage `NANGO_ENCRYPTION_KEY` as a flat secret, wrap it with your own KMS and set `NANGO_ENCRYPTION_KEY_WRAPPED` instead, plus exactly one wrapping-key variable:

| Provider | Variable | Example |
| - | - | - |
| AWS KMS | `NANGO_KMS_KEY_ARN` | `arn:aws:kms:us-west-2:123456789012:key/abcd-1234-efgh-5678` |
| GCP Cloud KMS | `NANGO_GCP_KMS_KEY_NAME` | `projects/my-project/locations/global/keyRings/nango/cryptoKeys/dek` |
| Azure Key Vault | `NANGO_AZURE_KMS_KEY_ID` | `https://my-vault.vault.azure.net/keys/dek/0123456789abcdef0123456789abcdef` |

Credentials come from the environment (IAM role, workload identity or managed identity), and Nango needs permission to unwrap with that key. On Azure, use an RSA key and include its version in the identifier.

<Warning>
  `NANGO_ENCRYPTION_KEY` is also required for Connect UI. It is used to generate sessions for [Connect UI](/docs/reference/frontend/frontend-sdk#connect-using-nango-connect-ui). Without this key, Connect UI will not work.
</Warning>

### Data retention

The default retention period for deleted connections is 31 days. You can configure this via the `CRON_DELETE_OLD_CONNECTIONS_MAX_DAYS` environment variable.

### Audit trail

The audit trail records control-plane activity — who created a connection, changed an integration, rotated an API key or signed in, from which IP, and whether it succeeded. Self-hosted instances keep it in Postgres, and teammates read it from **Audit trail** in the dashboard.

Enable it on the server:

```sh theme={null}
FLAG_AUDIT_TRAIL_ENABLED=true
NANGO_AUDIT_POSTGRES_DATABASE_URL=postgresql://user:password@host:port/dbname
NANGO_AUDIT_POSTGRES_SSL=true
```

Without a database there is nowhere to keep entries, so the trail stays off rather than showing a page that never fills. Set `NANGO_AUDIT_POSTGRES_SSL=true` when the database is external, as with the main one. The URL can point at the database Nango already uses or at a separate one, and the server creates the schema on startup along with the rest of it when `NANGO_MIGRATE_AT_START=true`.

Entries are kept for a year by default, and the server removes expired ones for you:

```sh theme={null}
NANGO_AUDIT_POSTGRES_RETENTION_DAYS=365
```

### Feature flags

Nango gates some behavior behind feature flags. Flags are served by the provider set in `NANGO_FLAG_PROVIDER`, which defaults to `noop`, so every flag uses its built-in default.

To serve flags from environment variables instead, set `NANGO_FLAG_PROVIDER=env`, then set one variable per flag, named after the flag key uppercased with dashes replaced by underscores and prefixed with `NANGO_FEATURE_FLAG_`:

```bash theme={null}
NANGO_FLAG_PROVIDER=env
NANGO_FEATURE_FLAG_PROXY_FORWARD_ALL_RESPONSE_HEADERS=true
```

This serves `true` for the `proxy-forward-all-response-headers` flag on every account on the instance. Flags with no variable set keep their built-in default. Boolean flags accept `true` or `false`. A value that doesn't match the flag's type is ignored, and the flag keeps its default.

The variables apply to the whole process, so there is no per-account targeting or gradual rollout. Set them on every service that evaluates the flag, and note that each service reads them at startup.

#### Two-factor authentication

Two-factor authentication is behind the `mfa` flag, which only the server evaluates. Set these on the server service to enable it:

```bash theme={null}
NANGO_FLAG_PROVIDER=env
NANGO_FEATURE_FLAG_MFA=true
```

Once the server restarts, users can turn it on from their profile settings by pairing an authenticator app and saving their backup codes. Nango then asks them for a code at login.

#### OAuth state cookie enforcement

When an OAuth flow starts, Nango sets a cookie in the user's browser that is tied to that flow. By default, a callback that arrives without this cookie is still accepted. To reject it instead, so the flow can only be completed by the browser that started it, set these on the server service:

```bash theme={null}
NANGO_FLAG_PROVIDER=env
NANGO_FEATURE_FLAG_OAUTH_STATE_COOKIE_ENFORCEMENT=true
```

With the flag on, a callback forwarded from your backend or sent by a `fetch` request without the state cookie fails with a `state_cookie_err` error. Make sure the callback reaches Nango as a redirect in the same browser that opened the authorization page.

The check covers OAuth 2 based flows, including MCP OAuth. It does not apply to App, OAuth 1, or plugin install flows.

### Server URL, callback URL, and custom domains

Add server environment variables for the instance URL and port in the `.env` file or directly in your hosting provider:

```sh theme={null}
NANGO_SERVER_URL=<INSTANCE-URL>
SERVER_PORT=<PORT>
```

The resulting OAuth callback URL is `<INSTANCE-URL>/oauth/callback`.

<Note>You can customize the callback URL by updating the "Callback URL" field in the "Environment Settings" tab in the Nango admin.</Note>

<Note>
  If you are using a custom domain, update `NANGO_SERVER_URL` to match it.
</Note>

If you front one Nango process with two ingresses (a public host for OAuth callbacks and webhooks, an internal host for the dashboard), set:

```sh theme={null}
NANGO_DASHBOARD_API_URL=/
```

`/` keeps dashboard API requests on whichever host served the page. Callback and webhook URLs still use `NANGO_SERVER_URL`. Set an absolute URL instead if the dashboard is hosted separately from the API.

### Management MCP server

The [Management MCP server](/docs/reference/backend/management-mcp) runs as part of the main Nango server. To enable it, configure a dedicated hostname and route traffic for that hostname to the Nango server:

```sh theme={null}
NANGO_MANAGEMENT_MCP_SERVER_URL=https://mcp.example.com
```

The hostname must be different from the hostname in `NANGO_SERVER_URL`. Nango uses the request hostname to distinguish Management MCP traffic from the public API. Restart the Nango server after setting the environment variable; the Management MCP endpoint is then available at `https://mcp.example.com/mcp`.

### Connect UI

Nango Connect is available for self-hosted deployments in the main Docker image.

```sh theme={null}
FLAG_SERVE_CONNECT_UI=true
NANGO_CONNECT_UI_PORT=3009
NANGO_PUBLIC_CONNECT_URL=<INSTANCE-URL>
```

Nango Connect is available by default at `http://localhost:3009`.

<Note>
  If you are using a custom domain, update `NANGO_PUBLIC_CONNECT_URL` to match it.
</Note>

#### Hosting under a base path

Connect UI works under any base path, for example behind a reverse proxy that routes services by path. Set `NANGO_PUBLIC_CONNECT_URL` to the full URL including the path:

```sh theme={null}
NANGO_PUBLIC_CONNECT_URL=https://example.com/nango/connect
```

Nango uses this URL when generating links to Connect UI and in the dashboard's security headers, so it must match what end users load in the browser.

Your reverse proxy must strip the base path prefix before forwarding to Connect UI: a request for `/nango/connect/assets/app.js` must reach Connect UI as `/assets/app.js`.

Connect UI is a single-page app: the server must respond with `index.html` for any path that isn't a built asset, including the base path without a trailing slash. Nango's static file server handles this out of the box; if you serve the built `dist` from your own static host, enable its SPA fallback.

See [Auth](/docs/guides/auth/auth-guide) for the end-user connection flow.

### Persistent storage

If you deploy with Docker Compose, the bundled database uses local container storage. This is not appropriate for production.

Connect Nango to an external Postgres database by setting the database environment variables:

```sh theme={null}
NANGO_DB_USER=<REPLACE>
NANGO_DB_PASSWORD=<REPLACE>
NANGO_DB_HOST=<REPLACE>
NANGO_DB_PORT=<REPLACE>
NANGO_DB_NAME=<REPLACE>
NANGO_DB_SSL=true
```

You can also use a database URL:

```sh theme={null}
NANGO_DATABASE_URL=postgresql://user:password@host:port/dbname
```

Special characters in `NANGO_DATABASE_URL` must be URL encoded.

<Note>
  Nango is incompatible with connection poolers using `pool_mode=transaction`. Use a direct database connection or configure the pooler to use a different mode.
</Note>

Records saved by sync functions can be persisted in a dedicated database. To use a dedicated records database, set `RECORDS_DATABASE_URL`:

```sh theme={null}
RECORDS_DATABASE_URL=postgresql://user:password@host:port/dbname
```

Special characters in `RECORDS_DATABASE_URL` must be URL encoded. If it is not specified, records are stored in the main database.

<Tip>
  Deploying with Render or Heroku automatically generates a persistent database connected to your Nango instance.
</Tip>

### External Redis

The bundled Redis is fine for local use but not for production. Connect Nango to an external Redis (or Valkey) with either a full URL or discrete variables:

```sh theme={null}
NANGO_REDIS_URL=rediss://:<password>@<host>:<port>
# or
NANGO_REDIS_HOST=<host>
NANGO_REDIS_PORT=<port>
NANGO_REDIS_AUTH=<password>
```

Use the `rediss://` scheme (or discrete variables, which default to TLS) to enable in-transit encryption.

#### IAM / short-lived token authentication

Managed Redis with IAM authentication (for example GCP Memorystore for Valkey) uses a short-lived token as the password and requires the token to be refreshed before it expires. Instead of a static `NANGO_REDIS_AUTH`, point Nango at a file that an external process (such as a sidecar) keeps up to date:

```sh theme={null}
NANGO_REDIS_HOST=<host>
NANGO_REDIS_PORT=<port>
NANGO_REDIS_AUTH_TOKEN_FILE=/path/to/token   # re-read on every (re)connect
NANGO_REDIS_USERNAME=<username>              # optional; defaults to "default"
```

Nango reads the token file on every connection and reconnection, so a rotated token is always picked up without a restart. This is cloud-agnostic: anything that writes the current token to the file works. The writer must update the file atomically — write a temporary file and `rename()` it into place — so a reconnect never reads a half-written token and fails authentication. When `NANGO_REDIS_AUTH_TOKEN_FILE` is set, do not embed credentials in `NANGO_REDIS_URL`.

The runner boundary has the same set of variables prefixed with `NANGO_CUSTOMER_REDIS_` (for example `NANGO_CUSTOMER_REDIS_AUTH_TOKEN_FILE`); it falls back to the system Redis when unset.

### Securing your instance

#### Proxy base URL override hardening

The [proxy](/docs/guides/platform/proxy-requests) can send authenticated requests to external APIs. Some proxy calls accept a **base URL override** (HTTP header `Base-Url-Override`, SDK `baseUrlOverride`, or an integration `custom.baseUrl`) to target a host that differs from the provider's default API base URL.

Because the proxy makes outbound HTTP requests from your Nango server (and from the runner for sync and action scripts), a caller with permission to use the proxy could use an override to reach hosts that were not meant to be exposed—such as cloud metadata services or `localhost` on the host making the request. This is a classic **SSRF** risk.

By default, Nango keeps base URL override **enabled** and blocks override targets and redirect hops whose hostnames match a built-in denylist (cloud metadata and loopback addresses). Configure these environment variables and restart the **server** and **runners** after changes:

```sh theme={null}
NANGO_PROXY_BASE_URL_OVERRIDE_ENABLED=true
NANGO_PROXY_BASE_URL_OVERRIDE_DENYLIST='["169.254.169.254","metadata.google.internal","localhost","127.0.0.1","[::1]"]'
```

| Variable | Default | Purpose |
| - | - | - |
| `NANGO_PROXY_BASE_URL_OVERRIDE_ENABLED` | `true` | Set to `false` to reject all base URL overrides |
| `NANGO_PROXY_BASE_URL_OVERRIDE_DENYLIST` | Secure defaults when unset | JSON array of hostnames or URLs to block; custom entries are merged with defaults |
| `NANGO_OUTBOUND_URL_POLICY` | Secure defaults when unset | JSON object controlling SSRF protection for proxy, customer webhooks, and `uncontrolledFetch` |
| `NANGO_OUTBOUND_URL_POLICY_OAUTH` | Inherits the base policy; RFC1918 allowed | JSON object overlay controlling SSRF protection for OAuth/token flows (token, refresh, STS, JWT-bearer endpoints) |

**Operator guidance:**

* **Production:** keep the default denylist. Add environment-specific hosts if your deployment exposes additional internal endpoints.
* **Legitimate localhost overrides (dev only):** set `NANGO_PROXY_BASE_URL_OVERRIDE_DENYLIST='[]'` to restore fail-open behavior.
* **No overrides at all:** set `NANGO_PROXY_BASE_URL_OVERRIDE_ENABLED=false`.

Denylist matching is hostname-based and applies to redirect hops as well as the initial override target.

#### Outbound URL policy (DNS rebinding, private IPs, redirects)

Beyond the hostname denylist, Nango routes every outbound connection on the **proxy**, **customer webhook delivery**, and **`uncontrolledFetch`** paths through DNS-pinning agents that re-validate the *resolved* IP address — closing DNS-rebinding and redirect-to-internal-address SSRF holes. The behavior is controlled by `NANGO_OUTBOUND_URL_POLICY`, a JSON object (applied on top of the denylist). It propagates automatically to runners.

The `mode` field selects how hostnames are filtered:

* **`denylist`** (default): every destination is reachable except hostnames on the denylist (plus the IP-based protections below). This is the out-of-the-box mode, since the denylist always carries the secure defaults.
* **`allowlist`**: only hostnames in `allowlist` are reachable (a leading `.` matches subdomains); everything else is rejected. The denylist and the IP-based protections still apply on top, so allowlist is strictly *more* restrictive — a listed hostname that resolves to a blocked IP is still rejected.
* **`permissive`**: no hostname-based filtering (the IP-based protections below still apply). You only reach this mode deliberately — either by emptying the server denylist with no explicit mode (see below), or by setting `mode: "permissive"`.

<Note>
  The IP-based protections apply in **every** mode, including `permissive`. Loopback (`127.0.0.0/8`, `::1`) and unspecified addresses are **always** blocked; private/RFC1918/CGNAT addresses (`blockPrivateIps`) and link-local addresses — including the cloud-metadata IP `169.254.169.254` (`blockLinkLocal`) — are blocked by default.

  The denylist is **never empty by default** — it is always seeded with the secure defaults (`localhost`, `metadata.google.internal`, `169.254.169.254`, …). It only becomes empty if you explicitly opt out: either `NANGO_PROXY_BASE_URL_OVERRIDE_DENYLIST='[]'` (server only, which then selects `permissive`) or `mode: "permissive"`. Even then, only hostname string matches such as `localhost` are dropped — literal or resolved internal IPs stay blocked unless you also set `blockPrivateIps`/`blockLinkLocal` to `false`. Runners re-apply the secure denylist defaults even when the server has emptied its denylist; note an explicit `mode: "permissive"` empties the denylist everywhere, runners included.
</Note>

```sh theme={null}
# Block private/link-local IPs (default) and cap redirects at 3
NANGO_OUTBOUND_URL_POLICY='{"blockPrivateIps":true,"blockLinkLocal":true,"maxRedirects":3}'

# Allowlist mode: only the listed hostnames (and their subdomains) are reachable
NANGO_OUTBOUND_URL_POLICY='{"mode":"allowlist","allowlist":[".hubspot.com","api.github.com"]}'
```

| Field | Default | Purpose |
| - | - | - |
| `mode` | `denylist` (auto-`permissive` only if you empty the denylist) | `denylist`, `allowlist`, or `permissive` (see above) |
| `allowlist` | `[]` | In `allowlist` mode, hostnames (a leading `.` matches subdomains) that may be reached |
| `blockPrivateIps` | `true` | Block RFC1918 / CGNAT addresses (all modes) |
| `blockLinkLocal` | `true` | Block link-local addresses incl. `169.254.169.254` (all modes) |
| `maxRedirects` | `5` | Maximum redirect hops followed |

Each resolved address (including on every redirect hop) is validated; in `allowlist` mode, only listed hostnames are reachable regardless of IP.

The same DNS-pinning protection also covers OAuth/token flows (token exchange, refresh, AWS STS, and JWT-bearer token endpoints). These use a separate overlay, `NANGO_OUTBOUND_URL_POLICY_OAUTH`, applied on top of `NANGO_OUTBOUND_URL_POLICY` but with the same JSON shape. The only difference is the default: `blockPrivateIps` is `false` for OAuth so self-hosted integrations whose token endpoints live on a private network keep working. Loopback and unspecified addresses stay blocked regardless. Link-local/metadata addresses (including the cloud-metadata IP `169.254.169.254`) stay blocked by default through `blockLinkLocal`, but that check is disabled if you set `blockLinkLocal:false`. Lock OAuth down to public hosts only by setting `blockPrivateIps`:

```sh theme={null}
NANGO_OUTBOUND_URL_POLICY_OAUTH='{"blockPrivateIps":true}'
```

#### Securing the dashboard

By default, the dashboard of your Nango instance is open to anyone who can access your instance URL.

You can secure it with Basic Auth by setting the following environment variables and restarting the server:

```bash theme={null}
FLAG_AUTH_ENABLED=false
NANGO_DASHBOARD_USERNAME=<PICK-A-USERNAME>
NANGO_DASHBOARD_PASSWORD=<PICK-A-PASSWORD>
```

#### Internal mTLS

If you terminate mutual TLS between Nango services at a load balancer or service mesh, Nango can present a client certificate on its service-to-service calls (server to orchestrator, jobs to orchestrator, jobs to runner, runner to jobs, runner to persist, runner to server).

Nango does not terminate TLS itself. Enforcement stays with your load balancer; these variables only control the certificate Nango sends as a client.

Provide each asset inline, as raw PEM or base64-encoded PEM:

```sh theme={null}
NANGO_INTERNAL_TLS_CERT=<CLIENT-CERTIFICATE-CHAIN-PEM>
NANGO_INTERNAL_TLS_KEY=<CLIENT-PRIVATE-KEY-PEM>
NANGO_INTERNAL_TLS_CA=<INTERNAL-CA-PEM>
```

Or point at mounted files, which is usually easier with Kubernetes secrets:

```sh theme={null}
NANGO_INTERNAL_TLS_CERT_FILE=/etc/nango/tls/tls.crt
NANGO_INTERNAL_TLS_KEY_FILE=/etc/nango/tls/tls.key
NANGO_INTERNAL_TLS_CA_FILE=/etc/nango/tls/ca.crt
```

| Variable | Purpose |
| - | - |
| `NANGO_INTERNAL_TLS_CERT` / `_FILE` | Client certificate chain Nango presents |
| `NANGO_INTERNAL_TLS_KEY` / `_FILE` | Private key for that certificate |
| `NANGO_INTERNAL_TLS_CA` / `_FILE` | CA used to verify the services Nango calls |
| `NANGO_INTERNAL_TLS_KEY_PASSPHRASE` | Passphrase, if the private key is encrypted |
| `NANGO_RUNNER_URL_SCHEME` | `http` (default) or `https`, for runner URLs built by the Kubernetes and Render providers |

**Operator guidance:**

* Set the variables on **every** service. Each one is both a client and a server in this topology.
* Runners started at runtime by the jobs service inherit the configuration automatically, as resolved PEM, regardless of which form you used on jobs itself. On Kubernetes the assets go into a per-runner `Secret` named `<runner>-internal-tls`, created and deleted alongside the runner's deployment, so the pod references the key instead of carrying it in its spec. This needs `secrets` permissions in the jobs role, which the Helm chart grants by default.
* Point the internal service URLs at your TLS listeners: `ORCHESTRATOR_SERVICE_URL`, `JOBS_SERVICE_URL`, `PERSIST_SERVICE_URL`, `SERVER_SERVICE_URL`, `PROVIDERS_URL`, and `RUNNER_SERVICE_URL` all need `https://` values. Set `NANGO_RUNNER_URL_SCHEME=https` so runner URLs generated at runtime match.
* Setting a cert without a key (or the reverse), setting both the inline and `_FILE` form of the same asset, pointing `_FILE` at an unreadable path, or supplying a wrong passphrase or a mismatched cert/key pair all fail at startup rather than silently falling back to plain TLS.
* `NANGO_INTERNAL_TLS_KEY_PASSPHRASE` is used exactly as given, including any leading or trailing whitespace, since that whitespace may be part of the passphrase. Surrounding whitespace *is* stripped from the other variables, where it is never significant.
* Prefer ECDSA P-256 over RSA-2048. The per-handshake signature is roughly an order of magnitude cheaper, which matters on the jobs-to-runner path.
* Certificates are read once at startup, including `_FILE` paths. Rotation requires a restart.

<Note>
  `NANGO_INTERNAL_TLS_CA` replaces the default root store for internal calls. One of those calls fetches `PROVIDERS_URL`—if that points at an endpoint with a publicly issued certificate, leave the CA unset and use `NODE_EXTRA_CA_CERTS` instead, which adds to the default roots rather than replacing them.
</Note>

#### CLI access through public mTLS

Internal mTLS only covers service-to-service calls. If the load balancer in front of the **public API** also requires a client certificate, the Nango CLI needs its own cert or `nango deploy`, `dryrun`, and `pull` will fail.

Set these in the `nango-integrations/.env` file (or the environment that runs the CLI):

```sh theme={null}
NANGO_CLI_TLS_CERT=/path/to/client.pem
```

A PEM that contains both the certificate and private key is enough. Use a separate key or CA when the files are split:

```sh theme={null}
NANGO_CLI_TLS_CERT=/path/to/client.crt
NANGO_CLI_TLS_KEY=/path/to/client.key
NANGO_CLI_TLS_CA=/path/to/ca.pem
NANGO_CLI_TLS_KEY_PASSPHRASE=
```

`NANGO_CLI_TLS_CA` is only needed when the public API uses a private CA. Set `NANGO_CLI_TLS_KEY_PASSPHRASE` if the private key is encrypted. These variables are independent of `NANGO_INTERNAL_TLS_*`.

#### Internal service auth

Orchestrator, jobs, and runner HTTP APIs accept an `Authorization: Bearer` credential except `GET /health`, which stays open for probes. Enforcement is off until you set `NANGO_INTERNAL_AUTH_REQUIRED=true`. Shipping the image with no new variables is a no-op.

New tokens are EdDSA JWTs with the same shape on every hop. The `kid` header selects the verifying key. Service tokens are minted by the caller (`iss` and `sub` are `service:jobs` or `service:server`) and last 5 minutes. Jobs mints `sub: task:<id>` or `node:<id>` for two hops: jobs → runner uses `aud: runner` and lasts 5 minutes; the runner carries `aud: jobs` back, where a task token lasts 25 hours by default, or the task lifetime, and a node token lasts a year. Runners never receive a private key.

Until you set a private key, callers keep minting the previous credentials, and every receiver still accepts them:

* **Static bearer** (`NANGO_INTERNAL_AUTH_TOKEN`): server and jobs calling orchestrator.
* **Task and node HMAC JWTs** (`NANGO_INTERNAL_AUTH_SIGNING_KEY`): runner calls back to jobs. Task tokens last 25 hours by default, or the task lifetime. Node tokens last a year.
* **Derived Ed25519 dispatch JWTs**: jobs calls the runner. The private key is derived from the HMAC signing key and never leaves jobs.

Set the new keys in this order so a rolling deploy keeps accepting in-flight tokens:

1. Deploy server, jobs, and orchestrator. Runners pick up the new image the next time jobs starts them.
2. Set the public keys, and do not set private keys yet. Set `NANGO_INTERNAL_AUTH_JOBS_PUBLIC_KEYS` on orchestrator and jobs, and `NANGO_INTERNAL_AUTH_SERVER_PUBLIC_KEYS` on the server and orchestrator. Jobs copies the jobs public keys onto new runners and still injects the legacy derived public key.
3. Recycle runners so every live runner can verify both shapes.
4. Set the server private key, then the jobs private key. Jobs then mints service and workload JWTs. In-flight HMAC task tokens (up to 25 hours) and node tokens on runners that have not been recycled still verify.

To rotate a key, append the new entry to the public-key JSON array and deploy the verifiers before you switch the minter's key id and private key.

```yaml theme={null}
NANGO_INTERNAL_AUTH_JOBS_PUBLIC_KEYS: |
  [
    {
      "kid": "jobs-2026-09",
      "publicKey": "<base64url of the raw 32-byte key>"
    },
    {
      "kid": "jobs-2026-10",
      "publicKey": "<base64url of the next key>"
    }
  ]
```

| Variable | Where | Purpose |
| - | - | - |
| `NANGO_INTERNAL_AUTH_TOKEN` | server, jobs, orchestrator | Legacy static Bearer. Used until that service's private key is set. Never the runner service. |
| `NANGO_INTERNAL_AUTH_SIGNING_KEY` | jobs only. Never set on runners. | Legacy HMAC key and seed for the derived runner-dispatch keypair. |
| `NANGO_INTERNAL_AUTH_RUNNER_PUBLIC_KEY` | runners (injected by jobs at node start) | Legacy Ed25519 public key. Verify-only. |
| `NANGO_INTERNAL_AUTH_JOBS_KEY_ID` | jobs | `kid` for the jobs private key, for example `jobs-2026-09`. |
| `NANGO_INTERNAL_AUTH_JOBS_PRIVATE_KEY` | jobs only. Never set on runners. | Unencrypted Ed25519 PKCS8 PEM. Mints service and workload tokens. |
| `NANGO_INTERNAL_AUTH_JOBS_PUBLIC_KEYS` | orchestrator, jobs, runners | JSON array of `{"kid","publicKey"}`. `publicKey` is the base64url raw 32-byte Ed25519 key. Must include the jobs key id before the private key is set. |
| `NANGO_INTERNAL_AUTH_SERVER_KEY_ID` | server | `kid` for the server private key, for example `server-2026-09`. |
| `NANGO_INTERNAL_AUTH_SERVER_PRIVATE_KEY` | server only | Unencrypted Ed25519 PKCS8 PEM. Mints tokens for orchestrator. |
| `NANGO_INTERNAL_AUTH_SERVER_PUBLIC_KEYS` | server, orchestrator | Same JSON array. Must include the server key id on the server process before the private key is set. |
| `NANGO_INTERNAL_AUTH_REQUIRED` | orchestrator, jobs, runners | Fail closed. Default `false`. Flip independently on orchestrator, then jobs. Runners inherit `REQUIRED` at node start. |

#### Custom websockets path

The Nango server serves websockets from `/` by default for use by `@nangohq/frontend` during the login flow.

To isolate websockets from the dashboard, set `NANGO_SERVER_WEBSOCKETS_PATH`:

```sh theme={null}
NANGO_SERVER_WEBSOCKETS_PATH=</YOUR-WEBSOCKETS-PATH>
```

If you set a custom path, configure `websocketsPath` when initializing the `Nango` object in the `@nangohq/frontend` SDK:

```js theme={null}
import Nango from '@nangohq/frontend';

let nango = new Nango({ host: 'https://<YOUR-NANGO-INSTANCE>', websocketsPath: '</YOUR-WEBSOCKETS-PATH>' });
```

Connect UI discovers the custom path on its own (from the connect session), so `websocketsPath` only needs to be configured for direct `nango.auth()` calls. If Nango is served behind a reverse proxy under a sub-path, direct `nango.auth()` needs the full public path in `websocketsPath` (e.g. `/<API-BASE-PATH></YOUR-WEBSOCKETS-PATH>`) because the SDK resolves it from the origin, while Connect UI composes the public path automatically from the `apiURL` base path and `NANGO_SERVER_WEBSOCKETS_PATH`.

### Proxy timeouts

| Variable | Default | Purpose |
| - | - | - |
| `NANGO_PROXY_IDLE_TIMEOUT_MS` | `300000` (5 minutes) | A proxy request fails when the provider takes longer than this to start responding, or stops sending data for this long partway through a response. A response that keeps sending data is never cut off. |
| `NANGO_PROXY_MAX_RETRY_WAIT_MS` | `600000` (10 minutes) | Longest wait between proxy retries, including waits requested by a provider's retry headers. A retry that would wait longer fails instead. |

Both apply to the API and to runners, and are passed to Kubernetes and Lambda runners automatically.

### Telemetry

Self-hosted instances do not automatically send telemetry back to Nango. Operational metrics and logs stay within your own infrastructure and are only exported if you configure the OpenTelemetry Export add-on.

### Logs

Nango stores execution logs and powers the logs UI with either Elasticsearch or OpenSearch. To keep lightweight deployments lighter, this stack is optional.

To enable logs:

* Host an Elasticsearch or OpenSearch cluster.
* Set `NANGO_LOGS_ENABLED=true`.
* Configure the relevant `NANGO_LOGS_ES_*` environment variables (these apply to both providers).

Nango uses Elasticsearch by default. To use OpenSearch instead, set:

```sh theme={null}
NANGO_LOGS_PROVIDER=opensearch
```

Elasticsearch hosting options include:

* Local: uncomment the service in [`docker-compose.yaml`](https://github.com/NangoHQ/nango/blob/master/docker-compose.yaml#L63) and run `docker-compose up`.
* Elastic Cloud: use [elastic.co](https://www.elastic.co/).
* Render: deploy an Elasticsearch instance with [Render](https://docs.render.com/deploy-elasticsearch).

If `NANGO_LOGS_ENABLED` is `false`, logs are sent to stdout and can be viewed in your host logs.

## FAQ

<AccordionGroup>
  <Accordion title="What support do we get?" defaultOpen="true">
    Support per your contract's SLA, plus the managed image releases and Helm chart maintained by Nango. Nango doesn't have visibility into your instance, so support relies on what you share with us — logs, dashboard state, error messages.
  </Accordion>

  <Accordion title="Who applies updates?">
    You do. We publish managed images on a two-week cadence plus hotfixes, and notify via the `managed-image-releases` repo — pulling and deploying them is on your team.
  </Accordion>

  <Accordion title="Which clouds are supported?">
    AWS, GCP, Azure, and on-prem Kubernetes — anywhere you can run the Helm chart.
  </Accordion>

  <Accordion title="Can we start on the free tier and move to Self-Managed later?">
    Yes — the free self-hosting option (see the [overview](/docs/guides/platform/self-hosting#free-self-hosting)) is Auth and Proxy only. Moving to Enterprise Self-Managed unlocks the rest of the feature set; [talk to us](https://nango.dev/contact) when you're ready.
  </Accordion>

  <Accordion title="Is autoscaling included?">
    Not out of the box — the default configuration scales far without it, and we can guide you on setting up autoscaling when you need it.
  </Accordion>

  <Accordion title="Does Nango have any visibility into our instance?">
    None. Self-hosted instances send nothing back to Nango automatically. If you need our help debugging something, you share what's relevant with us directly.
  </Accordion>
</AccordionGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.