# Deploy the solution

> Deploy Self-Hosted Deployment on your Kubernetes cluster using shdctl

Before deployment, check the [prerequisites](./prerequisites.md).

The deployment uses shdctl to manage the entire release lifecycle. If all prerequisites are met, the deployment takes 30 to 60 minutes. This time doesn't include the subsequent [administration tasks](/self-hosted-deployment/admin.md), such as setting up an identity provider for single sign-on (SSO).

> **Note:**
>
> For an end-to-end overview of how shdctl orchestrates the deployment (data flow, components, and trust boundaries), refer to [shdctl architecture](../../shdctl/architecture). This is useful for security teams reviewing the deployment.

Follow these steps to deploy Self-Hosted Deployment to your Kubernetes cluster.

## 1. Configure the registry credentials

Before you can pull releases, configure your ORAS registry credentials. This is a one-time setup.

Run the interactive configuration:

```sh
shdctl configure
```

For non-interactive setup and credential verification, refer to [Configure registry credentials](../../shdctl/install#configure-registry-credentials).

## 2. Initialize the manifest file

The manifest file `manifest.yaml` contains your deployment configuration. shdctl automatically discovers `manifest.yaml` in the current directory or in the parent directories.

If you do not have a manifest file, create one:

```sh
shdctl manifest init
```

The interactive prompts ask for the release version, the source registry and the registry to pull images from, the application domain, the Kubernetes namespace, the image pull secret, the autoscaling settings, the storage classes, the monitoring options, the sizing profile, the Traefik service type, and, if ArgoCD is installed, its repository URL, destination server, and target revision. They don't ask for `deployment.argocd.pathPrefix`: add it yourself, as [option B](#option-b:-argocd-deployment) describes.

### Validate the manifest

To validate your manifest against the built-in schema before continuing, run this command:

```sh
shdctl manifest validate
```

This command checks the required fields, the allowed values, and the cross-field rules (for example, `maxReplicas >= minReplicas`).

### Configure the manifest file

The manifest file controls the entire deployment. Here is a minimal skeleton:

```yaml
releaseVersion: 2.0.0

artifactSync:
  sourceRepository: uccmpprivatecloud.azurecr.io

configuration:
  networking:
    appDomain: example.com
    ingress:
      traefik:
        type: LoadBalancer
  kubernetes:
    namespace: asset-solutions
    docker:
      repository: <your-registry-url>
      namespace: <your-registry-namespace>
    imagePullSecret:
      name: regcred
    autoscaling:
      minReplicas: 1
      maxReplicas: 10
    storage:
      defaultStorageClass: <your-default-storage-class>
      readWriteManyStorageClass: <your-rwx-storage-class>
    # Optional. Annotations added to every pod the release creates. On AKS with
    # an HTTP proxy, this one keeps the proxy variables out of the platform pods:
    # podAnnotations:
    #   kubernetes.azure.com/no-http-proxy-vars: "true"
  infrastructure:
    sizing: medium
```

If your cluster sends its outbound traffic through an HTTP proxy, also give the automation-manager job the proxy, as [Clusters behind an HTTP proxy](./prerequisites.md#clusters-behind-an-http-proxy) describes.

For the full schema, all configurable sections (networking, kubernetes, transformations, monitoring, authentication, infrastructure, object storage), and the `deployment.argocd` options, refer to the [shdctl manifest reference](../../shdctl/manifest).

To terminate TLS in the cluster, create the TLS Secret and set `configuration.networking.ingress.traefik.tls` before you generate the charts, as [step 8](#8.-configure-the-dns-and-certificates) describes. Otherwise, you generate and deploy the charts a second time.

## 3. Validate the cluster

Verify that the cluster your current kubeconfig context points at meets the [prerequisites](./prerequisites.md):

```sh
shdctl cluster check
```

The command checks:

* The Kubernetes version.
* The number of schedulable nodes.
* The transformation node pools.
* The cluster's default storage class, and the storage classes your manifest names.
* The target namespace.
* The metrics API.
* The in-cluster ArgoCD installation if your manifest configures ArgoCD. On the Helm deployment path those checks are reported as skipped.

The command reads from the cluster and doesn't change it, and it prints a remediation hint for every check that doesn't pass. Resolve every failure before you continue. One failure is expected: if an autoscaler other than Karpenter scales the transformation node pool from zero, that pool's check fails while the pool is empty. Resolve the metrics API warning too: the command only warns about it, but the platform's horizontal pod autoscalers can't scale workloads without the API.

To verify that each storage class can provision a volume, including `ReadWriteMany` support, add `--probe-storage`. This creates a temporary 1 GiB test volume for each configured class and deletes it afterward:

```sh
shdctl cluster check --probe-storage
```

For the full list of checks and options, refer to [shdctl cluster](../../shdctl/commands/cluster).

## 4. Pull the release

Download the release package from the registry. The system reads the version from the manifest automatically:

```sh
shdctl release pull --clean-output
```

The system downloads the release archive and extracts it to `./extracted-release`.

For all flags (including `--version` to override the manifest version), refer to [shdctl release](../../shdctl/commands/release).

## 5. Sync the artifacts

Before deploying, sync the Docker images and the ORAS artifacts from the Unity source registry to your private registry.

> **Note:**
>
> If you pull container images directly from the Unity source registry rather than mirroring them to your own private registry, you can skip this step: set `configuration.kubernetes.docker.repository` to `uccmpprivatecloud.azurecr.io`, and leave out `docker.namespace`. Mirroring is recommended for air-gapped environments and for full control over image distribution.

### 5.1 Authenticate to both registries

You must authenticate to the source and target registries before syncing:

```sh
docker login uccmpprivatecloud.azurecr.io
docker login <your-registry-url>
```

### 5.2 Run the preflight check

Verify your credentials before running a full sync:

```sh
shdctl artifact sync preflight
```

This command syncs one Docker image and one ORAS artifact as a test. If the test fails, it prints troubleshooting hints.

### 5.3 Sync the Docker images

Preview the sync commands first, then run the sync:

```sh
shdctl artifact sync images --dry-run
shdctl artifact sync images
```

### 5.4 Sync the ORAS artifacts

ORAS artifacts include OCI artifacts used by Helm charts. The sync reads authentication from the Docker credential store (`~/.docker/config.json`) automatically.

```sh
shdctl artifact sync oras --dry-run
shdctl artifact sync oras
```

For all `artifact sync images` and `artifact sync oras` flags (including `--skip-existing`, `--cleanup`, `--name`, `--concurrency`), refer to [shdctl artifact](../../shdctl/commands/artifact).

### 5.5 Verify the target registry

Check that your registry holds every image your manifest needs:

```sh
shdctl artifact sync verify
```

The command lists each missing image and exits with an error. Run it before every deployment from a mirrored registry.

## 6. Generate and deploy the secrets

### Generate the secrets

To generate secrets:

1. Create a secrets import file based on the example in the [shdctl secret command reference](../../shdctl/commands/secret#import-file-format).

   This file contains secret values organized by secret name. Enter values in plain text, except the license files of `pixyz-license`, `pixyz-license-3dds`, and `uvcs-license`, which you base64-encode first: shdctl encodes everything else for you. The example covers the secrets you supply or may want to set yourself: the credentials of the registry your cluster pulls images from (your own registry's if you mirror, Unity's if you pull directly), an optional custom CA certificate, the Pixyz licenses, Elasticsearch, Mini-USF, Novu, MongoDB, PostgreSQL, RabbitMQ, object storage credentials, the UVCS license and configuration, and Valkey. If you set `configuration.objectStore.provider: s3`, put your bucket's access key pair in the `s3-api-storage-credentials` entry: shdctl doesn't generate it, and with `--use-defaults` it writes `TBD` instead of prompting for it.

2. Generate the Kubernetes secret manifests:

   ```sh
   shdctl secret generate --import secrets.import.yaml --use-defaults
   ```

   By default, automatically-generated passwords are alphanumeric only. This limitation is to avoid breaking connection strings.

   > **Note:**
   >
   > With the `--use-defaults` option, the process sets any required field that has neither a default value nor an automatic-generation rule to `TBD` and logs a warning. Before deployment, replace all `TBD` values.

   The output also holds the image pull secret named by `configuration.kubernetes.imagePullSecret`, built from your registry credentials. If something else in your cluster creates that secret, such as a token refresher for a registry's short-lived credentials, set `imagePullSecret.generate: false` in the manifest first, so that deploying the secrets doesn't overwrite it. Refer to [The image pull secret](../../shdctl/commands/secret#the-image-pull-secret).

3. Persist the generated values (recommended). Every later run, such as an upgrade, must start from a file that holds every value the cluster uses: shdctl generates afresh any value the file lacks, and deploying it breaks the services that use the old one. Persist on the run whose output you deploy. The `--persist` option requires an explicit file path:

   ```sh
   shdctl secret generate --import secrets.import.yaml --use-defaults --persist secrets.import.yaml
   ```

   This command writes the generated values back to the file you name. Pass the same file to later runs, with `--import`, `--persist`, or both, and they reuse the persisted values, which keeps your deployments consistent across upgrades.

> **Note:**
>
> The custody of secrets is your responsibility. shdctl bootstraps the initial values into Kubernetes secrets, but Unity doesn't store, rotate, or back up your secrets. These files all contain sensitive values: `secrets.import.yaml`, the persisted file written by `--persist`, and the generated `secrets.yaml`. Don't commit these files to Git, but add them to your `.gitignore` file, store them encrypted at rest, and grant access to only the people and systems that need them.
>
> For production environments, keep the source of truth for these values in a secret manager that you already operate (HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, GCP Secret Manager, or CyberArk). Feed the values into the `secrets.import.yaml` file from your secret manager at install or upgrade time. This pattern keeps rotation, audit, and access control under your existing security policies.

### Deploy the secrets

Preview the deployment, then deploy the secrets to your cluster:

```sh
shdctl secret deploy --dry-run
shdctl secret deploy
```

For all flags, refer to [shdctl secret](../../shdctl/commands/secret).

## 7. Generate and deploy the charts

Choose your deployment method: **Helm** or **ArgoCD**.

### Option A: Helm deployment

Generate the Helm charts:

```sh
shdctl release generate --clean-output
```

Preview the deployment first:

```sh
shdctl release deploy --dry-run
```

For a first-time deployment, deploy the release in waves, starting with the lowest wave, for example, `-3`. Validate each wave before deploying the next to ensure all dependencies are satisfied.

```sh
shdctl release deploy --wave -3
shdctl release deploy --wave -2
shdctl release deploy --wave -1
shdctl release deploy --wave 0
shdctl release deploy --wave 1
shdctl release deploy --wave 2
```

To deploy all charts at once:

```sh
shdctl release deploy
```

The deployment waits for the monitoring stack before it installs the rest of the release, and allows 10 minutes for each component. If a slow first image pull exceeds that, run the deployment again with a longer allowance, for example `shdctl release deploy --timeout 20m`.

#### Verify the deployment

To check the status of the deployments:

```sh
kubectl get pods -n <namespace> --watch
```

Ensure that all pods eventually move to the **Running** state, except the pods of one-off jobs, such as `novu-manager-job`, which finish as **Completed**. If a pod is in any other state, describe it and check its logs to find the issue.

### Option B: ArgoCD deployment

If the `deployment.argocd` section is set in the manifest, generate the ArgoCD charts. ArgoCD deploys from a Git repository, so run the commands below at the root of a clone of the repository that `deployment.argocd.repoURL` names, on the branch that `targetRevision` names, and set `deployment.argocd.pathPrefix: generated-charts`, the directory that the charts are generated into and committed under:

```sh
shdctl release generate --format argocd --clean-output
```

Commit the generated charts:

```sh
git add generated-charts/
git commit -m "Add release charts"
git push
```

Preview and run the deployment:

```sh
shdctl release deploy --format argocd --dry-run
shdctl release deploy --format argocd
```

For all `release generate` and `release deploy` flags, refer to [shdctl release](../../shdctl/commands/release).

## 8. Configure the DNS and certificates

Configure DNS records and TLS certificates for your domain to make the solution secure and accessible.

1. Register a fully qualified domain name (FQDN) for the frontend, for example, `example.com`.

2. Point the DNS record to the external IP or hostname of the Traefik load balancer. To find this information, run this command:

   ```sh
   kubectl get svc -n <namespace> traefik
   ```

3. Configure a TLS certificate for the domain. To terminate TLS in the cluster, create a Kubernetes TLS Secret for the certificate in the deployment namespace, and set `configuration.networking.ingress.traefik.tls` in the manifest to `enabled: true` and `certificate: <secret-name>`. To terminate TLS at your load balancer instead, reference the certificate through the Traefik load balancer annotations in the manifest, or configure it on the load balancer itself. If you change the manifest, generate and deploy the charts again.

## Troubleshooting

If a step fails, refer to [shdctl troubleshooting](../../shdctl/troubleshooting) for common issues and resolutions.

## Next steps

[Postdeployment](./postdeployment.md)
