Skip to content

On-Premise Deployment

What Is an On-Premise Deployment

An On-Premise deployment Plan allows you to package your Helm chart as a self-contained installer that your customers can deploy onto their own Kubernetes clusters. The Plan specification defines deployment requirements, API parameters, lifecycle hooks, and Helm chart configuration.

Note

The installer currently supports at most one Helm chart resource per Plan and one image registry for container image copy. If your application requires multiple Helm charts, bundle them into a single umbrella chart. If your images are spread across multiple registries, consolidate them into a single source registry before configuring the installer.

Note

The installer assumes that the customer running it has kubectl, helm, and a valid kubeconfig for the target cluster.

Configuring the deployment block

Set the product name and the On-Premise deployment requirements:

name: My Application

deployment:
  requirements:
    k8sVersion: ">=1.30.0"  # optional
  onPremDeployment:
    # AWS account that hosts the installer artifacts
    AwsAccountId: '<your-aws-account-id>'
    AwsBootstrapRoleAccountArn: 'arn:aws:iam::<your-aws-account-id>:role/omnistrate-bootstrap-role'
Field Description
name Display name for your installer product.
requirements.k8sVersion (Optional) Minimum Kubernetes version required on the target cluster.
onPremDeployment.AwsAccountId The AWS account ID that hosts the installer artifacts. This is the account where Omnistrate stores and retrieves installer resources.
onPremDeployment.AwsBootstrapRoleAccountArn The IAM role ARN in the installer artifact hosting account that Omnistrate assumes during installer packaging operations.

Helper user script

You can use the onPremInstallerTools.helperUserScript field to inject common reusable bash functions into the installer artifact. These functions are then available to all action hooks (validate, pre-install, post-install, backup), avoiding duplication across hooks:

  onPremInstallerTools:
    helperUserScript: |
      #!/bin/bash
      log_error() {
        echo "Error: $1" > /tmp/error.log
      }

Or reference an external file:

  onPremInstallerTools:
    helperUserScript: |
      {{ $file:./custom_scripts/helper.sh }}

Defining services

Under the services key, define one or more services. Each service maps to a deployable unit (typically a Helm release).

services:
  - name: MyApp

Configuring API parameters

API parameters are the user-facing configuration inputs shown during deployment. Add them under apiParameters for each service. For more details, see Deployment API Params.

    apiParameters:
      - name: releaseName
        key: releaseName
        type: String
        required: false
        modifiable: false
        export: true
        description: "Helm release name"
        defaultValue: "my-app"

      - name: namespace
        key: namespace
        type: String
        required: false
        modifiable: false
        export: true
        defaultValue: "my-app"
        description: "Kubernetes namespace to deploy into"

      - name: adminPassword
        key: adminPassword
        type: Password
        required: true
        modifiable: false
        export: true
        description: "Admin password for the application"

Parameter field reference

Field Description
name Human-readable display name.
key Internal reference key (used in {{ $var.<key> }} templates).
type Data type: String, Boolean, Password, etc.
required Whether the user must provide a value.
modifiable Whether the value can be changed after initial deployment.
export Whether the value is passed to Helm and hooks.
defaultValue Pre-filled default (user can override).
description Tooltip or help text shown in the UI.
options (Optional) Restrict input to an enumerated list of values.

Tip

Use type: Password for secrets so the value is masked in the UI.

Configuring action hooks

Action hooks run shell commands at specific points in the deployment lifecycle. Add them under actionHooks. For more details, see Action Hooks.

    actionHooks:
      - scope: CLUSTER
        type: VALIDATE
        commandTemplate: "echo 'Running validation checks...'"

      - scope: CLUSTER
        type: PRE_INSTALL
        commandTemplate: "echo 'Preparing environment...'"

      - scope: CLUSTER
        type: POST_INSTALL
        commandTemplate: "echo 'Post-install configuration...'"

      - scope: CLUSTER
        type: BACKUP
        commandTemplate: "echo 'Creating backup...'"
Hook Type When it runs
VALIDATE Before and after installation to verify the cluster meets requirements.
PRE_INSTALL After validation but before Helm install/upgrade.
POST_INSTALL After Helm install/upgrade completes successfully.
BACKUP Before upgrades to snapshot the current state.

