Install the server

This operations guide walks administrators through the first installation of a self-hosted Workspace server. You are done when /health/ready returns HTTP 200 and the System Admin sign-in works. Tenant creation and tenant initialization remain separate follow-up tasks.

Quick decision: the sign-in page is visible, but sign-in fails

Do not automatically download another release. The sign-in page may be visible while Workspace is still completing the first installation in a fail-closed state. Treat Liveness, Readiness, and Product Release as separate signals:

ObservationMeaningSafe next action
/health/live returns HTTP 200, /health/ready returns HTTP 503The process is alive, but sign-in and business APIs are not released.Keep the running release and inspect Product Release status.
releaseStatus: retry_waitWorkspace is waiting for the next controlled retry of a transiently failed step.Watch nextAttemptAt, the component, and actionKey; do not select another release.
releaseStatus: blockedA named product or persisted-state contract prevents the transition.Complete the reported action and continue with the same target release.
releaseStatus: failed or product_release_component_failedThe running binary cannot complete its release contract safely.Capture safe diagnostic fields and use only a provider-named forward fix.
HTTP 401 from protected statusThe session or System token is missing or invalid for this request.Pass the bootstrap System API key through --token-stdin only. Do not diagnose the user password from this response.
TLS validation failsThe client does not trust the issuing CA.Trust the CA locally or point SSL_CERT_FILE to the CA file; do not disable TLS verification.
pointOfNoReturnAt is set, even when schemaTransitionClass shows the last compatible individual stepThe database no longer permits an older runtime.Keep the target selected and repair forward, or restore the complete protected state. Never start the old binary.

A shortened maintenance response typically looks like this:

json
{
  "code": "product_release_not_ready",
  "live": true,
  "ready": false,
  "releaseStatus": "retry_wait",
  "runId": "<run-id>",
  "status": "product_release_maintenance"
}

Always read live and ready from the dedicated /health/live and /health/ready endpoints as well. A maintenance payload from another route does not replace those health checks. Release the installation only when /health/ready returns HTTP 200.

Choose an installation path

GoalRecommended pathEntry point
Test locally with DockerDocker bundle with PostgreSQL 18./start.sh start --detach
Operate Debian or Ubuntu.deb package with systemdnucleus-server-onboard
Operate RHEL, Fedora, or compatible Linux.rpm package with systemdnucleus-server-onboard
Use another glibc-based Linux amd64 hosttar.gzsudo ./install.sh
Operate KubernetesKustomize bundlekubectl apply -k base

nucleus-server onboard combines configuration, root-key bootstrap, server startup, and product release progress. It stays in the foreground. Running it again verifies the existing config and starts the server without replacing the key or configuration.

first_installation configure Configuration and root key release Product release configure->release ready /health/ready = 200 release->ready credentials Secure first-access handoff ready->credentials login System Admin sign-in credentials->login

Prerequisites

Prepare:

  • an approved Workspace image or nucleus-server package;
  • PostgreSQL 15 through 18; the Docker bundle provisions PostgreSQL 18 for new installations;
  • for native updates, PostgreSQL client tools from the same major version as the database or newer; for PostgreSQL 17 on Rocky Linux 9, for example, install the postgresql17 package from your approved PostgreSQL repository;
  • persistent storage for configuration, root key, and files;
  • a public administration URL for the self-hosted profile;
  • a secret manager for the root key and initial credentials.

Start Docker with one command

The release bundle includes docker/compose.yaml and docker/start.sh. Extract it. The release bundle already uses the matching version tag. Pin the approved immutable digest for a production installation:

bash
cd docker
export NUCLEUS_IMAGE=git.schukai.me/releases/nucleus@sha256:<digest>
./start.sh start --detach
./start.sh status

The script creates secrets/postgres-password once with mode 0600. .env contains only the local user and group IDs. The script creates data/ and backups/, starts PostgreSQL 18 and the Workspace Server, and waits for Readiness in detached mode. It never deletes volumes or overwrites the secret, root key, or configuration.

Open the URL printed by the start script only after Readiness succeeds. Trust the generated self-signed certificate only in this local test environment.

If Readiness does not succeed, ./start.sh doctor shows the container state, Liveness, and relevant lifecycle messages. product_release_maintenance does not mean that the password is wrong: the process is alive but intentionally keeps sign-in and business APIs closed.

The script waits for up to 30 minutes by default and reports lifecycle progress every 30 seconds. Set NUCLEUS_STARTUP_TIMEOUT_SECONDS to a higher value on intentionally slow hardware. A terminal lifecycle failure still stops the wait immediately.

Install a package or binary

Verify the package and checksums from the same release:

bash
sha256sum -c SHA256SUMS

Install the matching artifact:

bash
# Debian or Ubuntu
sudo apt install ./nucleus-server_<version>-1_amd64.deb

