---
sidebar_position: 9
question: Where does config resolve secrets in each deploy mode?
kind: task
via_web: available
via_cli: available
via_python: available
via_api: available
via_config: available
ref_web: System › Data › Secrets
ref_cli: abi secrets naas push-env-as-base64
ref_python: secret.set
ref_api: PUT /api/admin/system/resources/secret/entry
ref_config: .env
prereq: A deployed project: [Docker](/v2/capabilities/deploy/local-docker), [Dokploy](/v2/capabilities/deploy/dokploy) (ABI 3.x) or a [Naas Space](/v2/capabilities/deploy/production#naas-spaces).
next:
  - /v2/capabilities/build/secrets/rotate-a-secret
  - /v2/capabilities/deploy/security-compliance
---

> **You are reading the v2.x docs (engine modules). New projects use v3.** [Read this page in v3](/capabilities/deploy/add_secrets_to_production) · [Migrate from 2.x to 3.0](/capabilities/upgrade/migrate-to-3)

# Add secrets

ABI reads its secrets through the **secret service**. A config file refers to one as `{{ secret.NAME }}`, and the service looks it up in the adapters listed under `services.secret.secret_adapters`, in order. A generated project lists one, the `dotenv` adapter on the project's `.env`. The other adapter types are `naas` (the Naas secret store), `base64`, `custom`, and on ABI 3.x `nats_rpc`. Where `NAME` goes in production depends on how you deploy:

| Deployment | Where a new secret goes |
|---|---|
| Docker Compose (`abi deploy local`) | `.env`, next to `docker-compose.yml`. |
| Dokploy (`abi deploy dokploy`, ABI 3.x) | The Environment tab of the Dokploy application, filled from `.env.dokploy` (generated and gitignored). |
| Naas Space, from the CLI (`abi deploy naas`) | The `deploy.env` block of the config. |
| Naas Space, from the ABI repository's GitHub Actions | A repository secret, passed to the Space by `deploy_api.yml`. |

Never commit a secret. `.env` and `.env.*` are gitignored in a project generated on ABI 3.x; on 2.x the `.gitignore` lists only `.env`, so add `.env.*` yourself.

## Reference it in the config

Whichever way you add the value, the config reads it the same way:

```yaml title="config.local.yaml"
modules:
  - module: naas_abi_marketplace.ai.openrouter
    enabled: true
    config:
      openrouter_api_key: "{{ secret.OPENROUTER_API_KEY }}"
```

The config is rendered once, when the engine starts: restart it after you add a value. A `{{ secret.X }}` that nothing defines stops the engine: with a terminal it prompts for the value, without one (a container, CI) it fails. `{{ secret.X }}` takes a process environment variable of that name before any adapter. Code that calls `secret.get` reads the `.env` file first, then the environment; on ABI 3.x the adapter's `environment_first: true` reverses that. A full-line YAML comment is not rendered, so an example reference in a comment is safe.

## Web UI

ABI 3.x, super admins only: the System app does not exist on ABI 2.x, where you use the CLI, Python or Config steps. Nexus writes the value through the running engine, to every secret adapter that accepts writes.

### Open the secrets view

Sign in to the deployed Nexus (`https://nexus.<domain>` on Dokploy), open **System** in the sidebar, then the **Data** tab, then **Secrets**.

### Create the secret

Choose **New secret**, enter the name and the value, then **Create**. Replacing an existing name asks you to type the name back. Every change is audited.

Where the value lands:

- **Docker Compose:** the project's `.env`, which the `abi` service mounts.
- **Dokploy:** the engine's secrets file on the `abi_secrets` volume, which survives redeploys.

### Restart the engine

Restart the `abi` service before a new `{{ secret.NAME }}` resolves. Module code that calls `secret.get` sees the value at once.

:::warning What Nexus cannot change
A key set in Dokploy's Environment tab wins over the secrets file again after a restart. Values that Compose interpolates into other services (`POSTGRES_PASSWORD`, the `NATS_*` passwords on ABI 3.x) and a Naas Space's `deploy.env` are not secret adapters: change them in the Config tab's places.
:::

## CLI

For a project whose secrets live in the Naas secret store (a `naas` adapter, or `base64` over it). The stored secrets are one env file, and a push replaces all of it.

### Read what is stored

```bash
abi secrets naas get-base64-env --naas-secret-name abi_secrets > .env.prod
```

### Add the line and push the whole file

```bash title=".env.prod"
PARTNER_API_TOKEN=the-value
```

```bash
abi secrets naas push-env-as-base64 --naas-secret-name abi_secrets --env-file .env.prod
```

A file that holds only the new line deletes every other secret. Both commands print every value: run them in your own terminal, never where the output is logged. `--naas-api-key` defaults to `$NAAS_API_KEY`. Delete `.env.prod` afterwards.

### Restart the engine

Redeploy, or restart the engine, so the config is rendered again. See [`abi secrets naas`](/user-guide/reference/cli#abi-secrets-naas).

## Python

### Write it from a module

```python title="src/my_module/__init__.py"
secret = self.engine.services.secret
secret.set("PARTNER_API_TOKEN", token)
```

`set` writes every adapter that accepts writes, in the running deployment. The module must declare the `Secret` service in its dependencies: see [Add a secret](/v2/capabilities/build/secrets/add-a-secret?via=python).

### Restart for config

A module sees the value at once with `secret.get`. A `{{ secret.NAME }}` in the config resolves after the engine restarts.

## REST API

The System routes of the deployed Nexus API, on ABI 3.x only (ABI 2.x has no System routes: use the CLI, Python or Config steps). Auth: the access token of a super admin, from `POST /api/auth/login`.

### Write the secret

```bash
curl -X PUT "https://api.example.com/api/admin/system/resources/secret/entry?id=PARTNER_API_TOKEN" \
  -H "Authorization: Bearer $NEXUS_TOKEN" \
  --data-binary 'the-value'
```

The body is the value, as UTF-8 text. Replacing an existing name needs `&confirm=PARTNER_API_TOKEN`; without it the call answers 409. The same limits as the Web UI apply.

### Restart the engine

Restart the `abi` service before a new `{{ secret.NAME }}` resolves.

## Config

### Docker Compose

Add `NAME=value` to `.env`, then recreate the services that use it:

```bash title=".env"
PARTNER_API_TOKEN=the-value
```

```bash
docker compose up -d --force-recreate
```

`abi deploy local --regenerate` re-renders the deployment files and keeps `.env`. The stack's generated passwords (and on ABI 3.x the NATS broker's `NATS_ABI_PASSWORD` and `NATS_JWT_SECRET`) are written to `.env` for you; do not copy them from another environment.

### Dokploy

ABI 3.x only. Add the variable to the application's **Environment** tab, then **Deploy**. The tab is the source of truth: a value there wins over the engine's secrets file. `.env.dokploy` holds the starting values you paste into the tab. `abi deploy dokploy` keeps the ones already in it on later runs; `--regenerate` generates new secrets and backs up the previous file, so use it only to rotate them. See [Deploy with Dokploy](/v2/capabilities/deploy/dokploy#secrets).

### Naas Space, with `abi deploy naas`

`abi deploy naas` builds the project's image, pushes it to a Naas registry and creates or updates the Space named in the `deploy` block of the config. The Space gets the variables listed under `deploy.env`, and only those:

```yaml title="config.prod.yaml"
deploy:
  workspace_id: "<your workspace id>"
  space_name: "my-space"
  naas_api_key: "{{ secret.NAAS_API_KEY }}"
  env:
    NAAS_API_KEY: "{{ secret.NAAS_API_KEY }}"
    OPENROUTER_API_KEY: "{{ secret.OPENROUTER_API_KEY }}"
    ABI_API_KEY: "{{ secret.ABI_API_KEY }}"
```

Put the value in `.env`, add a line to `deploy.env`, and run `abi deploy naas` again.

### The ABI repository's GitHub Actions

The ABI repository can deploy its own API to a Naas Space with `.github/workflows/deploy_api.yml`, after a release (see [Releasing](/developer-guide/releasing/release-process#containers-and-the-api-deploy)). That workflow is disabled in the repository's Actions settings: enable it first. To add a secret:

1. In the repository, open **Settings > Secrets and variables > Actions** and add the secret.
2. In `deploy_api.yml`, job `build`, step **Push latest abi container**, add it to the step's `env:` section, as the existing ones are.
3. In the same step, add it to `ENV_CONFIG`, the JSON the Space is created or updated with.
4. If the engine must also see it when the API is tested before the deployment, add it to the `docker run` of the step **Test API with production secrets before deployment**.
5. Commit and push.

Other jobs (`streamlit-demo`, `mcp-server`) build their own `ENV_CONFIG`.
