How-To Guide

Deploy Multi-Architecture Applications

Target Audience: Developers deploying applications to the multi-architecture cluster

This guide explains how to deploy applications on the kup6s cluster. General workloads run on AMD64 (x86) dedicated workers; a single tainted ARM64 node exists only for CI builds.


Understanding the Cluster Architecture

The worker tier is AMD64 (x86) by default, with one special-purpose ARM64 node:

AMD64 Nodes (Default - all general workloads)

  • 2 dedicated bare-metal workers: 2x EX63 in Helsinki (kup6s-ex-hel-1, kup6s-ex-hel-2)

  • Specs (each): Intel Core Ultra 7 265 (20 cores), 64GB RAM, 2x 1TB NVMe

  • Untainted: Workloads schedule here by default

  • Storage: Longhorn lives on the NVMe disks; CNPG databases also run here

ARM64 Node (CI only - tainted)

  • 1 cloud node: CAX31 (8 vCPU, 16GB), kup6s-agent-cax31-fsn1-yim in fsn1

  • Tainted: dedicated=arm-ci:NoSchedule

  • Runs only: gitlab-runner-arm (dedicated ARM CI builder)

  • Not a general scheduling target - workloads land here only if they tolerate the dedicated=arm-ci taint AND select kubernetes.io/arch=arm64


Decision Tree: Which Architecture?

Is your image AMD64 or multi-arch?
├─ YES → Deploy as-is (default, no special config — lands on the EX workers)
└─ NO (ARM64-only)
   ├─ Can you rebuild as multi-arch / AMD64? → YES → Rebuild and deploy (default)
   └─ NO → You must target the tainted ARM CI node
            (only appropriate for CI build agents)

Deployment Patterns

Pattern 1: AMD64 / Multi-Arch Application (Default)

When to use: Your image supports AMD64 or is multi-arch (the common case)

No special configuration needed - workloads naturally schedule to the EX (AMD64) workers:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: modern-webapp
  namespace: websites
spec:
  replicas: 3
  selector:
    matchLabels:
      app: modern-webapp
  template:
    metadata:
      labels:
        app: modern-webapp
    spec:
      # No nodeSelector or tolerations needed!
      containers:
        - name: web
          image: myorg/webapp:latest  # AMD64 or multi-arch image
          ports:
            - containerPort: 8080
          resources:
            requests:
              cpu: 100m
              memory: 256Mi
            limits:
              cpu: 500m
              memory: 512Mi

Verification:

# Check where pods are running
kubectl get pods -n websites -o wide

# Should show the EX worker node names: kup6s-ex-hel-1 or kup6s-ex-hel-2

Pattern 2: ARM64-only Application on the CI Node (Special Case)

When to use: Almost never — this is only for the ARM CI builder. Regular ARM64-only images should be rebuilt as multi-arch instead.

Requires explicit targeting of the tainted ARM node with nodeSelector and toleration:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: arm-ci-builder
  namespace: gitlab
spec:
  replicas: 1
  selector:
    matchLabels:
      app: arm-ci-builder
  template:
    metadata:
      labels:
        app: arm-ci-builder
    spec:
      # Target the ARM64 CI node explicitly
      nodeSelector:
        kubernetes.io/arch: arm64

      # Tolerate the dedicated ARM-CI taint
      tolerations:
        - key: dedicated
          operator: Equal
          value: arm-ci
          effect: NoSchedule

      containers:
        - name: builder
          image: myorg/arm-builder:arm64-only  # ARM64-only image
          ports:
            - containerPort: 8080
          resources:
            requests:
              cpu: 100m
              memory: 256Mi
            limits:
              cpu: 500m
              memory: 512Mi

Verification:

# Check where pods are running
kubectl get pods -n gitlab -o wide

# Should show the ARM64 node name: kup6s-agent-cax31-fsn1-yim

Pattern 3: Multi-Arch Build, Default Scheduling

When to use: Image is multi-arch — let it run on the AMD64 EX workers (the default)

No special scheduling needed. Because the only ARM64 node is tainted for CI, multi-arch images simply land on the EX (AMD64) workers like any other workload:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: flexible-webapp
  namespace: websites
spec:
  replicas: 5
  selector:
    matchLabels:
      app: flexible-webapp
  template:
    metadata:
      labels:
        app: flexible-webapp
    spec:
      # No nodeSelector or tolerations needed — multi-arch images run on the
      # untainted EX (AMD64) workers by default.
      containers:
        - name: web
          image: myorg/flexible-webapp:latest  # Multi-arch manifest
          ports:
            - containerPort: 8080
          resources:
            requests:
              cpu: 100m
              memory: 256Mi
            limits:
              cpu: 500m
              memory: 512Mi