# RHEL, Fedora, or compatible Linux
sudo rpm -Uvh nucleus-server-<version>-1.x86_64.rpm

# Generic archive
tar -xzf nucleus-server-<version>-linux-amd64.tar.gz
cd nucleus-server-<version>-linux-amd64
sudo ./install.sh

All three native paths:

  • creates the nucleus system user and protected directories;
  • stages the immutable version under /opt/nucleus/releases/<version>;
  • installs a hardened systemd unit and diagnostic helpers;
  • does not start the service;
  • does not migrate an existing database during package installation.

Review /etc/nucleus/nucleus.env, especially the database host and public URL. Then pass the database password once through stdin:

bash
read -r -s NUCLEUS_DB_PASSWORD_INPUT
printf '%s\n' "$NUCLEUS_DB_PASSWORD_INPUT" |
  sudo nucleus-server-onboard --db-password-stdin
unset NUCLEUS_DB_PASSWORD_INPUT

Watch startup:

bash
journalctl -u nucleus-server -f
sudo nucleus-server-doctor

nucleus-server-onboard waits up to 30 minutes for Readiness by default and reports lifecycle progress every 30 seconds. A terminal failure stops the wait immediately and runs the safe diagnostic path. Set NUCLEUS_STARTUP_TIMEOUT_SECONDS to a higher value on intentionally slow hardware. Pass the value explicitly through the native root invocation, for example with sudo env NUCLEUS_STARTUP_TIMEOUT_SECONDS=3600 nucleus-server-onboard --db-password-stdin.

Choose an onboarding profile

ProfileRequired inputDerived values
localPostgreSQL connectionlocal HTTPS URL, TLS files, storage, and system domains
self-hostedPostgreSQL connection, public admin URL, and TLS modesystem domain, listen target, and default paths

After install.sh, the archive uses the same lifecycle as DEB and RPM. You do not have to create users, symlinks, or systemd units. Set the profile and connection values in /etc/nucleus/nucleus.env, read the database password without echo, and pass it once through stdin:

bash
read -r -s NUCLEUS_DB_PASSWORD_INPUT
printf '%s\n' "$NUCLEUS_DB_PASSWORD_INPUT" |
  sudo nucleus-server-onboard --db-password-stdin
unset NUCLEUS_DB_PASSWORD_INPUT

Do not set NUCLEUS_DB_PASSWORD, NUCLEUS_DB_PASSWORD_FILE, and --db-password-stdin at the same time. Workspace rejects ambiguous secret sources.

The self-hosted profile accepts generated, existing, or disabled as its TLS mode. Use disabled only behind controlled TLS termination; the public URL must then use http:// for the internal listen contract.

Secure the first-access credentials

The server writes the one-time Admin password and initial System API key to a private JSON file with mode 0600. The file contains exactly email, password, and apiKey. After a successful handoff, runtime logs show only the path and never print the secret values.

Installation pathCredential file
Dockerdata/bootstrap-credentials.json in the extracted bundle
DEB, RPM, or archive/var/lib/nucleus/runtime/bootstrap-credentials.json
Kustomize/data/bootstrap-credentials.json in the server pod and PersistentVolume

Move the file into your secret manager immediately. Then verify System Admin sign-in and remove the bootstrap artifact from its original location. A restart does not create it again and never overwrites an existing file.

For Kustomize, copy the file from a protected administration shell without sending its content to pod logs:

bash
umask 077
kubectl -n nucleus exec deployment/nucleus -- \
  cat /data/bootstrap-credentials.json > ./bootstrap-credentials.json
# After handoff and successful sign-in:
kubectl -n nucleus exec deployment/nucleus -- \
  rm -f /data/bootstrap-credentials.json

Do not paste this file into support tickets or diagnostic output. If it is lost before the first sign-in, use the documented local Admin password reset with the matching numin; do not restart bootstrap or edit the database.

Understand product release progress

Before the product release gate starts, the server creates the initial System Admin and a System API key. Read the key only from the protected credential file. It lets you inspect product release status before Readiness succeeds.

The sign-in page may already be visible while the gate is active. A visible page does not mean that sign-in or business APIs are ready. Use /health/ready as the only release criterion.

Check the limited public state:

bash
curl -sk https://127.0.0.1:8443/health/live
curl -sk https://127.0.0.1:8443/health/ready
curl -sk https://127.0.0.1:8443/health/product-release

If the gate waits or fails, inspect the protected status with the initial System API key. stdin keeps the key out of a nucli profile:

bash
read -r -s NUCLEUS_BOOTSTRAP_TOKEN
printf '%s\n' "$NUCLEUS_BOOTSTRAP_TOKEN" |
  nucli \
    --host https://workspace.example.com \
    --tenant system \
    system product-release status \
    --watch \
    --token-stdin
