The production API image contains two operator entrypoints: database migration and one-time administrator bootstrap. There is no general customer message-operation CLI; use the UI or REST API.
Run these as separate, explicitly started Azure Container Apps jobs. API/Worker images have no shell or package manager. The executable is /nodejs/bin/node; do not override the command with sh, pnpm or a PATH-dependent node.
Database migration
Use the approved 1.7.1 API image and the migration identity/database secret. The reference job runs from /app with:
command: [/nodejs/bin/node]
args:
- --import
- file:///app/apps/api/dist/observability/otel-preload.js
- apps/api/dist/cli/migrate.js
Supply NODE_ENV=production and DATABASE_URL (or DATABASE_URL_FILE), with applicable database CA trust. The migrator does not require the API’s runtime encryption keys. It applies ordered application migrations, installs/upgrades the PostgreSQL job queue, creates configured queues and grants the restricted runtime role access. It exits 0 on success, including an already-current database, or 1 on failure. It retries an initial unavailable connection 30 times with one-second delays.
Set RESOURCE_GROUP and MIGRATION_JOB to your resource names:
EXECUTION=$(az containerapp job start -g "$RESOURCE_GROUP" -n "$MIGRATION_JOB" --query name -o tsv)
az containerapp job execution show -g "$RESOURCE_GROUP" -n "$MIGRATION_JOB" \
--job-execution-name "$EXECUTION" --query properties.status -o tsv
Repeat the status query for that execution until it is terminal. Continue only for Succeeded, with Database migrations are up to date. in its logs and no migration errors. Use the named execution’s Portal/Log Analytics logs; completed replicas may no longer provide live streaming.
Verify the migration ledger matches the release, platform_settings and observability_destinations exist, and the job-queue schema/queues are installed. A SQL-only partial success is insufficient. If a later queue/grant stage fails, correct the cause and rerun the same release. Unknown migration versions indicate a release/database mismatch; never edit the ledger or drop production data to bypass it.
Runtime database login
The first migration creates message_ops_app as a restricted NOLOGIN role. Enable it only after migration succeeds. Using a protected PostgreSQL service/passfile or interactive prompt from an approved private-network host, connect as the migration owner:
PSQL_HISTORY=/dev/null psql 'service=message-ops-migration'
The named PostgreSQL service must already contain your host/database/user/TLS configuration; it is not installed by Message Ops. Then run:
ALTER ROLE message_ops_app LOGIN;
\password message_ops_app
GRANT CONNECT ON DATABASE message_ops TO message_ops_app;
Choose an independently generated password at the private prompt. Do not grant ownership, CREATE, CREATEROLE, CREATEDB or superuser rights. Build a percent-encoded TLS URL in a protected file. This operator-host example needs Python 3:
umask 077
python3 -c '
import getpass, os, warnings
from urllib.parse import quote
warnings.simplefilter("error", getpass.GetPassWarning)
host = input("PostgreSQL FQDN: ").strip()
if not host:
raise SystemExit("PostgreSQL FQDN is required.")
password = getpass.getpass("Runtime database password: ")
encoded_password = quote(password, safe="")
url = f"postgresql://message_ops_app:{encoded_password}@{host}:5432/message_ops?sslmode=verify-full"
fd = os.open("runtime-db-url.txt", os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
with os.fdopen(fd, "w") as output:
output.write(url)
' &&
az keyvault secret set --vault-name "$VAULT" --name "$RUNTIME_DB_SECRET" \
--file runtime-db-url.txt --output none &&
rm -f runtime-db-url.txt
Set VAULT and RUNTIME_DB_SECRET to the intended vault and runtime-secret names before running the block in an interactive terminal. Password input is hidden; the example refuses an insecure non-terminal fallback. The upload runs only after successful file creation, and cleanup runs only after a successful upload. An existing output file is never overwritten. If generation or upload fails, stop, investigate and handle any retained file under your approved temporary-workspace cleanup policy. Keep the migration URL distinct. Connect as the runtime role and check:
SELECT current_user;
SELECT has_schema_privilege(current_user, 'public', 'CREATE') AS public_ddl,
has_schema_privilege(current_user, 'pgboss', 'CREATE') AS queue_ddl,
has_function_privilege(current_user, 'purge_audit_events(timestamptz)', 'EXECUTE') AS audit_purge;
Expect message_ops_app, then false, false, true. Check reads of platform settings, role flags in pg_roles, and a completed queued test operation after activation. The migration owner remains responsible for future object/default privileges.
Administrator bootstrap
After migration and runtime credential verification, run apps/api/dist/cli/bootstrap-admin.js once with the runtime database credential. The CLI does not start the API server or create a browser session.
| Argument | Requirement |
|---|---|
--email | Founding administrator’s sign-in email. |
--display-name | Administrator display name. |
--password-file | Required for a non-interactive job; readable mounted secret file. |
--help or -h | Show usage. |
--password is rejected. Without a file, only a real interactive TTY may enter and confirm a password; piped stdin is not accepted. Password files preserve spaces and remove exactly one trailing LF or CRLF. Passwords must be 15–200 characters and pass the common/default/identity-derived password checks.
Create a temporary vault secret from a protected password file using az keyvault secret set --file ... --output none. Grant the application identity read access to that temporary secret and make the mount readable by UID 65532. Save this template as bootstrap-job.yaml, replacing every placeholder with your resource names, URIs, identity and approved digest. No placeholder should contain a raw secret:
location: <azure-region>
identity:
type: UserAssigned
userAssignedIdentities:
<app-identity-resource-id>: {}
properties:
environmentId: <container-app-environment-resource-id>
configuration:
triggerType: Manual
replicaTimeout: 300
replicaRetryLimit: 0
manualTriggerConfig:
parallelism: 1
replicaCompletionCount: 1
registries:
- server: <acr>.azurecr.io
identity: <app-identity-resource-id>
secrets:
- name: runtime-db-url
keyVaultUrl: https://<vault>.vault.azure.net/secrets/<runtime-db-secret>
identity: <app-identity-resource-id>
- name: admin-password
keyVaultUrl: https://<vault>.vault.azure.net/secrets/<temporary-bootstrap-secret>
identity: <app-identity-resource-id>
template:
containers:
- name: bootstrap
image: <acr>.azurecr.io/message-ops/api@sha256:<64-hex-digest>
command: [/nodejs/bin/node]
args:
- apps/api/dist/cli/bootstrap-admin.js
- --email
- <administrator-email>
- --display-name
- <administrator-display-name>
- --password-file
- /mnt/bootstrap/admin-password
env:
- name: DATABASE_URL
secretRef: runtime-db-url
- name: NODE_ENV
value: production
resources:
cpu: 0.5
memory: 1Gi
volumeMounts:
- volumeName: bootstrap-password
mountPath: /mnt/bootstrap
volumes:
- name: bootstrap-password
storageType: Secret
secrets:
- secretRef: admin-password
path: admin-password
Set BOOTSTRAP_JOB to a temporary job name and RESOURCE_GROUP to its resource group:
az containerapp job create -g "$RESOURCE_GROUP" -n "$BOOTSTRAP_JOB" --yaml bootstrap-job.yaml
BOOTSTRAP_EXECUTION=$(az containerapp job start -g "$RESOURCE_GROUP" -n "$BOOTSTRAP_JOB" --query name -o tsv)
az containerapp job execution show -g "$RESOURCE_GROUP" -n "$BOOTSTRAP_JOB" \
--job-execution-name "$BOOTSTRAP_EXECUTION" --query properties.status -o tsv
Add the database CA setting/mount to this job if the database uses a private CA. Follow the named execution to Succeeded and confirm Initial administrator provisioned successfully. in its logs. Sign in normally and complete mandatory MFA, then license activation and Application Setup.
| Exit code | Meaning |
|---|---|
| 0 | Administrator provisioned. |
| 1 | Internal or database connection failure. |
| 2 | Invalid arguments, missing input or password mismatch. |
| 3 | Installation already initialized. |
| 4 | An administrator already exists. |
| 5 | Password policy violation. |
After successful sign-in, remove the temporary job, password file, temporary secret access grant and secret under your retention policy. Preserve redacted execution evidence. Bootstrap is not a password reset or recovery command: it permanently marks initialization. Maintain more than one accountable administrator and saved MFA recovery methods. For lockout, use the controlled support process in Troubleshooting.