Install

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.

Obtain the Installer's compose file

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:

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

Note

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.

Prepare .env

Copy the example file and edit it:

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

VariableDefault in .env.exampleMeaning
VERSIONlatestImage 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.sockHost Docker socket the Installer drives. Use the rootless socket path on a rootless host.
NEST_PORT8000Port the API and UI listen on, published on the host
NEST_NODE_ENVproddev or prod; prod reduces the log levels
SWAGGER_ENABLEDfalseServe the OpenAPI UI at /api/docs
TZUTCContainer timezone
NEST_TYPEORM_LOGGINGfalseLog every SQL statement
NEST_ORIGINShttp://localhost:8000CORS allowlist. Irrelevant in the default setup: the Installer serves the UI same-origin
POSTGRES_USERpostgresDatabase user
POSTGRES_DATABASE_NAMEdelamainDatabase name
JWT_ACCESS_EXPIRY30mAccess-token lifetime
JWT_REFRESH_EXPIRY7dRefresh-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:

VariableWhere its value actually comes from
POSTGRES_PASSRead from secrets/pg.pass inside the delamain_secrets volume, generated by the PostgreSQL container on first start
JWT_SECRETGenerated by the backend into secrets/jwt.secret on first boot
NEST_ENCR_KEYGenerated by the backend into secrets/encr.key on first boot; encrypts the stored system secrets
CURATO_SERVICE_TOKENGenerated by the backend into secrets/curato.token and written into the deployment .env by the Installer
AZURE_CLIENT_SECRETEntered in the Installer UI by an administrator; stored encrypted in the Installer's database
BLOB_ARTIFACT_VARIANTNever set on a client host — it switches the Installer onto the development artifacts. Leave it unset for the production pair
POSTGRES_HOST, POSTGRES_PORTThe comment in .env.example is explicit: "POSTGRES_HOST/PORT are injected by compose (the postgres service name)"

Verify:

bash
grep -E '^(VERSION|DOCKER_SOCK|NEST_PORT)=' .env
docker compose config --quiet && echo 'compose file and .env resolve cleanly'

Start the stack, in order

The compose file encodes the startup order with dependency conditions, so one command performs the whole sequence:

  1. Database first — postgres starts and must pass its health check.
  2. Migrations — backend-init runs only after postgres reports condition: service_healthy, and must exit successfully.
  3. Application — backend runs only after postgres is healthy and backend-init has completed successfully.
bash
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:

bash
docker compose up -d postgres
docker compose up backend-init      # one-shot migration container, exits 0
docker compose up -d backend

What happens on the first start

StageWhat it does
PostgreSQL passwordThe 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.
Migrationsbackend-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 secretsBefore 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 surfaceThe 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 checkspostgres 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.

Confirm the Installer is healthy

Container states:

bash
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:

bash
curl -sS http://localhost:8000/api/health

Expected response:

json
{"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:

text
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):

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

Install the DataMind OS stack

The Installer is now running, but it has deployed nothing yet. Sign in (First login), then:

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

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

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

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

  5. Start the installation. This runs the deploy job:

    1. download the compose file and environment schema;
    2. pull every service image, streamed per service with byte and layer progress;
    3. verify that every expected image is now present locally;
    4. docker compose up -d --pull never --remove-orphans for the resolved service list;
    5. prune dangling images.

    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 pathContents
/opt/delamain/deployment/docker-compose.ymlThe DataMind OS stack, re-downloaded on every deploy
/opt/delamain/deployment/.env.unified.templateThe environment schema that seeds the configuration
/opt/delamain/deployment/.envGenerated by the Installer from the configuration records; the only file the stack reads
/opt/delamain/deployment/docker-compose.override.ymlOptional and user-managed. Merged into every compose command when present, and never written by the Installer

Verify the deployed stack:

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

Tip

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.