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:
Defining services¶
Under the services key, define one or more services. Each service maps to a deployable unit (typically a Helm release).
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:
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:
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:
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. |