For longer scripts, reference external files:

      - scope: CLUSTER
        type: VALIDATE
        commandTemplate: |
          {{ $file:./custom_scripts/validate.sh }}

Configuring the Helm chart

The helmChartConfiguration block tells Omnistrate which Helm chart to deploy and how to pass values:

    helmChartConfiguration:
      chartName: my-app
      chartVersion: 1.0.0
      chartRepoName: my-repo
      chartRepoURL: https://charts.example.com/
      releaseName: "{{ $var.releaseName }}"
      namespace: "{{ $var.namespace }}"
Field Description
chartName Name of the Helm chart.
chartVersion Chart version to install.
chartRepoName Logical name for the Helm repository.
chartRepoURL URL of the Helm chart repository (HTTPS or OCI).
releaseName Helm release name (use {{ $var.releaseName }} to reference a parameter).
namespace Kubernetes namespace (use {{ $var.namespace }} to reference a parameter).

Passing values with chartValues

Map API parameters into your Helm chart's values.yaml structure using the {{ $var.<key> }} template syntax:

      chartValues:
        config:
          adminPassword: "{{ $var.adminPassword }}"
          domain: "{{ $var.dnsEndpoint }}"
        ingress:
          enabled: true
          hosts:
            - host: "{{ $var.dnsEndpoint }}"
              paths:
                - path: /
                  pathType: Prefix

Platform-specific values with layeredChartValues

Use layeredChartValues to conditionally apply different Helm values based on the target platform or other parameter values. For more details, see Layered Chart Values.

      layeredChartValues:
        - scope:
            "{{ $var.onprem_platform }}": "EKS"
          values:
            {{ $file:./chart_value_templates/aws/values.yaml }}

        - scope:
            "{{ $var.onprem_platform }}": "AKS"
          values:
            {{ $file:./chart_value_templates/azure/values.yaml }}

        - scope:
            "{{ $var.onprem_platform }}": "GKE"
          values:
            {{ $file:./chart_value_templates/gcp/values.yaml }}

        - scope:
            "{{ $var.onprem_platform }}": "GENERIC"
          values:
            {{ $file:./chart_value_templates/generic/values.yaml }}

The scope block acts as a conditional — the values are only applied when all conditions match. {{ $var.onprem_platform }} is a built-in system variable set automatically based on the customer's cluster type.

Configuring container image registry copy

If your Helm chart references container images stored in a private registry (for example, Docker Hub with authentication, a private ECR, or any registry requiring credentials), the installer needs to know how to pull those images at build time so they can be copied into the customer's target environment. Without this configuration, the customer's cluster may fail to pull images if it has no direct access to your source registry.

