Save products you love by clicking the heart icon.
Evidence-based testing practice from production codebases — property-based invariants, seeded fault injection, contract tests, benchmark-verified performance, and quality gates that are enforced, not aspirational.
A practical guide to scaling Nextcloud, OpenDesk, and similar self-hosted cloud platforms through four growth tiers, with concrete configurations and architecture patterns.
You've seen the symptoms:
vm.swappiness is too aggressivekernel.shm* isn't tuned for shared memoryModern infrastructure layers introduce countless tunable knobs: kernel parameters, Docker daemon settings, container limits, Kubernetes resource policies, and workload-specific adjustments. Getting these right is the difference between a system that holds 10× the load and one that collapses at scale.
This guide codifies the tuning patterns from production experience, organized by isolation layer with workload-specific cheat sheets.
Resource controls operate at distinct layers, each with its own knobs and trade-offs:
Each layer can override the layer above it, but defaults flow down incorrectly if not configured intentionally.
| Parameter | Default | Production | Purpose |
|---|---|---|---|
net.core.somaxconn | 128 | 4096 | Max pending SYN queue |
net.core.netdev_max_backlog | 1000 | 5000 | Max packets in NIC backlog |
net.ipv4.tcp_max_syn_backlog | 1024 | 8192 | Max SYN requests |
net.ipv4.tcp_tw_reuse | 0 | 1 | Reuse TIME_WAIT sockets |
net.ipv4.ip_local_port_range | 32768-60999 | 1024-65535 | Ephemeral port range |
net.ipv4.tcp_fin_timeout | 60 | 30 | FIN timeout seconds |
net.ipv4.tcp_keepalive_time | 7200 | 600 | Keepalive idle seconds |
net.ipv4.tcp_keepalive_intvl | 75 | 30 | Keepalive probe interval |
net.ipv4.tcp_keepalive_probes | 9 | 5 | Keepalive probe count |
# /etc/sysctl.d/99-production.conf
net.core.somaxconn = 4096
net.core.netdev_max_backlog = 5000
net.ipv4.tcp_max_syn_backlog = 8192
net.ipv4.tcp_tw_reuse = 1
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_fin_timeout = 30
net.ipv4.tcp_keepalive_time = 600
net.ipv4.tcp_keepalive_intvl = 30
net.ipv4.tcp_keepalive_probes = 5
# Security
net.ipv4.tcp_syncookies = 1
net.ipv4.conf.all.rp_filter = 1
Apply immediately:
sudo sysctl -p /etc/sysctl.d/99-production.conf
| Parameter | Default | production | Benchmark guidance |
|---|---|---|---|
vm.swappiness | 60 | 1-10 (DB), 30 (app) | Lower for memory-pressure sensitive workloads |
vm.overcommit_memory | 0 | 1 (K8s nodes) | Allow allocator overcommit (kubelet recomputes) |
vm.overcommit_ratio | 50 | 100 | Percentage of RAM+swap to overcommit |
vm.dirty_ratio | 30 | 20 | % memory where dirty pages start blocking |
vm.dirty_background_ratio | 10 | 5 | % memory where background writeback starts |
vm.min_free_kbytes | 65536 | 1-2% of RAM | Emergency reserve against OOM |
# /etc/sysctl.d/99-memory.conf
vm.swappiness = 10 # Don't swap unless critical
vm.overcommit_memory = 1 # K8s nodes expect this
vm.overcommit_ratio = 100 # Full overcommit
vm.dirty_ratio = 20 # Start blocking writes at 20%
vm.dirty_background_ratio = 5 # Background writeback at 5%
vm.min_free_kbytes = 1048576 # 1GB reserve on 64GB node
# /etc/sysctl.d/99-fs.conf
fs.file-max = 2097152 # Global open file limit
fs.inotify.max_user_instances = 1024
fs.inotify.max_user_watches = 819200 # For etcd, collectd
# /etc/security/limits.conf
* soft nofile 1048576
* hard nofile 1048576
* soft nproc 65535
* hard nproc 65535
root soft nofile 1048576
root hard nofile 1048576
{
"storage-driver": "overlay2",
"log-driver": "json-file",
"log-opts": {
"max-size": "50m",
"max-file": "5"
},
"default-ulimits": {
"nofile": { "Name": "nofile", "Hard": 1048576, "Soft": 65535 },
"nproc": { "Name": "nproc", "Hard": 65535, "Soft": 4096 }
},
"live-restore": true,
"userns-remap": "default",
"no-new-privileges": false,
"max-concurrent-downloads": 10,
"max-concurrent-uploads": 10,
"data-root": "/var/lib/docker"
}
Docker's default resource roots are generous but can lead to noisy neighbors:
| Resource | Default | Recommended | When to Override |
|---|---|---|---|
--pids-limit | (none) | 100-500 per container | Fork-prone apps need higher |
--memory-swap | unlimited | memory × 2 | Disable with --memory-swap -1 to prevent swapping into disk for latency-sensitive DBs |
--memory-reservation | equals limit | 70-80% of limit | Prevent "burst then die" patterns |
--kernel-memory | unlimited | 100-200Mi | Controls non-resident page tables; low defaults cause container crashes under high connection counts |
security-opt no-new-privileges | false | true (recommended) | Blocks setuid escalation |
docker run -d \
--name myapp \
--memory=512m \
--memory-reservation=400m \
--memory-swap=-1 \
--pids-limit=200 \
--ulimit nofile=65535:65535 \
--ulimit nproc=4096:8192 \
--security-opt no-new-privileges \
--security-opt seccomp=default.json \
myapp:latest
Kubernetes assigns QoS based on requests and limits:
| QoS Class | Requirements | Eviction Priority |
|---|---|---|
| Guaranteed | requests.cpu == limits.cpu <br/> requests.memory == limits.memory | Last to evict |
| Burstable | Any requests set <br/> requests != limits | Medium |
| BestEffort | No requests <br/> No limits | First to evict |
# Guaranteed (best for DBs, latency-critical services)
resources:
requests:
cpu: "2"
memory: "8Gi"
limits:
cpu: "2"
memory: "8Gi"
# Burstable (typical for stateless services)
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "2"
memory: "4Gi"
# ❌ BestEffort (avoid in production)
resources: {} # or omit entirely
Production rule: Always set requests. At least assign requests.memory = limits.memory / 2 for non-latency-critical workloads.
On cgroup v2, memory.swap accounting is enabled. Ensure kubelet config:
# /var/lib/kubelet/config.yaml
memorySwap:
swapBehavior: NoSwap # NoSwap, LimitedSwap, UnlimitedSwap
limits.memory is ignored for swapapiVersion: v1
kind: Pod
metadata:
name: hugepages-pod
spec:
containers:
- name: app
image: myapp:latest
resources:
limits:
Hugepages-2Mi: 512Mi # 256 × 2Mi pages
memory: "1Gi"
volumeMounts:
- mountPath: /hugepages
name: hugepage
volumes:
- name: hugepage
emptyDir:
medium: HugePages
Use for: databases (PostgreSQL, MySQL), DPDK applications, in-memory caches.
# /var/lib/kubelet/config.yaml
cpuManagerPolicy: static # none (default), static
cpuManagerReconcilePeriod: 5s
requests.cpu = integer cores get exclusive cores. Ideal for low-latency workloads.# Pod requesting exclusive CPU 0-1
resources:
requests:
cpu: "2" # Must be integer
limits:
cpu: "2"
# /var/lib/kubelet/config.yaml
topologyManagerPolicy: best-effort # none, best-effort, restricted, single-numa-node
# Pods requesting aligned resources
resources:
requests:
cpu: "4"
memory: "8Gi"
cpu: "4" # Topology manager aligns with same NUMA node
devices.kubernetes.com/mock: "1" # Device affinity
apiVersion: v1
kind: LimitRange
metadata:
name: default-limits
namespace: production
spec:
limits:
- type: Container
default:
cpu: "500m"
memory: "256Mi"
defaultRequest:
cpu: "100m"
memory: "128Mi"
max:
cpu: "4"
memory: "8Gi"
min:
cpu: "50m"
memory: "64Mi"
maxLimitRequestRatio:
cpu: "10"
memory: "4"
apiVersion: v1
kind: ConfigMap
metadata:
name: postgres-tuning
data:
# Postgres expects shared memory segments aligned with kernel
# Adjust kernel.shm* if deploying to bare metal VM
# /etc/sysctl.d/99-postgres.conf:
# kernel.shmmax = 4294967296 # 4GB
# kernel.shmall = 1048576 # 4GB pages
# kernel.sem = 250 32000 100 128
postgresql.conf: |
shared_buffers = 4GB # 25% of RAM
effective_cache_size = 12GB # 75% of RAM
work_mem = 64MB # per-operation sort memory
maintenance_work_mem = 1GB
max_connections = 200
wal_buffers = 128MB
checkpoint_completion_target = 0.9
---
apiVersion: v1
kind: Pod
metadata:
name: postgres
spec:
containers:
- name: postgres
image: postgres:16-alpine
resources:
requests:
cpu: "2"
memory: "8Gi"
limits:
cpu: "4"
memory: "16Gi" # OOM target is 2× requests
securityContext:
fsGroup: 999 # Fix permission issues on /var/run/postgresql
volumeMounts:
- mountPath: /var/lib/postgresql/data
name: data
- mountPath: /var/run/postgresql
name: run
volumes:
- name: data
persistentVolumeClaim:
claimName: postgres-pvc
- name: run
emptyDir: {}
resources:
requests:
cpu: "500m"
memory: "4Gi"
limits:
cpu: "2"
memory: "8Gi"
# In redis.conf:
# maxmemory 6gb
# maxmemory-policy allkeys-lru
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "1"
memory: "512Mi"
# In nginx.conf:
# worker_processes auto;
# worker_connections 4096; # Watch open file limits
# worker_rlimit_nofile 65535;
# multi_accept on;
# access_log off; # For high-throughput, pipe to syslog
resources:
requests:
cpu: "2"
memory: "8Gi"
limits:
cpu: "8"
memory: "32Gi"
# In server.properties:
# log.dirs = /kafka/data
# num.network.threads = 8
# num.io.threads = 16
# socket.send.buffer.bytes = 102400
# socket.receive.buffer.bytes = 102400
# log.flush.interval.messages = 10000
# log.flush.interval.ms = 1000
# num Partitions: 2× broker count
resources:
requests:
cpu: "2"
memory: "8Gi"
limits:
cpu: "4"
memory: "16Gi"
# In jvm.options:
# -Xms8g
# -Xmx8g # Don't exceed 50% of pod memory for heap
# In elasticsearch.yml:
# bootstrap.memory_lock: true
# indices.queries.cache.size: 10%
# indices.fielddata.cache.size: 20%
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2"
memory: "4Gi"
# JVM args:
# -Xms512m # Start with 512MB heap
# -Xmx1536m # Limit heap to 50% of pod memory (non-heap overhead)
# -XX:+UseG1GC
# -XX:MaxGCPauseMillis=200
# -XX:+PrintGCDetails
# -XX:+PrintGCDateStamps
# -Xloggc:/logs/gc.log
apiVersion: v1
kind: Pod
metadata:
name: ml-worker
spec:
runtimeClassName: nvidia # For NVIDIA GPU support
containers:
- name: ml-app
image: pytorch:24.01-cuda12.1
resources:
requests:
cpu: "4"
memory: "16Gi"
nvidia.com/gpu: 1
limits:
cpu: "8"
memory: "32Gi"
nvidia.com/gpu: 1
env:
- name: CUDA_VISIBLE_DEVICES
value: "0"
- name: NVIDIA_VISIBLE_DEVICES
value: "all"
apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted
apiVersion: v1
kind: Pod
metadata:
name: secure-app
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: myapp:latest
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE # If needed
seccompProfile:
type: RuntimeDefault
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "512Mi"
ephemeral-storage: "1Gi"
volumeMounts:
- name: tmp
mountPath: /tmp
- name: run
mountPath: /var/run
volumes:
- name: tmp
emptyDir:
sizeLimit: "100Mi"
- name: run
emptyDir:
sizeLimit: "50Mi"
The RuntimeDefault profile blocks dangerous syscalls:
kexec_load, module_load, delete_module (kernel persistence)ptrace, process_vm_readv/writev (process manipulation)mount, umount, pivot_root (filesystem remapping)clock_settime, settimeofday (system time manipulation)bpf (unprivileged BPF is blocked by kernel.unprivileged_bpf_disabled=1)Use custom profiles only when required (e.g., some FUSE workloads).
# Network
sysctl net.core.somaxconn net.ipv4.tcp_tw_reuse net.ipv4.ip_local_port_range
# Memory
sysctl vm.swappiness vm.dirty_ratio vm.min_free_kbytes
# Filesystem
sysctl fs.file-max fs.inotify.max_user_watches
# Per-node
cat /proc/sys/vm/swappiness
cat /proc/sys/net/core/somaxconn
# For a container, find its cgroup path
docker inspect --format '{{.State.Pid}}' myapp | xargs cat /proc/$$/cgroup
# Check limits
cat /sys/fs/cgroup/memory.max # Memory limit
cat /sys/fs/cgroup/cpu.max # CPU quota (e.g., "200000 100000")
cat /sys/fs/cgroup/pids.max # PID limit
cat /sys/fs/cgroup/memory.swap.max # Swap limit
kubectl get pod -o jsonpath='{.metadata.name}{"\t"}{.status.qosClass}{"\n"}'
# Guaranteed? Look at .spec.containers[].resources
kubectl get pod -o json | jq '.items[] | select(.metadata.name=="myapp") |
{
name: .metadata.name,
qos: .status.qosClass,
resources: .spec.containers[].resources
}'
# From Prometheus
rate(container_cpu_cfs_throttled_periods_total[5m]) / rate(container_cpu_cfs_periods_total[5m])
# Alert if > 0.5 (50% throttled)
# Check per-container fd count
find /proc/*/fd 2>/dev/null | xargs -I{} sh -c 'echo $(dirname $(dirname {})): $(ls {}/fd | wc -l)' | sort -rn | head
# Or use cgroup
cat /proc/{PID}/limits | grep "Max open files"
# On host
dmesg | grep -i "killed process"
# On K8s
kubectl get pod -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.containerStatuses[*].lastState.terminated.reason}{"\n"}{end}' | grep OOMKilled
requests on All Pods# Bad
resources:
limits:
memory: "512Mi"
cpu: "1"
# becomes BestEffort if requests not set
Result: First to evict; unpredictable scheduling; node resource exhaustion.
kernel.shmmax Too Low# PostgreSQL fails to start:
# FATAL: could not create shared memory segment: Invalid argument
Fix: Adjust kernel parameters for database pods:
# /etc/sysctl.d/99-database.conf
kernel.shmmax = 68719476736 # 64GB
kernel.shmall = 16777216 # 64GB / 4096 (page size)
--pids-limit Not Set# Fork bomb in one container can crash the entire node
Fix: Set limits on daemon or container:
{
"default-pids-limit": 200
}
Or per-container:
docker run --pids-limit 200 myapp
# Default: limits.memory=512Mi, memory-swap=unlimited
# → Container can swap to disk (slow) and exhaust host swap
Fix: Set --memory-swap=-1 to disable swapping, or kubelet memorySwap.swapBehavior: NoSwap.
runAsNonRoot + fsGroup Mismatch# PostgreSQL sleeps at startup, can't read/write /var/run/postgresql
Fix: Set fsGroup to match DB user:
securityContext:
runAsUser: 999
runAsGroup: 999
fsGroup: 999
# Kubernetes operations break when clocks are set back in time
Fix: Use NTP with maxslewrate or makestep to allow large forward jumps but reject backward jumps:
# /etc/chrony.conf
maxslewrate 1000 # Allow 1000x faster clock catch-up
makestep 1.0 -1 # Step clock if offset > 1 second (forward only)
# Security review fails: "RuntimeDefault not set"
Fix: Explicitly set profile:
podSecurityContext:
seccompProfile:
type: RuntimeDefault
# Alert: Pod Memory Usage Nearing Limit
- alert: PodMemoryHigh
expr: container_memory_usage_bytes / container_spec_memory_limit_bytes > 0.9
for: 10m
labels:
severity: warning
annotations:
summary: "Pod {{ $labels.pod }} memory > 90% of limit"
# Alert: OOM Kill
- alert: PodOOMKilling
expr: rate(kube_pod_container_status_restarts_total{reason="OOMKilled"}[5m]) > 0
for: 0m
annotations:
summary: "Container {{ $labels.container }} in pod {{ $labels.pod }} OOM killed"
RCA:
limits.memory is appropriate (is app memory-intensive?)kubectl logs ... + heap dump (JVM) or ps + pmap (native)requests.memory and limits.memory# Alert: High CPU Throttling
- alert: HighCPUThrottling
expr: |
rate(container_cpu_cfs_throttled_periods_total[5m]) /
rate(container_cpu_cfs_periods_total[5m]) > 0.5
for: 10m
annotations:
summary: "Container {{ $labels.container }} throttled > 50%"
RCA:
limits.cpu)nodeSelectorstatic policy for guaranteed exclusive cores# Alert: TCP Listen Drops / SYN Flood
- alert: TCPListenDrops
expr: rate(netstat_Tcp_ListenDrops[5m]) > 10
for: 5m
annotations:
summary: "High TCP listen drops (increase net.core.somaxconn?)"
RCA:
net.core.somaxconn on nodenet.ipv4.tcp_max_syn_backlogsomaxconn=4096, tcp_max_syn_backlog=8192, tw_reuse=1swappiness=10 (DB), dirty_ratio=20, min_free_kbytes=1-2%file-max=2097152, inotify.max_user_watches=819200kexec_load_disabled=1, unprivileged_bpf_disabled=1, yama.ptrace_scope=2default-ulimits set (nofile, nproc)userns-remap=default (UID remap)live-restore=true (daemon restart resilience)log-opts.max-size=50m (log rotation)LimitRange defined (default requests + limits)ResourceQuota defined (prevent namespace exhaustion)pod-security.kubernetes.io/enforce=restrictedNetworkPolicy default-deny + explicit allowrequests.cpu and requests.memory set alwayslimits.cpu and limits.memory set (or reason documented)securityContext.runAsNonRootallowPrivilegeEscalation=falsereadOnlyRootFilesystem=true with tmpfs mountsseccompProfile.type=RuntimeDefaultpids-limit set (on daemon or container)volumeMounts for /tmp, /var/run, /var/cachekernel.shm* tuned via host sysctlsmaxmemory + maxmemory-policy setworker_connections, worker_rlimit_nofile aligned-Xms/-Xmx = 25-50% of pod memoryruntimeClassName: nvidia, nvidia.com/gpu requestsrequests: BestEffort is first to evict and unpredictablecap-drop ALL, no-new-privileges, seccomp=RuntimeDefault reduce attack surface without performance costdmesg, sysctl, and container logs to identify bottlenecksmaxmemory, JVM needs headroom beyond heap--file=/etc/sysctl.d/*.conf) and kubelet configsA well-tuned environment holds 10× the traffic, recovers gracefully from failures, and remains secure against common container escape vectors.
man sysctl, man cgroupsbcc-tools, bpftrace for advanced containers debug