# 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`. |

Precedence only applies to keys that are yours to define. The keys Lessly reserves for itself — every `LESSLY_` key and `PORT` — cannot be defined at any of the three levels at all, so the question of which level wins never comes up for them. See [Reserved keys](#reserved-keys).

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. That merged list is **Service → Variables** in the Product App: an inherited row is tagged `product` or `env` and is edited where it is defined, which for an environment-level variable is **Environment → Settings → Variables**.

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 |

These are regenerated on every deploy and you cannot edit or delete them. `PORT` in particular always tracks the container port configured on the service — change the port setting, not the variable. You can refer to all six 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 one part of the set you cannot refer to with `${{ }}`.** The commit is not known until the build has finished, which is after references are resolved, so `${{LESSLY_GIT_COMMIT_SHA}}` stops the deploy as a reference to a key that does not exist. Their values still reach the container like the rest.

## Reserved keys

Lessly reserves a whole namespace, not just the keys it happens to inject today. Two things are reserved:

- every key beginning with `LESSLY_`;
- the exact key `PORT`.

A variable with a reserved key cannot be created at any level — service, environment or product. The attempt is refused at the moment you save it, and the message names the key you tried to use:

```text
variable LESSLY_REGION is reserved by the platform and cannot be set: the "LESSLY_" prefix is reserved for platform-injected variables
```

`PORT` is refused with its own reason, because there is a proper place to set it:

```text
variable PORT is reserved by the platform and cannot be set: it is derived from the service containerPort — set the port on the service instead
```

**The match is case-sensitive, and only the beginning of the key counts.** `lessly_foo`, `port`, `MY_PORT` and `PORTAL_URL` are ordinary keys and remain yours to use; only `LESSLY_`-prefixed keys and `PORT` itself are refused.

The deploy is the second line of defence, for anything that reached storage before the rule existed. A variable of your own on a reserved key never wins: Lessly's value is what the container gets, yours is dropped from the release entirely, and a `deployment.variable.ignored` event is recorded on the deployment so the loss is visible in the Events feed. That covers the git provenance keys too — a container cannot be made to report a commit it was not built from.

## 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.
