Kubernetes deployment
SelectZero runs on Kubernetes as a single pod. This page is a complete runbook: follow it top to bottom and you have a working instance.
The recommended way to install is the Helm chart. It validates your settings before anything is created, and upgrades are one command. Where Helm is not an option, the raw manifests further down describe exactly the same deployment with every setting explained.
Everything here applies to any Kubernetes distribution. The examples name Amazon EKS because that is where the S3 storage mode is most often used.
Memory
Set the container's memory limit and keep JAVA_OPTIONS at -XX:MaxRAMPercentage=50.0 (the chart's default): the query engine behind comparisons and file connections uses memory outside the Java heap, in the other half. A 4Gi limit runs a test suite with three comparisons in parallel; the chart's 8Gi limit leaves room for several. Before the limit is reached, SelectZero first hands back unused Java heap, then stops the query engine work using the most memory, so that test case ends with memory usage too high instead of the pod being OOM-killed. See Memory sizing.
Choose a storage variant
Durable files are the execution results, test suite reports, uploaded data source files, JDBC drivers, connector certificates and the custom logo. They have to outlive the pod, so pick where they live before you deploy.
S3 storage mode | Persistent volume | |
|---|---|---|
Durable files in | An S3 bucket | A PersistentVolumeClaim (EBS, EFS, Azure Disk, GCE PD) |
Volumes on the pod |
| One PVC |
Application database | External PostgreSQL, mandatory | PostgreSQL recommended, SQLite possible |
Suits | A cluster policy of "no persistent volumes" | Everything else, and the simplest setup |
Configuration |
| Nothing extra |
Chart value |
|
|
Both keep the state outside the cluster in the sense that matters: a PersistentVolumeClaim is a mount declaration, and the data sits in the storage service behind it. The difference is which service, and whether a volume object exists at all.
If you accept a file share but not a block volume, use the persistent volume variant with Amazon EFS. It needs no application configuration and is covered under Persistent volume variant.
Prerequisites
For both variants:
A Kubernetes cluster and
kubectlaccess to it.Nodes with the amd64 (x86_64) architecture. The SelectZero image is published for amd64 only, so it does not start on arm64 nodes such as AWS Graviton; the usual symptom is
exec format error. In a cluster with both kinds of node, pin the pod to amd64 withnodeSelector: {kubernetes.io/arch: amd64}, which is the chart'snodeSelectorvalue.Access to Docker Hub, or a mirror of it. The nodes pull the image, and whatever runs
helmpulls the chart, over HTTPS from three hosts:registry-1.docker.io,auth.docker.ioandproduction.cloudfront.docker.com. The last one serves the actual downloads, so allowing only the first two lets a pull start and then time out. In a cluster without internet access, mirrorselectzero/selectzero(the image, linux/amd64) andselectzero/selectzero-helm(the chart) into your own registry, or use a pull-through cache of Docker Hub, and install from there.Network reachability from the pod to your application database, to the data sources you monitor, and, in S3 mode, to the S3 endpoint.
An ingress controller, load balancer or reverse proxy of your own. SelectZero does not ship one. See Reverse proxy for the headers it expects.
For S3 storage mode additionally:
External PostgreSQL. SQLite is not supported in S3 mode. Only the uploaded files, synced SharePoint workbooks, drivers, images and certificates are mirrored to the bucket; the SQLite database file itself is not, so on an
emptyDirit would be lost at every restart.A master encryption key supplied from outside the pod. In S3 mode the application refuses to start without
SZ_ENCRYPTION_KEY, because a key generated inside the pod would die with it and leave every object in the bucket permanently unreadable. The reason is one line on standard output; see When the pod does not start.An S3 bucket and an IAM identity that can read and write it.
For the persistent volume variant additionally:
The CSI driver for your storage class, for example the EBS CSI driver or the EFS CSI driver.
Install with Helm (recommended)
The chart is published as an OCI artifact alongside the application image. There is no repository to add.
Run helm show values oci://registry-1.docker.io/selectzero/selectzero-helm for the complete list of values. Every one of them is commented.
Values for S3 storage mode
Prepare the bucket, the lifecycle rules and the IAM identity first: the chart does not create them.
PostgreSQL and an encryption key are both mandatory in this mode, and the chart refuses to install without them rather than letting the pod start into a state that loses data. The key may hold an aws-sm:// reference instead of the key itself.
If workload identity is impossible, set storage.s3.staticCredentials.enabled and supply accessKey and secretKey. Prefer the role.
Values for a persistent volume
This is the default mode, so the smallest useful values file is the storage class and a database.
Leaving database.jdbcUrl out uses the embedded SQLite database on the volume, and leaving encryption.key out generates a key on the volume at first start. Both are acceptable here and neither is in S3 mode.
To use a claim you provisioned yourself, set storage.pvc.existingClaim and the chart creates none. The claim it does create carries helm.sh/resource-policy: keep, so helm uninstall leaves your data alone; set storage.pvc.keepOnUninstall: false if you would rather it were deleted with the release.
Bring your own Secret
By default the chart creates a Secret from the values above, which means those values sit in your values file. To keep them somewhere else, create the Secret yourself and name it:
The keys are named after the environment variables they become: SZ_ENCRYPTION_KEY, SZ_DATABASE_USER, SZ_DATABASE_PASSWORD, SZ_STORAGE_S3_ACCESS_KEY and SZ_STORAGE_S3_SECRET_KEY. Only the keys your other values actually reference have to exist. These are the same names the Secret manifest below uses, so the Secret you already have works unchanged.
What the chart does not install
No Ingress, no Gateway API route, no TLS certificate, no cert-manager wiring, no PostgreSQL and no monitoring. The Service is a ClusterIP. Putting a reverse proxy in front of it is yours to do.
X-Forwarded-ForandX-Forwarded-Protoare already honoured, so a proxy that sets them needs no application configuration. See Reverse proxy.When TLS terminates upstream, run with the
secure-sessionprofile so session cookies are marked secure:
Ship your own Ingress, NetworkPolicy or anything else through
extraObjectsrather than forking the chart. A string entry is passed through the template engine, so it can reference the release's own names:
There are matching extraEnv, extraEnvFrom, extraVolumes, extraVolumeMounts, extraInitContainers and extraContainers values for the pod itself.
Reaching it without an ingress
The Service is a ClusterIP by default, reachable only inside the cluster. For an evaluation, or a cluster with no ingress controller, expose it on every node instead:
SelectZero is then at http://ANY-NODE-IP:30090. LoadBalancer works the same way where your cluster provides load balancers. Both serve plain HTTP straight to the pod, so sign-in credentials cross the network unencrypted: keep this to trusted networks, or put a TLS-terminating load balancer in front. Do not combine plain HTTP with the secure-session profile, because a browser does not send secure cookies over HTTP and sign-in stops working.
Moving from the raw manifests to the chart
The chart renders the same objects the manifests below describe, under the same names when the release is called selectzero. Keep the Secret and the claim, delete the rest, and install with both referenced:
S3 storage mode on Amazon EKS
For the concepts behind this mode, including what is encrypted and by whom, see S3 storage mode. This section is the deployment procedure.
Bucket configuration
Create the bucket in the same region as the cluster, then:
Block all public access. The bucket holds customer data. Nothing about SelectZero needs it to be reachable from the internet.
Set default encryption. Every S3 bucket encrypts at rest with SSE-S3 by default. For a key you control and can rotate, configure SSE-KMS with a customer-managed key and enable S3 Bucket Keys, because SelectZero writes many small objects and Bucket Keys cut the KMS request cost. With SSE-KMS the pod's IAM identity also needs
kms:GenerateDataKeyandkms:Decrypton that key. Do not add a bucket policy that denies uploads without an explicitx-amz-server-side-encryptionheader: SelectZero relies on bucket default encryption and sends no such header. See Security model with file encryption disabled for how to verify this per object.Versioning is optional. If you enable it, add a noncurrent version expiration rule. Application deletes become delete markers, so without one the retention sweep stops freeing space.
Lifecycle rules
SelectZero prunes its own result and report objects from the bucket every 12 hours, using the retention settings on the General tab of Settings. Lifecycle rules are therefore a backstop for objects left behind by an interrupted run, not the primary mechanism.
Key prefix | Application setting | Default | Suggested expiration |
|---|---|---|---|
| CSV file history (days) | 29 | The setting plus 7 days |
| Sample data history (days) | 29 (upgrades inherit CSV file history) | The setting plus 7 days; nothing is stored at 0 |
| Test suite report file history (days) | 29 | The setting plus 7 days |
| none | Never expire | |
whole bucket | Abort incomplete multipart uploads after 7 days |
Never expire database/, drivers/ or keys/. Those prefixes hold the uploaded data source files, the JDBC drivers and the connector certificates, and nothing recreates them.
If you set SZ_STORAGE_S3_PREFIX, include that prefix in the lifecycle rule filters.
Identity: IRSA or static keys
Give the pod an IAM role rather than access keys. On EKS that is IAM Roles for Service Accounts (IRSA) or EKS Pod Identity; both are picked up by the AWS default credential chain with no application configuration.
The role needs these permissions:
With eksctl, one command creates the role, the trust policy for your cluster OIDC provider and the annotated ServiceAccount:
If a workload identity is genuinely impossible, put an access key pair in the Secret and uncomment the SZ_STORAGE_S3_ACCESS_KEY and SZ_STORAGE_S3_SECRET_KEY entries in the Deployment. Keys need rotating; a role does not.
When credentials do not resolve, the AWS SDK reports every provider it tried, most of which are expected to fail. Reading a credential failure explains which line to read.
Manifests
This is what the Helm chart renders in this mode. Apply it by hand when Helm is not available, or read it to see exactly what the chart creates.
Deploy in S3 storage mode
Create the Secret. Replace every
REPLACEplaceholder.# Each key below is named after the environment variable it becomes. The Deployment maps them # with secretKeyRef; the application reads them from the environment. Rename a key here and you # must rename it in the Deployment too. The full list of variables the application understands is # under "Supported environment variables" in Deploying SelectZero. apiVersion: v1 kind: Secret metadata: name: selectzero-secrets type: Opaque stringData: # The master encryption key. Generate once, store it in your own secret manager, and never # change it: every encrypted value in the database was written with this key. # This may hold an aws-sm://<secret-id> reference instead of the key itself, in which case it # is resolved at startup and the key never exists in the cluster. See "Secret references". SZ_ENCRYPTION_KEY: REPLACE_WITH_A_LONG_RANDOM_STRING SZ_DATABASE_USER: selectzero SZ_DATABASE_PASSWORD: REPLACE_WITH_THE_DATABASE_PASSWORD # Only needed when you cannot use IRSA or EKS Pod Identity. Uncomment the matching # environment variables in the Deployment as well. # SZ_STORAGE_S3_ACCESS_KEY: REPLACE # SZ_STORAGE_S3_SECRET_KEY: REPLACESelectZero is configured entirely through environment variables, and it never reads this Secret directly. Kubernetes does: each key is pulled into the container as an environment variable by a
secretKeyRefentry in the Deployment below. The keys are therefore named after the variables they become, soSZ_DATABASE_PASSWORDhere arrives asSZ_DATABASE_PASSWORDin the container. If you rename a key to fit your own conventions, change the matchingsecretKeyRefin the Deployment too, or the variable simply will not be set.Every variable the application understands, secret or not, is listed under Supported environment variables. Only the sensitive ones belong in a Secret; the rest are plain
enventries in the Deployment.The encryption key may hold an aws-sm:// reference instead of the key itself, in which case it is fetched at startup and never stored in the cluster.
Create the ServiceAccount carrying your IAM role. Skip this if
eksctlalready created it.apiVersion: v1 kind: ServiceAccount metadata: name: selectzero annotations: # IRSA: the IAM role that carries the S3 permissions. EKS injects AWS_ROLE_ARN and # AWS_WEB_IDENTITY_TOKEN_FILE into every pod using this service account. eks.amazonaws.com/role-arn: arn:aws:iam::ACCOUNT_ID:role/selectzero-s3Create the Deployment. Replace the
VERSIONimage tag: the current release is2026.10.0.# S3 storage mode: no PersistentVolumeClaim. Every local directory is an emptyDir that the pod # can lose at any time; the durable copies live in the bucket and in PostgreSQL. # Keep this file in sync with ../pv/deployment.yaml: a diff between the two must show only the # volumes, serviceAccountName and the SZ_STORAGE_* environment variables. apiVersion: apps/v1 kind: Deployment metadata: name: selectzero labels: app: selectzero spec: replicas: 1 # SelectZero runs its schedulers in-process; never run a second replica strategy: type: Recreate # two pods must not work on the same bucket keys during a rollout selector: matchLabels: app: selectzero template: metadata: labels: app: selectzero spec: serviceAccountName: selectzero securityContext: runAsNonRoot: true runAsUser: 1000 runAsGroup: 1000 fsGroup: 1000 seccompProfile: type: RuntimeDefault containers: - name: selectzero image: selectzero/selectzero:VERSION # replace with the version you deploy imagePullPolicy: IfNotPresent securityContext: readOnlyRootFilesystem: true allowPrivilegeEscalation: false capabilities: drop: ["ALL"] ports: - name: http containerPort: 8090 env: - name: SZ_STORAGE_MODE value: s3 - name: SZ_STORAGE_S3_BUCKET value: YOUR-BUCKET-NAME - name: SZ_STORAGE_S3_REGION value: YOUR-BUCKET-REGION # Optional key prefix, for a bucket shared with other workloads. # - name: SZ_STORAGE_S3_PREFIX # value: selectzero/ # Only when IRSA and Pod Identity are both impossible. Prefer a role. # - name: SZ_STORAGE_S3_ACCESS_KEY # valueFrom: # secretKeyRef: {name: selectzero-secrets, key: SZ_STORAGE_S3_ACCESS_KEY} # - name: SZ_STORAGE_S3_SECRET_KEY # valueFrom: # secretKeyRef: {name: selectzero-secrets, key: SZ_STORAGE_S3_SECRET_KEY} - name: SZ_ENCRYPTION_KEY valueFrom: secretKeyRef: name: selectzero-secrets key: SZ_ENCRYPTION_KEY # PostgreSQL is mandatory in S3 mode. The embedded SQLite database is not stored in # the bucket, so on an emptyDir it would be lost at every restart. - name: SZ_JDBC_URL value: jdbc:postgresql://YOUR-DATABASE-HOST:5432/selectzero - name: SZ_DATABASE_USER valueFrom: secretKeyRef: {name: selectzero-secrets, key: SZ_DATABASE_USER} - name: SZ_DATABASE_PASSWORD valueFrom: secretKeyRef: {name: selectzero-secrets, key: SZ_DATABASE_PASSWORD} - name: SZ_DATABASE_SSL_MODE value: SSL # NONE, SSL or SSL_VERIFY. An unrecognised value silently means NONE - name: JAVA_OPTIONS value: -XX:MaxRAMPercentage=50.0 resources: requests: cpu: "1" memory: 4Gi ephemeral-storage: 20Gi limits: memory: 8Gi # Must cover everything the emptyDir volumes below actually hold. ephemeral-storage: 50Gi # The application answers no HTTP request until it has restored the mirrored # directories from the bucket, so the startup probe carries the whole hydration time: # 60 x 10s = 10 minutes. Raise failureThreshold if you upload many gigabytes of files. startupProbe: httpGet: path: /actuator/health port: 8090 # Kubernetes waits 1 second by default, which a JVM exceeds during a garbage # collection or a busy moment. Six slow answers in a row restart the container. timeoutSeconds: 5 periodSeconds: 10 failureThreshold: 60 livenessProbe: httpGet: path: /actuator/health/liveness port: 8090 timeoutSeconds: 5 periodSeconds: 10 failureThreshold: 6 readinessProbe: httpGet: path: /actuator/health/readiness port: 8090 timeoutSeconds: 5 periodSeconds: 10 volumeMounts: - {name: database, mountPath: /usr/app/database} - {name: reports, mountPath: /usr/app/reports} - {name: drivers, mountPath: /usr/app/drivers} - {name: keys, mountPath: /usr/app/keys} - {name: temp-files, mountPath: /usr/app/tempFiles} - {name: logs, mountPath: /usr/app/logs} - {name: tmp, mountPath: /tmp} # Exceeding a sizeLimit evicts the pod, so size these for your data, not for the defaults. volumes: # Working copy of the uploaded data source files, restored from the bucket at startup. # Size it for the full set of uploaded files. - name: database emptyDir: sizeLimit: 20Gi # Result and report files. Written locally and to the bucket; the local copy is a # size-bounded cache that the application evicts on its own. - name: reports emptyDir: sizeLimit: 5Gi # JDBC drivers, restored from the bucket at startup. - name: drivers emptyDir: sizeLimit: 1Gi # Truststore and connector certificates, restored from the bucket at startup. - name: keys emptyDir: sizeLimit: 256Mi # Query working files, DuckDB spill, upload staging and the hydration cache. # The busiest directory: size it for your largest execution, not for your data set. - name: temp-files emptyDir: sizeLimit: 20Gi # Log files. Lost with the pod, so collect stdout as well. See "Logging on Kubernetes". - name: logs emptyDir: sizeLimit: 2Gi # Required by readOnlyRootFilesystem: JVM performance data, native library extraction # and the initial administrator password file are written here. - name: tmp emptyDir: sizeLimit: 1GiCreate the Service. It is a ClusterIP: put your own ingress or load balancer in front of it.
apiVersion: v1 kind: Service metadata: name: selectzero labels: app: selectzero spec: type: ClusterIP selector: app: selectzero ports: - name: http port: 80 targetPort: 8090Apply everything and wait for the rollout.
kubectl apply -n YOUR-NAMESPACE -f secret.yaml -f serviceaccount.yaml -f deployment.yaml -f service.yaml kubectl rollout status -n YOUR-NAMESPACE deployment/selectzero
Four things in that Deployment are load bearing.
The Recreate strategy. A rolling update would briefly run two pods against the same bucket keys and the same database.
The probes. /actuator/health and its liveness and readiness sub-paths are the only endpoints that answer without authentication; the rest of /actuator requires an administrator. The startup probe allows 10 minutes because the application restores its mirrored directories from the bucket before it serves anything, and that time grows with the amount of uploaded data. Raise failureThreshold if you hold many gigabytes; see Startup hydration.
The emptyDir volumes. Every application directory is ephemeral. Exceeding a sizeLimit evicts the pod, so size them for your data:
Mount | Holds | Size it for |
|---|---|---|
| Working copy of uploaded data source files and synced SharePoint workbooks, restored at startup | The full set of uploaded files plus every synced SharePoint workbook |
| Query working files, DuckDB spill, upload staging, hydration cache | Your largest single execution |
| Result and report files, a cache the application evicts itself | A few gigabytes |
| JDBC drivers, restored at startup | Your driver JARs |
| Truststore and connector certificates, restored at startup | Very little |
| Log files, lost with the pod | Your rotation window |
| JVM working data and the initial password file | Very little |
readOnlyRootFilesystem: true. The application writes only into the mounts above. The one thing it would write to the image itself is the initial administrator password file, and it falls back to /tmp on its own, which is why /tmp is mounted.
First login
The initial administrator password is generated at first start and written to a file. With a read-only root filesystem that file is /tmp/admin-init.txt, and the log says so:
Before you have an ingress, reach the application with a port forward:
Change the administrator password immediately. The file is deleted after the first login.
Startup hydration
On every pod start SelectZero restores the mirrored directories from the bucket before it becomes ready. Progress appears in the System log tab inside the application:
Roughly 1 to 3 minutes for 15GB of uploaded files in-region, downloading in parallel. This is the time the startup probe has to allow.
Verify that a pod restart loses nothing
Run this once after the first deployment. It is the check that proves the stateless setup actually works.
Restart verification
Log in, change the administrator password.
Create something in each durable category: upload a JDBC driver, upload a CSV or Excel data source file, attach a certificate to a connection, run a test case so it produces a result file, and run a test suite so it produces a report.
Confirm the bucket received them.
aws s3 ls --recursive s3://YOUR-BUCKET-NAME/ | grep -E "drivers/|database/files/|keys/|reports/"Restart the pod. This destroys every local directory.
kubectl rollout restart -n YOUR-NAMESPACE deployment/selectzero kubectl rollout status -n YOUR-NAMESPACE deployment/selectzeroOpen the System log tab and confirm the hydration lines from Startup hydration. They are written to the System log, not to standard output, so
kubectl logswill not show them.Confirm nothing was lost: the driver is still listed, the uploaded file still previews, the result file still downloads, the test suite report still opens, the certificate is still attached, and your changed administrator password still works.
Confirm the application did not fall back to a local database.
kubectl logs -n YOUR-NAMESPACE deploy/selectzero | grep "using SQLITE instead"This must print nothing. See Current limitations if it does.
Persistent volume variant
The same Deployment, minus the S3 configuration, plus one PersistentVolumeClaim holding every durable directory.
This is what the Helm chart renders in its default mode. Apply it by hand when Helm is not available, or read it to see exactly what the chart creates.
Deploy on a persistent volume
Create the PersistentVolumeClaim for your environment.
A file share rather than a block volume, and the closest thing to a stateless pod without using S3 mode: the data lives in EFS, outside the cluster, and the pod can be rescheduled to any node. First the storage class, once per cluster:
apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: efs-sc provisioner: efs.csi.aws.com parameters: provisioningMode: efs-ap # dynamic access points, one per claim fileSystemId: fs-REPLACE-WITH-YOUR-FILESYSTEM-ID directoryPerms: "700" uid: "1000" # the container's user, so the access point root is writable gid: "1000"Then the claim:
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: selectzero-storage-claim spec: storageClassName: efs-sc accessModes: - ReadWriteMany resources: requests: # EFS grows on demand and ignores this number, but Kubernetes requires it. storage: 20GiRequires the EFS CSI driver.
ReadWriteManyis what EFS offers; it is not permission to run a second replica.apiVersion: v1 kind: PersistentVolumeClaim metadata: name: selectzero-storage-claim spec: storageClassName: gp3 accessModes: - ReadWriteOnce resources: requests: storage: 20GiUses the Amazon EBS gp3 storage class, default on most EKS clusters. Requires the EBS CSI driver.
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: selectzero-storage-claim spec: storageClassName: managed-csi accessModes: - ReadWriteOnce resources: requests: storage: 20GiUses the Azure Managed Disk (CSI) storage class, available by default on AKS.
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: selectzero-storage-claim spec: storageClassName: standard-rwo accessModes: - ReadWriteOnce resources: requests: storage: 20GiUses the GCE Persistent Disk (CSI) storage class, available by default on GKE.
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: selectzero-storage-claim spec: accessModes: - ReadWriteOnce resources: requests: storage: 20GiOmitting
storageClassNameuses the cluster default. Check withkubectl get storageclassthat the default provides persistent storage and nothostPath.Create the Secret. The same file as in S3 mode, with the same key-to-variable mapping;
SZ_ENCRYPTION_KEYis optional here, because without it a key is generated on the volume at first start.# Each key below is named after the environment variable it becomes. The Deployment maps them # with secretKeyRef; the application reads them from the environment. Rename a key here and you # must rename it in the Deployment too. The full list of variables the application understands is # under "Supported environment variables" in Deploying SelectZero. apiVersion: v1 kind: Secret metadata: name: selectzero-secrets type: Opaque stringData: # The master encryption key. Generate once, store it in your own secret manager, and never # change it: every encrypted value in the database was written with this key. # This may hold an aws-sm://<secret-id> reference instead of the key itself, in which case it # is resolved at startup and the key never exists in the cluster. See "Secret references". SZ_ENCRYPTION_KEY: REPLACE_WITH_A_LONG_RANDOM_STRING SZ_DATABASE_USER: selectzero SZ_DATABASE_PASSWORD: REPLACE_WITH_THE_DATABASE_PASSWORD # Only needed when you cannot use IRSA or EKS Pod Identity. Uncomment the matching # environment variables in the Deployment as well. # SZ_STORAGE_S3_ACCESS_KEY: REPLACE # SZ_STORAGE_S3_SECRET_KEY: REPLACECreate the Deployment. Replace the
VERSIONimage tag: the current release is2026.10.0.# Persistent volume variant: one PersistentVolumeClaim holds every durable directory. # Keep this file in sync with ../s3/deployment.yaml: a diff between the two must show only the # volumes, serviceAccountName and the SZ_STORAGE_* environment variables. apiVersion: apps/v1 kind: Deployment metadata: name: selectzero labels: app: selectzero spec: replicas: 1 # SelectZero runs its schedulers in-process; never run a second replica strategy: type: Recreate # two pods must not work on the same files during a rollout selector: matchLabels: app: selectzero template: metadata: labels: app: selectzero spec: securityContext: runAsNonRoot: true runAsUser: 1000 runAsGroup: 1000 fsGroup: 1000 # makes the provisioned volume writable by the container's user seccompProfile: type: RuntimeDefault containers: - name: selectzero image: selectzero/selectzero:VERSION # replace with the version you deploy imagePullPolicy: IfNotPresent securityContext: readOnlyRootFilesystem: true allowPrivilegeEscalation: false capabilities: drop: ["ALL"] ports: - name: http containerPort: 8090 env: # PostgreSQL is recommended but not required here: without SZ_JDBC_URL the embedded # SQLite database is used and lives on the volume. - name: SZ_JDBC_URL value: jdbc:postgresql://YOUR-DATABASE-HOST:5432/selectzero - name: SZ_DATABASE_USER valueFrom: secretKeyRef: {name: selectzero-secrets, key: SZ_DATABASE_USER} - name: SZ_DATABASE_PASSWORD valueFrom: secretKeyRef: {name: selectzero-secrets, key: SZ_DATABASE_PASSWORD} - name: SZ_DATABASE_SSL_MODE value: SSL # NONE, SSL or SSL_VERIFY. An unrecognised value silently means NONE # Optional here: without it a key is generated on the volume at first start. # Supply it if you want the key to live in your own secret manager. - name: SZ_ENCRYPTION_KEY valueFrom: secretKeyRef: name: selectzero-secrets key: SZ_ENCRYPTION_KEY optional: true # the container still starts when the Secret does not hold it - name: JAVA_OPTIONS value: -XX:MaxRAMPercentage=50.0 resources: requests: cpu: "1" memory: 4Gi limits: memory: 8Gi startupProbe: httpGet: path: /actuator/health port: 8090 # Kubernetes waits 1 second by default, which a JVM exceeds during a garbage # collection or a busy moment. Six slow answers in a row restart the container. timeoutSeconds: 5 periodSeconds: 10 failureThreshold: 30 livenessProbe: httpGet: path: /actuator/health/liveness port: 8090 timeoutSeconds: 5 periodSeconds: 10 failureThreshold: 6 readinessProbe: httpGet: path: /actuator/health/readiness port: 8090 timeoutSeconds: 5 periodSeconds: 10 volumeMounts: - {name: storage, mountPath: /usr/app/database, subPath: database} - {name: storage, mountPath: /usr/app/reports, subPath: reports} - {name: storage, mountPath: /usr/app/drivers, subPath: drivers} - {name: storage, mountPath: /usr/app/keys, subPath: keys} - {name: storage, mountPath: /usr/app/tempFiles, subPath: tempFiles} - {name: storage, mountPath: /usr/app/logs, subPath: logs} - {name: tmp, mountPath: /tmp} volumes: - name: storage persistentVolumeClaim: claimName: selectzero-storage-claim # Required by readOnlyRootFilesystem: JVM performance data, native library extraction # and the initial administrator password file are written here. - name: tmp emptyDir: sizeLimit: 1GiCreate the Service and apply everything.
apiVersion: v1 kind: Service metadata: name: selectzero labels: app: selectzero spec: type: ClusterIP selector: app: selectzero ports: - name: http port: 80 targetPort: 8090kubectl apply -n YOUR-NAMESPACE -f pvc.yaml -f secret.yaml -f deployment.yaml -f service.yaml
fsGroup: 1000 in the pod security context makes the provisioned volume writable by the container user, so the folder permission fix is not needed on dynamically provisioned volumes.
JDBC drivers
Drivers uploaded through the Connectors settings tab persist in both variants. In S3 mode drivers is one of the mirrored prefixes: an upload is written through to the bucket and restored into the pod at the next start, which is why it appears in the hydration log line. In the persistent volume variant drivers live on the volume.
You can also bake drivers into a derived image if you prefer immutable deployments:
This is optional. Uploading through the interface is the supported path and survives restarts on its own.
Logging
The general application log goes to standard output as well as to a file, so kubectl logs and any cluster log collector receive it with no extra configuration. The SQL, Security, Import/Export, AI assistant and System logs are written only to files inside the pod. See Logging on Kubernetes for what that means and how to collect them, including the optional one-line JSON file (SZ_LOG_JSON) that a sidecar collector can tail for every stream at once.
When the pod does not start
When SelectZero refuses to start because of its encryption key or a secret reference, the pod restarts in a loop and kubectl logs shows a long stack trace. The reason is the line that contains Startup aborted, at the top of the trace or just before it:
--previous reads the log of the container that just exited; leave it out if the pod has not restarted yet. The line names one of four causes:
The line contains | Cause | Fix |
|---|---|---|
| S3 mode without | Set the key in the Secret, see Prerequisites |
| An | Check the secret id, the pod's IAM permission |
| The | |
| The key differs from the one the existing data was encrypted with | Restore the previous key; the line explains the override |
Updating the version
The Recreate strategy stops the old pod before starting the new one, so there is a short outage. In S3 mode the new pod hydrates from the bucket before it becomes ready.
With the Helm chart, upgrade to the new chart version. The chart version is always the SelectZero release it installs, so upgrading to a release means passing that release as --version. Always pass your values file: an upgrade with no values at all silently reuses the previous release's values, including any image tag you pinned, and --reuse-values keeps the old chart's defaults for anything the new version added.
To run a specific image without changing chart version, for example a patch release, set image.tag in your values file. Do not change the version on a Helm-managed installation with kubectl set image or kubectl edit: Helm 4 then refuses the next helm upgrade with a field conflict, and Helm 3 silently reverts your change.
With raw manifests, change the image tag and apply.
Read the release notes before a major version jump, and back up your database first.
Current limitations
One replica only. Covered at the top of this page. The Helm chart refuses to install above one replica; the raw manifests do not enforce it.
The log viewer shows the current pod. The in-application log viewer and the archived log download read files inside the running pod. After a restart they start empty.
Direct HTTPS on the container needs a mounted keystore. SelectZero decides whether to serve HTTPS itself before it restores anything from the bucket, so a certificate uploaded through the interface does not enable it in S3 mode. Terminate TLS at your ingress, which is the normal Kubernetes arrangement, and run with the secure-session profile when you do.