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-yimin fsn1Tainted:
dedicated=arm-ci:NoScheduleRuns only:
gitlab-runner-arm(dedicated ARM CI builder)Not a general scheduling target - workloads land here only if they tolerate the
dedicated=arm-citaint AND selectkubernetes.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 ( |
20 |
64GB |
AMD64, 2x 1TB NVMe |
EX63 worker #2 ( |
20 |
64GB |
AMD64, 2x 1TB NVMe |
ARM64 CI node ( |
8 |
16GB |
tainted |
General-workload total |
40 |
128GB |
the 2x EX63 |
Recommended Allocation¶
All applications and databases: Deploy as AMD64/multi-arch — they land on the EX workers.
ARM64 CI builds only: Target the tainted
...-yimnode (toleration + nodeSelector).Storage: Longhorn pool spans the EX NVMe disks (~2 TB usable across both hosts).
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¶
Architecture questions: See Cluster Capabilities Reference
Image building: Docker Buildx documentation
Performance tuning: Check Grafana dashboards