# Set variables and secrets

A variable is a named value Lessly passes into your container as an environment variable. Everything a service is configured with — the port it listens on, a connection string, a log level, an API key — is a variable, and variables are the only channel through which your configuration reaches a running container.

## Choose the level

The same key can be defined at three levels. When a service is deployed, Lessly merges all three into one set, and the narrowest definition wins: a service-level value overrides an environment-level one, and an environment-level value overrides a product-level one. Define a shared value once at the widest level that is true, and override it narrowly where it is not.

| Level | Who gets it | Reach for it when |
|---|---|---|
| Product | Every service in every environment of the product. | The value is true everywhere — `APP_NAME`, an error-reporting DSN. |
| Environment | Every service in that one environment. | The value belongs to the environment — `REDIS_URL`, a feature flag. |
| Service **(Recommended)** | One service in one environment. | One service owns the value, or you are overriding a shared one — `PORT`, `LOG_LEVEL`, `WORKER_CONCURRENCY`. |

Environments are otherwise isolated. An environment-level `REDIS_URL` in `production` and an environment-level `REDIS_URL` in `staging` are two separate records; changing one does not touch the other.

## Set a variable

1. Pick the level from the table above, then add the key and the value. A key is written in the usual environment-variable style — capital letters, digits and underscores, starting with a letter or an underscore, up to 128 characters. `DATABASE_URL` and `LOG_LEVEL` are valid; `databaseUrl` and `2ND_TRY` are not.
2. Paste the value as-is. A value is a string of up to 256 KiB and may span several lines, so a PEM key or a JSON blob reaches the container with its line breaks intact.
3. Mark it as a secret if the value is a credential. The value is encrypted and becomes write-only from that point.
4. Redeploy the service. Adding, editing or deleting a variable does not restart anything on its own — the running container keeps its current values until the next deploy.

Listing a service's variables shows the merged result, so you see what the service will actually run with rather than three lists to combine in your head.

Through the Lessly MCP server, an agent reads and writes the same records: [`deployment_variable_list`](/reference/mcp-tools/deployment_variable_list) and [`deployment_variable_set`](/reference/mcp-tools/deployment_variable_set) at service level, [`deployment_variable_set_env`](/reference/mcp-tools/deployment_variable_set_env) and [`deployment_variable_set_product`](/reference/mcp-tools/deployment_variable_set_product) at the wider levels, [`deployment_variable_unset`](/reference/mcp-tools/deployment_variable_unset) to remove one, and [`deployment_service_redeploy`](/reference/mcp-tools/deployment_service_redeploy) to apply the batch. Token scope: a key with write access to the product's variables.

## Secrets

Marking a variable as a secret encrypts its value. The interface shows it masked rather than in the clear, and listing variables returns the key with no value attached. You can overwrite a secret whenever you like, but you cannot read the old value back out of Lessly — keep your own copy of anything you cannot regenerate.

Two consequences are worth planning for.

| Behaviour | What it means for you |
|---|---|
| Marking a variable as a secret is irreversible. | There is no way to turn a secret back into a plain variable; the attempt is rejected. If you need the value visible again, delete the secret and create a plain variable in its place. |
| Secrets are not copied when an environment is forked. | Plain variables come along to the new environment at both environment and service level. Secrets are left behind entirely — not even the key is carried over. After a fork, set the secrets the new environment needs before you deploy into it. |

The second is deliberate: it keeps production credentials from spreading into short-lived copies by accident.

## Refer to another variable

A value can point at another variable instead of repeating it. Write `${{KEY}}` inside the value and Lessly substitutes the real string when it builds the release:

```text
GRAPHQL_ENDPOINT=http://localhost:${{PORT}}/graphql
```

References may be combined with ordinary text and with each other, and a value may contain as many as you like. A reference can also point at a value published by a managed database in the same environment, written as the database's slug, a dot, and the key:

```text
DATABASE_URL=${{postgres.DATABASE_URL}}
```

