Complete every step on the server that will run the deployment, as a user who can run docker and
sudo. Each step ends with a verification you can run before moving on.
unistream Docker networkThe Installer and the DataMind OS stack it deploys both join the same Docker network, and both
compose projects declare it external — so neither of them creates it. Create it once, by hand:
docker network create unistream
This is not optional. Without it the first docker compose up fails immediately with:
network unistream declared as external, but could not be found
The reason it is created out of band rather than owned by one project is recorded in the compose file:
external on both sides, no compose down — including the one inside a
self-update — can delete the network from under the other stack.The Installer's backend also pins a network alias, delamain-backend, so that the stack can reach it
by a stable name without depending on the container name (which carries a -dev suffix on the
development stack).
Verify:
docker network inspect unistream --format '{{.Name}} driver={{.Driver}}'Expected output starts with unistream driver=bridge. If the network already exists, the create
command reports an error and changes nothing — that is safe to ignore.
Choose the directory that will hold the Installer's compose file and its .env. Its name becomes the
compose project name, because the production compose file sets no name: key. Pick a stable, obvious
name such as /opt/delamain and do not rename it later.
sudo mkdir -p /opt/delamain cd /opt/delamain
Place the Installer's docker-compose.yml and .env here (see Install).
Verify:
ls -l /opt/delamain docker compose version
docker compose version must print a v2 version, 2.19.1 or newer. This is the plugin invocation
(docker compose), not the legacy standalone docker-compose binary.
The Installer publishes one host port: NEST_PORT, 8000 by default. Nothing else may be listening
on it.
Verify:
ss -ltnp | grep ':8000' || echo 'port 8000 is free'
If something is already there, either stop it or set NEST_PORT to another free port in .env
before the first start.
The Installer's PostgreSQL is not published in the production compose file — it is only reachable on the compose network. If you need a published database port, use the development compose file, which is not intended for a customer host.
The backend container mounts the host Docker socket, and every deployment action runs through it.
Verify on a standard rootful host:
ls -l /var/run/docker.sock
If the socket is not there, or Docker runs rootless, find the correct path and set DOCKER_SOCK in
.env:
id -u # gives the UID for the rootless socket path ls -l /run/user/$(id -u)/docker.sock
Then keep the default:
DOCKER_SOCK=/var/run/docker.sock
or point it at the rootless socket, for example:
DOCKER_SOCK=/run/user/1000/docker.sock
The Installer must be started by docker compose. Its self-update reads the compose project name,
working directory and compose-file path from the running container's labels, and refuses to run
when they are absent.
Check the two destinations the Installer contacts before any deployment.
Verify Azure Container Registry reachability:
curl -sS -o /dev/null -w '%{http_code}\n' https://unistream.azurecr.io/v2/A 401 is the expected answer: the registry is reachable and is asking for credentials.
Verify Blob Storage reachability (an HTTP status other than a DNS or TLS failure is enough — the container is private):
curl -sS -o /dev/null -w '%{http_code}\n' https://unistream.blob.core.windows.net/docker/env.schema.jsonAlso confirm the host's Docker daemon can reach Docker Hub, directly or through your proxy, since some stack images resolve there. The full destination list is on Requirements.
These paths are mounted from the host by the Installer's own compose file. Docker creates a missing bind-mount source as root, but creating them explicitly lets you set ownership and permissions first.
| Host path | Purpose | Mode |
|---|---|---|
./deployment (next to the compose file) | Where the DataMind OS stack's docker-compose.yml, .env.unified.template and .env are written | read-write |
/var/tmp/delamain | The compose engine's TMPDIR. Must be the same path on host and in the container — the self-updater receives a staged compose file through it | read-write |
/opt/delamain (overridable with MIGRATE_HOST_DIR) | Where the host-migration script is placed on the host | read-write |
/var/lib/jenkins (overridable with JENKINS_HOME) | Read-only mount, so the Installer can import configuration from a legacy Jenkins config.xml | read-only |
Verify:
sudo mkdir -p /var/tmp/delamain /opt/delamain ls -ld /var/tmp/delamain /opt/delamain ls -ld /var/lib/jenkins 2>/dev/null || echo 'no Jenkins on this host - expected for a greenfield install'
/var/tmp/delamain is the one mount whose path must not be changed to something else: it is
documented in the code as working "only because docker-compose.yml bind-mounts TMPDIR at the SAME
path on host and container".
The first docker compose up creates two named volumes. You do not create them by hand, but you must
keep them together — they hold the state that cannot be regenerated.
| Volume | Mounted at | Holds |
|---|---|---|
pgdata | /var/lib/postgresql/data | The Installer's database: users, configuration, jobs, audit trail |
delamain_secrets | /run/secrets in PostgreSQL, /usr/src/app/secrets in the backend | pg.pass, jwt.secret, encr.key, curato.token |
The PostgreSQL password is generated once, into delamain_secrets, and the cluster is initialised
with it. The JWT signing secret and the encryption key are generated once, into the same volume,
and the encryption key is what makes the stored Azure client secret readable. Removing
delamain_secrets while the database survives leaves the Installer unable to connect to its own
database, unable to validate existing sessions, and unable to decrypt the stored client secret.
Verify after the first start (see Install):
docker volume ls | grep -E 'delamain_(secrets)|pgdata' docker compose -f /opt/delamain/docker-compose.yml ps
Next: Install.