unset NUCLEUS_BOOTSTRAP_TOKEN

For a local certificate, trust its issuing CA in the operating system. For the self-signed certificate generated by onboarding, use nucleus.crt as the local trust anchor and point SSL_CERT_FILE to that file when required. Never point it at nucleus.key.

numin product-release status --json remains the local read-only fallback for an explicitly privileged environment with config, key, and PostgreSQL access. Use nucli first whenever the server is reachable.

If sign-in is not available or the target version cannot expose the protected status, use only the numin shipped with that running or staged version:

bash
sudo env NUCLEUS_KEY_FILE=/var/lib/nucleus/runtime/nucleus-key \
  /opt/nucleus/releases/<version>/numin \
  --config /var/lib/nucleus/runtime/nucleus.config.enc \
  deployment plan --json
sudo env NUCLEUS_KEY_FILE=/var/lib/nucleus/runtime/nucleus-key \
  /opt/nucleus/releases/<version>/numin \
  --config /var/lib/nucleus/runtime/nucleus.config.enc \
  schema migrations status --json

These commands are read-only. Do not use numin from an older release or apply direct SQL repairs.

Let an agent assist safely

Give an agent the built-in deployment skill first:

bash
nucli skills show deployment

An MCP client can read the same contract through nucleus_deployment_skill, then call nucleus_deployment_assess. The assessment is local and read-only. It needs neither a login nor a live server connection and does not change the installation. Pass only normalized observations such as operation, live, ready, releaseStatus, errorCode, HTTP status, TLS failure, transition class, and point-of-no-return state. Never pass passwords, tokens, credential files, certificate keys, or unfiltered logs.

The assessment returns a classification, versionAction, recoveryMode, specific next steps, and stop conditions. These values deterministically classify the observations you supplied. They do not replace server status, a signed release contract, or an approval.

Interpret failures

SymptomMeaningNext action
product_release_maintenanceThe process is alive but not ready.Watch the protected status with --watch --token-stdin.
retry_waitWorkspace will retry a transient step failure with backoff.Inspect phase, nextAttemptAt, component, and actionKey.
blockedA product or customer-owned conflict prevents the transition.Follow the reported action; do not edit the database directly.
failedThe binary or release contract cannot continue safely.Provide the stable code, version, and component to the release provider.
product_release_component_failedAt least one release component could not complete its contract.Capture the component, stable action keys, and target version with doctor, nucli, or the matching numin.
HTTP 401 for statusThe token is missing, invalid, or not bound to the System tenant.Verify the initial System API key and pass it again through stdin only.
TLS validation failsThe client does not trust the issuing CA.Trust the CA or point SSL_CERT_FILE to the CA file.

Do not download or select another release just because you see retry_wait or product_release_maintenance. These states first mean that the running release is still working or waiting for a safe retry. Keep that release selected and capture the doctor and product-release status. Switch only to a forward fix named by the provider. Never start an older release after the status reports that the point of no return has been reached.

Protected diagnostics expose stable error, reason, and action keys without internal error details or secrets.

Install with Kubernetes

The release contains a Kustomize bundle. Copy secret.example.yaml, replace the placeholder outside version control, and edit the database host and public URL in base/configmap.yaml. Digest pinning is mandatory:

bash
cd kustomize/base
kustomize edit set image \
  git.schukai.me/releases/nucleus=git.schukai.me/releases/nucleus@sha256:<digest>
kubectl apply -f ../secret.yaml
kubectl apply -k .
kubectl -n nucleus rollout status deployment/nucleus --timeout=30m

The PersistentVolume stores encrypted configuration, runtime key, and storage. Back it up together with PostgreSQL. The update helper runs its preflight with numin from the exact same image digest. The supplied Deployment and preflight Job disable automountServiceAccountToken. Neither process needs Kubernetes API credentials, and this keeps the reserved /run/secrets path available for the Workspace secret volume.

Hand the root key to a secret manager

Onboarding creates the root key once with mode 0600. Use this order:

  1. Copy the exact bundle into your secret manager.
  2. Mount it as /run/secrets/nucleus-key.
  3. Set NUCLEUS_KEY_FILE=/run/secrets/nucleus-key.
  4. Restart the Workspace Server and verify the reported fingerprint and /health/ready.
  5. Remove the local bootstrap file only after verification succeeds.

An explicitly selected but missing key file stops the server. The Workspace Server does not silently fall back to an environment variable or local bootstrap file.

Verify success

The first installation is complete when:

  • /health/live returns HTTP 200;
  • /health/ready returns HTTP 200;
  • /health/product-release reports succeeded;
  • System Admin sign-in works;
  • the root key, initial password, and initial System API key are stored safely.

Create and initialize a tenant only after these checks. That separate tenant contract is not part of server first installation.