Pattern 4: PostgreSQL (CNPG)

When to use: Production databases

There is no dedicated database node. CNPG runs on the EX (AMD64) workers like any other workload and stores its volumes on Longhorn. No nodeSelector or toleration is required — CNPG’s own anti-affinity spreads replicas across the EX hosts for HA:

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: production-db
  namespace: databases
spec:
  instances: 3  # HA with replication (CNPG spreads across the EX workers)

  storage:
    storageClass: longhorn
    size: 20Gi

  postgresql:
    parameters:
      max_connections: "200"
      shared_buffers: "512MB"
      effective_cache_size: "1536MB"

  backup:
    barmanObjectStore:
      destinationPath: s3://kup6s-db-backups/production-db
      s3Credentials:
        accessKeyId:
          name: db-backup-s3
          key: ACCESS_KEY_ID
        secretAccessKey:
          name: db-backup-s3
          key: SECRET_ACCESS_KEY
      wal:
        compression: gzip

Verification:

# Check database pod placement
kubectl get pods -n databases -o wide

# Will schedule on the EX workers: kup6s-ex-hel-1 / kup6s-ex-hel-2

Building Multi-Architecture Images

Using Docker Buildx

Build for both architectures:

# Create buildx builder (first time only)
docker buildx create --name multiarch-builder --use

# Build and push multi-arch image
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t myorg/myapp:latest \
  --push \
  .

Verify multi-arch manifest:

docker manifest inspect myorg/myapp:latest | grep architecture

# Should show both:
# "architecture": "amd64"
# "architecture": "arm64"

Dockerfile Best Practices

Architecture-agnostic Dockerfile:

# Use multi-arch base images
FROM node:20-alpine

# Install dependencies
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

# Copy application
COPY . .

# Run application
EXPOSE 8080
CMD ["node", "server.js"]

Architecture-specific builds (if needed):

FROM --platform=$BUILDPLATFORM node:20-alpine AS builder
ARG TARGETPLATFORM
ARG BUILDPLATFORM

# Build steps here...

FROM node:20-alpine
# Copy from builder...

Ensuring AMD64 / Multi-Arch Image Availability

Because general workloads run on the AMD64 EX workers, every image must provide an amd64 variant (multi-arch images do this automatically). For an ARM64-only image, rebuild it as multi-arch with Docker Buildx (see above) before deploying.

# Check which architectures an image supports
docker manifest inspect myorg/myapp:latest

# Look for "architecture": "amd64"

If NO AMD64 variant:

  • Rebuild as multi-arch using Docker Buildx (see above)

  • OR find an AMD64-compatible alternative


Common Issues & Troubleshooting

Issue: Pods Pending on “Insufficient cpu/memory”

Cause: The EX workers are full.

Solution:

# Check node resource usage
kubectl top nodes

# Reduce resource requests, or contact the cluster admin about adding capacity

Issue: Pods Stuck in “Pending” with untolerated taint

Cause: An ARM64-only image was scheduled to the tainted CI node without the matching toleration, or a multi-arch image accidentally pinned to arch=arm64.

Solution: Deploy as AMD64/multi-arch (default), or — only for the CI builder — add the dedicated=arm-ci toleration plus nodeSelector: kubernetes.io/arch=arm64 (see Pattern 2).

# Check why pending
kubectl describe pod <pod-name> -n <namespace>

# Look for: "node(s) had untolerated taint {dedicated: arm-ci}"

Issue: Image Pull / exec format error on the EX workers

Cause: Image has no AMD64 variant (ARM64-only) and was scheduled to an EX (AMD64) worker.

Solution:

# Verify image architecture support
docker manifest inspect <image-name>

# If no AMD64 variant:
# 1. Rebuild the image as multi-arch (linux/amd64,linux/arm64)
# 2. Or find an AMD64-compatible alternative

Resource Planning

Current Cluster Capacity

Tier

vCPU (cores)

RAM

Notes

EX63 worker #1 (kup6s-ex-hel-1)

20

64GB

AMD64, 2x 1TB NVMe

EX63 worker #2 (kup6s-ex-hel-2)

20

64GB

AMD64, 2x 1TB NVMe

ARM64 CI node (...-yim)

8

16GB

tainted dedicated=arm-ci, CI only

General-workload total

40

128GB

the 2x EX63


Reference: Node Labels and Taints

EX Worker Nodes (AMD64 — default scheduling target)

labels:
  kubernetes.io/arch: amd64
  # No taints - default scheduling target

ARM64 CI Node (tainted)

labels:
  kubernetes.io/arch: arm64
taints:
  - key: dedicated
    value: arm-ci
    effect: NoSchedule

Getting Help