Skip to content

Application Deployment

Deploy and manage applications on your Kubernetes clusters.

Deployment Methods

Overview

You can deploy applications using:

Method Best For Difficulty
Web Console Quick deploys, visual management Easy
Helm Charts Packaged applications, versioning Medium
kubectl + YAML Custom configurations, GitOps Medium
GitOps (ArgoCD/Flux) Automated deployments, CD pipelines Advanced

Deploy with Web Console

See Console Usage for console deployment instructions.

Quick Steps:

  1. Navigate to cluster → WorkloadsDeploy
  2. Choose deployment method (Helm, YAML, or container image)
  3. Configure application settings
  4. Deploy and monitor

Deploy with Helm

Install Helm Chart

# Add chart repository
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update

# Install chart
helm install my-app bitnami/nginx \
  --namespace my-app \
  --create-namespace \
  --set replicaCount=3 \
  --set service.type=LoadBalancer

# Check installation
helm list -n my-app
kubectl get pods -n my-app

Custom Values File

# values.yaml
replicaCount: 3

image:
  repository: nginx
  tag: "1.25"
  pullPolicy: IfNotPresent

service:
  type: LoadBalancer
  port: 80

resources:
  limits:
    cpu: 500m
    memory: 512Mi
  requests:
    cpu: 250m
    memory: 256Mi

ingress:
  enabled: true
  className: nginx
  hosts:

    - host: my-app.company.com
      paths:

        - path: /
          pathType: Prefix

Deploy with custom values:

helm install my-app bitnami/nginx \
  --namespace my-app \
  --create-namespace \
  -f values.yaml

Upgrade Application

# Upgrade to new version
helm upgrade my-app bitnami/nginx \
  --namespace my-app \
  --set image.tag=1.26 \
  -f values.yaml

# Check upgrade status
helm status my-app -n my-app

# View release history
helm history my-app -n my-app

# Rollback if needed
helm rollback my-app 1 -n my-app

Uninstall Application

# Uninstall release
helm uninstall my-app -n my-app

# Verify removal
kubectl get pods -n my-app

Deploy with kubectl

Simple Deployment

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  namespace: my-app
  labels:
    app: my-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:

      - name: app
        image: nginx:1.25
        ports:

        - containerPort: 80
        resources:
          limits:
            cpu: 500m
            memory: 512Mi
          requests:
            cpu: 250m
            memory: 256Mi
        livenessProbe:
          httpGet:
            path: /
            port: 80
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /
            port: 80
          initialDelaySeconds: 5
          periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: my-app
  namespace: my-app
spec:
  type: LoadBalancer
  selector:
    app: my-app
  ports:

  - port: 80
    targetPort: 80
    protocol: TCP

Apply configuration:

# Create namespace
kubectl create namespace my-app

# Apply deployment
kubectl apply -f deployment.yaml

# Check status
kubectl get deployments -n my-app
kubectl get pods -n my-app
kubectl get service -n my-app

Add Ingress

# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app
  namespace: my-app
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  tls:

  - hosts:
    - my-app.company.com
    secretName: my-app-tls
  rules:

  - host: my-app.company.com
    http:
      paths:

      - path: /
        pathType: Prefix
        backend:
          service:
            name: my-app
            port:
              number: 80

Apply ingress:

kubectl apply -f ingress.yaml

# Check ingress
kubectl get ingress -n my-app

# Test access
curl https://my-app.company.com

StatefulSet with Storage

# statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: database
  namespace: my-app
spec:
  serviceName: database
  replicas: 3
  selector:
    matchLabels:
      app: database
  template:
    metadata:
      labels:
        app: database
    spec:
      containers:

      - name: postgres
        image: postgres:15
        ports:

        - containerPort: 5432
        env:

        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: db-credentials
              key: password
        volumeMounts:

        - name: data
          mountPath: /var/lib/postgresql/data
        resources:
          limits:
            cpu: 1000m
            memory: 2Gi
          requests:
            cpu: 500m
            memory: 1Gi
  volumeClaimTemplates:

  - metadata:
      name: data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: kubevirt    # tenant-cluster StorageClass; ask your admin which is available
      resources:
        requests:
          storage: 10Gi

Deploy statefulset:

# Create secret for database password
kubectl create secret generic db-credentials \
  --from-literal=password=secure-password \
  -n my-app

# Apply statefulset
kubectl apply -f statefulset.yaml

# Check status
kubectl get statefulset -n my-app
kubectl get pvc -n my-app

Exposing Services

Three patterns are available to expose a workload running on a tenant cluster. Pick the one that matches the traffic shape:

Pattern When to use
Service type: LoadBalancer Generic TCP/UDP exposure, any port. Default for public-facing apps.
Ingress (HTTP/HTTPS) Many HTTP services behind a single IP, host/path routing, TLS termination.
Service type: NodePort Internal-only access from operators on the data network, or as a fallback during testing. Not the recommended user-facing surface.

LoadBalancer services

