There are two separate layers of variables, and confusing them is the most common source of avoidable mistakes:
| Layer | Where it lives | Who edits it | What it configures |
|---|---|---|---|
| Installer's own environment | .env next to the Installer's docker-compose.yml | You, over SSH | The Installer containers themselves — port, socket, database, session lifetimes |
| DataMind OS deployment environment | deployment/.env, rendered by the Installer | The Installer, from configuration rows | Every service in the DataMind OS stack |
Change the first by editing a file and recreating the Installer. Change the second through the interface or the configuration API — never by hand.
Stored secrets and operator-supplied values are rotatable; the Installer's own cryptographic material is not. The rotatable set, and how each one is rotated:
| Value | Rotate with | Effect |
|---|---|---|
AZURE_CLIENT_SECRET | Advanced configuration → System secrets, or PUT /api/system-secrets/azure | Replaces the encrypted value; validated on demand by a real token request |
Any user-source configuration value | Advanced configuration, or PATCH /api/configs/values | Rewrites deployment/.env and marks affected services stale |
VM_USER | POST /api/migrate/vm-user | Changes what <USER> expands to in path values |
A generated seed whose public key is derived (ed25519-pub:<CODE>) | Replace the seed value | The public key is recomputed automatically on the next sync |
JWT_ACCESS_EXPIRY, JWT_REFRESH_EXPIRY | Installer .env, then recreate the backend container | Changes session lifetimes for new tokens |
NEST_PORT | Installer .env, then recreate the backend container | Moves the interface and API to another port |
NEST_TYPEORM_LOGGING, SWAGGER_ENABLED, TZ | Installer .env, then recreate the backend container | Verbosity, API docs, container timezone |
Rotating a secret in the deployment layer changes the file, not the running process. A value reaches a container when that container is recreated — that is what Apply changes does.
The Installer's signing material, its encryption key and its database identity. Changing any of these either locks you out or destroys data:
| Value | Where it lives | Why it must not change |
|---|---|---|
JWT_SECRET | delamain_secrets/jwt.secret | Every issued access and refresh token becomes invalid; all sessions drop |
NEST_ENCR_KEY | delamain_secrets/encr.key | Every stored secret and every generated configuration value becomes undecryptable. The Installer surfaces this as a decryption error on the Azure secret and asks you to re-enter it |
pg.pass | delamain_secrets/pg.pass | The file is the password of the running PostgreSQL role. Editing the file without changing the role password breaks the Installer's database connection |
CURATO_SERVICE_TOKEN | delamain_secrets/curato.token | The Installer owns this value and injects it into the deployment .env on every render. It is deliberately not a configuration row, so no edit can drift it away from the value the deployment compares against |
POSTGRES_USER, POSTGRES_DATABASE_NAME | Installer .env | These identify the existing database. Changing them points the Installer at a different (empty) database |
DOCKER_SOCK | Installer .env | Must be the host path that actually exists for your Docker installation |
Memory rows (*_MEM_LIMIT, *_MEM_RESERVATION, *_INTERNAL_LIMIT_MB, *_ENABLED) | Configuration rows, managed | These are recomputed from the host on every generation. Editing one freezes it — see the memory-limits question below |
does not decrypt is what an encryption-key change looks like from the outside. If you ever see
that on a stored secret and you did not touch encr.key, treat it as a restored-backup problem,
not as a lost key. See Data and backups.
Nothing immediately — the value is written to deployment/.env, and the affected services are
reported as stale until you apply the change. The sequence is:
userModified and stamps its updated_at.deployment/.env from all configuration rows.GET /api/deployment/stale-services compares each container's creation time with the
updated_at of every configuration row that targets it. A row newer than the container makes
that service stale.Some services read the mounted .env at start but keep their baked-in environment, so a plain
restart can leave them running a mix of old and new values. The Installer emits an explicit
warning when such a service is in the target set — use Apply changes for it.
user, constant, generated and computed values?They describe where a value comes from, and whether an edit sticks. The interface marks each row with a source icon:
| Source | Meaning | Editable? | Does an edit persist? |
|---|---|---|---|
constant | A fixed value the template declares | Yes | Yes, but there is rarely a reason to |
user | A value only you can supply — URLs, ports, credentials | Yes | Yes |
generated | Drawn once when the template is seeded, then left alone | Yes | Yes — editing sets userModified and freezes it |
computed | Derived from the host — memory limits | Yes | Yes, and a hand-set value wins over the calculation |
generated and secret values are encrypted at rest with the Installer's encryption key. In
GET /api/configs they are returned as a fixed mask for anyone who is not an administrator.
A generated value is drawn once and then never redrawn — that is deliberate. Rotating it means
replacing it deliberately, not re-seeding. Re-seeding the template does not touch any row you
have modified.
Enter the new secret in Advanced configuration, or call the system-secrets endpoint. The value is encrypted before it is stored, and you can validate it immediately.
curl -X PUT http://localhost:8000/api/system-secrets/azure \
-H "Authorization: Bearer <access-token>" \
-H "Content-Type: application/json" \
-d '{"clientSecret":"<new-secret>"}'Then confirm that it actually works:
curl -s http://localhost:8000/api/system-secrets/azure/validate \ -H "Authorization: Bearer <access-token>"
Validation attempts a real Azure AD token request. A failure returns a code such as AADSTS…,
NETWORK_ERROR, NOT_CONFIGURED or DECRYPT_ERROR, and the interface shows it against the
field.
PLATFORM_URL, and why is it special?Edit it like any other value — but expect the Installer to rewrite it. PLATFORM_URL is the
root of every derived URL in the deployment .env: the CORS origin the edge compares against, the
front end's base URLs, the reporting host and the internal platform URL all reference it.
The Installer normalizes it on every write path — the wizard, the Advanced editor, a direct
PATCH /api/configs/values, a Jenkins import and a restored configuration set all funnel through
the same gate — so a scheme-less host cannot reach the .env:
| You type | What lands in the file |
|---|---|
localhost | http://localhost |
192.168.10.20 | http://192.168.10.20 |
portal.example.com | https://portal.example.com |
A non-http/https value is refused with an operator-facing message.
A scheme-less PLATFORM_URL used to crash-loop one of the deployment's services. The middleware
that panicked is gone, so today the same bad value fails silently instead of loudly — which is
worse. Keep the scheme in the value you inspect.
.env by hand?Your change is overwritten the next time the Installer generates the file. Every save, every
seed and every deployment-file restore rewrites deployment/.env in full from the configuration
rows. The generated file wraps its memory-limits block in marker comments saying the block was
generated and must not be edited, and it records any hand-edited rows inside that same block.
Edit the value in the interface instead, so it survives and so the Installer knows which services are stale.
.env on disk?At deployment/.env, next to the Installer's own compose file on the host. Inside the
container the same file is at /usr/src/app/deployment/.env, and that is the path handed to every
docker compose invocation as --env-file.
See Installation and setup for the full layout of the deployment directory.
It derives them from the host's total memory, and yes — a hand-set value wins. The Installer
reads /proc/meminfo, picks a host class from the schema's buckets, and emits a set of rows per
service:
| Code pattern | Meaning |
|---|---|
<SERVICE>_MEM_LIMIT | Container memory limit |
<SERVICE>_MEM_RESERVATION | Reserved memory |
<SERVICE>_INTERNAL_LIMIT_MB | Internal limit for services that manage their own heap, where the schema declares one |
<SERVICE>_ENABLED | Whether an optional service is enabled on this host class |
If the sum of reservations exceeds the budget, all reservations are scaled down by a single factor
and the file records the calculation in a comment. Setting one of these rows by hand sets
userModified, and from then on the Installer records it as hand-edited, not recalculated.
On a host that is not the Linux VM these limits describe — for example running the backend outside a container on macOS — no limits are emitted at all, with the reason written into the limits block.
<USER> substitution?Set it in the migration step, or call the migration endpoint. Several path values are stored
as templates containing <USER> and are expanded with this value when the .env is rendered:
curl -X POST http://localhost:8000/api/migrate/vm-user \
-H "Authorization: Bearer <access-token>" \
-H "Content-Type: application/json" \
-d '{"vmUser":"ubuntu"}'The value is stored encrypted in the Installer's system secrets. Path values that still contain
<USER> are refused as unresolved by the Jenkins import preview.
No — the interface renders every configuration row, including secrets and computed ones. You can filter by key, and the value editor is the same surface the API uses.
What is genuinely not possible is adding a new code. The template publishes the set of valid
codes; PATCH /api/configs/values returns a not-found error for a code that does not exist, and
re-downloading the template soft-deletes any row the template no longer lists.
A required row blocks deployment while it is empty. A row with a requiredIf dependency is
required only when the row it names already has a value. Both are enforced before a deploy or an
apply, and the failure message lists the codes:
Missing required configurations: PLATFORM_URL, CLICKHOUSE_PASSWORD
Yes — change NEST_PORT in the Installer's own .env and recreate the container. The port is
published and used symmetrically, so the mapping follows the variable:
In the .env next to the Installer's docker-compose.yml:
NEST_PORT=9000
Then recreate the Installer's backend service. Note that the container healthcheck, the interface and any link you saved all move with it.
This is the Installer's port, not a DataMind OS service port. DataMind OS ports belong to the
service_ports group of the deployment configuration.
Edit JWT_ACCESS_EXPIRY or JWT_REFRESH_EXPIRY in the Installer's own .env and recreate the
backend. The values are read when the process starts, and the durations drive both the signed
token lifetime and the expiry of the browser cookies.
JWT_ACCESS_EXPIRY=15m JWT_REFRESH_EXPIRY=24h
Existing tokens keep their original expiry; new tokens use the new value.
Every stored secret and generated value becomes unreadable. The key encrypts secret
configuration values, generated values and the system secrets table with AES-256-GCM. After a key
change the Installer cannot decrypt them, and validation reports DECRYPT_ERROR with the message
that the stored secret could not be decrypted and must be re-entered.
There is no re-key command. The recovery is to restore the original encr.key, or to re-enter the
affected values one by one.
Do not edit pg.pass alone. That file is the password: the PostgreSQL container generates it
on first start and the backend reads it at boot. Changing the file without changing the role's
password in the database leaves the Installer unable to connect.
If you must rotate it, change the role password inside PostgreSQL first, then write the same value
to delamain_secrets/pg.pass and recreate the backend.
Use the Jenkins import — preview first, then import. The Installer reads
/var/lib/jenkins/config.xml from the read-only host mount, extracts matching codes, and adds the
standard migration path defaults.
curl -s http://localhost:8000/api/configs/import/jenkins/preview \ -H "Authorization: Bearer <access-token>"
The preview writes nothing. Then apply it:
curl -X POST http://localhost:8000/api/configs/import/jenkins \ -H "Authorization: Bearer <access-token>"
Import skips rows that are constant-sourced, rows the Installer manages, and rows you have
already modified. Every imported value is marked as modified, so it freezes that row. If
config.xml is missing, only the path defaults are applied and the response says so.
Because a container it targets was created before that value was last changed. The Installer compares the row's update time against the container's creation time. The badge tooltip names the services involved.
Fix it with Apply changes, which recreates exactly the stale services and verifies each dependency tier as it goes. A plain restart will not clear the badge.
Yes, but treat it as a recovery action. Re-downloading the environment template rewrites the schema and re-seeds rows, preserving the values of anything you have modified, and soft-deleting rows the new template no longer declares. The download endpoint is administrator-only:
curl -X POST http://localhost:8000/api/registry/download-env-template \ -H "Authorization: Bearer <access-token>"
Re-seeding can retire configuration codes if the published schema has changed. Review the deployment status and the stale-services list afterwards, then apply changes to bring services onto the new set. Take a backup first — see Data and backups.
For the complete variable reference with every group and code, see Environment variables. The rotation procedure in full is in Rotating values, and Secrets covers the encrypted system secrets. For how the service groups and the memory computation are described, see Service catalog and Compose and files.