Installing the DataMind Installer is a Docker Compose project on the host. Starting it deploys the Installation's own three containers; the DataMind OS stack itself is deployed afterwards from the Installer's UI.
Complete Prepare the host first — in
particular, the unistream network must already exist.
The Installer's own deployment is described by docker-compose.yml at the root of the Installer
repository. Copy it into the install directory you created:
sudo cp docker-compose.yml /opt/delamain/docker-compose.yml cd /opt/delamain
The same file is published to Azure Blob Storage by the publish-compose pipeline on every push to
main that touches it — account unistream, container delamain, blob name docker-compose.yml.
That published copy is the canonical one for the production channel: it is what the Installer
downloads when it updates itself. The development variant of the file goes to container
delamain-dev as docker-compose.dev.yml and must not be used on a customer host.
The file expects the repository layout to be reachable, because the backend and backend-init
services declare both an image: and a build: section. On a normal install the published image is
used; see step 3.
The production compose file sets no name: key, so the compose project is named after the
directory that contains the file. Do not rename /opt/delamain after the first start: the
self-update and the audit trail both work from the labels that were stamped at creation time.
.envCopy the example file and edit it:
cp .env.example .env
.env is used by Compose twice over: for ${VAR} interpolation in the compose file and as the
runtime environment of the containers.
| Variable | Default in .env.example | Meaning |
|---|---|---|
VERSION | latest | Image tag for unistream.azurecr.io/delamain:<VERSION>. When VERSION is unset, the compose file falls back to prod-latest. Set it to the tag you are installing. |
DOCKER_SOCK | /var/run/docker.sock | Host Docker socket the Installer drives. Use the rootless socket path on a rootless host. |
NEST_PORT | 8000 | Port the API and UI listen on, published on the host |
NEST_NODE_ENV | prod | dev or prod; prod reduces the log levels |
SWAGGER_ENABLED | false | Serve the OpenAPI UI at /api/docs |
TZ | UTC | Container timezone |
NEST_TYPEORM_LOGGING | false | Log every SQL statement |
NEST_ORIGINS | http://localhost:8000 | CORS allowlist. Irrelevant in the default setup: the Installer serves the UI same-origin |
POSTGRES_USER | postgres | Database user |
POSTGRES_DATABASE_NAME | delamain | Database name |
JWT_ACCESS_EXPIRY | 30m | Access-token lifetime |
JWT_REFRESH_EXPIRY | 7d | Refresh-token lifetime |
Do not put these in .env. Each is generated or supplied elsewhere, and adding it by hand
creates a value the system does not expect:
| Variable | Where its value actually comes from |
|---|---|
POSTGRES_PASS | Read from secrets/pg.pass inside the delamain_secrets volume, generated by the PostgreSQL container on first start |
JWT_SECRET | Generated by the backend into secrets/jwt.secret on first boot |
NEST_ENCR_KEY | Generated by the backend into secrets/encr.key on first boot; encrypts the stored system secrets |
CURATO_SERVICE_TOKEN | Generated by the backend into secrets/curato.token and written into the deployment .env by the Installer |
AZURE_CLIENT_SECRET | Entered in the Installer UI by an administrator; stored encrypted in the Installer's database |
BLOB_ARTIFACT_VARIANT | Never set on a client host — it switches the Installer onto the development artifacts. Leave it unset for the production pair |
POSTGRES_HOST, POSTGRES_PORT | The comment in .env.example is explicit: "POSTGRES_HOST/PORT are injected by compose (the postgres service name)" |
Verify:
grep -E '^(VERSION|DOCKER_SOCK|NEST_PORT)=' .env docker compose config --quiet && echo 'compose file and .env resolve cleanly'
The compose file encodes the startup order with dependency conditions, so one command performs the whole sequence:
postgres starts and must pass its health check.backend-init runs only after postgres reports
condition: service_healthy, and must exit successfully.backend runs only after postgres is healthy and backend-init has
completed successfully.docker compose pull docker compose up -d
docker compose pull requires credentials for unistream.azurecr.io on the host. See
Registry and Azure access for what the repository
does and does not document about that. Without a working pull, Compose falls back to building the
backend image from this repository, which needs the full checkout and the pnpm toolchain.
To run the same order explicitly — useful when you want to see each stage finish:
docker compose up -d postgres docker compose up backend-init # one-shot migration container, exits 0 docker compose up -d backend
| Stage | What it does |
|---|---|
| PostgreSQL password | The postgres service's entrypoint checks /run/secrets/pg.pass. If the file is missing or empty it writes 24 random bytes, hex-encoded, with umask 077, then starts PostgreSQL with POSTGRES_PASSWORD_FILE=/run/secrets/pg.pass. The password is generated once and reused on every later start. |
| Migrations | backend-init runs npx typeorm -d dist/src/typeorm.config.js migration:run and exits. It has no restart policy (restart: 'no') — it is a one-shot. It authenticates with the same pg.pass file, read from the mounted secrets volume when POSTGRES_PASS is not set in the environment. |
| Installer secrets | Before the application is created, the backend runs its secrets bootstrap: it generates jwt.secret (64 random bytes, hex, mode 0600) if absent, encr.key (32 random bytes, base64, mode 0600) if absent, and curato.token (48 alphanumeric characters) if absent. It then loads pg.pass and fails to start if that file is missing. Success is logged as JWT secret generated and saved, Encryption key generated and saved and curato service token generated and saved. |
| HTTP surface | The application mounts everything under the global prefix api, sets a Helmet content-security policy, parses cookies, enables CORS from NEST_ORIGINS, and serves the built single-page app from /usr/src/app/client for every route except /api/*. Swagger is registered at /api/docs only when SWAGGER_ENABLED is true. |
| Health checks | postgres runs pg_isready -U <POSTGRES_USER> every 10s, 5s timeout, 5 retries. backend runs wget -qO- http://localhost:<NEST_PORT>/api/health every 10s, 5s timeout, 5 retries, with a 20s start period. Both services use restart: unless-stopped; backend-init does not restart. |
Container states:
docker compose -f /opt/delamain/docker-compose.yml ps
Expected: postgres is Up (healthy), backend-init is Exited (0), backend is Up (healthy).
A backend-init container that exited non-zero is a migration failure — read its output with
docker compose logs backend-init.
Health endpoint — this is the endpoint the container's own health check calls:
curl -sS http://localhost:8000/api/health
Expected response:
{"status":"ok","db":"up"}The endpoint is public and verifies the database connection with a SELECT 1. If the database is
unreachable it returns HTTP 503 with Database connection failed and logs
Health check failed — DB unreachable. A healthy Installer therefore guarantees that both the API
and its database are up.
UI:
http://<host>:8000/
Replace 8000 with your NEST_PORT. The UI is served by the same container as the API, on the same
origin — no separate web server and no CORS configuration are needed.
Swagger (only if enabled):
http://<host>:8000/api/docs
The shipped .env.example sets SWAGGER_ENABLED=false, so this is off on a default install. Turn it
on only for a debugging session, and turn it back off: it publishes the full API surface.
The Installer is now running, but it has deployed nothing yet. Sign in (First login), then:
Choose a setup mode. The Status screen offers First-time install or Migrate from Jenkins. Choose the second only when there is a legacy Jenkins-managed installation on this host to retire.
Enter the platform details. The platform URL is required. The Azure client secret is required unless one is already stored. The migration path additionally requires the legacy VM user, and optionally the SSL certificate and key paths.
Save & continue. This stores the Azure client secret, downloads the environment schema and the
DataMind OS docker-compose.yml into the deployment directory (which seeds the configuration
records), normalises and saves the platform URL, and — on the migration path — runs the host
retirement and imports the Jenkins configuration.
Review the configuration. The configuration editor opens with the seeded values grouped by
area. Check the platform URLs, ports and the paths that still contain a <USER> placeholder.
Start the installation. This runs the deploy job:
docker compose up -d --pull never --remove-orphans for the resolved service list;The job streams to the screen and can be cancelled. Every action is recorded in the deployment audit trail with the acting user.
Where the deployment files land. Inside the backend container they are at
/usr/src/app/deployment; on the host that is the deployment/ directory beside the Installer's
compose file:
| Host path | Contents |
|---|---|
/opt/delamain/deployment/docker-compose.yml | The DataMind OS stack, re-downloaded on every deploy |
/opt/delamain/deployment/.env.unified.template | The environment schema that seeds the configuration |
/opt/delamain/deployment/.env | Generated by the Installer from the configuration records; the only file the stack reads |
/opt/delamain/deployment/docker-compose.override.yml | Optional and user-managed. Merged into every compose command when present, and never written by the Installer |
Verify the deployed stack:
docker compose -f /opt/delamain/deployment/docker-compose.yml ps docker compose -f /opt/delamain/docker-compose.yml logs --tail 50 backend
The Status screen should show every service running, with its health and uptime. If a service is not running, open it in the service list to read its container logs and its last job output.
POST /api/deployment/compose-up refuses to start a stack whose images are not yet local, with
Missing local images: <list>. Run Update to pull them first. A plain Start, after an image
change, is not a substitute for an update.
Next: First login.