Deployment
Introduction
Kubernetes is a platform that helps manage and run applications on multiple computers working together. It automates tasks like scaling, updates, and keeping services running smoothly. It can run in the cloud or on-premise. Its benefits for reliability and automation make it valuable for managing modern data science platforms. Minikube quickly sets up a local Kubernetes cluster on macOS, Linux, and Windows.
This page describes how to deploy the Laiki Operator, a core component of the Laiki platform. To get a better understanding of how the Laiki Operator works, read the introduction page.
The Laiki Operator currently only supports Kubernetes deployments (although the broader Laiki platform can run on Docker-only hosts). For testing, we recommend using Minikube.
Deploying ShinyProxy
The Laiki Operator creates and manages the configuration of other tools in the Laiki platform. For this demonstration, we use ShinyProxy.
First, deploy ShinyProxy on Minikube using the ShinyProxy Operator. Follow the tutorial on the ShinyProxy website.
Verifying the ShinyProxy installation
Before proceeding with the Laiki Operator, verify that ShinyProxy is properly installed and accessible at https://shinyproxy-demo.local.
As part of the deployment, a ShinyProxy custom resource is created, called
shinyproxy in the shinyproxy namespace. Verify that it exists in Kubernetes:
kubectl get -n shinyproxy shinyproxy
We recommend the use of OpenJDK, using the packages of your Linux distribution, Eclipse Temurin (Adoptium), Zulu or Amazon Corretto.
To check whether your version of Java is at least version 21, run:
java -version
Running Laiki Operator
For initial testing, you can run the Laiki Operator on your local machine. This requires Java version 21 to be installed.
Download the JAR file from the Downloads page, then run:
java -jar laiki-operator-0.1.0.jar
This will quickly go through all the phases of the Laiki Operator, but won't perform any actionable
tasks without input files. Since ShinyProxy is already deployed in the cluster,
we'll create a ShinyProxyTarget file that re-deploys this existing instance.
# input/shinyproxy.yaml
laiki-metadata:
target-type: ShinyProxyTarget
name: shinyproxy
namespace: shinyproxy
laiki-spec:
server:
secureCookies: true
frameOptions: sameorigin
forward-headers-strategy: native
spring:
session:
store-type: redis
data:
redis:
password: ${REDIS_PASSWORD}
sentinel:
master: shinyproxy
password: ${REDIS_PASSWORD}
nodes: redis-server-0.redis:26379, redis-server-1.redis:26379, redis-server-2.redis:26379
proxy:
store-mode: Redis
stop-proxies-on-shutdown: false
title: ShinyProxy
logoUrl: ""
landingPage: /
containerBackend: kubernetes
kubernetes:
namespace: shinyproxy
internal-networking: true
image-pull-policy: Always
authentication: simple
admin-groups: scientists
my-apps-mode: Inline
users:
- name: jack
password: password
groups: scientists
- name: jeff
password: password
groups: mathematicians
specs:
- id: 01_hello
display-name: Hello Application
description: Application which demonstrates the basics of a Shiny app
container-cmd: [ "R", "-e", "shinyproxy::run_01_hello()" ]
container-image: openanalytics/shinyproxy-demo
access-groups: [ scientists, mathematicians ]
- id: 06_tabsets
display-name: Tabsets Application
container-cmd: [ "R", "-e", "shinyproxy::run_06_tabsets()" ]
container-image: openanalytics/shinyproxy-demo
access-groups: scientists
- id: rstudio
displayName: RStudio
description: RStudio
containerImage: openanalytics/shinyproxy-rstudio-ide-demo:1.4.1106__4.0.4
port: 8787
container-env:
DISABLE_AUTH: true
WWW_ROOT_PATH: "#{proxy.getRuntimeValue('SHINYPROXY_PUBLIC_PATH')}"
kubernetesPodTemplateSpecPatches: |
- op: add
path: /spec/containers/0/env/-
value:
name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: redis
key: redis-password
- op: add
path: /spec/containers/0/resources
value:
limits:
cpu: 1
memory: 1Gi
requests:
cpu: 0.5
memory: 1Gi
- op: add
path: /spec/serviceAccountName
value: shinyproxy-sa
kubernetesIngressPatches: |
- op: add
path: /metadata/annotations
value:
nginx.ingress.kubernetes.io/proxy-buffer-size: "128k"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/proxy-body-size: 300m
- op: add
path: /spec/ingressClassName
value: nginx
- op: add
path: /spec/tls
value:
- hosts:
- shinyproxy-demo.local
# secretName: example # uncomment and change this line if needed
image: openanalytics/shinyproxy:3.2.1
imagePullPolicy: Always
fqdn: shinyproxy-demo.local
This is the same configuration file used in the ShinyProxy Operator example, but with Laiki-specific metadata at the top that identifies it as a Laiki Operator configuration file rather than a raw Kubernetes resource.
Save this file as input/shinyproxy.yaml and run the Laiki Operator:
java -jar laiki-operator-0.1.0.jar
The operator reports an error:
Fatal: Error with input: Error parsing file: 'input/shinyproxy.yaml' as target of type: 'ShinyProxyTarget': Property 'proxy.specs' should not be defined
This error highlights the difference between the Laiki Operator and the ShinyProxy Operator:
- the ShinyProxy Operator deploys any ShinyProxy configuration, using a single input file
- the Laiki Operator reads multiple, more high-level (or abstract) input files and compiles them into a single ShinyProxy configuration file. It doesn't directly deploy ShinyProxy.
- the Laiki Operator supports multiple tools in addition to ShinyProxy: Crane, RDepot ...
Because of these differences, Laiki does not allow specifying the app specs in the configuration file of ShinyProxy. Allowing this would conflict with the idea behind the Laiki Operator. Instead, each app must be defined in its own file.
Remove the specs section from the configuration file (input/shinyproxy.yaml)
and try again. This time, the operator successfully updates the ShinyProxy
resource in Kubernetes and waits for the deployment to complete (approximately
one minute). The output should contain something similar to:
11:22:57.264 Stage 5: deploying resources
11:22:59.555 [Target type: 'ShinyProxyTarget', namespace: 'shinyproxy', name: 'shinyproxy']: starting new instance at 2026-08-03T09:22:59.521936Z
11:23:35.436 [Target type: 'ShinyProxyTarget', namespace: 'shinyproxy', name: 'shinyproxy']: new instance ready at 2026-08-03T09:23:35.426296Z
11:23:35.445 Stage 6: shutting down
Access ShinyProxy again at https://shinyproxy-demo.local. You'll notice that
no apps are displayed, which is expected since we removed the specs from the
configuration file. Let's add the apps by creating separate configuration files
for each app.
# input/01_hello.yaml
laiki-metadata:
resource-type: ShinyProxyApp
target-name: shinyproxy
target-namespace: shinyproxy
laiki-spec:
id: 01_hello
display-name: Hello Application
description: Application which demonstrates the basics of a Shiny app
container-cmd: [ "R", "-e", "shinyproxy::run_01_hello()" ]
container-image: openanalytics/shinyproxy-demo
access-groups: [ scientists, mathematicians ]
input/rstudio.yaml
# input/rstudio.yaml
laiki-metadata:
resource-type: ShinyProxyApp
target-name: shinyproxy
target-namespace: shinyproxy
laiki-spec:
id: rstudio
displayName: RStudio
description: RStudio
containerImage: openanalytics/shinyproxy-rstudio-ide-demo:1.4.1106__4.0.4
port: 8787
container-env:
DISABLE_AUTH: true
WWW_ROOT_PATH: "#{proxy.getRuntimeValue('SHINYPROXY_PUBLIC_PATH')}"
Run the Laiki Operator again. It now recognizes the two new input files, listing them in the generated resources table:
| Resource Type | Id | Target Type | Target Namespace | Target Name | Location | Workflow |
|-----------------|----------|------------------|------------------|-------------|---------------------------|-----------------------|
| ShinyProxyApp | 01_hello | ShinyProxyTarget | shinyproxy | shinyproxy | file://input/01_hello.yaml | ShinyProxyAppWorkflow |
| ShinyProxyApp | rstudio | ShinyProxyTarget | shinyproxy | shinyproxy | file://input/rstudio.yaml | ShinyProxyAppWorkflow |
The Laiki Operator again waits for the deployment to complete.
Git is a system that tracks changes to files. It saves different versions of a document so you can see who changed what and when, and easily revert to any previous version if needed. It works with both text files and code. Although it takes some time to learn it, its benefits for collaboration and version control make it well worth the effort. Many organizations make use of Git collaboration tools like GitHub, GitLab, Azure DevOps ...
Where to run Laiki?
As a first step, store your Laiki input files in a Git repository. The Laiki Operator is currently a one-shot command: it reads the input files, processes them, and deploys the resulting configuration.
CI/CD is a practice used to automatically build, test, and deploy code and configuration changes as soon as they are stored in Git. Automation ensures that workflows are repeatable, consistent, and free from human error, saving time and improving reliability.
The recommended approach is to run Laiki in a CI/CD pipeline. This ensures that Laiki executes automatically whenever changes are made to your Git repository. In future versions, we plan to make Laiki a continuous operator that runs as a container and automatically responds to changes in Git.
Deployment on common CI/CD systems (Jenkins, Forgejo, GitHub Actions, GitLab CI, and others) is straightforward:
- use the official Docker image or include the JAR file directly in your CI/CD pipeline
- have the input files ready in the
input/directory (e.g., automatically cloned by your CI/CD system) - give the CI/CD system access to your Kubernetes cluster
Next steps
Given this basic Laiki Operator setup, you can expand the configuration, see the resources page.