A Linux host running Docker with the Compose v2 plugin. The Installer is a container that
drives docker compose on the host through the Docker socket, so it needs a working Docker
Engine and the docker compose subcommand — not the legacy docker-compose binary.
It does not need Node.js, PostgreSQL or any build tooling on the host: the runtime image bundles
the Docker CLI, the Compose plugin and util-linux (for nsenter, used only by the host
migration feature).
The Installer's own PostgreSQL runs as a container from the same compose project. You do not provision a database separately.
Yes — run docker network create unistream once, before the first start. Both the Installer's
compose project and the DataMind OS stack it deploys attach to that network, and both declare it
external, so neither project creates it.
docker network create unistream
Skipping this step fails immediately and legibly:
network unistream declared as external, but could not be found
Do not let either compose project own this network. Both sides are external on purpose: a
network that already exists without the project's own labels is rejected by Compose 2.19.1+,
and compose down on an owner — including the Installer's own self-update — would delete a
network the other stack is still using.
The literal network name unistream is fixed and must keep that spelling.
.env?Copy .env.example to .env in the install directory and edit it. Compose reads this file
both for variable interpolation and as the containers' runtime environment.
cp .env.example .env
| Variable | Purpose | Default |
|---|---|---|
VERSION | Image tag of the Installer to run | latest |
DOCKER_SOCK | Host path of the Docker socket the Installer drives | /var/run/docker.sock |
NEST_PORT | Port the Installer backend and UI listen on | 8000 |
NEST_NODE_ENV | prod or dev | prod |
SWAGGER_ENABLED | Serve the OpenAPI UI at /api/docs | false |
TZ | Container timezone | UTC |
NEST_TYPEORM_LOGGING | Log every SQL statement the Installer runs | false |
NEST_ORIGINS | CORS allowlist for the Installer API | http://localhost:8000 |
POSTGRES_USER | Owner of the Installer's internal database | postgres |
POSTGRES_DATABASE_NAME | Name of the Installer's internal database | delamain |
JWT_ACCESS_EXPIRY | Access-token lifetime for Installer sessions | 30m |
JWT_REFRESH_EXPIRY | Refresh-token lifetime for Installer sessions | 7d |
The JWT signing secret and the encryption key are not set here. They are generated into the
delamain_secrets volume on first boot.
See Configuration variables for the full reference.
Only ${NEST_PORT}, which defaults to 8000. That single port serves the Installer's API
and the web interface, because the backend serves the built front end from the same origin at
/api and /.
ports:
- '${NEST_PORT:-8000}:${NEST_PORT:-8000}'The Installer's PostgreSQL is not published in the production compose file — it is reachable only
on the internal network. In the development compose file it is published on
${POSTGRES_PORT:-5432} for convenience.
Point DOCKER_SOCK at the rootless socket. Find your UID with id -u, then set the path:
id -u
If it prints 1000, the socket path for that installation is:
DOCKER_SOCK=/run/user/1000/docker.sock
The default /var/run/docker.sock is the rootful socket. In the compose file the socket path is
mounted host path to container path one-to-one, so whatever you set must be a path that exists on
the host.
In the delamain_secrets volume, mounted at /usr/src/app/secrets. On first boot the backend
generates what is missing and reuses what is already there:
| File | Contents | Created by |
|---|---|---|
jwt.secret | 64-byte hex signing key for Installer session tokens | Installer backend, first boot |
encr.key | AES-256-GCM key (v2:-prefixed ciphertext) for stored secrets and generated values | Installer backend, first boot |
curato.token | 48-character alphanumeric service token | Installer backend, first boot |
pg.pass | Password of the Installer's own PostgreSQL role | PostgreSQL container entrypoint, first boot |
Files are written with mode 0600. The same volume is mounted into the backend-init one-shot
that runs the database migrations.
Losing encr.key makes every stored secret unreadable — the Installer will report a decryption
failure and you must re-enter the Azure client secret. See
Data and backups.
It runs database migrations, generates its secrets, and comes up with an empty deployment directory. The startup sequence is:
postgres starts and generates pg.pass if the file does not exist yet.backend-init runs typeorm migration:run against the Installer's database and exits.backend starts only after backend-init completes successfully, generates any missing
secret files, and begins listening on NEST_PORT.At that point no DataMind OS deployment exists: the deployment directory has no compose file and
no .env, and the interface shows the setup screen.
In the deployment/ directory next to the compose file, mounted into the backend at
/usr/src/app/deployment. Three files live there:
| Path (host) | Path (container) | Contents |
|---|---|---|
./deployment/docker-compose.yml | /usr/src/app/deployment/docker-compose.yml | The DataMind OS compose file, downloaded from blob storage |
./deployment/.env.unified.template | /usr/src/app/deployment/.env.unified.template | The environment schema that seeds the Installer's configuration rows |
./deployment/.env | /usr/src/app/deployment/.env | The generated environment file that the DataMind OS stack consumes |
An optional deployment/docker-compose.override.yml is merged into every docker compose
command when it exists. The Installer never writes that file — it is yours.
The deployment directory is on the host, not in a volume, so the downloaded files survive a container replacement and are easy to inspect over SSH.
With an Azure client secret, exchanged for an Azure Container Registry token. The Installer
requests an Azure AD token using AZURE_TENANT_ID and AZURE_CLIENT_ID (set in the compose file)
plus the client secret you store in the interface, then exchanges it for an ACR refresh token and
pulls from unistream.azurecr.io.
Docker Hub images are pulled through the same registry as a cache, under the docker-hub/
repository prefix. Public Docker Hub images get an anonymous token instead.
No. The install works on a bare IP address or localhost. PLATFORM_URL is the root of every
derived URL in the generated .env, and it is normalized: an explicitly typed scheme is kept,
otherwise localhost, *.localhost, *.local and any IP literal get http://, while a real
domain name gets https://.
localhost 192.168.10.20 portal.example.com https://portal.example.com
If you type a real domain name, the Installer assumes https://. Make sure the deployment
actually terminates TLS for that name before users open it. See
Certificates and TLS.
It authorizes Azure access for image pulls and artifact downloads. Store it in the interface — the setup wizard asks for it, and it can be replaced later from the Advanced configuration view.
Under the hood it is written to the Installer's system_secrets table, encrypted with
encr.key, through PUT /api/system-secrets/azure. The Installer can validate it on demand by
attempting a real token acquisition, so you find out immediately if a secret is wrong or expired.
The installation step downloads the compose file and the environment template from Azure Blob Storage, and that download uses this same secret. There is no way to install without it.
It stores your essentials, downloads the deployment artifacts, and starts the deploy job. In order:
PUT /api/system-secrets/azure) — required first, because the
next step authenticates with it.docker-compose.yml and .env.unified.template from blob storage and seeds the
configuration rows from the template.PLATFORM_URL.SSL_CERT_PATH / SSL_KEY_PATH.POST /api/deployment/deploy): download → pull → compose up.Use Start in the interface, or POST /api/deployment/deploy. A deploy job runs three phases:
it downloads the compose file and environment template, pulls every service image, then runs
docker compose up -d --pull never for all service targets.
curl -X POST http://localhost:8000/api/deployment/deploy \
-H "Authorization: Bearer <access-token>" \
-H "Content-Type: application/json" \
-d '{}'The response carries a jobId. Follow progress with
GET /api/deployment/jobs/<jobId>/logs, which is a server-sent event stream.
Start refuses to run if a required configuration value is still missing, and it refuses if any
image is not present locally — the error message names the missing configuration codes or
images. Pull first if you see that.
Check the Installer's health endpoint, then the deployment status. The health endpoint is public and verifies both the process and its database connection:
curl -s http://localhost:8000/api/health
The response is:
{"status":"ok","db":"up"}Then read the deployment status, which reports the prerequisites and every container:
curl -s http://localhost:8000/api/deployment/status \ -H "Authorization: Bearer <access-token>"
prerequisites.allReady is true when both the compose file and the .env exist;
containers.items[] gives each service's condition, state, restart count and uptime.
The first account you create becomes an administrator, and it can only be created once.
POST /api/user/register/initial is public and is refused with 403 as soon as any user exists.
The login screen uses GET /api/user/exists to decide whether to show the create-admin form or
the normal sign-in form.
Once an administrator exists, further accounts are created by an administrator from the Register User view. See Access and security.
Not as a supported procedure. The DataMind OS stack is a compose project plus a generated
.env, and the Installer is the component that produces both: it downloads the compose file and
the environment schema from blob storage and renders the .env from configuration rows that
carry dependencies, required-if rules, generated values and host-computed memory limits.
You could in principle run the compose file by hand with a hand-written .env, but nothing in the
product verifies that file, and every value the deployment derives from the host — memory limits
in particular — would be missing or wrong.
The Installer does ship one standalone script for the opposite direction: migrate-host.sh
retires a legacy bare-metal platform. That is a migration step, not an installation path. See
Migrating from bare metal.
| Symptom | Cause | Fix |
|---|---|---|
network unistream declared as external, but could not be found | The shared network was never created | docker network create unistream |
| Install fails immediately on the artifact download | Azure client secret missing, wrong or expired | Store it in Advanced configuration; use the validate action |
Missing required configurations: … | Required values still empty | Fill the named configuration codes |
Missing local images: … | Start was pressed before a pull | Run Update, then Start |
Missing deployment files: … | Compose file or .env absent and the restore failed | Check outbound access to blob storage, then re-run the download |
| Login screen shows only sign-in | An account already exists | Sign in, or reset the password from an admin account |
For diagnosis in depth see Installation fails.
Prerequisites, the full install walkthrough and the first sign-in are covered in Requirements, Install and First login, and the post-install checklist is the fastest way to confirm a new deployment is healthy.