> Solana Developer Platform docs, served as Markdown. The [docs index](https://platform-beta.solana.com/docs.md) lists every page and [llms.txt](https://platform-beta.solana.com/docs/ai/llms.txt) the key ones; [SKILL.md](https://platform-beta.solana.com/SKILL.md) has the rules for building on SDP.

# Environment Reference

The configuration variables a self-hosted Solana Developer Platform reads from .env.

The stack reads one `.env` file next to `compose.yml`. The [configurator](/docs/self-hosting/configurator) writes it, with the defaults and validation of the release you installed. This page covers how the file is organized and the variables you're most likely to set by hand.

The configurator groups the variables into sections:

| Section | What it covers |
|---|---|
| Basic | `ENVIRONMENT`. The deployment mode is always `self_hosted`. |
| Database | Bundled Postgres or an external database. |
| Cache | Bundled Redis or an external one. |
| Solana RPC | The devnet and mainnet RPC endpoints. |
| Authentication (Clerk) | Clerk keys, issuer, dashboard URL, and webhook secret. |
| Signing provider | Which signers to enable, the default one, and their credentials. |
| Fee payment | The Kora that sponsors each cluster's transaction fees, if any. |
| Secrets | App secrets, generated where you run the configurator. |
| Advanced | Image source, ports, public URLs, proxy trust, and optional features. |

## Required variables

`compose.yml` refuses to start without these:

| Variable | Description |
|---|---|
| `POSTGRES_PASSWORD` | Password for the bundled Postgres. Required even with an external `DATABASE_URL`, because the bundled `postgres` service still starts. |
| `API_KEY_PEPPER` | Server-side pepper for API key hashes. Generate it with `openssl rand -hex 32`. |
| `CUSTODY_ENCRYPTION_KEY` | Encrypts custody secrets at rest. Generate it with `openssl rand -base64 32`. |
| `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` | Clerk publishable key for the dashboard. |
| `CLERK_SECRET_KEY` | Clerk secret key. |

The configurator also requires these. The stack starts without them, but most leave a core flow broken:

| Variable | Description |
|---|---|
| `CLERK_ISSUER` | Clerk issuer URL. Without it, dashboard sessions can't be verified, so sign-in fails. |
| `FRONTEND_URL` | The dashboard URL the browser loads. With `ENVIRONMENT=production`, the compose default, the API accepts only Clerk sessions issued to this origin, and won't start if it isn't an `http(s)` URL. |
| `SOLANA_RPC_DEFAULT_DEVNET_API_KEY_URL` | The devnet endpoint Sandbox projects read and send through. Without it, they get `503 RPC_UNAVAILABLE`. |
| `SIGNING_PROVIDER` | The configurator writes `local` unless you pick another provider. The API reads it only at startup, where `local` (also its value when unset) requires `SDP_DEPLOYMENT_MODE=self_hosted`. |
| `CUSTODY_PRIVATE_KEY` | With local signing selected: SDP offers the local signer only while this is set. |
| `CREDENTIAL_FINGERPRINT_PEPPER` | Without it, saving or rotating provider credentials in the dashboard fails. Keep it stable. |

`SOLANA_RPC_DEFAULT_MAINNET_API_KEY_URL` is optional: without it, SDP refuses Production projects' chain reads and sends.

The configurator generates `API_KEY_PEPPER`, `CREDENTIAL_FINGERPRINT_PEPPER`, and `CUSTODY_ENCRYPTION_KEY` for you, and can generate `POSTGRES_PASSWORD`.

## Database

The configurator's database choice (`DATABASE_MODE`) shapes the file, but isn't written to it.

- **Bundled:** set `POSTGRES_DB`, `POSTGRES_USER`, and `POSTGRES_PASSWORD`. The API and `sdp-migrate` build their connection string from them.
- **External:** set `DATABASE_URL` to your `postgresql://…` string, and the API and migrations connect there. The bundled `postgres` container still starts, unused, so it still needs `POSTGRES_PASSWORD`. The configurator hides that field and generates a value for it.

## Cache

The cache choice (`CACHE_MODE`) isn't written to the file either. For an external cache, set `REDIS_URL` to your `redis://…` or `rediss://…` endpoint. The bundled `redis` container still starts, unused.

## Signing providers

The configurator offers `local`, `fireblocks`, `privy`, `coinbase_cdp`, `para`, `turnkey`, and `utila`. Its list of providers to enable (`SIGNING_PROVIDERS`) only decides which credential fields it shows, and isn't written to the file. Each provider other than `local` needs its own credentials, such as an API key and secret for Fireblocks. Each project picks its signer when you set up custody in the dashboard.

Privy, Coinbase CDP, Para, and Turnkey are open to every organization. The others, `local` included, also need a per-organization activation: [First devnet deployment](/docs/self-hosting/first-devnet-deployment) shows how for the local signer.

For provider co-signers that run outside the stack, see [External co-signers](/docs/self-hosting/external-co-signers).

## Fee payment

SDP pays transaction fees through a Kora on each project's cluster, and the stack doesn't bundle one. The configurator's choice of clusters (`FEE_SPONSOR_CLUSTERS`) isn't written to the file: it shows each chosen cluster's fields. Without a Kora, sends SDP sponsors, such as payment transfers, answer `503 PROVIDER_UNAVAILABLE`.

| Variable | Description |
|---|---|
| `KORA_DEVNET_URL` | The Kora that pays fees for Sandbox projects. |
| `KORA_DEVNET_RPC_URL` | The Solana RPC endpoint that Kora reads. SDP prices and settles sponsored fees through it. Set it with `KORA_DEVNET_URL`: the API won't start with only one of the two. |
| `KORA_DEVNET_API_KEY` | Optional. The API key your Kora requires, if any. |
| `KORA_DEVNET_CLOUD_RUN_AUDIENCE` | Optional. For a private Kora on Cloud Run: SDP sends it a Google identity token for this audience, which needs the API on Google Cloud and the Kora URL on the audience's `https` origin. |

The `KORA_MAINNET_*` variables do the same for Production projects.

## Advanced

| Variable | Default | Description |
|---|---|---|
| `SDP_IMAGE_REGISTRY` | `ghcr.io/solana-foundation/sdp` | Registry the images come from. |
| `SDP_VERSION` | `latest` | Image tag for every SDP service. Pin it to a release tag, such as `vX.Y.Z`, in production. |
| `SDP_API_PORT` | `8787` | Host port for the API. |
| `SDP_WEB_PORT` | `3000` | Host port for the dashboard. |
| `SDP_DOCS_PORT` | `3001` | Host port for these docs. |
| `TRUST_PROXY_HEADERS` | `false` | How to read `X-Forwarded-For`. `true` trusts the first address: use it only behind an ingress that replaces caller-supplied forwarding headers. `gclb` is for ingress restricted to a Google Cloud external Application Load Balancer, which appends the verified client address. With `false`, SDP can't see caller addresses: per-IP limits count every caller as one, and an IP allowlist refuses every request. |

`SDP_API_BASE_URL` (default `http://sdp-api:8787`) is how the dashboard's server reaches the API inside the Docker network. The browser-facing `NEXT_PUBLIC_*` URLs default to `localhost`, such as `http://localhost:8787` for the API and `http://localhost:3000` for the dashboard. Change them, and `FRONTEND_URL`, when you put the services behind your own domains.

`compose.yml` also reads `SDP_RELEASE_CHANNEL`, which the configurator doesn't set. It decides which modules and providers the API and dashboard run (`experimental`, `beta`, or `stable`), and defaults to `experimental`.
