Skip to content

Deploy a Custom Workload

Tutorial for deploying your own applications to LoKO.

This tutorial covers:

  • Creating a custom workload definition
  • Deploying from custom Helm charts
  • Using your own Docker images
  • Configuring ingress and services
  • Adding to the local catalog

Time: 15 minutes

Prerequisites:

  • LoKO environment running
  • Basic Kubernetes/Helm knowledge
  • Docker image (or use example)

We’ll deploy a custom web application with:

  • Frontend (Nginx serving static files)
  • Backend API (Node.js/Express)
  • Database connection (PostgreSQL)
  • Ingress for HTTP access

Method 1: Quick Deployment (No Helm Chart)

Section titled “Method 1: Quick Deployment (No Helm Chart)”

app.js (Simple Express API):

const express = require('express')
const app = express()
app.get('/api/health', (req, res) => {
res.json({ status: 'healthy', timestamp: new Date() })
})
app.get('/api/hello', (req, res) => {
res.json({ message: 'Hello from LoKO!' })
})
app.listen(3000, () => console.log('API listening on port 3000'))

Dockerfile:

FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["node", "app.js"]

Build and push:

Terminal window
# Build
docker build -t my-api:latest .
# Tag for local registry
docker tag my-api:latest cr.dev.me:5000/my-api:latest
# Push to LoKO registry
docker push cr.dev.me:5000/my-api:latest

deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
name: my-api
namespace: default
spec:
replicas: 2
selector:
matchLabels:
app: my-api
template:
metadata:
labels:
app: my-api
spec:
containers:
- name: api
image: cr.dev.me:5000/my-api:latest
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: development
---
apiVersion: v1
kind: Service
metadata:
name: my-api
namespace: default
spec:
selector:
app: my-api
ports:
- port: 3000
targetPort: 3000
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-api
namespace: default
spec:
rules:
- host: api.dev.me
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-api
port:
number: 3000
Terminal window
kubectl apply -f deployment.yaml
Terminal window
# Wait for pods
kubectl get pods
# Test API
curl http://api.dev.me/api/health
curl http://api.dev.me/api/hello

Terminal window
# Create chart structure
helm create my-app
cd my-app

Edit values.yaml:

replicaCount: 2
image:
repository: cr.dev.me:5000/my-api
tag: latest
pullPolicy: Always
service:
type: ClusterIP
port: 3000
ingress:
enabled: true
className: traefik
hosts:
- host: myapp.dev.me
paths:
- path: /
pathType: Prefix
tls: []
resources:
limits:
cpu: 200m
memory: 256Mi
requests:
cpu: 100m
memory: 128Mi
env:
- name: NODE_ENV
value: development
- name: DB_HOST
value: postgres.dev.me
- name: DB_PORT
value: "5432"

Edit templates/deployment.yaml: Add environment variables from values:

{{- if .Values.env }}
env:
{{- toYaml .Values.env | nindent 12 }}
{{- end }}
Terminal window
# Package chart
helm package .
# Output: my-app-0.1.0.tgz
Terminal window
# Install
helm install my-app ./my-app-0.1.0.tgz -n default
# Upgrade
helm upgrade my-app ./my-app-0.1.0.tgz -n default
# Check status
helm status my-app -n default
Terminal window
curl http://myapp.dev.me/api/health

Edit ~/.loko/catalog/catalog.yaml:

workloads:
my-app:
category: "application"
description: "My custom application"
chart:
repo: "local" # Or your Helm repo
name: "my-app"
version: "0.1.0"
defaults:
namespace: "default"
ports: [] # No TCP ports needed (HTTP only)
presets:
replicaCount: 2
image:
repository: cr.dev.me:5000/my-api
tag: latest
ingress:
enabled: true
hosts:
- host: myapp.dev.me
paths:
- path: /
pathType: Prefix

If using remote Helm repository:

helm-repositories:
- name: "my-repo"
url: "https://charts.example.com"

Now you can manage it like built-in workloads:

Terminal window
# Add to environment
loko workloads add my-app
# Deploy
loko workloads deploy my-app
# Check status
loko workloads list
# Remove
loko workloads remove my-app --now

  • Frontend: React app (Nginx)
  • Backend: Node.js API
  • Database: PostgreSQL
  • Cache: Redis
Terminal window
# Deploy database and cache
loko workloads add postgres --now
loko workloads add valkey --now

values-backend.yaml:

replicaCount: 2
image:
repository: cr.dev.me:5000/my-backend
tag: latest
service:
port: 3000
env:
- name: DB_HOST
value: postgres.dev.me
- name: DB_PORT
value: "5432"
- name: DB_NAME
value: myapp
- name: REDIS_HOST
value: valkey.dev.me
- name: REDIS_PORT
value: "6379"
envFrom:
- secretRef:
name: backend-secrets # Create manually or auto-generate
ingress:
enabled: true
hosts:
- host: api.dev.me
paths:
- path: /api
pathType: Prefix

values-frontend.yaml:

replicaCount: 2
image:
repository: cr.dev.me:5000/my-frontend
tag: latest
service:
port: 80
env:
- name: API_URL
value: http://api.dev.me
ingress:
enabled: true
hosts:
- host: app.dev.me
paths:
- path: /
pathType: Prefix
Terminal window
# Deploy backend
helm install backend ./my-backend -f values-backend.yaml
# Deploy frontend
helm install frontend ./my-frontend -f values-frontend.yaml
# Check status
kubectl get pods
kubectl get ingress

env:
- name: LOG_LEVEL
value: debug
- name: PORT
value: "3000"
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: password
Terminal window
# Create secret
kubectl create secret generic app-secrets \
--from-literal=api-key=abc123 \
--from-literal=db-password=secret \
-n default
# Reference in deployment
envFrom:
- secretRef:
name: app-secrets
Terminal window
# Create config map
kubectl create configmap app-config \
--from-file=config.json \
-n default
# Mount in deployment
volumeMounts:
- name: config
mountPath: /etc/config
volumes:
- name: config
configMap:
name: app-config
persistence:
enabled: true
storageClass: standard
size: 10Gi
mountPath: /data

Terminal window
# Check registry
kubectl get pods -n loko-components | grep registry
# Check image exists
curl http://cr.dev.me:5000/v2/_catalog
# Check image in registry
docker pull cr.dev.me:5000/my-api:latest
Terminal window
# Check ingress
kubectl get ingress
# Check Traefik
kubectl get pods -n loko-components | grep traefik
# Test internal service
kubectl run -it test --rm --image=alpine -- sh
wget -O- http://my-api.default.svc.cluster.local:3000/api/health
Terminal window
# Check pods
kubectl get pods
# Check events
kubectl describe pod <pod-name>
# Check logs
kubectl logs <pod-name>
# Check resources
kubectl top nodes
kubectl top pods

livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 200m
memory: 256Mi
replicaCount: 2 # High availability
Terminal window
# Create namespace
kubectl create namespace my-app
# Deploy to namespace
helm install my-app ./chart -n my-app
image:
tag: "1.0.0" # Not "latest"