Create a Service with type: LoadBalancer from inside the tenant cluster — nothing else is required. An external IP is allocated automatically and appears in EXTERNAL-IP:

# From inside the tenant cluster (kubectl --kubeconfig=<tenant>.kubeconfig)
kubectl expose deployment my-app --port=80 --target-port=8080 --type=LoadBalancer

kubectl get svc my-app
# NAME     TYPE           CLUSTER-IP     EXTERNAL-IP      PORT(S)        AGE
# my-app   LoadBalancer   10.96.x.x      77.220.68.116    80:31234/TCP   12s

The IP comes from the platform's provider network. Traffic to that IP is delivered directly to a healthy backend pod — there is no intermediate proxy on the under cluster.

Mechanics, briefly. The platform runs a cloud-provider-ovn controller per tenant cluster (in the under cluster's tenant namespace). It watches LoadBalancer services in your tenant cluster and programs the matching OVN external IP + load balancer entries. You do not deploy or manage this controller — only the platform admin does.

Pinning an external IP across recreations

By default the platform allocates a fresh IP each time you create a LoadBalancer Service and releases it on delete. To keep the same IP across recreations (e.g., for a stable DNS record), use the kmetal.io/external-ip annotation with a name that the platform admin has reserved for you:

apiVersion: v1
kind: Service
metadata:
  name: my-app
  annotations:
    kmetal.io/external-ip: my-app-prod-ip   # name of a pre-allocated EIP
spec:
  type: LoadBalancer
  selector:
    app: my-app
  ports:
    - port: 80
      targetPort: 8080

Ask your platform admin to pre-allocate the EIP on the under cluster (it is an OvnEip resource in the provider subnet). Once it exists, any number of Service recreations will rebind to the same name → same IP.

Verifying

# Service shows EXTERNAL-IP
kubectl get svc my-app -o wide

# From outside the cluster:
curl http://77.220.68.116/

# If EXTERNAL-IP stays <pending>:
kubectl describe svc my-app          # check events for allocation errors

See Troubleshooting: Networking for the under-cluster side of the picture.

Ingress

For HTTP/HTTPS traffic with host or path routing, install an ingress controller inside the tenant cluster and create Ingress objects. A single Service type: LoadBalancer fronts the ingress controller; everything else flows through it.

A worked Ingress YAML example is shown above under Add Ingress. The ingress controller (e.g., HAProxy Ingress, NGINX Ingress, Traefik) is tenant-installed — the platform does not provide one for you on the tenant cluster.

NodePort and headless services

NodePort exposes the service on every tenant worker VM's IP on the tenant overlay. This is only reachable from inside the tenant's VPC — it is not a public-facing surface in overlay mode. Use it for cluster-internal admin tools (e.g., a port-forwarded debug dashboard), not for end-user traffic.

Headless services (clusterIP: None) work normally for stateful workloads that resolve pod IPs directly inside the tenant cluster.

Persistent Storage

Persistent storage inside the tenant cluster goes through one StorageClass: kubevirt (the default class, provisioned by csi.kubevirt.io). Behind the scenes every PVC becomes a KubeVirt DataVolume on the under cluster's backing storage and is attached as an extra disk to your worker VMs.

Claiming a volume

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-data
spec:
  accessModes: [ "ReadWriteOnce" ]
  storageClassName: kubevirt
  resources:
    requests:
      storage: 5Gi

The StatefulSet example earlier on this page uses the same shape via volumeClaimTemplates.

Tenant storage quota

Your tenant has a fixed storage budget set by the platform admin. The cap is enforced when you create the PVC — if it would push your tenant's total over the limit, the PVC creation is rejected with a ResourceQuota … exhausted error.

# See claims and bound volumes in your tenant cluster
kubectl get pvc -A
kubectl get pv

# Check what's used vs. requested
kubectl describe pvc my-data

If you hit the quota and need more, ask your platform admin to raise the tenant's ResourceQuota on the under cluster.

Snapshots

Volume snapshots depend on the under cluster's backing storage. If the admin has installed a snapshot-capable CSI driver under the hood, VolumeSnapshot works from inside the tenant cluster the usual way; if not, snapshots are unavailable. Confirm with kubectl get volumesnapshotclass in your tenant cluster before relying on them.

Configuration Management

ConfigMaps

# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
  namespace: my-app
data:
  app.properties: |
    server.port=8080
    database.url=jdbc:postgresql://database:5432/mydb
    cache.enabled=true
  nginx.conf: |
    server {
      listen 80;
      server_name my-app.company.com;
      location / {
        proxy_pass http://backend:8080;
      }
    }

Use in deployment:

spec:
  containers:

  - name: app
    image: my-app:latest
    envFrom:

    - configMapRef:
        name: app-config
    volumeMounts:

    - name: config
      mountPath: /etc/config
  volumes:

  - name: config
    configMap:
      name: app-config

Secrets

# Create secret from literals
kubectl create secret generic app-secrets \
  --from-literal=api-key=abc123 \
  --from-literal=db-password=secret \
  -n my-app

# Create secret from file
kubectl create secret generic tls-cert \
  --from-file=tls.crt=./cert.pem \
  --from-file=tls.key=./key.pem \
  -n my-app

# View secrets (values are base64 encoded)
kubectl get secrets -n my-app
kubectl describe secret app-secrets -n my-app

Use secrets in deployment:

spec:
  containers:

  - name: app
    image: my-app:latest
    env:

    - name: API_KEY
      valueFrom:
        secretKeyRef:
          name: app-secrets
          key: api-key

    - name: DB_PASSWORD
      valueFrom:
        secretKeyRef:
          name: app-secrets
          key: db-password

Application Updates

Rolling Update

# Update image version
kubectl set image deployment/my-app \
  app=nginx:1.26 \
  -n my-app

# Monitor rollout
kubectl rollout status deployment/my-app -n my-app

# Check rollout history
kubectl rollout history deployment/my-app -n my-app

Rollback

# Rollback to previous version
kubectl rollout undo deployment/my-app -n my-app

# Rollback to specific revision
kubectl rollout undo deployment/my-app --to-revision=2 -n my-app

# Verify rollback
kubectl rollout status deployment/my-app -n my-app

Scale Application

# Scale manually
kubectl scale deployment/my-app --replicas=5 -n my-app

# Enable autoscaling
kubectl autoscale deployment/my-app \
  --min=3 --max=10 \
  --cpu-percent=80 \
  -n my-app

# Check autoscaler status
kubectl get hpa -n my-app

Monitoring Applications

Check Application Status

# View deployments
kubectl get deployments -n my-app

# View pods
kubectl get pods -n my-app

# Describe pod for details
kubectl describe pod <pod-name> -n my-app

# View events
kubectl get events -n my-app --sort-by='.lastTimestamp'

View Logs

# View pod logs
kubectl logs <pod-name> -n my-app

# Follow logs
kubectl logs -f <pod-name> -n my-app

# View previous container logs
kubectl logs <pod-name> --previous -n my-app

# Logs from all pods with label
kubectl logs -l app=my-app -n my-app --tail=100

Resource Usage

# Pod resource usage
kubectl top pods -n my-app

# Node resource usage
kubectl top nodes

# Detailed resource info
kubectl describe deployment my-app -n my-app

Troubleshooting

Pod Issues

Pod not starting:

# Check pod status
kubectl get pod <pod-name> -n my-app

# View pod events
kubectl describe pod <pod-name> -n my-app

# Check logs
kubectl logs <pod-name> -n my-app

# Common issues:
# - ImagePullBackOff: Image doesn't exist or no pull access
# - CrashLoopBackOff: Application crashing on startup
# - Pending: Resource constraints or scheduling issues

Fix common pod issues:

# Delete failed pod (will recreate)
kubectl delete pod <pod-name> -n my-app

# Check resource availability
kubectl describe nodes

# Verify image exists
docker pull <image-name>

# Check service account permissions
kubectl get serviceaccount -n my-app

Service Issues

Service not accessible:

# Check service
kubectl get service my-app -n my-app

# Check endpoints
kubectl get endpoints my-app -n my-app

# If no endpoints, check pod labels match service selector
kubectl get pods -n my-app --show-labels

Ingress Issues

Ingress not working:

# Check ingress
kubectl get ingress -n my-app
kubectl describe ingress my-app -n my-app

# Verify your tenant cluster's ingress controller is running (namespace depends on how you installed it)
kubectl get pods -A | grep ingress

# Check DNS resolves
nslookup my-app.company.com

# Test from inside cluster
kubectl run -it --rm debug --image=curlimages/curl --restart=Never -- \
  curl http://my-app.my-app.svc.cluster.local

Application Debugging

# Execute commands in pod
kubectl exec -it <pod-name> -n my-app -- /bin/bash

# Port forward for local testing
kubectl port-forward service/my-app 8080:80 -n my-app
# Access via http://localhost:8080

# Copy files from pod
kubectl cp my-app/<pod-name>:/path/to/file ./local-file -n my-app

# Check application configuration
kubectl exec <pod-name> -n my-app -- env | grep CONFIG

Best Practices

Deployment

  1. Use namespaces to isolate applications
  2. Set resource limits and requests
  3. Implement health checks (liveness and readiness probes)
  4. Use appropriate update strategies (rolling updates)
  5. Version container images (avoid "latest" tag in production)

Security

  1. Don't store secrets in plain YAML (use Secrets or external secret managers)
  2. Run containers as non-root when possible
  3. Use read-only root filesystem where applicable
  4. Scan images for vulnerabilities before deployment
  5. Apply network policies to restrict traffic

Operations

  1. Monitor application metrics and logs
  2. Set up alerting for failures
  3. Use GitOps for deployment automation
  4. Test deployments in non-production first
  5. Document application dependencies and requirements