That is the intended way to wire a service to a managed database: you never handle the credentials yourself, and the next deploy picks up the current connection details. The database's own page lists every key with the exact reference to copy — see [Choose where state lives](/ship/deployment/data).

References are resolved before anything starts, and four situations stop the deploy rather than producing a broken container:

| The deploy is refused when | Fix |
|---|---|
| The reference points at a key that does not exist. | Define the key, or correct the reference. |
| The references form a cycle — `A` needs `B` and `B` needs `A`. | Break the cycle. |
| The chain of references nests more than ten deep. | Flatten it. |
| A plain variable refers to a secret. | Mark the referring variable as a secret too, and it resolves normally. Resolving it otherwise would write the secret's value into a non-secret entry. |

## Variables Lessly sets for you

Every container also receives a small set of variables describing where it is running:

| Key | Value |
|---|---|
| `PORT` | The container port configured on the service |
| `LESSLY_SERVICE_ID` | The service being run |
| `LESSLY_ENVIRONMENT_ID` | The environment it belongs to |
| `LESSLY_ENVIRONMENT_NAME` | That environment's slug, for example `production` |
| `LESSLY_PRODUCT_ID` | The product the environment belongs to |
| `LESSLY_DEPLOYMENT_ID` | The release currently running |

The six in the table are regenerated on every deploy and you cannot edit or delete them. They also win: if you define a variable of your own with one of those keys, yours is discarded, the deploy continues with Lessly's value, and a `deployment.variable.ignored` event is recorded on the deployment, so the loss is visible in the Events feed. Being the narrowest definition does not make a service-level value the winner against these. `PORT` in particular always tracks the container port configured on the service — change the port setting, not the variable. You can refer to them from your own values like any other variable, for example `${{PORT}}`.

A service built from a git repository receives `LESSLY_GIT_COMMIT_SHA` and `LESSLY_GIT_BRANCH` as well, so a running container can report exactly which commit it was built from.

Those two are the exception, and they run the other way. They are added only if the key is still free, so a variable of your own named `LESSLY_GIT_COMMIT_SHA` or `LESSLY_GIT_BRANCH` wins: the real commit value is not injected at all, and the container reports yours instead. The two keys are displaced independently — taking over `LESSLY_GIT_COMMIT_SHA` leaves `LESSLY_GIT_BRANCH` alone — and setting one at the product level does it for every git-backed service in the product at once.

It happens silently. Nothing warns you, and no `deployment.variable.ignored` event is recorded — that event belongs to the six above, which take the collision the other way. If you rely on either variable to tell you what is running, and that is what they are for, do not define your own under those names.

This behaviour is under review and may change; what is written here is how it works today.

## When a change takes effect

At the moment a deploy starts, Lessly takes a snapshot: it merges the three levels, decrypts the secrets, resolves every reference, and freezes the result into that release.

- **From then on the release is fixed.** Editing a variable afterwards does not reach the running container, and neither does a change in something a reference points at — those wait for the next deploy.
- **That is intentional.** It lets you stage a batch of related configuration changes and apply them together in a single release rather than one restart at a time.
- **A redeploy needs no new commit.** It builds a release from the current source and the current variables.

**A rollback does not undo a variable change.** Rolling back restores the image, not the configuration. Variables are read fresh every time a release is built, so the rolled-back release runs with today's values, not with the values that were in place when the original release was deployed. If a variable change is what broke you, change the variable back yourself and deploy.

## Next steps

- [Choose where state lives](/ship/deployment/data): provision a managed database and wire it with `${{slug.KEY}}` instead of pasting credentials.
- [Run a command inside a service](/ship/deployment/terminal): check what the running process actually sees, with the service's own variables.
- [Watch a service](/ship/deployment/observability): find the `deployment.variable.ignored` event when a key of yours collided with one Lessly owns.
- [Roll back a deploy](/ship/deployment/deploy): promote the previous release when the image, not the configuration, is what broke.
- [Understand how deployment works](/ship/deployment): the product, environment and service model these three levels sit on.
