Deployment

Introduction

What is Kubernetes?

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
Which JDK should I use?

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.

What is Git?

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.

What is CI/CD?

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.