The containerImagesRegistryCopyConfiguration block defines where to pull images from (source registry + credentials) and where to push them to (the customer's private registry). This is essential for:

  • Air-gapped or restricted environments — the target cluster has no internet access
  • Private source registries — your images require authentication to pull
  • Registry locality — customers want images in their own registry for performance, compliance, or security reasons

How it works

The installer uses a two-service pattern: a dedicated image sync service handles the registry copy, and your main application service depends on it. This ensures all images are available in the customer's registry before the Helm chart is installed.

┌─────────────────────┐       ┌─────────────────────┐
│  ImageSync Service  │──────▶│   MyApp Service     │
│  (internal: true)   │       │  (dependsOn:        │
│                     │       │    - ImageSync)      │
│  Copies images from │       │                     │
│  source → target    │       │  Installs Helm chart│
│  registry           │       │  (images now local) │
└─────────────────────┘       └─────────────────────┘

Image discovery with autoDiscoverImagesTag

The installer needs to know which images to copy. Rather than listing every image manually, you can optionally add autoDiscoverImagesTag to your helmChartConfiguration. This tells the installer to look for the matching annotation in the Helm chart's root Chart.yaml metadata and automatically discover all container images referenced by the chart.

In the installer spec, set the tag value:

    helmChartConfiguration:
      chartName: my-app
      chartVersion: 1.0.0
      chartRepoName: my-repo
      chartRepoURL: oci://registry-1.docker.io/my-org
      # ...other fields...
      autoDiscoverImagesTag: "my-org.com/images"

In your Helm chart's Chart.yaml, add the corresponding annotation. Each entry includes a name and an image reference. You can use Helm's {{.Chart.AppVersion}} template to keep image tags in sync with the chart version:

apiVersion: v2
name: my-app
version: 1.0.0
appVersion: 2.5.0
annotations:
  my-org.com/images: |
    - name: app-server
      image: docker.io/my-org/app-server:{{.Chart.AppVersion}}
    - name: app-worker
      image: docker.io/my-org/app-worker:{{.Chart.AppVersion}}
    - name: mongodb
      image: docker.io/my-org/mirror-mongodb:7.0-build-335
    - name: postgresql-repmgr
      image: docker.io/my-org/mirror-postgresql-repmgr:14.15.0-debian-12-r2
    - name: redis
      image: docker.io/my-org/mirror-redis-server:7.4-build-248
    - name: rabbitmq
      image: docker.io/my-org/mirror-rabbitmq:4.0.5-build-175

At build time, the installer reads the Chart.yaml annotations, matches the autoDiscoverImagesTag key, and extracts the listed container images for the registry copy operation. This keeps the image list in sync with your chart — update Chart.yaml when you change images, and the installer picks up the changes automatically.

Warning

Using autoDiscoverImagesTag requires a properly configured image registry copy resource (containerImagesRegistryCopyConfiguration) in your spec. The auto-discovery feature reads image references from the chart metadata and feeds them into the registry copy pipeline — without a configured image sync service, there is nowhere to copy the discovered images. See Defining the image sync service for setup details.

Tip

The autoDiscoverImagesTag field is optional. If you omit it, you can manually specify the images to copy using the images list in containerImagesRegistryCopyConfiguration instead (see the end-to-end example).

Note

If your Helm chart is in a private OCI registry, you also need authProvider to authenticate when pulling the chart itself:

authProvider:
  username: '{{ $secret.REGISTRY_USERNAME }}'
  password: '{{ $secret.REGISTRY_PASSWORD }}'

Pull modes

The pullMode field controls when and how images are transferred:

Mode Behavior Best for
INSTALLER_EMBED Images are downloaded from the source registry at build time and packaged into the installer artifact. At install time, images are pushed from the local artifact to the target registry. No internet access required during installation. Air-gapped environments, large-scale deployments where you want deterministic installs. Requires more disk space in the installer artifact.
RUNTIME_PULL Images are pulled from the source registry and pushed to the target registry at install/upgrade time. The installer environment must have network access to both registries. Connected environments where disk space is a concern, or when you always want the latest image digests.

Defining the image sync service

Create a separate service with internal: true (hidden from end users) that owns the containerImagesRegistryCopyConfiguration:

services:
  - name: ImageSync
    internal: true
    apiParameters:
      - name: Private Image Registry URL
        key: privateRegistryUrl
        description: "Customer's private registry where images will be pushed"
        type: String
        required: true
        export: true
        modifiable: true
      - name: Pull Mode
        key: pullMode
        description: "Image sync strategy"
        type: String
        required: true
        export: true
        modifiable: true
        defaultValue: INSTALLER_EMBED
        options:
          - INSTALLER_EMBED
          - RUNTIME_PULL
    containerImagesRegistryCopyConfiguration:
      pullMode: "{{ $var.pullMode }}"
      pullSource:
        registryURL: "docker.io"
        repositoryName: "my-org"
        credentials:
          username: "{{ $secret.REGISTRY_USERNAME }}"
          password: "{{ $secret.REGISTRY_PASSWORD }}"
      pushTarget:
        registryURL: "{{ $var.privateRegistryUrl }}"
        repositoryName: "my-org"

Configuration reference

Field Description
pullMode INSTALLER_EMBED or RUNTIME_PULL (see Pull modes above).
pullSource.registryURL Source registry to pull images from (e.g., docker.io, ghcr.io).
pullSource.repositoryName Repository or organization name in the source registry (e.g., my-org).
pullSource.credentials (Optional) Credentials for authenticating to the source registry. Use {{ $secret.<name> }} to reference Omnistrate-managed secrets. Required if the source registry is private.
pushTarget.registryURL Customer's target private registry. Typically references an API parameter (e.g., {{ $var.privateRegistryUrl }}).
pushTarget.repositoryName Repository or organization name in the target registry. Usually mirrors the source.

Wiring the main service with dependsOn

Your main application service must declare dependsOn to ensure image sync completes first. Additionally, if the main service needs to reference the same parameters (e.g., privateRegistryUrl, pullMode), use parameterDependencyMap to link them across services rather than duplicating values. For more details on dependencies, see Resource Dependencies.

  - name: MyApp
    dependsOn:
      - ImageSync
    apiParameters:
      - name: Private Image Registry URL
        key: privateRegistryUrl
        description: "Private registry URL"
        type: String
        required: true
        export: true
        modifiable: true
        parameterDependencyMap:
          ImageSync: privateRegistryUrl    # passes this value to ImageSync's privateRegistryUrl
      - name: Pull Mode
        key: pullMode
        type: String
        required: true
        export: true
        defaultValue: INSTALLER_EMBED
        options:
          - INSTALLER_EMBED
          - RUNTIME_PULL
        parameterDependencyMap:
          ImageSync: pullMode              # passes this value to ImageSync's pullMode
    helmChartConfiguration:
      chartName: my-app
      # ...
      autoDiscoverImagesTag: "my-org.com/images"

With parameterDependencyMap, the user only fills in the value once (on the main service), and it is automatically forwarded to the internal ImageSync service. The format is:

parameterDependencyMap:
  <target-service-name>: <target-parameter-key>

Disabling image sync

You can make image sync optional by adding a boolean parameter and using the disable field on the image sync service:

  - name: ImageSync
    internal: true
    disable: "{{ $var.skipImageSync }}"
    apiParameters:
      - name: Skip Image Sync
        key: skipImageSync
        type: Boolean
        required: false
        export: true
        defaultValue: 'false'
        modifiable: false
      # ...other parameters...
    containerImagesRegistryCopyConfiguration:
      # ...

When skipImageSync is set to true, the entire image copy step is skipped. Use parameterDependencyMap on the main service to let the user control this toggle:

  # In the main service's apiParameters:
      - name: Skip Image Sync
        key: skipImageSync
        type: Boolean
        required: false
        export: true
        defaultValue: 'false'
        modifiable: false
        parameterDependencyMap:
          ImageSync: skipImageSync

End-to-end example

Here is a complete two-service setup with image registry copy:

services:
  # 1. Internal image sync service
  - name: DockerIO
    internal: true
    apiParameters:
      - name: Skip Custom Image Registry
        key: skipCustomImageRegistry
        type: Boolean
        required: false
        export: true
        defaultValue: 'true'
        modifiable: false
      - name: Private Image Registry URL
        key: privateRegistryUrl
        type: String
        required: true
        export: true
        modifiable: true
      - name: Pull Mode
        key: pullMode
        type: String
        required: true
        export: true
        modifiable: true
        defaultValue: INSTALLER_EMBED
        options:
          - INSTALLER_EMBED
          - RUNTIME_PULL
    disable: "{{ $var.skipCustomImageRegistry }}"
    containerImagesRegistryCopyConfiguration:
      pullMode: "{{ $var.pullMode }}"
      pullSource:
        registryURL: "docker.io"
        repositoryName: "my-org"
        credentials:
          username: "{{ $secret.DOCKERHUB_USERNAME }}"
          password: "{{ $secret.DOCKERHUB_PASSWORD }}"
      pushTarget:
        registryURL: "{{ $var.privateRegistryUrl }}"
        repositoryName: "my-org"
      images:
        - imageName: "my-image"
          imageTag: "my-tag"

  # 2. Main application service
  - name: MyApp
    dependsOn:
      - DockerIO
    apiParameters:
      - name: Skip Custom Image Registry
        key: skipCustomImageRegistry
        type: Boolean
        required: false
        export: true
        defaultValue: 'false'
        modifiable: false
        parameterDependencyMap:
          DockerIO: skipCustomImageRegistry
      - name: Private Image Registry URL
        key: privateRegistryUrl
        type: String
        required: true
        export: true
        modifiable: true
        parameterDependencyMap:
          DockerIO: privateRegistryUrl
      - name: Pull Mode
        key: pullMode
        type: String
        required: true
        export: true
        defaultValue: INSTALLER_EMBED
        options:
          - INSTALLER_EMBED
          - RUNTIME_PULL
        parameterDependencyMap:
          DockerIO: pullMode
      # ...other app parameters...
    helmChartConfiguration:
      chartName: my-app
      chartVersion: 1.0.0
      chartRepoName: my-org
      chartRepoURL: oci://registry-1.docker.io/my-org
      releaseName: "{{ $var.releaseName }}"
      namespace: "{{ $var.namespace }}"
      autoDiscoverImagesTag: "my-org.com/images"
      authProvider:
        username: '{{ $secret.DOCKERHUB_USERNAME }}'
        password: '{{ $secret.DOCKERHUB_PASSWORD }}'
      # ...chartValues, layeredChartValues, etc.

Building and releasing the installer

Use the Omnistrate CLI to build and release the installer:

omnistrate-ctl build \
  --spec-type ServicePlanSpec \
  --file installer-spec.yaml \
  --product-name "My Application On-Premise Installer" \
  --release \
  --release-description "v1.0.0 initial release"

Full Example

The following example shows a complete minimal spec that packages Supabase as an On-Premise installer. It includes a deployment block, API parameters for the release name, namespace, JWT secret, database password, and DNS endpoint, along with lifecycle action hooks and a Helm chart configuration.

The video walkthrough below demonstrates the end-to-end experience of building, releasing, and installing using this exact spec — Watch on YouTube.

Complete Supabase specification
# yaml-language-server: $schema=https://api.omnistrate.cloud/2022-09-01-00/schema/service-spec-schema.json
# yaml-language-server: $schema=https://api.omnistrate.cloud/2022-09-01-00/schema/system-parameters-schema.json

name: Supabase
deployment:
  requirements:
    k8sVersion: ">=1.30.0"  # optional
  onPremDeployment:
    # AWS account that hosts the installer artifacts
    AwsAccountId: '<your-aws-account-id>'
    AwsBootstrapRoleAccountArn: 'arn:aws:iam::<your-aws-account-id>:role/omnistrate-bootstrap-role'
  onPremInstallerTools:
    helperUserScript: |
      #!/bin/bash
      log_error() {
        echo "Error: $1" > /tmp/error.log
      }

services:
  - name: Supabase
    apiParameters:
      - name: releaseName
        key: releaseName
        type: String
        required: false
        modifiable: false
        export: true
        description: "Helm release name"
        defaultValue: "supabase"
      - name: namespace
        key: namespace
        type: String
        required: false
        modifiable: false
        export: true
        defaultValue: "supabase"
        description: "Kubernetes namespace to deploy Supabase into"
      - name: jwtSecret
        key: jwtSecret
        type: Password
        required: false
        modifiable: false
        export: true
        description: "JWT Secret"
      - name: dbPassword
        key: dbPassword
        description: "Database Password"
        type: Password
        modifiable: false
        export: true
        required: false
      - name: dnsEndpoint
        key: dnsEndpoint
        type: String
        required: false
        modifiable: false
        export: true
        description: "DNS Endpoint for Supabase Ingress"
        defaultValue: "example.com"

    actionHooks:
      - scope: CLUSTER
        type: VALIDATE
        commandTemplate: "echo 'Validate hook'"
      - scope: CLUSTER
        type: PRE_INSTALL
        commandTemplate: "echo 'Pre-install hook'"
      - scope: CLUSTER
        type: POST_INSTALL
        commandTemplate: "echo 'Post-install hook'"
      - scope: CLUSTER
        type: BACKUP
        commandTemplate: "echo 'Backup hook'"

    helmChartConfiguration:
      chartName: supabase
      chartVersion: 0.1.3
      chartRepoName: supabase
      chartRepoURL: https://example.github.io/supabase-charts/
      releaseName: "{{ $var.releaseName }}"
      namespace: "{{ $var.namespace }}"
      chartValues:
        secret:
          jwt:
            secret: "{{ $var.jwtSecret }}"
          db:
            password: "{{ $var.dbPassword }}"
        kong:
          ingress:
            enabled: true
            className: "nginx"
            hosts:
              - host: "{{ $var.dnsEndpoint }}"
                paths:
                  - path: /
                    pathType: Prefix

Template syntax reference

Syntax Description
{{ $var.<key> }} References an API parameter by its key.
{{ $file:<path> }} Inlines the contents of a file at build time.
{{ $secret.<name> }} References an Omnistrate-managed secret.
{{ $var.onprem_platform }} Built-in variable: EKS, AKS, GKE, or GENERIC.